vibrating_alarm_m5stickc.py 8.5 KB

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