diff --git a/trains/api.py b/trains/api.py new file mode 100644 index 0000000..4da4eaf --- /dev/null +++ b/trains/api.py @@ -0,0 +1,23 @@ +import http +import ujson +from tilda import LED + +API_URL = "https://huxley.apphb.com/all/{}?expand=true&accessToken=D102521A-06C6-44C9-8693-7A0394C757EF" + +def get_trains(station_code='LBG'): + print('trains/api: Getting trains for {}'.format(station_code)) + station_data = None + + LED(LED.RED).on() # Red for total get_trains + try: + station_json = http.get(API_URL.format( + station_code)).raise_for_status().content + LED(LED.GREEN).on() # Green for parsing + station_data = ujson.loads(station_json) + except Exception as e: + print('Error:') + print(e) + + LED(LED.RED).off() + LED(LED.GREEN).off() + return station_data diff --git a/trains/departure_screen.py b/trains/departure_screen.py new file mode 100644 index 0000000..2d50dde --- /dev/null +++ b/trains/departure_screen.py @@ -0,0 +1,110 @@ +import sleep +import ugfx +import database +from time import time +from homescreen import time_as_string +from tilda import Buttons +from trains.screen import Screen, S_CONTINUE, S_TO_SETTINGS +from trains.api import get_trains +from trains.utils import get_departure, get_title, is_red + +UPDATE_INTERVAL_SECS = 30 + +class DepartureScreen(Screen): + def __init__(self): + self.station_data = None + self.has_error = False + self.last_update = 0 + self.should_redraw = True + + self._names = None + self._old_names = None + + def enter(self): + self.next_state = S_CONTINUE + self.station_code = database.get('trains.station_code', 'LBG') + self.last_update = 0 + Buttons.enable_interrupt( + Buttons.BTN_A, + lambda t: self.set_next_state(S_TO_SETTINGS), + on_press=True, + on_release=False + ) + + def set_next_state(self, s): + self.next_state = s + + def update(self): + now = time() + if self.last_update < (now - UPDATE_INTERVAL_SECS): + print('trains/departure_screen: Updating data') + new_station_data = get_trains(self.station_code) + if new_station_data == None: + self.has_error = True + self.should_redraw = True + else: + self.station_data = new_station_data + self.has_error = False + self.should_redraw = True + self.last_update = now + + def tick(self): + self.update() + + if self.should_redraw: + if self.station_data == None: + self.show_error() + else: + self.show_trains() + else: + self._destroy_old_names() + + sleep.sleep_ms(500) + + return self.next_state + + def _get_names_container(self): + if self._names != None: + self._names.hide() + self._old_names = self._names + names = ugfx.Container(0, 25, 190, 295) + self._names = names + return names + + def _destroy_old_names(self): + if self._old_names != None: + self._old_names.destroy() + self._old_names = None + def _destroy_names(self): + if self._names != None: + self._names.destroy() + self._names = None + + def show_trains(self): + ugfx.clear() + ugfx.area(0, 0, 240, 25, ugfx.RED if self.has_error else ugfx.GRAY) + title = get_title(self.station_data['locationName'], self.has_error) + ugfx.text(5, 5, title, ugfx.WHITE if self.has_error else ugfx.BLACK) + ugfx.text(195, 5, time_as_string(), ugfx.BLUE) + + names = self._get_names_container() + names.show() + row_num = 0 + for service in self.station_data['trainServices']: + departure = get_departure(service) + if departure: + names.text(5, 15 * row_num, service['destination'][0]['locationName'], ugfx.BLACK) + ugfx.text(195, 25 + (15 * row_num), departure,ugfx.RED if is_red(service) else ugfx.BLUE) + row_num += 1 + + self.should_redraw = False + + def show_error(self): + ugfx.clear() + ugfx.text(5, 5, 'Error :(', ugfx.RED) + self.should_redraw = False + + def exit(self): + self._destroy_old_names() + self._destroy_names() + Buttons.disable_all_interrupt() diff --git a/trains/main.py b/trains/main.py index d5a0ffc..0d681e6 100644 --- a/trains/main.py +++ b/trains/main.py @@ -6,107 +6,87 @@ ___title___ = "trains" ___license___ = "MIT" ___dependencies___ = ["app", "sleep", "wifi", "http", "ugfx_helper"] ___categories___ = ["Homescreens", "Other"] +___bootstrapped___ = False -# Config - -STATION_CODE = "DEP" -API_URL = "https://huxley.apphb.com/all/{}?expand=true&accessToken=D102521A-06C6-44C9-8693-7A0394C757EF" +import database import wifi import ugfx -import http -import ujson import app import sleep +import ntp from tilda import Buttons, LED +from trains import api +from trains import screen +from trains.departure_screen import DepartureScreen +from trains.settings_screen import SettingsScreen + +def init_screen(orientation): + # initialize screen + ugfx.clear() + ugfx.orientation(orientation) + ugfx.backlight(50) + # show initial screen + # photo credit: https://www.flickr.com/photos/remedy451/8061918891 + ugfx.display_image(0, 0, 'trains/splash.gif', 90) + def init(): - # initialize screen + print('trains/main: Init') ugfx.init() - ugfx.clear() - ugfx.orientation(90) - ugfx.backlight(50) - + ntp.set_NTP_time() # ensure wifi connection if not wifi.is_connected(): - wifi.connect(show_wait_message=True) - - # show initial screen - ugfx.text(5, 5, "Will monitor station:", ugfx.BLACK) - ugfx.text(200, 5, STATION_CODE, ugfx.BLUE) - -def get_trains(): - station_data = None + wifi.connect(show_wait_message=False) - LED(LED.RED).on() # Red for total get_trains - try: - station_json = http.get(API_URL.format(STATION_CODE)).raise_for_status().content - LED(LED.GREEN).on() # Green for parsing - station_data = ujson.loads(station_json) - except: - print('Fuck') - LED(LED.RED).off() - LED(LED.GREEN).off() - return station_data - -def get_time(station_data): - return ':'.join(station_data['generatedAt'].split('T')[1].split(':')[0:2]) - -def is_red(service): - return service['isCancelled'] or service['etd'] != 'On time' - -def get_arrival(service): - if service['isCancelled']: - return 'CANX' - - if service['eta'] == 'On time': - return service['sta'] - - return service['eta'] - -def get_title(name, has_error): - if has_error: - return 'ERR ' + name - - return name - -def show_trains(station_data, has_error): +def exit(): + print('trains/main: Exit') ugfx.clear() - ugfx.area(0, 0, 240, 25, - ugfx.RED if has_error else ugfx.GRAY) - title = get_title(station_data['locationName'], has_error) - ugfx.text(5, 5, title, - ugfx.WHITE if has_error else ugfx.BLACK) - ugfx.text(195, 5, get_time(station_data), ugfx.BLUE) - names = ugfx.Container(0, 25, 190, 295) - names.show() - for idx, service in enumerate(station_data['trainServices']): - names.text(5, 15 * idx, service['destination'][0]['locationName'], ugfx.BLACK) - ugfx.text(195, 25 + (15 * idx), get_arrival(service), ugfx.RED if is_red(service) else ugfx.BLUE) + app.restart_to_default() -def show_error(): - ugfx.clear() - ugfx.text(5, 5, 'Error :(', ugfx.RED) + +app_screens = { + screen.SETTINGS: SettingsScreen(), + screen.DEPARTURES: DepartureScreen() +} + + +def get_initial_screen(): + station_code = database.get('trains.station_code', None) + if station_code == None: + return app_screens[screen.SETTINGS] + return app_screens[screen.DEPARTURES] + + +def run_screen(instance): + print('trains/main: Starting screen {}'.format(instance)) + instance.enter() + + is_running = True + next_screen_name = None + while is_running: + status, value = instance.tick() + + if status == screen.SWITCH_SCREEN: + is_running = False + next_screen_name = value + elif status == screen.EXIT_APP: + is_running = False + + print('trains/main: Stopping screen {} (next = {})'.format(instance, next_screen_name)) + instance.exit() + return next_screen_name init() -station_data = None -has_error = False -while (not Buttons.is_pressed(Buttons.BTN_A)) and (not Buttons.is_pressed(Buttons.BTN_B)) and (not Buttons.is_pressed(Buttons.BTN_Menu)): - new_station_data = get_trains() - if new_station_data == None: - has_error = True - else: - station_data = new_station_data - has_error = False - - if station_data == None: - show_error() - else: - show_trains(station_data, has_error) - sleep.sleep_ms(30 * 1000) +current_screen = get_initial_screen() +is_app_running = True +while is_app_running: + init_screen(current_screen.orientation()) + next_screen_name = run_screen(current_screen) - -# closing -ugfx.clear() -app.restart_to_default() + if next_screen_name != None: + current_screen = app_screens[next_screen_name] + else: + is_app_running = False + exit() diff --git a/trains/screen.py b/trains/screen.py new file mode 100644 index 0000000..54752c5 --- /dev/null +++ b/trains/screen.py @@ -0,0 +1,28 @@ +CONTINUE = 1 +SWITCH_SCREEN = 2 +EXIT_APP = 3 + +DEPARTURES = 10 +SETTINGS = 11 + +S_CONTINUE = (CONTINUE, None) +S_TO_SETTINGS = (SWITCH_SCREEN, SETTINGS) +S_TO_TRAINS = (SWITCH_SCREEN, DEPARTURES) +S_EXIT = (EXIT_APP, None) + + +class Screen(): + def __init__(self): + pass + + def orientation(self): + return 90 + + def enter(self): + pass + + def tick(self): + return S_CONTINUE + + def exit(self): + pass diff --git a/trains/settings_screen.py b/trains/settings_screen.py new file mode 100644 index 0000000..42f6421 --- /dev/null +++ b/trains/settings_screen.py @@ -0,0 +1,19 @@ +import database +import ugfx +from dialogs import prompt_text +from trains.screen import Screen, S_CONTINUE, S_TO_TRAINS + +class SettingsScreen(Screen): + def __init__(self): + self.next_state = S_TO_TRAINS + + def orientation(self): + return 270 + + def tick(self): + with database.Database() as db: + crs = prompt_text('Enter your station\'s CRS code', db.get('trains.station_code', '')) + db.set('trains.station_code', crs) + + return self.next_state + \ No newline at end of file diff --git a/trains/splash.gif b/trains/splash.gif new file mode 100644 index 0000000..d3d64b0 Binary files /dev/null and b/trains/splash.gif differ diff --git a/trains/utils.py b/trains/utils.py new file mode 100644 index 0000000..fa6c902 --- /dev/null +++ b/trains/utils.py @@ -0,0 +1,19 @@ +def is_red(service): + return service['isCancelled'] or service['etd'] != 'On time' + + +def get_departure(service): + if service['isCancelled']: + return 'CANX' + + if service['etd'] == 'On time': + return service['std'] + + return service['etd'] + + +def get_title(name, has_error): + if has_error: + return 'ERR ' + name + + return name