vibrating_alarm_m5stickc.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """
  2. tested on uiflow for stickc v1.8.1
  3. """
  4. import json
  5. # pylint: disable=import-error
  6. import esp32
  7. import m5ui
  8. import machine
  9. import micropython
  10. import utils
  11. import utime
  12. from m5stack import axp, btnA, btnB, lcd, rtc
  13. _FONT = lcd.FONT_DejaVu40
  14. _DEFAULT_FONT_COLOR = lcd.WHITE
  15. _ALARM_TIME_PATH = "alarm.json"
  16. _SCREEN_WIDTH, _SCREEN_HEIGHT = lcd.winsize()
  17. _AWAKE_SECONDS = 8
  18. def _handle_pending_events():
  19. # > [...] a millisecond sleep larger than 10ms will check for pending (soft)
  20. # > interrupts during the sleep. [...] The reason for the 10ms value is
  21. # > because the FreeRTOS tick is 10ms, [...]
  22. # https://github.com/micropython/micropython/issues/3493#issuecomment-352617624
  23. # https://github.com/micropython/micropython/commit/4ed586528047d3eced28a9f4af11dbbe64fa99bb
  24. utime.sleep_ms(11)
  25. class App:
  26. # pylint: disable=too-few-public-methods,too-many-instance-attributes
  27. def __init__(self) -> None:
  28. self._clock_text_box = None
  29. self._menu_position = 0
  30. self._wait_for_sleep_start_time_update = False
  31. self._sleep_start_time_seconds = 0.0
  32. self._alarm_hour = None
  33. self._alarm_minute = None
  34. self._alarm_hour_text_box = None
  35. self._alarm_minute_text_box = None
  36. self._alarm_timer = None
  37. @property
  38. def _now_time(self) -> int:
  39. # > [contradictory to] official micropython documentation, to set RTC,
  40. # > use particular tuple (year , month, day, week=0, hour, minute, second, timestamp=0)
  41. # https://community.m5stack.com/topic/3108/m5stack-core2-micropython-rtc-example
  42. hour, minute, seconds = rtc.now()[3:]
  43. return (hour * 60 + minute) * 60 + seconds
  44. @property
  45. def _alarm_time(self) -> int:
  46. return (self._alarm_hour * 60 + self._alarm_minute) * 60 # type: ignore
  47. def _load_alarm_time(self) -> None:
  48. with open(_ALARM_TIME_PATH, "r") as alarm_time_file:
  49. alarm_time = json.load(alarm_time_file)
  50. self._alarm_hour = alarm_time["hour"]
  51. self._alarm_minute = alarm_time["minute"]
  52. def _save_alarm_time(self) -> None:
  53. with open(_ALARM_TIME_PATH, "w") as alarm_time_file:
  54. json.dump(
  55. {"hour": self._alarm_hour, "minute": self._alarm_minute},
  56. alarm_time_file,
  57. )
  58. def _reschedule_sleep(self, interrupt: bool) -> None:
  59. if interrupt:
  60. self._wait_for_sleep_start_time_update = True
  61. micropython.schedule(self._reschedule_sleep, False)
  62. else:
  63. self._sleep_start_time_seconds = utime.time() + _AWAKE_SECONDS
  64. self._wait_for_sleep_start_time_update = False
  65. def _update_menu(self, event_arg: None) -> None:
  66. # pylint: disable=unused-argument; callback
  67. self._alarm_hour_text_box.setColor( # type: ignore
  68. lcd.GREEN
  69. if self._menu_position == 1
  70. else (lcd.RED if self._menu_position == 2 else _DEFAULT_FONT_COLOR)
  71. )
  72. self._alarm_minute_text_box.setColor( # type: ignore
  73. lcd.GREEN
  74. if self._menu_position == 3
  75. else (lcd.RED if self._menu_position == 4 else _DEFAULT_FONT_COLOR)
  76. )
  77. def _button_a_pressed(self) -> None:
  78. self._reschedule_sleep(interrupt=True)
  79. self._menu_position = (self._menu_position + 1) % 5
  80. # https://docs.micropython.org/en/latest/library/micropython.html#micropython.schedule
  81. micropython.schedule(self._update_menu, None)
  82. @staticmethod
  83. def _alert() -> None:
  84. print("ALARM")
  85. def _alarm(self, timer: machine.Timer) -> None:
  86. # pylint: disable=unused-argument; callback
  87. micropython.schedule(lambda n: self._alert(), None)
  88. micropython.schedule(lambda n: self._configure_alarm_timer(), None)
  89. def _configure_alarm_timer(self) -> None:
  90. seconds_until_alarm = (self._alarm_time - self._now_time - 1) % (
  91. 24 * 60 * 60
  92. ) + 1
  93. print("alarm in ", seconds_until_alarm / 60, " min")
  94. # TODO configure wake from sleep
  95. self._alarm_timer.init( # type: ignore
  96. period=seconds_until_alarm * 1000, # ms
  97. mode=machine.Timer.ONE_SHOT,
  98. callback=self._alarm,
  99. )
  100. def _update_alarm_time(self, event_arg: None) -> None:
  101. # pylint: disable=unused-argument; callback
  102. self._alarm_hour_text_box.setText( # type: ignore
  103. "{:02d}".format(self._alarm_hour) # type: ignore
  104. )
  105. self._alarm_minute_text_box.setText( # type: ignore
  106. "{:02d}".format(self._alarm_minute) # type: ignore
  107. )
  108. self._save_alarm_time()
  109. self._configure_alarm_timer()
  110. def _button_b_pressed(self) -> None:
  111. self._reschedule_sleep(interrupt=True)
  112. if self._menu_position == 1:
  113. self._alarm_hour += 1 # type: ignore
  114. elif self._menu_position == 2:
  115. self._alarm_hour -= 1 # type: ignore
  116. elif self._menu_position == 3:
  117. self._alarm_minute += 1 # type: ignore
  118. elif self._menu_position == 4:
  119. self._alarm_minute -= 1 # type: ignore
  120. self._alarm_hour %= 24 # type: ignore
  121. self._alarm_minute %= 60 # type: ignore
  122. micropython.schedule(self._update_alarm_time, None)
  123. @staticmethod
  124. def _format_time() -> str:
  125. return "{:02d}:{:02d}".format(*rtc.now()[3:5])
  126. def _setup_clock(self) -> None:
  127. # https://github.com/m5stack/UIFlow-Code/wiki/M5UI#textbox
  128. self._clock_text_box = m5ui.M5TextBox(
  129. _SCREEN_WIDTH - 1,
  130. 0,
  131. self._format_time(),
  132. _FONT,
  133. _DEFAULT_FONT_COLOR,
  134. rotate=90,
  135. )
  136. machine.Timer(0).init(
  137. period=4000, # ms
  138. mode=machine.Timer.PERIODIC,
  139. callback=lambda t: self._clock_text_box.setText(self._format_time()), # type: ignore
  140. )
  141. def _setup_alarm(self) -> None:
  142. if not utils.exists(_ALARM_TIME_PATH):
  143. self._alarm_hour = self._alarm_minute = 0 # type: ignore
  144. self._save_alarm_time()
  145. else:
  146. self._load_alarm_time()
  147. self._alarm_hour_text_box = m5ui.M5TextBox(
  148. _SCREEN_WIDTH // 2,
  149. 0,
  150. "{:02d}".format(self._alarm_hour), # type: ignore
  151. _FONT,
  152. _DEFAULT_FONT_COLOR,
  153. rotate=90,
  154. )
  155. m5ui.M5TextBox(
  156. _SCREEN_WIDTH // 2, 53, ":", _FONT, _DEFAULT_FONT_COLOR, rotate=90
  157. )
  158. self._alarm_minute_text_box = m5ui.M5TextBox(
  159. _SCREEN_WIDTH // 2,
  160. 66,
  161. "{:02d}".format(self._alarm_minute), # type: ignore
  162. _FONT,
  163. _DEFAULT_FONT_COLOR,
  164. rotate=90,
  165. )
  166. # machine.Timer(1).init(...) breaks button handling
  167. self._alarm_timer = machine.Timer(2)
  168. self._configure_alarm_timer()
  169. def run(self) -> None:
  170. m5ui.setScreenColor(0x000000) # clear screen
  171. print("battery:", axp.getBatVoltage(), "V")
  172. self._setup_clock()
  173. self._setup_alarm()
  174. btnA.wasPressed(self._button_a_pressed)
  175. btnB.wasPressed(self._button_b_pressed)
  176. # not sure whether ext0 would be better
  177. esp32.wake_on_ext1([btnA.pin], esp32.WAKEUP_ALL_LOW)
  178. self._reschedule_sleep(interrupt=False)
  179. while True:
  180. while utime.time() < self._sleep_start_time_seconds:
  181. utime.sleep(1) # seconds
  182. # TODO turn off display
  183. axp.setLcdBrightness(30)
  184. machine.lightsleep()
  185. # TODO turn on display
  186. axp.setLcdBrightness(100)
  187. print("wake reason:", machine.wake_reason())
  188. _handle_pending_events()
  189. while self._wait_for_sleep_start_time_update:
  190. _handle_pending_events()
  191. App().run()