vibrating_alarm_m5stickc.py 9.3 KB

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