The Time state is the most complex. It:
- displays the current time, syncing it occasionally from a time service,
- displays the current weather condition and temperature fetched occasionally from a weather service,
- triggers the alarm when the alarm time is reached,
- triggers the alarm when snooze timeout expires, and
- provides access to the settings and mugsy states.
It also has the busiest screen.
__init__
The constructor sets up the background files, timers, and screen elements. Notice that each button includes the left, right, top, and bottom of the area it covers, as well as the state to transition to if that button is touched.
def __init__(self): super().__init__() self.background_day = 'main_background_day.bmp' self.background_night = 'main_background_night.bmp' self.refresh_time = None self.update_time = None self.weather_refresh = None text_area_configs = [dict(x=88, y=170, size=5, color=0xFFFFFF, font=time_font), dict(x=210, y=50, size=5, color=0xFF0000, font=alarm_font), dict(x=88, y=90, size=6, color=0xFFFFFF, font=temperature_font)] self.text_areas = create_text_areas(text_area_configs) self.weather_icon = displayio.Group() self.weather_icon.x = 88 self.weather_icon.y = 20 self.icon_file = None self.snooze_icon = displayio.Group() self.snooze_icon.x = 260 self.snooze_icon.y = 70 self.snooze_file = None # each button has it's edges as well as the state to transition to when touched self.buttons = [dict(left=0, top=50, right=80, bottom=120, next_state='settings'), dict(left=0, top=155, right=80, bottom=220, next_state='mugsy')]
The time state has a helper method to adjust the screen for lighting level. At low levels, it will dim the display backlight and switch to a red-based background. At higher light levels, it sets the display to full brightness and uses a blue-based background.
def adjust_backlight_based_on_light(self, force=False): """Check light level. Adjust the backlight and background image if it's dark.""" global low_light if light.value <= 1000 and (force or not low_light): pyportal.set_backlight(0.01) pyportal.set_background(self.background_night) low_light = True elif force or (light.value >= 2000 and low_light): pyportal.set_backlight(1.00) pyportal.set_background(self.background_day) low_light = False
This state has two things to do with snoozing:
- Check if the snooze button is pressed. If so turn off snooze. If snooze was active, remove the snooze indicator from the screen.
- Check if the snooze timer has timed out. If so transition to the alarm state.
# is the snooze button pushed? Cancel the snooze if so. if not snooze_button.value: if snooze_time: self.snooze_icon.pop() snooze_time = None alarm_armed = False # is snooze active and the snooze time has passed? Transition to alram is so. if snooze_time and ((now - snooze_time) >= snooze_interval): change_to_state('alarm') return
Once snoozing is dealt with, the background is adjusted using the method described earlier.
# check light level and adjust background & backlight self.adjust_backlight_based_on_light()
Every hour the time is synced with a time service. This keeps the time accurate enough without requiring real time clock (RTC) hardware. It only syncs hourly so as to not consume too much time/power doing the WiFi communication as well as keeping use of the service minimal. This is handled by the PyPortal
class, so the code here is short:
# only query the online time once per hour (and on first run) if (not self.refresh_time) or ((now - self.refresh_time) > 3600): try: pyportal.get_local_time(location=secrets['timezone']) self.refresh_time = now except RuntimeError as e: self.refresh_time = now - 3000 # delay 10 minutes before retrying logger.error('Some error occured, retrying! - %s', str(e))
Similarly, the weather data is fetched periodically. In this case. every 10 minutes. Even though this uses the PyPortal
data fetch and extract support, it is a bit more involved: In addition to formatting and displaying the temperature, the appropriate weather icon needs to be selected and displayed.
# only query the weather every 10 minutes (and on first run) if (not self.weather_refresh) or (now - self.weather_refresh) > 600: logger.debug('Fetching weather') try: value = pyportal.fetch() weather = json.loads(value) # set the icon/background weather_icon_name = weather['weather'][0]['icon'] try: self.weather_icon.pop() except IndexError: pass filename = "/icons/"+weather_icon_name+".bmp" if filename: # CircuitPython 6 & 7 compatible if self.icon_file: self.icon_file.close() self.icon_file = open(filename, "rb") icon = displayio.OnDiskBitmap(self.icon_file) icon_sprite = displayio.TileGrid(icon, pixel_shader=getattr(icon, 'pixel_shader', displayio.ColorConverter()), x=0, y=0) # # CircuitPython 7+ compatible # icon = displayio.OnDiskBitmap(filename) # icon_sprite = displayio.TileGrid(icon, pixel_shader=icon.pixel_shader) self.weather_icon.append(icon_sprite) temperature = weather['main']['temp'] - 273.15 # its...in kelvin if celcius: temperature_text = '%3d C' % round(temperature) else: temperature_text = '%3d F' % round(((temperature * 9 / 5) + 32)) self.text_areas[2].text = temperature_text self.weather_refresh = now try: board.DISPLAY.refresh(target_frames_per_second=60) except AttributeError: board.DISPLAY.refresh_soon() board.DISPLAY.wait_for_frame() except RuntimeError as e: self.weather_refresh = now - 540 # delay a minute before retrying logger.error("Some error occured, retrying! - %s", str(e))
The final part of this method checks to see if it's time to update the displayed time. If so, it does.
If we are not snoozing. we check whether it is time to sound the alarm (remember that snoozing is handled earlier by using a timer). If so, and the alarm is armed we transition to the alarm state. If it's not alarm time. we arm the alarm (if it's enabled) so that it will sound the next day when the time again reaches the set time.
if (not update_time) or ((now - update_time) > 30): # Update the time update_time = now current_time = time.localtime() time_string = '%02d:%02d' % (current_time.tm_hour,current_time.tm_min) self.text_areas[0].text = time_string board.DISPLAY.refresh_soon() board.DISPLAY.wait_for_frame() # Check if alarm should sound if current_time is not None and not snooze_time: minutes_now = current_time.tm_hour * 60 + current_time.tm_min minutes_alarm = alarm_hour * 60 + alarm_minute if minutes_now == minutes_alarm: if alarm_armed: change_to_state('alarm') else: alarm_armed = alarm_enabled
touch
This is only concerned with the alarm settings and mugsy buttons. For each button, it checks if the touch (if there is one) is in its area. If so the associated state is transitioned to and the loop terminates.
def touch(self, t, touched): if t: logger.debug('touched: %d, %d', t[0], t[1]) if t and not touched: # only process the initial touch for button_index in range(len(self.buttons)): b = self.buttons[button_index] if touch_in_button(t, b): change_to_state(b['next_state']) break return bool(t)
enter
This is another busy method. It starts by updating the background, forcing the update regardless of what it thinks the light level is.
The text areas created in the constructor are added to the display, as is the weather icon and, if appropriate, the snooze indicator.
If the alarm is enabled, the alarm time is displayed.
Finally the display is updated.
def enter(self): self.adjust_backlight_based_on_light(force=True) for ta in self.text_areas: pyportal.splash.append(ta) pyportal.splash.append(self.weather_icon) if snooze_time: # CircuitPython 6 & 7 compatible if self.snooze_file: self.snooze_file.close() self.snooze_file = open('/icons/zzz.bmp', "rb") icon = displayio.OnDiskBitmap(self.snooze_file) icon_sprite = displayio.TileGrid(icon, pixel_shader=getattr(icon, 'pixel_shader', displayio.ColorConverter())) # # CircuitPython 7+ compatible # icon = displayio.OnDiskBitmap("/icons/zzz.bmp") # icon_sprite = displayio.TileGrid(icon, pixel_shader=icon.pixel_shader) self.snooze_icon.append(icon_sprite) pyportal.splash.append(self.snooze_icon) if alarm_enabled: self.text_areas[1].text = '%2d:%02d' % (alarm_hour, alarm_minute) else: self.text_areas[1].text = ' ' try: board.DISPLAY.refresh(target_frames_per_second=60) except AttributeError: board.DISPLAY.refresh_soon() board.DISPLAY.wait_for_frame()
exit
The exit
method calls the super to clear out the views, as well as clearing out the snooze indicator.
def exit(self): super().exit() for _ in range(len(self.snooze_icon)): self.snooze_icon.pop()
Text editor powered by tinymce.