S&B Volcano vaporizer remote control with Pi Pico W
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

states.py 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #!/usr/bin/env python3
  2. # ----------------------------------------------------------------------------
  3. # Copyright (c) 2023 Thomas Buck (thomas@xythobuz.de)
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # See <http://www.gnu.org/licenses/>.
  16. # ----------------------------------------------------------------------------
  17. from lcd import LCD
  18. lcd = LCD()
  19. import uasyncio as asyncio
  20. import io
  21. import sys
  22. import machine
  23. import os
  24. import gc
  25. import time
  26. from state_wait_temp import from_hsv, translate
  27. # https://github.com/pimoroni/pimoroni-pico/blob/main/micropython/examples/pico_lipo_shim/battery_pico.py
  28. # https://github.com/pimoroni/enviro/pull/146
  29. # TODO https://github.com/micropython/micropython/issues/11185
  30. full_battery = 4.1
  31. empty_battery = 3.2
  32. batt_warn_limit = 15
  33. batt_reread_limit = 2.7
  34. charging = machine.Pin("WL_GPIO2", machine.Pin.IN)
  35. conversion_factor = 3 * 3.3 / 65535
  36. cachedVoltage = None
  37. lastCaching = time.time()
  38. def set_pad(gpio, value):
  39. machine.mem32[0x4001c000 | (4 + (4 * gpio))] = value
  40. def get_pad(gpio):
  41. return machine.mem32[0x4001c000 | (4 + (4 * gpio))]
  42. def batteryVoltageRead():
  43. vsys = machine.ADC(3)
  44. voltage = vsys.read_u16() * conversion_factor
  45. return voltage
  46. def batteryVoltageAverage():
  47. old_pad = get_pad(29)
  48. set_pad(29, 128) # no pulls, no output, no input
  49. sample_count = 3
  50. voltage = 0
  51. for i in range(0, sample_count):
  52. voltage += batteryVoltageRead()
  53. voltage /= sample_count
  54. set_pad(29, old_pad)
  55. return voltage
  56. def batteryVoltage():
  57. global cachedVoltage, lastCaching
  58. if ((time.time() - lastCaching) > 0) or (cachedVoltage == None):
  59. lastCaching = time.time()
  60. cachedVoltage = batteryVoltageAverage()
  61. if cachedVoltage <= batt_reread_limit:
  62. cachedVoltage = batteryVoltageAverage()
  63. percentage = 100.0 * ((cachedVoltage - empty_battery) / (full_battery - empty_battery))
  64. if percentage >= 100.0:
  65. percentage = 99.0
  66. elif percentage < 0.0:
  67. percentage = 0.0
  68. return cachedVoltage, percentage
  69. class States:
  70. def __init__(self, lcd):
  71. self.lcd = lcd
  72. self.states = []
  73. self.current = None
  74. def add(self, s):
  75. self.states.append(s)
  76. async def draw(self):
  77. self.lcd.fill(self.lcd.black)
  78. self.lcd.text("Volcano Remote Control App", 0, 0, self.lcd.green)
  79. ret = await self.states[self.current].draw()
  80. voltage, percentage = batteryVoltage()
  81. s = "Charging"
  82. c = self.lcd.white
  83. if charging.value() != 1:
  84. s = "{:.0f}% ({:.2f}V)".format(percentage, voltage)
  85. if percentage <= batt_warn_limit:
  86. c = self.lcd.red
  87. else:
  88. hue = translate(percentage, batt_warn_limit, 100, 0.0, 0.333)
  89. r, g, b = from_hsv(hue, 1.0, 1.0)
  90. c = self.lcd.color(r, g, b)
  91. whole = "Batt: {}".format(s)
  92. self.lcd.text(whole, 0, self.lcd.height - 10, c)
  93. off = (len(whole) + 1) * 8
  94. if percentage <= batt_warn_limit:
  95. self.lcd.text("CHARGE NOW!", off, self.lcd.height - 10, self.lcd.red)
  96. elif charging.value() != 1:
  97. self.lcd.rect(off, self.lcd.height - 10, self.lcd.width - off, 8, c, False)
  98. max_w = self.lcd.width - off - 2
  99. w = int(percentage / 100.0 * max_w)
  100. self.lcd.rect(off + 1, self.lcd.height - 9, w, 6, c, True)
  101. else:
  102. pass # TODO charge indicator (lightning bolt?)
  103. self.lcd.show()
  104. return ret
  105. def run(self):
  106. if self.current == None:
  107. self.current = 0
  108. self.states[self.current].enter()
  109. next = asyncio.run(self.draw())
  110. if next >= 0:
  111. val = self.states[self.current].exit()
  112. self.current = next
  113. self.states[self.current].enter(val)
  114. def state_machine(lcd):
  115. states = States(lcd)
  116. # 0 - Scan
  117. from state_scan import StateScan
  118. scan = StateScan(lcd)
  119. states.add(scan)
  120. # 1 - Connect
  121. from state_connect import StateConnect
  122. conn = StateConnect(lcd, True)
  123. states.add(conn)
  124. # 2 - Select
  125. from state_select import StateSelect
  126. select = StateSelect(lcd)
  127. states.add(select)
  128. # 3 - Heater On
  129. from state_heat import StateHeat
  130. heatOn = StateHeat(lcd, True)
  131. states.add(heatOn)
  132. # 4 - Heater Off
  133. heatOff = StateHeat(lcd, False)
  134. states.add(heatOff)
  135. # 5 - Disconnect
  136. disconn = StateConnect(lcd, False)
  137. states.add(disconn)
  138. # 6 - Wait for temperature
  139. from state_wait_temp import StateWaitTemp
  140. waitTemp = StateWaitTemp(lcd)
  141. states.add(waitTemp)
  142. # 7 - Wait for time
  143. from state_wait_time import StateWaitTime
  144. waitTime = StateWaitTime(lcd)
  145. states.add(waitTime)
  146. # 8 - Pump
  147. from state_pump import StatePump
  148. pump = StatePump(lcd)
  149. states.add(pump)
  150. # 9 - Notify
  151. from state_notify import StateNotify
  152. notify = StateNotify(lcd)
  153. states.add(notify)
  154. # 10 - OTA Update
  155. #from ota import StateUpdate
  156. #update = StateUpdate(lcd)
  157. #states.add(update)
  158. while True:
  159. states.run()
  160. def main():
  161. # splash screen
  162. from state_wait_temp import from_hsv
  163. for x in range(0, lcd.width):
  164. hue = x / (lcd.width - 1)
  165. r, g, b = from_hsv(hue, 1.0, 1.0)
  166. c = lcd.color(r, g, b)
  167. lcd.rect(x, 0, 1, lcd.height, c)
  168. lcd.textC("S&B Volcano Remote", int(lcd.width / 2), 10, lcd.green, lcd.black)
  169. lcd.textC("by xythobuz", int(lcd.width / 2), 20, lcd.yellow, lcd.black)
  170. lcd.textC("Initializing...", int(lcd.width / 2), 30, lcd.white, lcd.black)
  171. import _git
  172. lcd.textC(_git.git_branch, int(lcd.width / 2), int(lcd.height / 2) - 10, lcd.green, lcd.black)
  173. lcd.textC(_git.git_hash, int(lcd.width / 2), int(lcd.height / 2), lcd.yellow, lcd.black)
  174. lcd.textC(_git.build_date, int(lcd.width / 2), int(lcd.height / 2) + 10, lcd.white, lcd.black)
  175. lcd.textC(os.uname()[0][ 0 : 30], int(lcd.width / 2), lcd.height - 50, lcd.green, lcd.black)
  176. lcd.textC(os.uname()[3][ 0 : 30], int(lcd.width / 2), lcd.height - 40, lcd.yellow, lcd.black)
  177. lcd.textC(os.uname()[3][30 : 60], int(lcd.width / 2), lcd.height - 30, lcd.yellow, lcd.black)
  178. lcd.textC(os.uname()[4][ 0 : 30], int(lcd.width / 2), lcd.height - 20, lcd.white, lcd.black)
  179. lcd.textC(os.uname()[4][30 : 60], int(lcd.width / 2), lcd.height - 10, lcd.white, lcd.black)
  180. lcd.show()
  181. # bootloader access with face buttons
  182. keys = lcd.buttons()
  183. if keys.once("a") and keys.once("b"):
  184. machine.bootloader()
  185. state_machine(lcd)
  186. try:
  187. main()
  188. except Exception as e:
  189. sys.print_exception(e)
  190. gc.collect()
  191. os = io.StringIO()
  192. sys.print_exception(e, os)
  193. s = os.getvalue()
  194. os.close()
  195. lcd.fill(lcd.black)
  196. lcd.textBlock(s, lcd.white)
  197. lcd.show()
  198. raise e