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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. import uasyncio as asyncio
  18. import io
  19. import sys
  20. import machine
  21. import os
  22. import gc
  23. import time
  24. # https://github.com/pimoroni/pimoroni-pico/blob/main/micropython/examples/pico_lipo_shim/battery_pico.py
  25. # https://github.com/pimoroni/enviro/pull/146
  26. # TODO https://github.com/micropython/micropython/issues/11185
  27. full_battery = 4.2
  28. empty_battery = 3.2
  29. charging = machine.Pin("WL_GPIO2", machine.Pin.IN)
  30. conversion_factor = 3 * 3.3 / 65535
  31. cachedVoltage = None
  32. lastCaching = time.time()
  33. def set_pad(gpio, value):
  34. machine.mem32[0x4001c000 | (4 + (4 * gpio))] = value
  35. def get_pad(gpio):
  36. return machine.mem32[0x4001c000 | (4 + (4 * gpio))]
  37. def batteryVoltageRead():
  38. vsys = machine.ADC(3)
  39. voltage = vsys.read_u16() * conversion_factor
  40. return voltage
  41. def batteryVoltageAverage():
  42. old_pad = get_pad(29)
  43. set_pad(29, 128) # no pulls, no output, no input
  44. sample_count = 10
  45. voltage = 0
  46. for i in range(0, sample_count):
  47. voltage += batteryVoltageRead()
  48. voltage /= sample_count
  49. set_pad(29, old_pad)
  50. return voltage
  51. def batteryVoltage():
  52. global cachedVoltage, lastCaching
  53. if ((time.time() - lastCaching) >= 2) or (cachedVoltage == None):
  54. lastCaching = time.time()
  55. cachedVoltage = batteryVoltageAverage()
  56. percentage = 100.0 * ((cachedVoltage - empty_battery) / (full_battery - empty_battery))
  57. if percentage > 100.0:
  58. percentage = 100.0
  59. return cachedVoltage, percentage
  60. class States:
  61. def __init__(self, lcd):
  62. self.lcd = lcd
  63. self.states = []
  64. self.current = None
  65. def add(self, s):
  66. self.states.append(s)
  67. async def draw(self):
  68. self.lcd.fill(self.lcd.black)
  69. self.lcd.text("Volcano Remote Control App", 0, 0, self.lcd.green)
  70. r = await self.states[self.current].draw()
  71. voltage, percentage = batteryVoltage()
  72. s = "Charging ({:.2f}V)".format(voltage)
  73. c = self.lcd.green
  74. if charging.value() != 1:
  75. s = "{:.0f}% ({:.2f}V)".format(percentage, voltage)
  76. c = self.lcd.white
  77. if percentage <= 20:
  78. c = self.lcd.red
  79. self.lcd.text("Battery: {}".format(s), 0, self.lcd.height - 10, c)
  80. self.lcd.show()
  81. return r
  82. def run(self):
  83. if self.current == None:
  84. self.current = 0
  85. self.states[self.current].enter()
  86. next = asyncio.run(self.draw())
  87. if next >= 0:
  88. val = self.states[self.current].exit()
  89. self.current = next
  90. self.states[self.current].enter(val)
  91. def state_machine(lcd):
  92. states = States(lcd)
  93. # 0 - Scan
  94. from state_scan import StateScan
  95. scan = StateScan(lcd)
  96. states.add(scan)
  97. # 1 - Connect
  98. from state_connect import StateConnect
  99. conn = StateConnect(lcd, True)
  100. states.add(conn)
  101. # 2 - Select
  102. from state_select import StateSelect
  103. select = StateSelect(lcd)
  104. states.add(select)
  105. # 3 - Heater On
  106. from state_heat import StateHeat
  107. heatOn = StateHeat(lcd, True)
  108. states.add(heatOn)
  109. # 4 - Heater Off
  110. heatOff = StateHeat(lcd, False)
  111. states.add(heatOff)
  112. # 5 - Disconnect
  113. disconn = StateConnect(lcd, False)
  114. states.add(disconn)
  115. # 6 - Wait for temperature
  116. from state_wait_temp import StateWaitTemp
  117. waitTemp = StateWaitTemp(lcd)
  118. states.add(waitTemp)
  119. # 7 - Wait for time
  120. from state_wait_time import StateWaitTime
  121. waitTime = StateWaitTime(lcd)
  122. states.add(waitTime)
  123. # 8 - Pump
  124. from state_pump import StatePump
  125. pump = StatePump(lcd)
  126. states.add(pump)
  127. # 9 - Notify
  128. from state_notify import StateNotify
  129. notify = StateNotify(lcd)
  130. states.add(notify)
  131. while True:
  132. states.run()
  133. from lcd import LCD
  134. lcd = LCD()
  135. def main():
  136. # splash screen
  137. from state_wait_temp import from_hsv
  138. for x in range(0, lcd.width):
  139. hue = x / (lcd.width - 1)
  140. r, g, b = from_hsv(hue, 1.0, 1.0)
  141. c = lcd.color(r, g, b)
  142. lcd.rect(x, 0, 1, lcd.height, c)
  143. lcd.textC("S&B Volcano Remote", int(lcd.width / 2), 10, lcd.green, lcd.black)
  144. lcd.textC("by xythobuz", int(lcd.width / 2), 20, lcd.yellow, lcd.black)
  145. lcd.textC("Initializing...", int(lcd.width / 2), 30, lcd.white, lcd.black)
  146. import _git
  147. lcd.textC(_git.git_branch, int(lcd.width / 2), int(lcd.height / 2) - 10, lcd.green, lcd.black)
  148. lcd.textC(_git.git_hash, int(lcd.width / 2), int(lcd.height / 2), lcd.yellow, lcd.black)
  149. lcd.textC(_git.build_date, int(lcd.width / 2), int(lcd.height / 2) + 10, lcd.white, lcd.black)
  150. lcd.textC(os.uname()[0][ 0 : 30], int(lcd.width / 2), lcd.height - 50, lcd.green, lcd.black)
  151. lcd.textC(os.uname()[3][ 0 : 30], int(lcd.width / 2), lcd.height - 40, lcd.yellow, lcd.black)
  152. lcd.textC(os.uname()[3][30 : 60], int(lcd.width / 2), lcd.height - 30, lcd.yellow, lcd.black)
  153. lcd.textC(os.uname()[4][ 0 : 30], int(lcd.width / 2), lcd.height - 20, lcd.white, lcd.black)
  154. lcd.textC(os.uname()[4][30 : 60], int(lcd.width / 2), lcd.height - 10, lcd.white, lcd.black)
  155. lcd.show()
  156. lcd.brightness(1.0)
  157. # bootloader access with face buttons
  158. keys = lcd.buttons()
  159. if keys.once("a") and keys.once("b"):
  160. machine.bootloader()
  161. state_machine(lcd)
  162. try:
  163. main()
  164. except Exception as e:
  165. sys.print_exception(e)
  166. gc.collect()
  167. os = io.StringIO()
  168. sys.print_exception(e, os)
  169. s = os.getvalue()
  170. os.close()
  171. lcd.fill(lcd.black)
  172. lcd.textBlock(s, lcd.white)
  173. lcd.show()
  174. raise e