vibrating_alarm_m5stickc.py 9.3 KB

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