# tested on uiflow for stickc v1.8.1 import json # pylint: disable=import-error import esp32 import m5ui import machine import micropython import uos import utils import utime from m5stack import axp, btnA, btnB, lcd, rtc _LARGE_FONT = lcd.FONT_DejaVu40 _SMALL_FONT = lcd.FONT_DejaVu18 _DEFAULT_FONT_COLOR = lcd.WHITE _LEFT_PADDING = 8 _ALARM_TIME_PATH = "alarm.json" _SCREEN_WIDTH, _SCREEN_HEIGHT = lcd.winsize() _AWAKE_SECONDS = 8 _VIBRATION_MOTOR_PIN = 26 def _handle_pending_events(): # > [...] a millisecond sleep larger than 10ms will check for pending (soft) # > interrupts during the sleep. [...] The reason for the 10ms value is # > because the FreeRTOS tick is 10ms, [...] # https://github.com/micropython/micropython/issues/3493#issuecomment-352617624 # https://github.com/micropython/micropython/commit/4ed586528047d3eced28a9f4af11dbbe64fa99bb utime.sleep_ms(11) def _get_current_daytime() -> int: # > [contradictory to] official micropython documentation, to set RTC, # > use particular tuple (year , month, day, week=0, hour, minute, second, timestamp=0) # https://community.m5stack.com/topic/3108/m5stack-core2-micropython-rtc-example hour, minute, seconds = rtc.now()[3:] return (hour * 60 + minute) * 60 + seconds def _sleep(duration_seconds: int) -> None: # https://docs.m5stack.com/en/core/m5stickc axp.setLDO2State(False) # TFT backlight axp.setLDO3State(False) # TFT IC machine.lightsleep(duration_seconds * 1000) axp.setLDO2State(True) # TFT backlight axp.setLDO3State(True) # TFT IC def _random() -> float: # like random.random return ord(uos.urandom(1)) / (1 << 8) def _randrange(start: float, stop: float) -> float: # like random.randrange return _random() * (stop - start) + start class _AlarmConfig: def __init__(self): self._hour = 0 self._minute = 0 @property def hour(self) -> int: return self._hour @property def minute(self) -> int: return self._minute def shift_hour(self, delta: int) -> None: self._hour = (self._hour + delta) % 24 def shift_minute(self, delta: int) -> None: self._minute = (self._minute + delta) % 60 @property def _daytime(self) -> int: return (self._hour * 60 + self._minute) * 60 @property def _seconds_until_next(self) -> int: return (self._daytime - _get_current_daytime() - 1) % (24 * 60 * 60) + 1 @property def next_time(self) -> int: return utime.time() + self._seconds_until_next def load(self, path: str) -> None: with open(path, "r") as alarm_time_file: alarm_time = json.load(alarm_time_file) self._hour = alarm_time["hour"] self._minute = alarm_time["minute"] def save(self, path: str) -> None: with open(path, "w") as alarm_time_file: json.dump( {"hour": self._hour, "minute": self._minute}, alarm_time_file, ) class App: # pylint: disable=too-few-public-methods,too-many-instance-attributes def __init__(self) -> None: self._clock_text_box = None self._menu_position = 0 self._wait_for_sleep_start_time_update = False self._sleep_start_time = 0.0 self._alarm_config = _AlarmConfig() self._next_alarm_time = self._alarm_config.next_time self._alarm_hour_text_box = None self._alarm_minute_text_box = None self._battery_status_text_box = None self._alarm_active = False self._vibration_motor_pin = None def _reschedule_sleep(self, interrupt: bool) -> None: if interrupt: self._wait_for_sleep_start_time_update = True micropython.schedule(self._reschedule_sleep, False) else: self._sleep_start_time = utime.time() + _AWAKE_SECONDS self._wait_for_sleep_start_time_update = False def _update_menu(self, event_arg: None) -> None: # pylint: disable=unused-argument; callback self._alarm_hour_text_box.setColor( # type: ignore lcd.GREEN if self._menu_position == 1 else (lcd.RED if self._menu_position == 2 else _DEFAULT_FONT_COLOR) ) self._alarm_minute_text_box.setColor( # type: ignore lcd.GREEN if self._menu_position == 3 else (lcd.RED if self._menu_position == 4 else _DEFAULT_FONT_COLOR) ) def _button_a_pressed(self) -> None: self._reschedule_sleep(interrupt=True) if self._alarm_active: print("alarm stopped") self._alarm_active = False else: self._menu_position = (self._menu_position + 1) % 5 # https://docs.micropython.org/en/latest/library/micropython.html#micropython.schedule micropython.schedule(self._update_menu, None) def _alarm(self) -> None: print("ALARM") self._alarm_active = True while self._alarm_active: self._vibration_motor_pin.on() # type: ignore utime.sleep(_randrange(0.2, 0.8)) self._vibration_motor_pin.off() # type: ignore utime.sleep(_randrange(0.2, 0.8)) self._next_alarm_time = self._alarm_config.next_time def _update_alarm_time(self, event_arg: None) -> None: # pylint: disable=unused-argument; callback self._next_alarm_time = self._alarm_config.next_time self._alarm_config.save(_ALARM_TIME_PATH) self._alarm_hour_text_box.setText( # type: ignore "{:02d}".format(self._alarm_config.hour) ) self._alarm_minute_text_box.setText( # type: ignore "{:02d}".format(self._alarm_config.minute) ) def _button_b_pressed(self) -> None: self._reschedule_sleep(interrupt=True) if self._menu_position == 1: self._alarm_config.shift_hour(1) elif self._menu_position == 2: self._alarm_config.shift_hour(-1) elif self._menu_position == 3: self._alarm_config.shift_minute(1) elif self._menu_position == 4: self._alarm_config.shift_minute(-1) micropython.schedule(self._update_alarm_time, None) @staticmethod def _format_time() -> str: return "{:02d}:{:02d}".format(*rtc.now()[3:5]) def _setup_clock(self) -> None: # https://github.com/m5stack/UIFlow-Code/wiki/M5UI#textbox self._clock_text_box = m5ui.M5TextBox( _SCREEN_WIDTH - 1, _LEFT_PADDING, self._format_time(), _LARGE_FONT, _DEFAULT_FONT_COLOR, rotate=90, ) machine.Timer(0).init( period=4000, # ms mode=machine.Timer.PERIODIC, callback=lambda t: self._clock_text_box.setText(self._format_time()), # type: ignore ) def _setup_alarm(self) -> None: if utils.exists(_ALARM_TIME_PATH): self._alarm_config.load(_ALARM_TIME_PATH) self._alarm_hour_text_box = m5ui.M5TextBox( _SCREEN_WIDTH // 2, _LEFT_PADDING, "{:02d}".format(self._alarm_config.hour), _LARGE_FONT, _DEFAULT_FONT_COLOR, rotate=90, ) m5ui.M5TextBox( _SCREEN_WIDTH // 2, _LEFT_PADDING + 53, ":", _LARGE_FONT, _DEFAULT_FONT_COLOR, rotate=90, ) self._alarm_minute_text_box = m5ui.M5TextBox( _SCREEN_WIDTH // 2, _LEFT_PADDING + 66, "{:02d}".format(self._alarm_config.minute), _LARGE_FONT, _DEFAULT_FONT_COLOR, rotate=90, ) def _update_battery_status_info(self) -> None: self._battery_status_text_box.setText( # type: ignore "{:.02f}V".format(axp.getBatVoltage()) ) def _setup_battery_status_info(self) -> None: self._battery_status_text_box = m5ui.M5TextBox( 12, _SCREEN_HEIGHT - 20, "", _SMALL_FONT, _DEFAULT_FONT_COLOR, rotate=0, ) def run(self) -> None: m5ui.setScreenColor(0x000000) # clear screen self._setup_clock() self._setup_alarm() self._setup_battery_status_info() self._vibration_motor_pin = machine.Pin(_VIBRATION_MOTOR_PIN, machine.Pin.OUT) btnA.wasPressed(self._button_a_pressed) btnB.wasPressed(self._button_b_pressed) # not sure whether ext0 would be better esp32.wake_on_ext1([btnA.pin], esp32.WAKEUP_ALL_LOW) self._reschedule_sleep(interrupt=False) self._update_battery_status_info() while True: seconds_until_alarm = self._next_alarm_time - utime.time() print("seconds_until_alarm =", seconds_until_alarm) # TODO remove if seconds_until_alarm <= 0: self._alarm() elif seconds_until_alarm < _AWAKE_SECONDS: utime.sleep(seconds_until_alarm) elif utime.time() < self._sleep_start_time: utime.sleep(1) # second else: _sleep(duration_seconds=seconds_until_alarm) _handle_pending_events() self._update_battery_status_info() while self._wait_for_sleep_start_time_update: _handle_pending_events() App().run()