diff --git a/.envrc b/.envrc
deleted file mode 100644
index 94840b3..0000000
--- a/.envrc
+++ /dev/null
@@ -1 +0,0 @@
-layout python3
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 0bbdfbc..03e1fee 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,7 @@
-
+
+
+
+
\ No newline at end of file
diff --git a/.idea/plant-badge.iml b/.idea/plant-badge.iml
index 34acb50..f781b9c 100644
--- a/.idea/plant-badge.iml
+++ b/.idea/plant-badge.iml
@@ -13,7 +13,7 @@
-
+
diff --git a/.rtx.toml b/.rtx.toml
new file mode 100644
index 0000000..f295e18
--- /dev/null
+++ b/.rtx.toml
@@ -0,0 +1,2 @@
+[tools]
+python = {version='latest', virtualenv='.venv'}
diff --git a/.tool-versions b/.tool-versions
deleted file mode 100644
index 09895e5..0000000
--- a/.tool-versions
+++ /dev/null
@@ -1 +0,0 @@
-python@3.10.9
diff --git a/provisioning.yaml b/provisioning.yaml
index 5b33921..114172b 100644
--- a/provisioning.yaml
+++ b/provisioning.yaml
@@ -94,20 +94,20 @@ e6614864d3269c34:
ha_plant_min_dli: number.aloe_vera_min_dli
e6614864d3938334:
- HA_PLANT_ID: plant.senecio_himalaya
- HA_PLANT_MOISTURE_SENSOR: sensor.senecio_himalaya_soil_moisture
- HA_PLANT_TEMPERATURE_SENSOR: sensor.senecio_himalaya_temperature
- HA_PLANT_CONDUCTIVITY_SENSOR: sensor.senecio_himalaya_conductivity
- HA_PLANT_ILLUMINANCE_SENSOR: sensor.senecio_himalaya_illuminance
- HA_PLANT_DLI_SENSOR: sensor.senecio_himalaya_dli
+ HA_PLANT_ID: plant.chlorophytum_comosum
+ HA_PLANT_MOISTURE_SENSOR: sensor.chlorophytum_comosum_soil_moisture
+ HA_PLANT_TEMPERATURE_SENSOR: sensor.chlorophytum_comosum_temperature
+ HA_PLANT_CONDUCTIVITY_SENSOR: sensor.chlorophytum_comosum_conductivity
+ HA_PLANT_ILLUMINANCE_SENSOR: sensor.chlorophytum_comosum_illuminance
+ HA_PLANT_DLI_SENSOR: sensor.chlorophytum_comosum_dli
- ha_plant_max_moisture: number.senecio_himalaya_max_soil_moisture
- ha_plant_min_moisture: number.senecio_himalaya_min_soil_moisture
- ha_plant_max_temperature: number.senecio_himalaya_max_temperature
- ha_plant_min_temperature: number.senecio_himalaya_min_temperature
- ha_plant_max_illuminance: number.senecio_himalaya_max_illuminance
- ha_plant_min_illuminance: number.senecio_himalaya_min_illuminance
- ha_plant_max_conductivity: number.senecio_himalaya_max_conductivity
- ha_plant_min_conductivity: number.senecio_himalaya_min_conductivity
- ha_plant_max_dli: number.senecio_himalaya_max_dli
- ha_plant_min_dli: number.senecio_himalaya_min_dli
+ ha_plant_max_moisture: number.chlorophytum_comosum_max_soil_moisture
+ ha_plant_min_moisture: number.chlorophytum_comosum_min_soil_moisture
+ ha_plant_max_temperature: number.chlorophytum_comosum_max_temperature
+ ha_plant_min_temperature: number.chlorophytum_comosum_min_temperature
+ ha_plant_max_illuminance: number.chlorophytum_comosum_max_illuminance
+ ha_plant_min_illuminance: number.chlorophytum_comosum_min_illuminance
+ ha_plant_max_conductivity: number.chlorophytum_comosum_max_conductivity
+ ha_plant_min_conductivity: number.chlorophytum_comosum_min_conductivity
+ ha_plant_max_dli: number.chlorophytum_comosum_max_dli
+ ha_plant_min_dli: number.chlorophytum_comosum_min_dli
diff --git a/src/apps/plant.py b/src/apps/plant.py
index 896d3ac..82522b9 100644
--- a/src/apps/plant.py
+++ b/src/apps/plant.py
@@ -1,5 +1,6 @@
import urequests
import jpegdec
+import sys
from badger2040 import (
WIDTH,
@@ -224,9 +225,13 @@ def display_header(text):
display.rectangle(0, 0, WIDTH, 20)
# Write text in header
+ if len(text) > 15:
+ text = text.split(" ")[0]
+ if len(text) > 15:
+ text = text[:14] + "."
display.set_font("bitmap6")
display.set_pen(WHITE)
- display.text(text, 3, 4)
+ display.text(text[:16], 3, 4)
# Display time
hour, minute = get_time()
@@ -249,7 +254,7 @@ while True:
try:
main()
except Exception as e:
- print(e)
+ sys.print_exception(e)
warning(display, str(e))
display.set_timer_minutes_with_jitter(secrets.ERROR_REFRESH_INTERVAL_MINUTES)
display.halt()
diff --git a/src/launcher.py b/src/launcher.py
index 39ea8ed..ab0c249 100644
--- a/src/launcher.py
+++ b/src/launcher.py
@@ -165,6 +165,10 @@ if exited_to_launcher or not woken_by_button:
display.set_update_speed(badger2040.UPDATE_FAST)
while True:
+ # Sometimes a button press or hold will keep the system
+ # powered *through* HALT, so latch the power back on.
+ display.keepalive()
+
if display.pressed(badger2040.BUTTON_A):
button(badger2040.BUTTON_A)
if display.pressed(badger2040.BUTTON_B):
diff --git a/src/lib/badger2040.py b/src/lib/badger2040.py
deleted file mode 100644
index 9d680e0..0000000
--- a/src/lib/badger2040.py
+++ /dev/null
@@ -1,183 +0,0 @@
-import machine
-import micropython
-from picographics import PicoGraphics, DISPLAY_INKY_PACK
-import network
-from network_manager import NetworkManager
-import WIFI_CONFIG
-import uasyncio
-import time
-import gc
-import wakeup
-
-
-BUTTON_DOWN = 11
-BUTTON_A = 12
-BUTTON_B = 13
-BUTTON_C = 14
-BUTTON_UP = 15
-BUTTON_USER = None # User button not available on W
-
-BUTTON_MASK = 0b11111 << 11
-
-SYSTEM_VERY_SLOW = 0
-SYSTEM_SLOW = 1
-SYSTEM_NORMAL = 2
-SYSTEM_FAST = 3
-SYSTEM_TURBO = 4
-
-UPDATE_NORMAL = 0
-UPDATE_MEDIUM = 1
-UPDATE_FAST = 2
-UPDATE_TURBO = 3
-
-LED = 22
-ENABLE_3V3 = 10
-BUSY = 26
-
-WIDTH = 296
-HEIGHT = 128
-
-SYSTEM_FREQS = [4000000, 12000000, 48000000, 133000000, 250000000]
-
-BUTTONS = {
- BUTTON_DOWN: machine.Pin(BUTTON_DOWN, machine.Pin.IN, machine.Pin.PULL_DOWN),
- BUTTON_A: machine.Pin(BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN),
- BUTTON_B: machine.Pin(BUTTON_B, machine.Pin.IN, machine.Pin.PULL_DOWN),
- BUTTON_C: machine.Pin(BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN),
- BUTTON_UP: machine.Pin(BUTTON_UP, machine.Pin.IN, machine.Pin.PULL_DOWN),
-}
-
-WAKEUP_MASK = 0
-
-
-def is_wireless():
- return True
-
-
-def woken_by_button():
- return wakeup.get_gpio_state() & BUTTON_MASK > 0
-
-
-def pressed_to_wake(button):
- return wakeup.get_gpio_state() & (1 << button) > 0
-
-
-def reset_pressed_to_wake():
- wakeup.reset_gpio_state()
-
-
-def pressed_to_wake_get_once(button):
- global WAKEUP_MASK
- result = (wakeup.get_gpio_state() & ~WAKEUP_MASK & (1 << button)) > 0
- WAKEUP_MASK |= 1 << button
- return result
-
-
-def system_speed(speed):
- try:
- machine.freq(SYSTEM_FREQS[speed])
- except IndexError:
- pass
-
-
-class Badger2040:
- def __init__(self):
- self.display = PicoGraphics(DISPLAY_INKY_PACK)
- self._led = machine.PWM(machine.Pin(LED))
- self._led.freq(1000)
- self._led.duty_u16(0)
- self._update_speed = 0
-
- def __getattr__(self, item):
- # Glue to redirect calls to PicoGraphics
- return getattr(self.display, item)
-
- def update(self):
- t_start = time.ticks_ms()
- self.display.update()
- t_elapsed = time.ticks_ms() - t_start
-
- delay_ms = [4700, 2600, 900, 250][self._update_speed]
-
- if t_elapsed < delay_ms:
- time.sleep((delay_ms - t_elapsed) / 1000)
-
- def set_update_speed(self, speed):
- self.display.set_update_speed(speed)
- self._update_speed = speed
-
- def led(self, brightness):
- brightness = max(0, min(255, brightness))
- self._led.duty_u16(int(brightness * 256))
-
- def invert(self, invert):
- raise RuntimeError("Display invert not supported in PicoGraphics.")
-
- def thickness(self, thickness):
- raise RuntimeError("Thickness not supported in PicoGraphics.")
-
- def halt(self):
- time.sleep(0.05)
- enable = machine.Pin(ENABLE_3V3, machine.Pin.OUT)
- enable.off()
- while not self.pressed_any():
- pass
-
- def pressed(self, button):
- return BUTTONS[button].value() == 1 or pressed_to_wake_get_once(button)
-
- def pressed_any(self):
- for button in BUTTONS.values():
- if button.value():
- return True
- return False
-
- @micropython.native
- def icon(self, data, index, data_w, icon_size, x, y):
- s_x = (index * icon_size) % data_w
- s_y = int((index * icon_size) / data_w)
-
- for o_y in range(icon_size):
- for o_x in range(icon_size):
- o = ((o_y + s_y) * data_w) + (o_x + s_x)
- bm = 0b10000000 >> (o & 0b111)
- if data[o >> 3] & bm:
- self.display.pixel(x + o_x, y + o_y)
-
- def image(self, data, w, h, x, y):
- for oy in range(h):
- row = data[oy]
- for ox in range(w):
- if row & 0b1 == 0:
- self.display.pixel(x + ox, y + oy)
- row >>= 1
-
- def status_handler(self, mode, status, ip):
- print(mode, status, ip)
- self.display.set_pen(15)
- self.display.clear()
- self.display.set_pen(0)
- if status:
- self.display.text("Connected!", 10, 10, 300, 0.5)
- self.display.text(ip, 10, 30, 300, 0.5)
- else:
- self.display.text("Connecting...", 10, 10, 300, 0.5)
- self.update()
-
- def isconnected(self):
- return network.WLAN(network.STA_IF).isconnected()
-
- def ip_address(self):
- return network.WLAN(network.STA_IF).ifconfig()[0]
-
- def connect(self):
- if WIFI_CONFIG.COUNTRY == "":
- raise RuntimeError("You must populate WIFI_CONFIG.py for networking.")
- self.display.set_update_speed(2)
- network_manager = NetworkManager(
- WIFI_CONFIG.COUNTRY, status_handler=self.status_handler
- )
- uasyncio.get_event_loop().run_until_complete(
- network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)
- )
- gc.collect()
diff --git a/src/lib/badger_os.py b/src/lib/badger_os.py
deleted file mode 100644
index e6bf406..0000000
--- a/src/lib/badger_os.py
+++ /dev/null
@@ -1,211 +0,0 @@
-import os
-import gc
-import time
-import json
-import machine
-import badger2040
-
-
-def get_battery_level():
- # Battery measurement
- vbat_adc = machine.ADC(badger2040.PIN_BATTERY)
- vref_adc = machine.ADC(badger2040.PIN_1V2_REF)
- vref_en = machine.Pin(badger2040.PIN_VREF_POWER)
- vref_en.init(machine.Pin.OUT)
- vref_en.value(0)
-
- # Enable the onboard voltage reference
- vref_en.value(1)
-
- # Calculate the logic supply voltage, as will be lower that the usual 3.3V when running off low batteries
- vdd = 1.24 * (65535 / vref_adc.read_u16())
- vbat = (
- (vbat_adc.read_u16() / 65535) * 3 * vdd
- ) # 3 in this is a gain, not rounding of 3.3V
-
- # Disable the onboard voltage reference
- vref_en.value(0)
-
- # Convert the voltage to a level to display onscreen
- return vbat
-
-
-def get_disk_usage():
- # f_bfree and f_bavail should be the same?
- # f_files, f_ffree, f_favail and f_flag are unsupported.
- f_bsize, f_frsize, f_blocks, f_bfree, _, _, _, _, _, f_namemax = os.statvfs("/")
-
- f_total_size = f_frsize * f_blocks
- f_total_free = f_bsize * f_bfree
- f_total_used = f_total_size - f_total_free
-
- f_used = 100 / f_total_size * f_total_used
- f_free = 100 / f_total_size * f_total_free
-
- return f_total_size, f_used, f_free
-
-
-def state_running():
- state = {"running": "launcher"}
- state_load("launcher", state)
- return state["running"]
-
-
-def state_clear_running():
- running = state_running()
- state_modify("launcher", {"running": "launcher"})
- return running != "launcher"
-
-
-def state_set_running(app):
- state_modify("launcher", {"running": app})
-
-
-def state_launch():
- app = state_running()
- if app is not None and app != "launcher":
- launch(app)
-
-
-def state_delete(app):
- try:
- os.remove("/state/{}.json".format(app))
- except OSError:
- pass
-
-
-def state_save(app, data):
- try:
- with open("/state/{}.json".format(app), "w") as f:
- f.write(json.dumps(data))
- f.flush()
- except OSError:
- import os
-
- try:
- os.stat("/state")
- except OSError:
- os.mkdir("/state")
- state_save(app, data)
-
-
-def state_modify(app, data):
- state = {}
- state_load(app, state)
- state.update(data)
- state_save(app, state)
-
-
-def state_load(app, defaults):
- try:
- data = json.loads(open("/state/{}.json".format(app), "r").read())
- if type(data) is dict:
- defaults.update(data)
- return True
- except (OSError, ValueError):
- pass
-
- state_save(app, defaults)
- return False
-
-
-def launch(file):
- state_set_running(file)
-
- gc.collect()
-
- button_a = machine.Pin(badger2040.BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN)
- button_c = machine.Pin(badger2040.BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN)
-
- def quit_to_launcher(pin):
- if button_a.value() and button_c.value():
- machine.reset()
-
- button_a.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher)
- button_c.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher)
-
- try:
- __import__(file)
-
- except ImportError as e:
- # If the app doesn't exist, notify the user
- warning(None, f"Could not launch: {file}. {e}")
- time.sleep(4.0)
- except Exception as e:
- # If the app throws an error, catch it and display!
- print(e)
- state_clear_running()
- display = badger2040.Badger2040()
- warning(display, str(e))
- display.halt()
-
- # If the app exits or errors, do not relaunch!
- state_clear_running()
- machine.reset() # Exit back to launcher
-
-
-# Draw an overlay box with a given message within it
-def warning(
- display,
- message,
- width=badger2040.WIDTH - 20,
- height=badger2040.HEIGHT - 20,
- line_spacing=20,
- text_size=0.6,
-):
- print(message)
-
- if display is None:
- display = badger2040.Badger2040()
- display.led(128)
-
- # Draw a light grey background
- display.set_pen(12)
- display.rectangle(
- (badger2040.WIDTH - width) // 2,
- (badger2040.HEIGHT - height) // 2,
- width,
- height,
- )
-
- width -= 20
- height -= 20
-
- display.set_pen(15)
- display.rectangle(
- (badger2040.WIDTH - width) // 2,
- (badger2040.HEIGHT - height) // 2,
- width,
- height,
- )
-
- # Take the provided message and split it up into
- # lines that fit within the specified width
- words = message.split(" ")
-
- lines = []
- current_line = ""
- for word in words:
- if display.measure_text(current_line + word + " ", text_size) < width:
- current_line += word + " "
- else:
- lines.append(current_line.strip())
- current_line = word + " "
- lines.append(current_line.strip())
-
- display.set_pen(0)
-
- # Display each line of text from the message, centre-aligned
- num_lines = len(lines)
- for i in range(num_lines):
- length = display.measure_text(lines[i], text_size)
- current_line = (i * line_spacing) - ((num_lines - 1) * line_spacing) // 2
- display.text(
- lines[i],
- (badger2040.WIDTH - length) // 2,
- (badger2040.HEIGHT // 2) + current_line,
- badger2040.WIDTH,
- text_size,
- )
-
- display.update()
diff --git a/src/lib/network_manager.py b/src/lib/network_manager.py
deleted file mode 100644
index 8feffaf..0000000
--- a/src/lib/network_manager.py
+++ /dev/null
@@ -1,117 +0,0 @@
-import rp2
-import network
-import machine
-import uasyncio
-
-
-class NetworkManager:
- _ifname = ("Client", "Access Point")
-
- def __init__(
- self,
- country="GB",
- client_timeout=30,
- access_point_timeout=5,
- status_handler=None,
- error_handler=None,
- ):
- rp2.country(country)
- self._ap_if = network.WLAN(network.AP_IF)
- self._sta_if = network.WLAN(network.STA_IF)
-
- self._mode = network.STA_IF
- self._client_timeout = client_timeout
- self._access_point_timeout = access_point_timeout
- self._status_handler = status_handler
- self._error_handler = error_handler
- self.UID = ("{:02X}" * 8).format(*machine.unique_id())
-
- def isconnected(self):
- return self._sta_if.isconnected() or self._ap_if.isconnected()
-
- def config(self, var):
- if self._sta_if.active():
- return self._sta_if.config(var)
- else:
- if var == "password":
- return self.UID
- return self._ap_if.config(var)
-
- def mode(self):
- if self._sta_if.isconnected():
- return self._ifname[0]
- if self._ap_if.isconnected():
- return self._ifname[1]
- return None
-
- def ifaddress(self):
- if self._sta_if.isconnected():
- return self._sta_if.ifconfig()[0]
- if self._ap_if.isconnected():
- return self._ap_if.ifconfig()[0]
- return "0.0.0.0"
-
- def disconnect(self):
- if self._sta_if.isconnected():
- self._sta_if.disconnect()
- if self._ap_if.isconnected():
- self._ap_if.disconnect()
-
- async def wait(self, mode):
- while not self.isconnected():
- self._handle_status(mode, None)
- await uasyncio.sleep_ms(1000)
-
- def _handle_status(self, mode, status):
- if callable(self._status_handler):
- self._status_handler(self._ifname[mode], status, self.ifaddress())
-
- def _handle_error(self, mode, msg):
- if callable(self._error_handler):
- if self._error_handler(self._ifname[mode], msg):
- return
- raise RuntimeError(msg)
-
- async def client(self, ssid, psk):
- if self._sta_if.isconnected():
- self._handle_status(network.STA_IF, True)
- return
-
- self._ap_if.disconnect()
- self._ap_if.active(False)
-
- self._sta_if.active(True)
- self._sta_if.connect(ssid, psk)
- self._sta_if.config(pm=0xA11140)
-
- try:
- await uasyncio.wait_for(self.wait(network.STA_IF), self._client_timeout)
- self._handle_status(network.STA_IF, True)
-
- except uasyncio.TimeoutError:
- self._sta_if.active(False)
- self._handle_status(network.STA_IF, False)
- self._handle_error(network.STA_IF, "WIFI Client Failed")
-
- async def access_point(self):
- if self._ap_if.isconnected():
- self._handle_status(network.AP_IF, True)
- return
-
- self._sta_if.disconnect()
- self._sta_if.active(False)
-
- self._ap_if.ifconfig(("10.10.1.1", "255.255.255.0", "10.10.1.1", "10.10.1.1"))
- self._ap_if.config(password=self.UID)
- self._ap_if.active(True)
-
- try:
- await uasyncio.wait_for(
- self.wait(network.AP_IF), self._access_point_timeout
- )
- self._handle_status(network.AP_IF, True)
-
- except uasyncio.TimeoutError:
- self._sta_if.active(False)
- self._handle_status(network.AP_IF, False)
- self._handle_error(network.AP_IF, "WIFI Client Failed")