vibrating_alarm_m5stickc.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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. self._battery_status_text_box = None
  38. @property
  39. def _now_time(self) -> int:
  40. # > [contradictory to] official micropython documentation, to set RTC,
  41. # > use particular tuple (year , month, day, week=0, hour, minute, second, timestamp=0)
  42. # https://community.m5stack.com/topic/3108/m5stack-core2-micropython-rtc-example
  43. hour, minute, seconds = rtc.now()[3:]
  44. return (hour * 60 + minute) * 60 + seconds
  45. @property
  46. def _alarm_time(self) -> int:
  47. return (self._alarm_hour * 60 + self._alarm_minute) * 60 # type: ignore
  48. def _load_alarm_time(self) -> None:
  49. with open(_ALARM_TIME_PATH, "r") as alarm_time_file:
  50. alarm_time = json.load(alarm_time_file)
  51. self._alarm_hour = alarm_time["hour"]
  52. self._alarm_minute = alarm_time["minute"]
  53. def _save_alarm_time(self) -> None:
  54. with open(_ALARM_TIME_PATH, "w") as alarm_time_file:
  55. json.dump(
  56. {"hour": self._alarm_hour, "minute": self._alarm_minute},
  57. alarm_time_file,
  58. )
  59. def _reschedule_sleep(self, interrupt: bool) -> None:
  60. if interrupt:
  61. self._wait_for_sleep_start_time_update = True
  62. micropython.schedule(self._reschedule_sleep, False)
  63. else:
  64. self._sleep_start_time_seconds = utime.time() + _AWAKE_SECONDS
  65. self._wait_for_sleep_start_time_update = False
  66. def _update_menu(self, event_arg: None) -> None:
  67. # pylint: disable=unused-argument; callback
  68. self._alarm_hour_text_box.setColor( # type: ignore
  69. lcd.GREEN
  70. if self._menu_position == 1
  71. else (lcd.RED if self._menu_position == 2 else _DEFAULT_FONT_COLOR)
  72. )
  73. self._alarm_minute_text_box.setColor( # type: ignore
  74. lcd.GREEN
  75. if self._menu_position == 3
  76. else (lcd.RED if self._menu_position == 4 else _DEFAULT_FONT_COLOR)
  77. )
  78. def _button_a_pressed(self) -> None:
  79. self._reschedule_sleep(interrupt=True)
  80. self._menu_position = (self._menu_position + 1) % 5
  81. # https://docs.micropython.org/en/latest/library/micropython.html#micropython.schedule
  82. micropython.schedule(self._update_menu, None)
  83. @staticmethod
  84. def _alert() -> None:
  85. print("ALARM")
  86. def _alarm(self, timer: machine.Timer) -> None:
  87. # pylint: disable=unused-argument; callback
  88. micropython.schedule(lambda n: self._alert(), None)
  89. micropython.schedule(lambda n: self._configure_alarm_timer(), None)
  90. def _configure_alarm_timer(self) -> None:
  91. seconds_until_alarm = (self._alarm_time - self._now_time - 1) % (
  92. 24 * 60 * 60
  93. ) + 1
  94. print("alarm in ", seconds_until_alarm / 60, " min")
  95. # TODO configure wake from sleep
  96. self._alarm_timer.init( # type: ignore
  97. period=seconds_until_alarm * 1000, # ms
  98. mode=machine.Timer.ONE_SHOT,
  99. callback=self._alarm,
  100. )
  101. def _update_alarm_time(self, event_arg: None) -> None:
  102. # pylint: disable=unused-argument; callback
  103. self._alarm_hour_text_box.setText( # type: ignore
  104. "{:02d}".format(self._alarm_hour) # type: ignore
  105. )
  106. self._alarm_minute_text_box.setText( # type: ignore
  107. "{:02d}".format(self._alarm_minute) # type: ignore
  108. )
  109. self._save_alarm_time()
  110. self._configure_alarm_timer()
  111. def _button_b_pressed(self) -> None:
  112. self._reschedule_sleep(interrupt=True)
  113. if self._menu_position == 1:
  114. self._alarm_hour += 1 # type: ignore
  115. elif self._menu_position == 2:
  116. self._alarm_hour -= 1 # type: ignore
  117. elif self._menu_position == 3:
  118. self._alarm_minute += 1 # type: ignore
  119. elif self._menu_position == 4:
  120. self._alarm_minute -= 1 # type: ignore
  121. self._alarm_hour %= 24 # type: ignore
  122. self._alarm_minute %= 60 # type: ignore
  123. micropython.schedule(self._update_alarm_time, None)
  124. @staticmethod
  125. def _format_time() -> str:
  126. return "{:02d}:{:02d}".format(*rtc.now()[3:5])
  127. def _setup_clock(self) -> None:
  128. # https://github.com/m5stack/UIFlow-Code/wiki/M5UI#textbox
  129. self._clock_text_box = m5ui.M5TextBox(
  130. _SCREEN_WIDTH - 1,
  131. 0,
  132. self._format_time(),
  133. _FONT,
  134. _DEFAULT_FONT_COLOR,
  135. rotate=90,
  136. )
  137. machine.Timer(0).init(
  138. period=4000, # ms
  139. mode=machine.Timer.PERIODIC,
  140. callback=lambda t: self._clock_text_box.setText(self._format_time()), # type: ignore
  141. )
  142. def _setup_alarm(self) -> None:
  143. if not utils.exists(_ALARM_TIME_PATH):
  144. self._alarm_hour = self._alarm_minute = 0 # type: ignore
  145. self._save_alarm_time()
  146. else:
  147. self._load_alarm_time()
  148. self._alarm_hour_text_box = m5ui.M5TextBox(
  149. _SCREEN_WIDTH // 2,
  150. 0,
  151. "{:02d}".format(self._alarm_hour), # type: ignore
  152. _FONT,
  153. _DEFAULT_FONT_COLOR,
  154. rotate=90,
  155. )
  156. m5ui.M5TextBox(
  157. _SCREEN_WIDTH // 2, 53, ":", _FONT, _DEFAULT_FONT_COLOR, rotate=90
  158. )
  159. self._alarm_minute_text_box = m5ui.M5TextBox(
  160. _SCREEN_WIDTH // 2,
  161. 66,
  162. "{:02d}".format(self._alarm_minute), # type: ignore
  163. _FONT,
  164. _DEFAULT_FONT_COLOR,
  165. rotate=90,
  166. )
  167. # machine.Timer(1).init(...) breaks button handling
  168. self._alarm_timer = machine.Timer(2)
  169. self._configure_alarm_timer()
  170. def _update_battery_status_info(self) -> None:
  171. self._battery_status_text_box.setText( # type: ignore
  172. "{:.02f}V".format(axp.getBatVoltage())
  173. )
  174. def _setup_battery_status_info(self) -> None:
  175. self._battery_status_text_box = m5ui.M5TextBox(
  176. 12,
  177. _SCREEN_HEIGHT - 20,
  178. "",
  179. lcd.FONT_DejaVu18,
  180. _DEFAULT_FONT_COLOR,
  181. rotate=0,
  182. )
  183. def run(self) -> None:
  184. m5ui.setScreenColor(0x000000) # clear screen
  185. self._setup_clock()
  186. self._setup_alarm()
  187. self._setup_battery_status_info()
  188. btnA.wasPressed(self._button_a_pressed)
  189. btnB.wasPressed(self._button_b_pressed)
  190. # not sure whether ext0 would be better
  191. esp32.wake_on_ext1([btnA.pin], esp32.WAKEUP_ALL_LOW)
  192. self._reschedule_sleep(interrupt=False)
  193. while True:
  194. self._update_battery_status_info()
  195. while utime.time() < self._sleep_start_time_seconds:
  196. utime.sleep(1) # seconds
  197. # TODO turn off display
  198. axp.setLcdBrightness(30)
  199. machine.lightsleep()
  200. # TODO turn on display
  201. axp.setLcdBrightness(100)
  202. print("wake reason:", machine.wake_reason())
  203. _handle_pending_events()
  204. while self._wait_for_sleep_start_time_update:
  205. _handle_pending_events()
  206. App().run()