From b8fb9acabbc63b3c5ed8dd80b27ec44ae290de32 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 18 Oct 2022 17:17:53 +0100 Subject: [PATCH 01/22] Added detection for woken by USB --- enviro/__init__.py | 8 +++++++- enviro/constants.py | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index 956e704..92640a9 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -99,6 +99,9 @@ def stop_activity_led(): from pcf85063a import PCF85063A import enviro.helpers as helpers +# read the state of vsys to know if we were woken up by USB +vsys_present = Pin("WL_GPIO2", Pin.IN).value() + # read battery voltage - we have to toggle the wifi chip select # pin to take the reading - this is probably not ideal but doesn't # seem to cause issues. there is no obvious way to shut down the @@ -217,6 +220,8 @@ def get_wake_reason(): wake_reason = WAKE_REASON_RTC_ALARM elif not external_trigger_pin.value(): wake_reason = WAKE_REASON_EXTERNAL_TRIGGER + elif vsys_present: + wake_reason = WAKE_REASON_USB_POWERED return wake_reason # convert a wake reason into it's name @@ -227,7 +232,8 @@ def wake_reason_name(wake_reason): WAKE_REASON_BUTTON_PRESS: "button", WAKE_REASON_RTC_ALARM: "rtc_alarm", WAKE_REASON_EXTERNAL_TRIGGER: "external_trigger", - WAKE_REASON_RAIN_TRIGGER: "rain_sensor" + WAKE_REASON_RAIN_TRIGGER: "rain_sensor", + WAKE_REASON_USB_POWERED: "usb_powered" } return names.get(wake_reason) diff --git a/enviro/constants.py b/enviro/constants.py index de1b2d2..ab02d25 100644 --- a/enviro/constants.py +++ b/enviro/constants.py @@ -26,6 +26,7 @@ WAKE_REASON_RTC_ALARM = 3 WAKE_REASON_EXTERNAL_TRIGGER = 4 WAKE_REASON_RAIN_TRIGGER = 5 +WAKE_REASON_USB_POWERED = 6 # warning led states WARN_LED_OFF = 0 From 5a43f3e28f163884c0c4d6b856796c0bacf9113a Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Fri, 4 Nov 2022 18:39:13 +0000 Subject: [PATCH 02/22] Progress on fixing a whole bunch of issues with Enviro --- enviro/__init__.py | 114 +++++++++++++++++------------ enviro/boards/grow.py | 98 +++++++------------------ enviro/boards/indoor.py | 2 +- enviro/boards/urban.py | 4 +- enviro/boards/weather.py | 65 ++++++++++------ enviro/destinations/adafruit_io.py | 66 +++++++++++------ enviro/destinations/http.py | 34 ++++++--- enviro/destinations/mqtt.py | 52 +++++++++---- enviro/helpers.py | 29 ++++++++ enviro/mqttsimple.py | 3 +- enviro/provisioning.py | 4 +- main.py | 25 ++++--- 12 files changed, 290 insertions(+), 206 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index 92640a9..7a30386 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -93,7 +93,7 @@ def stop_activity_led(): # control never returns to here, provisioning takes over completely # all the other imports, so many shiny modules -import machine, sys, os, json +import machine, sys, os, ujson from machine import RTC, ADC import phew from pcf85063a import PCF85063A @@ -102,6 +102,8 @@ def stop_activity_led(): # read the state of vsys to know if we were woken up by USB vsys_present = Pin("WL_GPIO2", Pin.IN).value() +#BUG Temporarily disabling battery reading, as it seems to cause issues when connected to Thonny +""" # read battery voltage - we have to toggle the wifi chip select # pin to take the reading - this is probably not ideal but doesn't # seem to cause issues. there is no obvious way to shut down the @@ -116,10 +118,12 @@ def stop_activity_led(): battery_voltage /= sample_count battery_voltage = round(battery_voltage, 3) Pin(WIFI_CS_PIN).value(old_state) +""" # set up the button, external trigger, and rtc alarm pins rtc_alarm_pin = Pin(RTC_ALARM_PIN, Pin.IN, Pin.PULL_DOWN) -external_trigger_pin = Pin(EXTERNAL_INTERRUPT_PIN, Pin.IN, Pin.PULL_DOWN) +# BUG This should only be set up for Enviro Camera +# external_trigger_pin = Pin(EXTERNAL_INTERRUPT_PIN, Pin.IN, Pin.PULL_DOWN) # intialise the pcf85063a real time clock chip rtc = PCF85063A(i2c) @@ -127,6 +131,7 @@ def stop_activity_led(): rtc.enable_timer_interrupt(False) t = rtc.datetime() +# BUG ERRNO 22, EINVAL, when date read from RTC is invalid for the pico's RTC. RTC().datetime((t[0], t[1], t[2], t[6], t[3], t[4], t[5], 0)) # synch PR2040 rtc too # jazz up that console! toot toot! @@ -146,30 +151,39 @@ def stop_activity_led(): print("") - +import network def connect_to_wifi(): - if phew.is_connected_to_wifi(): - logging.info(f"> already connected to wifi") - return True wifi_ssid = config.wifi_ssid wifi_password = config.wifi_password logging.info(f"> connecting to wifi network '{wifi_ssid}'") - ip = phew.connect_to_wifi(wifi_ssid, wifi_password, timeout_seconds=30) - if not ip: + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + wlan.connect(wifi_ssid, wifi_password) + + start = time.ticks_ms() + while (time.ticks_ms() - start) < 30000: + if wlan.status() < 0 or wlan.status() >= 3: + break + time.sleep(0.5) + + seconds_to_connect = int((time.ticks_ms() - start) / 1000) + + if wlan.status() != 3: logging.error(f"! failed to connect to wireless network {wifi_ssid}") return False - logging.info(" - ip address: ", ip) + # a slow connection time will drain the battery faster and may + # indicate a poor quality connection + if seconds_to_connect > 5: + logging.warn(" - took", seconds_to_connect, "seconds to connect to wifi") - return True + ip_address = wlan.ifconfig()[0] + logging.info(" - ip address: ", ip_address) -# returns the reason we woke up -def wake_reason(): - reason = get_wake_reason() - return wake_reason_name(reason) + return True # log the error, blink the warning led, and go back to sleep def halt(message): @@ -192,10 +206,13 @@ def sync_clock_from_ntp(): from phew import ntp if not connect_to_wifi(): return False + #TODO Fetch only does one attempt. Can also optionally set Pico RTC (do we want this?) timestamp = ntp.fetch() if not timestamp: + logging.error(" - failed to fetch time from ntp server") return False rtc.datetime(timestamp) # set the time on the rtc chip + logging.info(" - rtc synched") return True # set the state of the warning led (off, on, blinking) @@ -213,13 +230,16 @@ def warn_led(state): # returns the reason the board woke up from deep sleep def get_wake_reason(): + import wakeup + wake_reason = None - if button_pin.value(): + if wakeup.get_gpio_state() & (1 << BUTTON_PIN): wake_reason = WAKE_REASON_BUTTON_PRESS - elif rtc_alarm_pin.value(): + elif wakeup.get_gpio_state() & (1 << RTC_ALARM_PIN): wake_reason = WAKE_REASON_RTC_ALARM - elif not external_trigger_pin.value(): - wake_reason = WAKE_REASON_EXTERNAL_TRIGGER + # TODO Temporarily removing this as false reporting on non-camera boards + #elif not external_trigger_pin.value(): + # wake_reason = WAKE_REASON_EXTERNAL_TRIGGER elif vsys_present: wake_reason = WAKE_REASON_USB_POWERED return wake_reason @@ -240,7 +260,7 @@ def wake_reason_name(wake_reason): # get the readings from the on board sensors def get_sensor_readings(): readings = get_board().get_sensor_readings() - readings["voltage"] = battery_voltage + readings["voltage"] = 0.0 # battery_voltage #Temporarily removed until issue is fixed return readings # save the provided readings into a todays readings data file @@ -252,7 +272,8 @@ def save_reading(readings): with open(readings_filename, "a") as f: if new_file: # new readings file so write out column headings first - f.write("timestamp," + ",".join(readings.keys()) + "\r\n") + f.write("time," + ",".join(readings.keys()) + "\r\n") + # write sensor data row = [helpers.datetime_string()] for key in readings.keys(): @@ -262,17 +283,10 @@ def save_reading(readings): # save the provided readings into a cache file for future uploading def cache_upload(readings): - payload = { - "nickname": config.nickname, - "timestamp": helpers.datetime_string(), - "readings": readings, - "model": model, - "uid": helpers.uid() - } uploads_filename = f"uploads/{helpers.datetime_string()}.json" helpers.mkdir_safe("uploads") with open(uploads_filename, "w") as upload_file: - json.dump(payload, upload_file) + upload_file.write(ujson.dumps(readings)) # return the number of cached results waiting to be uploaded def cached_upload_count(): @@ -285,37 +299,37 @@ def is_upload_needed(): # upload cached readings to the configured destination def upload_readings(): if not connect_to_wifi(): + logging.error(f" - cannot upload readings, wifi connection failed") return False destination = config.destination exec(f"import enviro.destinations.{destination}") destination_module = sys.modules[f"enviro.destinations.{destination}"] - for cache_file in os.ilistdir("uploads"): - with open(f"uploads/{cache_file[0]}", "r") as upload_file: - success = destination_module.upload_reading(json.load(upload_file)) - if not success: - logging.error(f"! failed to upload '{cache_file[0]}' to {destination}") - return False - - # remove the cache file now uploaded - logging.info(f" - uploaded {cache_file[0]} to {destination}") - - os.remove(f"uploads/{cache_file[0]}") - + + destination_module.upload_readings() + return True def startup(): # write startup banner into log file logging.debug("> performing startup") + # get the reason we were woken up + reason = get_wake_reason() + # give each board a chance to perform any startup it needs # =========================================================================== board = get_board() if hasattr(board, "startup"): - board.startup() + continue_startup = board.startup(reason) + # put the board back to sleep if the startup doesn't need to continue + # and the RTC has not triggered since we were awoken + if not continue_startup and not rtc.read_alarm_flag(): + logging.debug(" - wake reason: trigger") + sleep() # log the wake reason - logging.info(" - wake reason:", wake_reason()) + logging.info(" - wake reason:", wake_reason_name(reason)) # also immediately turn on the LED to indicate that we're doing something logging.debug(" - turn on activity led") @@ -325,7 +339,8 @@ def sleep(): logging.info("> going to sleep") # make sure the rtc flags are cleared before going back to sleep - logging.debug(" - clearing and disabling timer and alarm") + logging.debug(" - clearing and disabling previous alarm") + rtc.clear_timer_flag() rtc.clear_alarm_flag() # set alarm to wake us up for next reading @@ -362,9 +377,13 @@ def sleep(): sys.exit() # we'll wait here until the rtc timer triggers and then reset the board - logging.debug(" - on usb power (so can't shutdown) halt and reset instead") - while not rtc.read_alarm_flag(): - time.sleep(0.25) + logging.debug(" - on usb power (so can't shutdown). Halt and wait for alarm or user reset instead") + board = get_board() + while not rtc.read_alarm_flag(): + if hasattr(board, "check_trigger"): + board.check_trigger() + + #time.sleep(0.25) if button_pin.value(): # allow button to force reset break @@ -372,4 +391,5 @@ def sleep(): logging.debug(" - reset") # reset the board - machine.reset() \ No newline at end of file + machine.reset() + \ No newline at end of file diff --git a/enviro/boards/grow.py b/enviro/boards/grow.py index dc4a1d8..f706994 100644 --- a/enviro/boards/grow.py +++ b/enviro/boards/grow.py @@ -3,7 +3,6 @@ from breakout_ltr559 import BreakoutLTR559 from machine import Pin, PWM from enviro import i2c -from phew import logging bme280 = BreakoutBME280(i2c, 0x77) ltr559 = BreakoutLTR559(i2c) @@ -16,83 +15,43 @@ Pin(13, Pin.IN, Pin.PULL_DOWN) ] -pump_pins = [ - Pin(12, Pin.OUT, value=0), - Pin(11, Pin.OUT, value=0), - Pin(10, Pin.OUT, value=0) -] +def moisture_readings(sample_time_ms=500): + # get initial sensor state + state = [sensor.value() for sensor in moisture_sensor_pins] -def moisture_readings(): + # create an array for each sensor to log the times when the sensor state changed + # then we can use those values to calculate an average tick time for each sensor + changes = [[], [], []] + + start = time.ticks_ms() + while time.ticks_ms() - start <= sample_time_ms: + for i in range(0, len(state)): + now = moisture_sensor_pins[i].value() + if now != state[i]: # sensor output changed + # record the time of the change and update the state + changes[i].append(time.ticks_ms()) + state[i] = now + + # now we can average the readings for each sensor results = [] - - for i in range(0, 3): - # count time for sensor to "tick" 25 times - sensor = moisture_sensor_pins[i] - - last_value = sensor.value() - start = time.ticks_ms() - first = None - last = None - ticks = 0 - while ticks < 10 and time.ticks_ms() - start <= 1000: - value = sensor.value() - if last_value != value: - if first == None: - first = time.ticks_ms() - last = time.ticks_ms() - ticks += 1 - last_value = value - - if not first or not last: - results.append(0.0) + for i in range(0, len(changes)): + # if no sensor connected to change then we have no readings, skip + if len(changes[i]) < 2: + results.append(0) continue # calculate the average tick between transitions in ms - average = (last - first) / ticks + average = (changes[i][-1] - changes[i][0]) / (len(changes[i]) - 1) + # scale the result to a 0...100 range where 0 is very dry # and 100 is standing in water # - # dry = 10ms per transition, wet = 80ms per transition - min_ms = 20 - max_ms = 80 - average = max(min_ms, min(max_ms, average)) # clamp range - scaled = ((average - min_ms) / (max_ms - min_ms)) * 100 - results.append(round(scaled, 2)) + # dry = 20ms per transition, wet = 60ms per transition + scaled = (min(40, max(0, average - 20)) / 40) * 100 + results.append(scaled) return results -# make a semi convincing drip noise -def drip_noise(): - piezo_pwm.duty_u16(32768) - for i in range(0, 10): - f = i * 20 - piezo_pwm.freq((f * f) + 1000) - time.sleep(0.02) - piezo_pwm.duty_u16(0) - -def water(moisture_levels): - from enviro import config - targets = [ - config.moisture_target_1, - config.moisture_target_2, - config.moisture_target_3 - ] - - for i in range(0, 3): - if moisture_levels[i] < targets[i]: - # determine a duration to run the pump for - duration = round((targets[i] - moisture_levels[i]) / 25, 1) - - if config.auto_water: - logging.info(f"> running pump {i} for {duration} second (currently at {int(moisture_levels[i])}, target {targets[i]})") - pump_pins[i].value(1) - time.sleep(duration) - pump_pins[i].value(0) - else: - for j in range(0, i + 1): - drip_noise() - time.sleep(0.5) - def get_sensor_readings(): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first @@ -102,12 +61,9 @@ def get_sensor_readings(): ltr_data = ltr559.get_reading() - moisture_levels = moisture_readings() - - water(moisture_levels) # run pumps if needed + moisture_levels = moisture_readings(2000) from ucollections import OrderedDict - return OrderedDict({ "temperature": round(bme280_data[0], 2), "humidity": round(bme280_data[2], 2), diff --git a/enviro/boards/indoor.py b/enviro/boards/indoor.py index 2b8b794..b07a472 100644 --- a/enviro/boards/indoor.py +++ b/enviro/boards/indoor.py @@ -63,5 +63,5 @@ def get_sensor_readings(): "gas_resistance": gas_resistance, "aqi": aqi, "luminance": lux_from_rgbc(r, g, b, c), - "color_temperature": colour_temperature_from_rgbc(r, g, b, c), + "color_temperature": colour_temperature_from_rgbc(r, g, b, c) }) \ No newline at end of file diff --git a/enviro/boards/urban.py b/enviro/boards/urban.py index 4180fd5..9abf9e3 100644 --- a/enviro/boards/urban.py +++ b/enviro/boards/urban.py @@ -2,7 +2,7 @@ from machine import Pin, ADC from breakout_bme280 import BreakoutBME280 from pimoroni_i2c import PimoroniI2C -from phew import logging +from enviro import logging from enviro import i2c sensor_reset_pin = Pin(9, Pin.OUT, value=True) @@ -71,6 +71,6 @@ def get_sensor_readings(): "noise": round(noise_vpp, 2), "pm1": particulates(particulate_data, PM1_UGM3), "pm2_5": particulates(particulate_data, PM2_5_UGM3), - "pm10": particulates(particulate_data, PM10_UGM3), + "pm10": particulates(particulate_data, PM10_UGM3) }) diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index bddb4c9..21139af 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -3,9 +3,10 @@ from breakout_ltr559 import BreakoutLTR559 from machine import Pin, PWM from pimoroni import Analog -from enviro import i2c, hold_vsys_en_pin +from enviro import i2c, activity_led import enviro.helpers as helpers from phew import logging +from enviro.constants import WAKE_REASON_RTC_ALARM, WAKE_REASON_BUTTON_PRESS RAIN_MM_PER_TICK = 0.2794 @@ -14,9 +15,10 @@ wind_direction_pin = Analog(26) wind_speed_pin = Pin(9, Pin.IN, Pin.PULL_UP) +rain_pin = Pin(10, Pin.IN, Pin.PULL_DOWN) -def startup(): - import wakeup +def startup(reason): + import wakeup # check if rain sensor triggered wake rain_sensor_trigger = wakeup.get_gpio_state() & (1 << 10) @@ -41,8 +43,39 @@ def startup(): with open("rain.txt", "w") as rainfile: rainfile.write("\n".join(rain_entries)) - # go immediately back to sleep, we'll wake up at next scheduled reading - hold_vsys_en_pin.init(Pin.IN) + # if we were woken by the RTC or a Poke continue with the startup + return (reason is WAKE_REASON_RTC_ALARM + or reason is WAKE_REASON_BUTTON_PRESS) + + # there was no rain trigger so continue with the startup + return True + +def check_trigger(): + rain_sensor_trigger = rain_pin.value() + + if rain_sensor_trigger: + activity_led(100) + time.sleep(0.05) + activity_led(0) + + # read the current rain entries + rain_entries = [] + if helpers.file_exists("rain.txt"): + with open("rain.txt", "r") as rainfile: + rain_entries = rainfile.read().split("\n") + + # add new entry + logging.info("> add new rain trigger at {helpers.datetime_string()}") + rain_entries.append(helpers.datetime_string()) + + # limit number of entries to 190 - each entry is 21 bytes including + # newline so this keeps the total rain.txt filesize just under one + # filesystem block (4096 bytes) + rain_entries = rain_entries[-190:] + + # write out adjusted rain log + with open("rain.txt", "w") as rainfile: + rainfile.write("\n".join(rain_entries)) def wind_speed(sample_time_ms=1000): # get initial sensor state @@ -86,29 +119,16 @@ def wind_direction(): # to determine the heading ADC_TO_DEGREES = (0.9, 2.0, 3.0, 2.8, 2.5, 1.5, 0.3, 0.6) + value = wind_direction_pin.read_voltage() closest_index = -1 - last_index = None - - # ensure we have two readings that match in a row as otherwise if - # you read during transition between two values it can glitch - # fixes https://github.com/pimoroni/enviro/issues/20 - while True: - value = wind_direction_pin.read_voltage() + closest_value = float('inf') - closest_index = -1 - closest_value = float('inf') - - for i in range(8): + for i in range(8): distance = abs(ADC_TO_DEGREES[i] - value) if distance < closest_value: closest_value = distance closest_index = i - if last_index == closest_index: - break - - last_index = closest_index - return closest_index * 45 def timestamp(dt): @@ -119,7 +139,7 @@ def timestamp(dt): minute = int(dt[14:16]) second = int(dt[17:19]) return time.mktime((year, month, day, hour, minute, second, 0, 0)) - + def rainfall(): if not helpers.file_exists("rain.txt"): return 0 @@ -157,4 +177,3 @@ def get_sensor_readings(): "rain": rainfall(), "wind_direction": wind_direction() }) - diff --git a/enviro/destinations/adafruit_io.py b/enviro/destinations/adafruit_io.py index 742e860..ba0fd90 100644 --- a/enviro/destinations/adafruit_io.py +++ b/enviro/destinations/adafruit_io.py @@ -1,29 +1,49 @@ -import urequests +from enviro import logging +import urequests, ujson, os, time import config -def upload_reading(reading): - # create adafruit.io payload format - payload = { - "created_at": reading["timestamp"], - "feeds": [] - } +def upload_readings(): + username = config.adafruit_io_username + headers = {'X-AIO-Key': config.adafruit_io_key, 'Content-Type': 'application/json'} - # add all the sensor readings nickname = config.nickname - for key, value in reading["readings"].items(): - key = key.replace("_", "-") - payload["feeds"].append({"key": f"{nickname}-{key}", "value": value}) - # send the payload - username = config.adafruit_io_username - headers = {'X-AIO-Key': config.adafruit_io_key, 'Content-Type': 'application/json'} - url = f"http://io.adafruit.com/api/v2/{username}/groups/enviro/data" + for cache_file in os.ilistdir("uploads"): + cache_file = cache_file[0] + try: + with open(f"uploads/{cache_file}", "r") as f: + timestamp = cache_file.split(".")[0] + data = ujson.load(f) + + timestamp = timestamp.replace(" ", "T") + "Z" + payload = { + "created_at": timestamp, + "feeds": [] + } + for key, value in data.items(): + key = key.replace("_", "-") + payload["feeds"].append({ + "key": f"{nickname}-{key}", + "value": value + }) + + url = f"http://io.adafruit.com/api/v2/{username}/groups/enviro/data" + result = urequests.post(url, json=payload, headers=headers) + if result.status_code == 429: + result.close() + logging.info(f" - rate limited, cooling off for thirty seconds") + time.sleep(30) + # try the request again + result = urequests.post(url, json=payload, headers=headers) + # TODO currently if this fails a second time it carrys on to the next file. Is this what we want? + + if result.status_code == 200: + os.remove(f"uploads/{cache_file}") + logging.info(f" - uploaded {cache_file}") + else: + logging.error(f"! failed to upload '{cache_file}' ({result.status_code} {result.reason})", cache_file) + # TODO should this break out of the file loop to avoid uploading files out-of-order? - try: - result = urequests.post(url, json=payload, headers=headers) - result.close() - return result.status_code == 200 - except: - pass - - return False \ No newline at end of file + result.close() + except OSError as e: + logging.error(f" - failed to upload '{cache_file}'") diff --git a/enviro/destinations/http.py b/enviro/destinations/http.py index 0b5b42d..0c73d7b 100644 --- a/enviro/destinations/http.py +++ b/enviro/destinations/http.py @@ -1,19 +1,33 @@ -import urequests +from enviro import logging +import urequests, ujson, os import config -def upload_reading(reading): +def upload_readings(): url = config.custom_http_url + logging.info(f"> uploading cached readings to {url}") auth = None if config.custom_http_username: auth = (config.custom_http_username, config.custom_http_password) - try: - # post reading data to http endpoint - result = urequests.post(url, auth=auth, json=reading) - result.close() - return result.status_code in [200, 201, 202] - except: - pass + nickname = config.nickname - return False \ No newline at end of file + for cache_file in os.ilistdir("uploads"): + cache_file = cache_file[0] + try: + with open(f"uploads/{cache_file}", "r") as f: + timestamp = cache_file.split(".")[0] + payload = { + "nickname": nickname, + "timestamp": timestamp, + "readings": ujson.load(f) + } + result = urequests.post(url, auth=auth, json=payload) + if result.status_code != 200: + logging.error(f" - failed to upload '{cache_file}' ({result.status_code} {result.reason})", cache_file) + else: + logging.info(f" - uploaded {cache_file}") + os.remove(f"uploads/{cache_file}") + + except OSError as e: + logging.error(f" - failed to upload '{cache_file}'") diff --git a/enviro/destinations/mqtt.py b/enviro/destinations/mqtt.py index 804cf52..5364905 100644 --- a/enviro/destinations/mqtt.py +++ b/enviro/destinations/mqtt.py @@ -1,21 +1,43 @@ +from enviro import logging +import ujson, os from enviro.mqttsimple import MQTTClient -import ujson import config -def upload_reading(reading): +def upload_readings(): server = config.mqtt_broker_address username = config.mqtt_broker_username password = config.mqtt_broker_password - nickname = reading["nickname"] - - try: - # attempt to publish reading - mqtt_client = MQTTClient(reading["uid"], server, user=username, password=password) - mqtt_client.connect() - mqtt_client.publish(f"enviro/{nickname}", ujson.dumps(reading), retain=True) - mqtt_client.disconnect() - return True - except: - pass - - return False \ No newline at end of file + nickname = config.nickname + + logging.info(f"> uploading cached readings to {server}") + + mqtt_client = MQTTClient(nickname, server, user=username, password=password, keepalive=60) + mqtt_client.connect() + + for cache_file in os.ilistdir("uploads"): + cache_file = cache_file[0] + try: + with open(f"uploads/{cache_file}", "r") as f: + timestamp = cache_file.split(".")[0] + data = ujson.load(f) + + payload = { + "timestamp": timestamp, + "device": nickname + } + for key, value in data.items(): + payload[key] = value + + topic = f"enviro/{nickname}" + # by default the MQTT messages will be published with the retain flag + # set, so that if a consumer is not subscribed, the most recent set + # of readings can still be read by another subscriber later. Change + # retain to False (or drop from the method call) below to change this + mqtt_client.publish(topic, ujson.dumps(payload), retain=True) + + logging.info(f" - uploaded {cache_file}") + os.remove(f"uploads/{cache_file}") + except OSError as e: + logging.error(f" - failed to upload '{cache_file}'") + + mqtt_client.disconnect() diff --git a/enviro/helpers.py b/enviro/helpers.py index c2e123a..62d4ae7 100644 --- a/enviro/helpers.py +++ b/enviro/helpers.py @@ -37,6 +37,35 @@ def mkdir_safe(path): raise pass # directory already exists, this is fine +# Keeping the below around for later comparisons with PHEW +""" +import machine, os, time, network, usocket, struct +from phew import logging +def update_rtc_from_ntp(max_attempts = 5): + logging.info("> fetching date and time from ntp server") + ntp_host = "pool.ntp.org" + attempt = 1 + while attempt < max_attempts: + try: + logging.info(" - synching rtc attempt", attempt) + query = bytearray(48) + query[0] = 0x1b + address = usocket.getaddrinfo(ntp_host, 123)[0][-1] + socket = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM) + socket.settimeout(30) + socket.sendto(query, address) + data = socket.recv(48) + socket.close() + local_epoch = 2208988800 # selected by Chris, by experiment. blame him. :-D + timestamp = struct.unpack("!I", data[40:44])[0] - local_epoch + t = time.gmtime(timestamp) + return t + except Exception as e: + logging.error(e) + + attempt += 1 + return False +""" def copy_file(source, target): with open(source, "rb") as infile: with open(target, "wb") as outfile: diff --git a/enviro/mqttsimple.py b/enviro/mqttsimple.py index afc4049..0cea8d6 100644 --- a/enviro/mqttsimple.py +++ b/enviro/mqttsimple.py @@ -62,9 +62,8 @@ def set_last_will(self, topic, msg, retain=False, qos=0): self.lw_qos = qos self.lw_retain = retain - def connect(self, clean_session=True, timeout=30): + def connect(self, clean_session=True): self.sock = socket.socket() - self.sock.settimeout(timeout) addr = socket.getaddrinfo(self.server, self.port)[0][-1] self.sock.connect(addr) if self.ssl: diff --git a/enviro/provisioning.py b/enviro/provisioning.py index 8ce0e4e..e1c0896 100644 --- a/enviro/provisioning.py +++ b/enviro/provisioning.py @@ -6,6 +6,8 @@ DOMAIN = "pico.wireless" +# BUG Issues with page loading when running provisioning a while after a board reset (where other things have been run before) + # create fresh config file if missing if not helpers.file_exists("config.py"): helpers.copy_file("enviro/config_template.py", "config.py") @@ -50,7 +52,7 @@ def write_config(): logging.info("> creating web server...") - +# TODO This did not seem to work for me... @server.route("/wrong-host-redirect", methods=["GET"]) def wrong_host_redirect(request): # if the client requested a resource at the wrong host then present diff --git a/main.py b/main.py index dd8f4a8..e5404a7 100644 --- a/main.py +++ b/main.py @@ -21,23 +21,17 @@ # import enviro firmware, this will trigger provisioning if needed import enviro +import os # initialise enviro enviro.startup() -# now that we know the device is provisioned import the config -try: - import config -except: - enviro.halt("! failed to load config.py") - # if the clock isn't set... if not enviro.is_clock_set(): enviro.logging.info("> clock not set, synchronise from ntp server") if not enviro.sync_clock_from_ntp(): # failed to talk to ntp server go back to sleep for another cycle - enviro.halt("! failed to synchronise clock") - enviro.logging.info(" - rtc synched") + enviro.halt("! failed to synchronise clock") # check disk space... if enviro.low_disk_space(): @@ -45,7 +39,13 @@ # are not getting uploaded so warn the user and halt with an error enviro.halt("! low disk space") +# TODO this seems to be useful to keep around? +filesystem_stats = os.statvfs(".") +enviro.logging.debug(f"> {filesystem_stats[3]} blocks free out of {filesystem_stats[2]}") + +# TODO should the board auto take a reading when the timer has been set, or wait for the time? # take a reading from the onboard sensors +enviro.logging.debug(f"> taking new reading") reading = enviro.get_sensor_readings() # here you can customise the sensor readings by adding extra information @@ -58,15 +58,18 @@ # is an upload destination set? if enviro.config.destination: # if so cache this reading for upload later + enviro.logging.debug(f"> caching reading for upload") enviro.cache_upload(reading) # if we have enough cached uploads... if enviro.is_upload_needed(): - enviro.logging.info(f"> {enviro.cached_upload_count()} cache files need uploading") - if not enviro.upload_readings(): - enviro.halt("! reading upload failed") + enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) need uploading") + enviro.upload_readings() + else: + enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) not being uploaded. Waiting until there are {enviro.config.upload_frequency} file(s)") else: # otherwise save reading to local csv file (look in "/readings") + enviro.logging.debug(f"> saving reading locally") enviro.save_reading(reading) # go to sleep until our next scheduled reading From e670f91c293a6444f8c0534a514a6739cc5669d0 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 8 Nov 2022 15:30:10 +0000 Subject: [PATCH 03/22] Possible improvement to rainfall calculation --- enviro/boards/weather.py | 70 ++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index 21139af..d3f95a2 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -31,7 +31,7 @@ def startup(reason): rain_entries = rainfile.read().split("\n") # add new entry - logging.info("> add new rain trigger at {helpers.datetime_string()}") + logging.info(f"> add new rain trigger at {helpers.datetime_string()}") rain_entries.append(helpers.datetime_string()) # limit number of entries to 190 - each entry is 21 bytes including @@ -65,7 +65,7 @@ def check_trigger(): rain_entries = rainfile.read().split("\n") # add new entry - logging.info("> add new rain trigger at {helpers.datetime_string()}") + logging.info(f"> add new rain trigger at {helpers.datetime_string()}") rain_entries.append(helpers.datetime_string()) # limit number of entries to 190 - each entry is 21 bytes including @@ -118,6 +118,30 @@ def wind_direction(): # we find the closest matching value in the array and use the index # to determine the heading ADC_TO_DEGREES = (0.9, 2.0, 3.0, 2.8, 2.5, 1.5, 0.3, 0.6) + """ TODO new wind code from 0.0.8 + closest_index = -1 + last_index = None + + # ensure we have two readings that match in a row as otherwise if + # you read during transition between two values it can glitch + # fixes https://github.com/pimoroni/enviro/issues/20 + while True: + value = wind_direction_pin.read_voltage() + + closest_index = -1 + closest_value = float('inf') + + for i in range(8): + distance = abs(ADC_TO_DEGREES[i] - value) + if distance < closest_value: + closest_value = distance + closest_index = i + + if last_index == closest_index: + break + + last_index = closest_index + """ value = wind_direction_pin.read_voltage() closest_index = -1 @@ -141,20 +165,38 @@ def timestamp(dt): return time.mktime((year, month, day, hour, minute, second, 0, 0)) def rainfall(): - if not helpers.file_exists("rain.txt"): - return 0 + amount = 0 + now_str = helpers.datetime_string() + if helpers.file_exists("last_time.txt"): + now = timestamp(now_str) - now = timestamp(helpers.datetime_string()) - with open("rain.txt", "r") as rainfile: - rain_entries = rainfile.read().split("\n") + time_entries = [] + with open("last_time.txt", "r") as timefile: + time_entries = timefile.read().split("\n") - # count how many rain ticks in past hour - amount = 0 - for entry in rain_entries: - if entry: - ts = timestamp(entry) - if now - ts < 60 * 60: - amount += RAIN_MM_PER_TICK + # read the first line from the time file + last = now + for entry in time_entries: + if entry: + last = timestamp(entry) + break + + logging.info(f" - seconds since last reading: {now - last}") + + if helpers.file_exists("rain.txt"): + with open("rain.txt", "r") as rainfile: + rain_entries = rainfile.read().split("\n") + + # count how many rain ticks since the last reading + for entry in rain_entries: + if entry: + ts = timestamp(entry) + if now - ts < now - last: + amount += RAIN_MM_PER_TICK + + # write out adjusted rain log + with open("last_time.txt", "w") as timefile: + timefile.write(now_str) return amount From bac8625e34cec50d1cb1c83f0f25bc1106dce4fa Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Thu, 10 Nov 2022 12:39:38 +0000 Subject: [PATCH 04/22] Re-add wind direction fix from #20 --- enviro/boards/weather.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index d3f95a2..25cb63d 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -118,7 +118,7 @@ def wind_direction(): # we find the closest matching value in the array and use the index # to determine the heading ADC_TO_DEGREES = (0.9, 2.0, 3.0, 2.8, 2.5, 1.5, 0.3, 0.6) - """ TODO new wind code from 0.0.8 + closest_index = -1 last_index = None @@ -141,17 +141,6 @@ def wind_direction(): break last_index = closest_index - """ - - value = wind_direction_pin.read_voltage() - closest_index = -1 - closest_value = float('inf') - - for i in range(8): - distance = abs(ADC_TO_DEGREES[i] - value) - if distance < closest_value: - closest_value = distance - closest_index = i return closest_index * 45 From 60d29eadd95219acb6eed9b7dac10d3c0397f0e8 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Thu, 10 Nov 2022 15:11:27 +0000 Subject: [PATCH 05/22] Raised reading capture from weather to main enviro --- enviro/__init__.py | 70 +++++++++++++++++++++++-- enviro/boards/grow.py | 107 ++++++++++++++++++++++++++++++++++++++- enviro/boards/indoor.py | 2 +- enviro/boards/urban.py | 4 +- enviro/boards/weather.py | 62 ++++++++--------------- enviro/helpers.py | 13 ++++- main.py | 3 +- 7 files changed, 209 insertions(+), 52 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index 7a30386..c223eba 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -151,14 +151,27 @@ def stop_activity_led(): print("") -import network +import network # TODO this was removed from 0.0.8 def connect_to_wifi(): + """ TODO what it was changed to + if phew.is_connected_to_wifi(): + logging.info(f"> already connected to wifi") + return True + """ wifi_ssid = config.wifi_ssid wifi_password = config.wifi_password logging.info(f"> connecting to wifi network '{wifi_ssid}'") + """ TODO what it was changed to + ip = phew.connect_to_wifi(wifi_ssid, wifi_password, timeout_seconds=30) + if not ip: + logging.error(f"! failed to connect to wireless network {wifi_ssid}") + return False + + logging.info(" - ip address: ", ip) + """ wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(wifi_ssid, wifi_password) @@ -259,8 +272,33 @@ def wake_reason_name(wake_reason): # get the readings from the on board sensors def get_sensor_readings(): - readings = get_board().get_sensor_readings() + seconds_since_last = 0 + now_str = helpers.datetime_string() + if helpers.file_exists("last_time.txt"): + now = helpers.timestamp(now_str) + + time_entries = [] + with open("last_time.txt", "r") as timefile: + time_entries = timefile.read().split("\n") + + # read the first line from the time file + last = now + for entry in time_entries: + if entry: + last = helpers.timestamp(entry) + break + + seconds_since_last = now - last + logging.info(f" - seconds since last reading: {seconds_since_last}") + + + readings = get_board().get_sensor_readings(seconds_since_last) readings["voltage"] = 0.0 # battery_voltage #Temporarily removed until issue is fixed + + # write out the last time log + with open("last_time.txt", "w") as timefile: + timefile.write(now_str) + return readings # save the provided readings into a todays readings data file @@ -272,6 +310,7 @@ def save_reading(readings): with open(readings_filename, "a") as f: if new_file: # new readings file so write out column headings first + #f.write("timestamp," + ",".join(readings.keys()) + "\r\n") # TODO what it was changed to f.write("time," + ",".join(readings.keys()) + "\r\n") # write sensor data @@ -283,9 +322,19 @@ def save_reading(readings): # save the provided readings into a cache file for future uploading def cache_upload(readings): + """ TODO what it was changed to + payload = { + "nickname": config.nickname, + "timestamp": helpers.datetime_string(), + "readings": readings, + "model": model, + "uid": helpers.uid() + } + """ uploads_filename = f"uploads/{helpers.datetime_string()}.json" helpers.mkdir_safe("uploads") with open(uploads_filename, "w") as upload_file: + #json.dump(payload, upload_file) # TODO what it was changed to upload_file.write(ujson.dumps(readings)) # return the number of cached results waiting to be uploaded @@ -305,7 +354,19 @@ def upload_readings(): destination = config.destination exec(f"import enviro.destinations.{destination}") destination_module = sys.modules[f"enviro.destinations.{destination}"] - + """ TODO what it was changed to + for cache_file in os.ilistdir("uploads"): + with open(f"uploads/{cache_file[0]}", "r") as upload_file: + success = destination_module.upload_reading(json.load(upload_file)) + if not success: + logging.error(f"! failed to upload '{cache_file[0]}' to {destination}") + return False + + # remove the cache file now uploaded + logging.info(f" - uploaded {cache_file[0]} to {destination}") + + os.remove(f"uploads/{cache_file[0]}") + """ destination_module.upload_readings() return True @@ -340,7 +401,7 @@ def sleep(): # make sure the rtc flags are cleared before going back to sleep logging.debug(" - clearing and disabling previous alarm") - rtc.clear_timer_flag() + rtc.clear_timer_flag() # TODO this was removed from 0.0.8 rtc.clear_alarm_flag() # set alarm to wake us up for next reading @@ -392,4 +453,3 @@ def sleep(): # reset the board machine.reset() - \ No newline at end of file diff --git a/enviro/boards/grow.py b/enviro/boards/grow.py index f706994..7ee5b2a 100644 --- a/enviro/boards/grow.py +++ b/enviro/boards/grow.py @@ -3,6 +3,7 @@ from breakout_ltr559 import BreakoutLTR559 from machine import Pin, PWM from enviro import i2c +#from phew import logging # TODO from 0.0.8 bme280 = BreakoutBME280(i2c, 0x77) ltr559 = BreakoutLTR559(i2c) @@ -52,7 +53,7 @@ def moisture_readings(sample_time_ms=500): return results -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first bme280.read() @@ -73,6 +74,110 @@ def get_sensor_readings(): "moisture_2": round(moisture_levels[1], 2), "moisture_3": round(moisture_levels[2], 2) }) + +""" TODO from 0.0.8 +pump_pins = [ + Pin(12, Pin.OUT, value=0), + Pin(11, Pin.OUT, value=0), + Pin(10, Pin.OUT, value=0) +] + +def moisture_readings(): + results = [] + + for i in range(0, 3): + # count time for sensor to "tick" 25 times + sensor = moisture_sensor_pins[i] + + last_value = sensor.value() + start = time.ticks_ms() + first = None + last = None + ticks = 0 + while ticks < 10 and time.ticks_ms() - start <= 1000: + value = sensor.value() + if last_value != value: + if first == None: + first = time.ticks_ms() + last = time.ticks_ms() + ticks += 1 + last_value = value + + if not first or not last: + results.append(0.0) + continue + + # calculate the average tick between transitions in ms + average = (last - first) / ticks + # scale the result to a 0...100 range where 0 is very dry + # and 100 is standing in water + # + # dry = 10ms per transition, wet = 80ms per transition + min_ms = 20 + max_ms = 80 + average = max(min_ms, min(max_ms, average)) # clamp range + scaled = ((average - min_ms) / (max_ms - min_ms)) * 100 + results.append(round(scaled, 2)) + + return results + +# make a semi convincing drip noise +def drip_noise(): + piezo_pwm.duty_u16(32768) + for i in range(0, 10): + f = i * 20 + piezo_pwm.freq((f * f) + 1000) + time.sleep(0.02) + piezo_pwm.duty_u16(0) + +def water(moisture_levels): + from enviro import config + targets = [ + config.moisture_target_1, + config.moisture_target_2, + config.moisture_target_3 + ] + + for i in range(0, 3): + if moisture_levels[i] < targets[i]: + # determine a duration to run the pump for + duration = round((targets[i] - moisture_levels[i]) / 25, 1) + + if config.auto_water: + logging.info(f"> running pump {i} for {duration} second (currently at {int(moisture_levels[i])}, target {targets[i]})") + pump_pins[i].value(1) + time.sleep(duration) + pump_pins[i].value(0) + else: + for j in range(0, i + 1): + drip_noise() + time.sleep(0.5) + +def get_sensor_readings(): + # bme280 returns the register contents immediately and then starts a new reading + # we want the current reading so do a dummy read to discard register contents first + bme280.read() + time.sleep(0.1) + bme280_data = bme280.read() + + ltr_data = ltr559.get_reading() + + moisture_levels = moisture_readings() + + water(moisture_levels) # run pumps if needed + + from ucollections import OrderedDict + + return OrderedDict({ + "temperature": round(bme280_data[0], 2), + "humidity": round(bme280_data[2], 2), + "pressure": round(bme280_data[1] / 100.0, 2), + "luminance": round(ltr_data[BreakoutLTR559.LUX], 2), + "moisture_1": round(moisture_levels[0], 2), + "moisture_2": round(moisture_levels[1], 2), + "moisture_3": round(moisture_levels[2], 2) + }) +""" def play_tone(frequency = None): if frequency: diff --git a/enviro/boards/indoor.py b/enviro/boards/indoor.py index b07a472..a06bc24 100644 --- a/enviro/boards/indoor.py +++ b/enviro/boards/indoor.py @@ -40,7 +40,7 @@ def colour_temperature_from_rgbc(r, g, b, c): ct = 10000 return round(ct) -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): data = bme688.read() temperature = round(data[0], 2) diff --git a/enviro/boards/urban.py b/enviro/boards/urban.py index 9abf9e3..c9074a8 100644 --- a/enviro/boards/urban.py +++ b/enviro/boards/urban.py @@ -2,7 +2,7 @@ from machine import Pin, ADC from breakout_bme280 import BreakoutBME280 from pimoroni_i2c import PimoroniI2C -from enviro import logging +from phew import logging from enviro import i2c sensor_reset_pin = Pin(9, Pin.OUT, value=True) @@ -31,7 +31,7 @@ def particulates(particulate_data, measure): multiplier = 10 if measure >= PM0_3_PER_LITRE else 1 return ((particulate_data[measure * 2] << 8) | particulate_data[measure * 2 + 1]) * multiplier -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first bme280.read() diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index 25cb63d..5a5f6cd 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -144,52 +144,32 @@ def wind_direction(): return closest_index * 45 -def timestamp(dt): - year = int(dt[0:4]) - month = int(dt[5:7]) - day = int(dt[8:10]) - hour = int(dt[11:13]) - minute = int(dt[14:16]) - second = int(dt[17:19]) - return time.mktime((year, month, day, hour, minute, second, 0, 0)) - -def rainfall(): +def rainfall(seconds_since_last): amount = 0 - now_str = helpers.datetime_string() - if helpers.file_exists("last_time.txt"): - now = timestamp(now_str) + now = helpers.timestamp(helpers.datetime_string()) - time_entries = [] - with open("last_time.txt", "r") as timefile: - time_entries = timefile.read().split("\n") + if helpers.file_exists("rain.txt"): + with open("rain.txt", "r") as rainfile: + rain_entries = rainfile.read().split("\n") - # read the first line from the time file - last = now - for entry in time_entries: + # count how many rain ticks since the last reading + for entry in rain_entries: if entry: - last = timestamp(entry) - break - - logging.info(f" - seconds since last reading: {now - last}") - - if helpers.file_exists("rain.txt"): - with open("rain.txt", "r") as rainfile: - rain_entries = rainfile.read().split("\n") - - # count how many rain ticks since the last reading - for entry in rain_entries: - if entry: - ts = timestamp(entry) - if now - ts < now - last: - amount += RAIN_MM_PER_TICK + ts = helpers.timestamp(entry) + if now - ts < seconds_since_last: + amount += RAIN_MM_PER_TICK + + per_second = 0 + if seconds_since_last > 0: + per_second = amount / seconds_since_last - # write out adjusted rain log - with open("last_time.txt", "w") as timefile: - timefile.write(now_str) + # clear the rain log by overwriting it + with open("rain.txt", "w") as rainfile: + rainfile.write("") - return amount + return amount, per_second -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first bme280.read() @@ -197,6 +177,7 @@ def get_sensor_readings(): bme280_data = bme280.read() ltr_data = ltr559.get_reading() + rain, rain_per_second = rainfall(seconds_since_last) from ucollections import OrderedDict return OrderedDict({ @@ -205,6 +186,7 @@ def get_sensor_readings(): "pressure": round(bme280_data[1] / 100.0, 2), "light": round(ltr_data[BreakoutLTR559.LUX], 2), "wind_speed": wind_speed(), - "rain": rainfall(), + "rain": rain, + "rain_per_second": rain_per_second, "wind_direction": wind_direction() }) diff --git a/enviro/helpers.py b/enviro/helpers.py index 62d4ae7..8775eba 100644 --- a/enviro/helpers.py +++ b/enviro/helpers.py @@ -1,6 +1,6 @@ from enviro.constants import * -import machine, os +import machine, os, time # miscellany # =========================================================================== @@ -12,6 +12,15 @@ def date_string(): dt = machine.RTC().datetime() return "{0:04d}-{1:02d}-{2:02d}".format(*dt) +def timestamp(dt): + year = int(dt[0:4]) + month = int(dt[5:7]) + day = int(dt[8:10]) + hour = int(dt[11:13]) + minute = int(dt[14:16]) + second = int(dt[17:19]) + return time.mktime((year, month, day, hour, minute, second, 0, 0)) + def uid(): return "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}".format(*machine.unique_id()) @@ -37,7 +46,7 @@ def mkdir_safe(path): raise pass # directory already exists, this is fine -# Keeping the below around for later comparisons with PHEW +# TODO Keeping the below around for later comparisons with PHEW """ import machine, os, time, network, usocket, struct from phew import logging diff --git a/main.py b/main.py index e5404a7..fe2cfb3 100644 --- a/main.py +++ b/main.py @@ -64,7 +64,8 @@ # if we have enough cached uploads... if enviro.is_upload_needed(): enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) need uploading") - enviro.upload_readings() + if not enviro.upload_readings(): + enviro.halt("! reading upload failed") else: enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) not being uploaded. Waiting until there are {enviro.config.upload_frequency} file(s)") else: From a6884d402e1cb2d69247d391e3f741bf48862e04 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Thu, 10 Nov 2022 15:34:49 +0000 Subject: [PATCH 06/22] Fix for being unable to download readings on Windows --- enviro/__init__.py | 4 ++-- enviro/helpers.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index c223eba..49fe3d3 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -305,7 +305,7 @@ def get_sensor_readings(): def save_reading(readings): # open todays reading file and save readings helpers.mkdir_safe("readings") - readings_filename = f"readings/{helpers.date_string()}.txt" + readings_filename = f"readings/{helpers.datetime_file_string()}.txt" new_file = not helpers.file_exists(readings_filename) with open(readings_filename, "a") as f: if new_file: @@ -331,7 +331,7 @@ def cache_upload(readings): "uid": helpers.uid() } """ - uploads_filename = f"uploads/{helpers.datetime_string()}.json" + uploads_filename = f"uploads/{helpers.datetime_file_string()}.json" helpers.mkdir_safe("uploads") with open(uploads_filename, "w") as upload_file: #json.dump(payload, upload_file) # TODO what it was changed to diff --git a/enviro/helpers.py b/enviro/helpers.py index 8775eba..fe51e55 100644 --- a/enviro/helpers.py +++ b/enviro/helpers.py @@ -8,6 +8,10 @@ def datetime_string(): dt = machine.RTC().datetime() return "{0:04d}-{1:02d}-{2:02d}T{4:02d}:{5:02d}:{6:02d}Z".format(*dt) +def datetime_file_string(): + dt = machine.RTC().datetime() + return "{0:04d}-{1:02d}-{2:02d}T{4:02d}_{5:02d}_{6:02d}Z".format(*dt) + def date_string(): dt = machine.RTC().datetime() return "{0:04d}-{1:02d}-{2:02d}".format(*dt) From 194ffeccce8ff1991c3597ae2bed05dc1dbb72ed Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Thu, 10 Nov 2022 15:54:19 +0000 Subject: [PATCH 07/22] rain file now gets removed if there are no new readings --- enviro/boards/weather.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index 5a5f6cd..7459910 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -1,4 +1,4 @@ -import time, math +import time, math, os from breakout_bme280 import BreakoutBME280 from breakout_ltr559 import BreakoutLTR559 from machine import Pin, PWM @@ -158,15 +158,13 @@ def rainfall(seconds_since_last): ts = helpers.timestamp(entry) if now - ts < seconds_since_last: amount += RAIN_MM_PER_TICK + + os.remove("rain.txt") per_second = 0 if seconds_since_last > 0: per_second = amount / seconds_since_last - # clear the rain log by overwriting it - with open("rain.txt", "w") as rainfile: - rainfile.write("") - return amount, per_second def get_sensor_readings(seconds_since_last): From 2e7077638c096f829aae6acc1ee328bfe4d04b15 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Fri, 11 Nov 2022 13:22:08 +0000 Subject: [PATCH 08/22] changed all ms checks to use ticks_diff --- enviro/__init__.py | 4 ++-- enviro/boards/grow.py | 8 ++++---- enviro/boards/urban.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index 49fe3d3..808a3d1 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -177,12 +177,12 @@ def connect_to_wifi(): wlan.connect(wifi_ssid, wifi_password) start = time.ticks_ms() - while (time.ticks_ms() - start) < 30000: + while time.ticks_diff(time.ticks_ms(), start) < 30000: if wlan.status() < 0 or wlan.status() >= 3: break time.sleep(0.5) - seconds_to_connect = int((time.ticks_ms() - start) / 1000) + seconds_to_connect = int(time.ticks_diff(time.ticks_ms(), start) / 1000) if wlan.status() != 3: logging.error(f"! failed to connect to wireless network {wifi_ssid}") diff --git a/enviro/boards/grow.py b/enviro/boards/grow.py index 7ee5b2a..ca93e69 100644 --- a/enviro/boards/grow.py +++ b/enviro/boards/grow.py @@ -25,7 +25,7 @@ def moisture_readings(sample_time_ms=500): changes = [[], [], []] start = time.ticks_ms() - while time.ticks_ms() - start <= sample_time_ms: + while time.ticks_diff(time.ticks_ms(), start) <= sample_time_ms: for i in range(0, len(state)): now = moisture_sensor_pins[i].value() if now != state[i]: # sensor output changed @@ -42,7 +42,7 @@ def moisture_readings(sample_time_ms=500): continue # calculate the average tick between transitions in ms - average = (changes[i][-1] - changes[i][0]) / (len(changes[i]) - 1) + average = time.ticks_diff(changes[i][-1], changes[i][0]) / (len(changes[i]) - 1) # scale the result to a 0...100 range where 0 is very dry # and 100 is standing in water @@ -94,7 +94,7 @@ def moisture_readings(): first = None last = None ticks = 0 - while ticks < 10 and time.ticks_ms() - start <= 1000: + while ticks < 10 and time.ticks_diff(time.ticks_ms(), start) <= 1000: value = sensor.value() if last_value != value: if first == None: @@ -108,7 +108,7 @@ def moisture_readings(): continue # calculate the average tick between transitions in ms - average = (last - first) / ticks + average = time.ticks_diff(last, first) / ticks # scale the result to a 0...100 range where 0 is very dry # and 100 is standing in water # diff --git a/enviro/boards/urban.py b/enviro/boards/urban.py index c9074a8..04e43de 100644 --- a/enviro/boards/urban.py +++ b/enviro/boards/urban.py @@ -56,7 +56,7 @@ def get_sensor_readings(seconds_since_last): start = time.ticks_ms() min_value = 1.65 max_value = 1.65 - while time.ticks_ms() - start < sample_time_ms: + while time.ticks_diff(time.ticks_ms(), start) < sample_time_ms: value = noise_adc.read_u16() / (3.3 * 65535) min_value = min(min_value, value) max_value = max(max_value, value) From 4ef121a4a0e55273796809bd466b037bc2eb1e5a Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Fri, 11 Nov 2022 15:20:01 +0000 Subject: [PATCH 09/22] Possible Urban mic fix --- enviro/boards/urban.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enviro/boards/urban.py b/enviro/boards/urban.py index 04e43de..fa2bf47 100644 --- a/enviro/boards/urban.py +++ b/enviro/boards/urban.py @@ -57,18 +57,18 @@ def get_sensor_readings(seconds_since_last): min_value = 1.65 max_value = 1.65 while time.ticks_diff(time.ticks_ms(), start) < sample_time_ms: - value = noise_adc.read_u16() / (3.3 * 65535) + value = (noise_adc.read_u16() * 3.3) / 65535 min_value = min(min_value, value) max_value = max(max_value, value) - noise_vpp = round((max_value - min_value), 3) + noise_vpp = max_value - min_value from ucollections import OrderedDict return OrderedDict({ "temperature": round(bme280_data[0], 2), "humidity": round(bme280_data[2], 2), "pressure": round(bme280_data[1] / 100.0, 2), - "noise": round(noise_vpp, 2), + "noise": round(noise_vpp, 3), "pm1": particulates(particulate_data, PM1_UGM3), "pm2_5": particulates(particulate_data, PM2_5_UGM3), "pm10": particulates(particulate_data, PM10_UGM3) From 9063a19d4de75b8dd2da5bf4735e623f7e5fd89f Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Fri, 11 Nov 2022 15:47:07 +0000 Subject: [PATCH 10/22] made mic capture time a "constant" and added log --- enviro/boards/urban.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/enviro/boards/urban.py b/enviro/boards/urban.py index fa2bf47..6f5a07e 100644 --- a/enviro/boards/urban.py +++ b/enviro/boards/urban.py @@ -5,6 +5,9 @@ from phew import logging from enviro import i2c +# how long to capture the microphone signal for when taking a reading, in milliseconds +MIC_SAMPLE_TIME_MS = 500 + sensor_reset_pin = Pin(9, Pin.OUT, value=True) sensor_enable_pin = Pin(10, Pin.OUT, value=False) boost_enable_pin = Pin(11, Pin.OUT, value=False) @@ -52,11 +55,11 @@ def get_sensor_readings(seconds_since_last): sensor_enable_pin.value(False) boost_enable_pin.value(False) - sample_time_ms = 500 + logging.debug(" - taking microphone reading") start = time.ticks_ms() min_value = 1.65 max_value = 1.65 - while time.ticks_diff(time.ticks_ms(), start) < sample_time_ms: + while time.ticks_diff(time.ticks_ms(), start) < MIC_SAMPLE_TIME_MS: value = (noise_adc.read_u16() * 3.3) / 65535 min_value = min(min_value, value) max_value = max(max_value, value) From deb389e4de2e35a16adde7f90a6eef5ec9a1e58d Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Mon, 14 Nov 2022 16:25:22 +0000 Subject: [PATCH 11/22] Brought forward 0.0.8's upload_reading changes and improved rate limiting handling --- enviro/__init__.py | 75 +++++++++++++++++++--------- enviro/config_template.py | 2 +- enviro/constants.py | 5 ++ enviro/destinations/adafruit_io.py | 78 ++++++++++++++---------------- enviro/destinations/http.py | 38 ++++++--------- enviro/destinations/influxdb.py | 15 ++++-- enviro/destinations/mqtt.py | 55 ++++++++------------- enviro/mqttsimple.py | 2 + 8 files changed, 145 insertions(+), 125 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index 808a3d1..8b87e8b 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -310,8 +310,7 @@ def save_reading(readings): with open(readings_filename, "a") as f: if new_file: # new readings file so write out column headings first - #f.write("timestamp," + ",".join(readings.keys()) + "\r\n") # TODO what it was changed to - f.write("time," + ",".join(readings.keys()) + "\r\n") + f.write("timestamp," + ",".join(readings.keys()) + "\r\n") # write sensor data row = [helpers.datetime_string()] @@ -322,7 +321,6 @@ def save_reading(readings): # save the provided readings into a cache file for future uploading def cache_upload(readings): - """ TODO what it was changed to payload = { "nickname": config.nickname, "timestamp": helpers.datetime_string(), @@ -330,12 +328,12 @@ def cache_upload(readings): "model": model, "uid": helpers.uid() } - """ + uploads_filename = f"uploads/{helpers.datetime_file_string()}.json" helpers.mkdir_safe("uploads") with open(uploads_filename, "w") as upload_file: #json.dump(payload, upload_file) # TODO what it was changed to - upload_file.write(ujson.dumps(readings)) + upload_file.write(ujson.dumps(payload)) # return the number of cached results waiting to be uploaded def cached_upload_count(): @@ -352,22 +350,35 @@ def upload_readings(): return False destination = config.destination - exec(f"import enviro.destinations.{destination}") - destination_module = sys.modules[f"enviro.destinations.{destination}"] - """ TODO what it was changed to - for cache_file in os.ilistdir("uploads"): - with open(f"uploads/{cache_file[0]}", "r") as upload_file: - success = destination_module.upload_reading(json.load(upload_file)) - if not success: - logging.error(f"! failed to upload '{cache_file[0]}' to {destination}") + try: + exec(f"import enviro.destinations.{destination}") + destination_module = sys.modules[f"enviro.destinations.{destination}"] + destination_module.log_destination() + + for cache_file in os.ilistdir("uploads"): + try: + with open(f"uploads/{cache_file[0]}", "r") as upload_file: + status = destination_module.upload_reading(ujson.load(upload_file)) + if status == UPLOAD_SUCCESS: + os.remove(f"uploads/{cache_file[0]}") + logging.info(f" - uploaded {cache_file[0]}") + elif status == UPLOAD_RATE_LIMITED: + # write out that we want to attempt a reupload + with open("reattempt_upload.txt", "w") as attemptfile: + attemptfile.write("") + + logging.info(f" - rate limited, going to sleep for 1 minute") + sleep(1) + else: + logging.error(f" ! failed to upload '{cache_file[0]}' to {destination}") + return False + + except OSError: + logging.error(f" ! failed to open '{cache_file[0]}' to {destination}") return False - - # remove the cache file now uploaded - logging.info(f" - uploaded {cache_file[0]} to {destination}") - - os.remove(f"uploads/{cache_file[0]}") - """ - destination_module.upload_readings() + except ImportError: + logging.error(f"! cannot find destination {destination}") + return False return True @@ -396,7 +407,21 @@ def startup(): logging.debug(" - turn on activity led") pulse_activity_led(0.5) -def sleep(): + # see if we were woken to attempt a reupload + if helpers.file_exists("reattempt_upload.txt"): + os.remove("reattempt_upload.txt") + + logging.info(f"> {cached_upload_count()} cache file(s) still to upload") + if not upload_readings(): + halt("! reading upload failed") + + # if it was the RTC that woke us, go to sleep until our next scheduled reading + # otherwise continue with taking new readings etc + # Note, this *may* result in a missed reading + if reason == WAKE_REASON_RTC_ALARM: + sleep() + +def sleep(time_override=None): logging.info("> going to sleep") # make sure the rtc flags are cleared before going back to sleep @@ -409,8 +434,12 @@ def sleep(): hour, minute = dt[3:5] # calculate how many minutes into the day we are - minute = math.floor(minute / config.reading_frequency) * config.reading_frequency - minute += config.reading_frequency + if time_override is not None: + minute += time_override + else: + minute = math.floor(minute / config.reading_frequency) * config.reading_frequency + minute += config.reading_frequency + while minute >= 60: minute -= 60 hour += 1 diff --git a/enviro/config_template.py b/enviro/config_template.py index 8cc8641..5ad2186 100644 --- a/enviro/config_template.py +++ b/enviro/config_template.py @@ -15,7 +15,7 @@ # how often to wake up and take a reading (in minutes) reading_frequency = 15 -# where to upload to ("web_hook", "mqtt", "adafruitio") +# where to upload to ("http", "mqtt", "adafruit_io", "influxdb") destination = None # how often to upload data (number of cached readings) diff --git a/enviro/constants.py b/enviro/constants.py index ab02d25..8d57b80 100644 --- a/enviro/constants.py +++ b/enviro/constants.py @@ -32,3 +32,8 @@ WARN_LED_OFF = 0 WARN_LED_ON = 1 WARN_LED_BLINK = 2 + +# upload status +UPLOAD_SUCCESS = 0 +UPLOAD_FAILED = 1 +UPLOAD_RATE_LIMITED = 2 diff --git a/enviro/destinations/adafruit_io.py b/enviro/destinations/adafruit_io.py index ba0fd90..4409bbb 100644 --- a/enviro/destinations/adafruit_io.py +++ b/enviro/destinations/adafruit_io.py @@ -1,49 +1,43 @@ from enviro import logging -import urequests, ujson, os, time +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED, UPLOAD_RATE_LIMITED +import urequests import config -def upload_readings(): +def log_destination(): + logging.info(f"> uploading cached readings to Adafruit.io: {config.adafruit_io_username}") + +def upload_reading(reading): + # create adafruit.io payload format + payload = { + "created_at": reading["timestamp"], + "feeds": [] + } + + # add all the sensor readings + nickname = config.nickname + for key, value in reading["readings"].items(): + key = key.replace("_", "-") + payload["feeds"].append({ + "key": f"{nickname}-{key}", + "value": value + }) + + # send the payload username = config.adafruit_io_username headers = {'X-AIO-Key': config.adafruit_io_key, 'Content-Type': 'application/json'} + url = f"http://io.adafruit.com/api/v2/{username}/groups/enviro/data" - nickname = config.nickname + try: + result = urequests.post(url, json=payload, headers=headers) + result.close() + if result.status_code == 429: + return UPLOAD_RATE_LIMITED + + if result.status_code == 200: + return UPLOAD_SUCCESS + + logging.debug(f" - upload issue ({result.status_code} {result.reason})") + except: + logging.debug(f" - an exception occurred when uploading") - for cache_file in os.ilistdir("uploads"): - cache_file = cache_file[0] - try: - with open(f"uploads/{cache_file}", "r") as f: - timestamp = cache_file.split(".")[0] - data = ujson.load(f) - - timestamp = timestamp.replace(" ", "T") + "Z" - payload = { - "created_at": timestamp, - "feeds": [] - } - for key, value in data.items(): - key = key.replace("_", "-") - payload["feeds"].append({ - "key": f"{nickname}-{key}", - "value": value - }) - - url = f"http://io.adafruit.com/api/v2/{username}/groups/enviro/data" - result = urequests.post(url, json=payload, headers=headers) - if result.status_code == 429: - result.close() - logging.info(f" - rate limited, cooling off for thirty seconds") - time.sleep(30) - # try the request again - result = urequests.post(url, json=payload, headers=headers) - # TODO currently if this fails a second time it carrys on to the next file. Is this what we want? - - if result.status_code == 200: - os.remove(f"uploads/{cache_file}") - logging.info(f" - uploaded {cache_file}") - else: - logging.error(f"! failed to upload '{cache_file}' ({result.status_code} {result.reason})", cache_file) - # TODO should this break out of the file loop to avoid uploading files out-of-order? - - result.close() - except OSError as e: - logging.error(f" - failed to upload '{cache_file}'") + return UPLOAD_FAILED diff --git a/enviro/destinations/http.py b/enviro/destinations/http.py index 0c73d7b..d4f3cb7 100644 --- a/enviro/destinations/http.py +++ b/enviro/destinations/http.py @@ -1,33 +1,27 @@ from enviro import logging -import urequests, ujson, os +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED +import urequests import config -def upload_readings(): +def log_destination(): + logging.info(f"> uploading cached readings to url: {config.custom_http_url}") + +def upload_reading(reading): url = config.custom_http_url - logging.info(f"> uploading cached readings to {url}") auth = None if config.custom_http_username: auth = (config.custom_http_username, config.custom_http_password) - nickname = config.nickname + try: + result = urequests.post(url, auth=auth, json=reading) + result.close() + + if result.status_code in [200, 201, 202]: + return UPLOAD_SUCCESS - for cache_file in os.ilistdir("uploads"): - cache_file = cache_file[0] - try: - with open(f"uploads/{cache_file}", "r") as f: - timestamp = cache_file.split(".")[0] - payload = { - "nickname": nickname, - "timestamp": timestamp, - "readings": ujson.load(f) - } - result = urequests.post(url, auth=auth, json=payload) - if result.status_code != 200: - logging.error(f" - failed to upload '{cache_file}' ({result.status_code} {result.reason})", cache_file) - else: - logging.info(f" - uploaded {cache_file}") - os.remove(f"uploads/{cache_file}") + logging.debug(f" - upload issue ({result.status_code} {result.reason})") + except: + logging.debug(f" - an exception occurred when uploading") - except OSError as e: - logging.error(f" - failed to upload '{cache_file}'") + return UPLOAD_FAILED diff --git a/enviro/destinations/influxdb.py b/enviro/destinations/influxdb.py index b449aac..57e013d 100644 --- a/enviro/destinations/influxdb.py +++ b/enviro/destinations/influxdb.py @@ -1,3 +1,5 @@ +from enviro import logging +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED import urequests, time import config @@ -13,6 +15,9 @@ def url_encode(t): result += f"%{ord(c):02X}" return result +def log_destination(): + logging.info(f"> uploading cached readings to Influxdb bucket: {config.influxdb_bucket}") + def upload_reading(reading): bucket = config.influxdb_bucket @@ -46,8 +51,12 @@ def upload_reading(reading): # post reading data to http endpoint result = urequests.post(url, headers=headers, data=payload) result.close() - return result.status_code == 204 # why 204? we'll never know... + + if result.status_code == 204: # why 204? we'll never know... + return UPLOAD_SUCCESS + + logging.debug(f" - upload issue ({result.status_code} {result.reason})") except: - pass + logging.debug(f" - an exception occurred when uploading") - return False \ No newline at end of file + return UPLOAD_FAILED \ No newline at end of file diff --git a/enviro/destinations/mqtt.py b/enviro/destinations/mqtt.py index 5364905..5d1fd28 100644 --- a/enviro/destinations/mqtt.py +++ b/enviro/destinations/mqtt.py @@ -1,43 +1,30 @@ from enviro import logging -import ujson, os +from enviro.constants import UPLOAD_SUCCESS, UPLOAD_FAILED from enviro.mqttsimple import MQTTClient +import ujson import config -def upload_readings(): +def log_destination(): + logging.info(f"> uploading cached readings to MQTT broker: {config.mqtt_broker_address}") + +def upload_reading(reading): server = config.mqtt_broker_address username = config.mqtt_broker_username password = config.mqtt_broker_password nickname = config.nickname - logging.info(f"> uploading cached readings to {server}") - - mqtt_client = MQTTClient(nickname, server, user=username, password=password, keepalive=60) - mqtt_client.connect() - - for cache_file in os.ilistdir("uploads"): - cache_file = cache_file[0] - try: - with open(f"uploads/{cache_file}", "r") as f: - timestamp = cache_file.split(".")[0] - data = ujson.load(f) - - payload = { - "timestamp": timestamp, - "device": nickname - } - for key, value in data.items(): - payload[key] = value - - topic = f"enviro/{nickname}" - # by default the MQTT messages will be published with the retain flag - # set, so that if a consumer is not subscribed, the most recent set - # of readings can still be read by another subscriber later. Change - # retain to False (or drop from the method call) below to change this - mqtt_client.publish(topic, ujson.dumps(payload), retain=True) - - logging.info(f" - uploaded {cache_file}") - os.remove(f"uploads/{cache_file}") - except OSError as e: - logging.error(f" - failed to upload '{cache_file}'") - - mqtt_client.disconnect() + try: + mqtt_client = MQTTClient(reading["uid"], server, user=username, password=password, keepalive=60) + mqtt_client.connect() + + # by default the MQTT messages will be published with the retain flag + # set, so that if a consumer is not subscribed, the most recent set + # of readings can still be read by another subscriber later. Change + # retain to False (or drop from the method call) below to change this + mqtt_client.publish(f"enviro/{nickname}", ujson.dumps(reading), retain=True) + mqtt_client.disconnect() + return UPLOAD_SUCCESS + except: + logging.debug(f" - an exception occurred when uploading") + + return UPLOAD_FAILED diff --git a/enviro/mqttsimple.py b/enviro/mqttsimple.py index 0cea8d6..c648c80 100644 --- a/enviro/mqttsimple.py +++ b/enviro/mqttsimple.py @@ -63,7 +63,9 @@ def set_last_will(self, topic, msg, retain=False, qos=0): self.lw_retain = retain def connect(self, clean_session=True): + #def connect(self, clean_session=True, timeout=30): # TODO this was added to 0.0.8 self.sock = socket.socket() + #self.sock.settimeout(timeout) # TODO this was added to 0.0.8 addr = socket.getaddrinfo(self.server, self.port)[0][-1] self.sock.connect(addr) if self.ssl: From d3634c1306bd203e36f114481d0656f53cce9df6 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Mon, 14 Nov 2022 18:17:01 +0000 Subject: [PATCH 12/22] Added error handling for older upload files --- enviro/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index 8b87e8b..3d6e85e 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -374,8 +374,12 @@ def upload_readings(): return False except OSError: - logging.error(f" ! failed to open '{cache_file[0]}' to {destination}") + logging.error(f" ! failed to open '{cache_file[0]}'") return False + + except KeyError: + logging.error(f" ! skipping '{cache_file[0]}' as it is missing data. It was likely created by an older version of the enviro firmware") + except ImportError: logging.error(f"! cannot find destination {destination}") return False From 51b8507a7ff8d9f81464c763e68aa0c716929db9 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 15 Nov 2022 15:14:24 +0000 Subject: [PATCH 13/22] Minor tidy --- enviro/destinations/http.py | 1 + enviro/destinations/mqtt.py | 8 ++------ enviro/helpers.py | 30 ------------------------------ 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/enviro/destinations/http.py b/enviro/destinations/http.py index d4f3cb7..681bbd7 100644 --- a/enviro/destinations/http.py +++ b/enviro/destinations/http.py @@ -14,6 +14,7 @@ def upload_reading(reading): auth = (config.custom_http_username, config.custom_http_password) try: + # post reading data to http endpoint result = urequests.post(url, auth=auth, json=reading) result.close() diff --git a/enviro/destinations/mqtt.py b/enviro/destinations/mqtt.py index 5d1fd28..e6d26cf 100644 --- a/enviro/destinations/mqtt.py +++ b/enviro/destinations/mqtt.py @@ -11,16 +11,12 @@ def upload_reading(reading): server = config.mqtt_broker_address username = config.mqtt_broker_username password = config.mqtt_broker_password - nickname = config.nickname + nickname = reading["nickname"] try: + # attempt to publish reading mqtt_client = MQTTClient(reading["uid"], server, user=username, password=password, keepalive=60) mqtt_client.connect() - - # by default the MQTT messages will be published with the retain flag - # set, so that if a consumer is not subscribed, the most recent set - # of readings can still be read by another subscriber later. Change - # retain to False (or drop from the method call) below to change this mqtt_client.publish(f"enviro/{nickname}", ujson.dumps(reading), retain=True) mqtt_client.disconnect() return UPLOAD_SUCCESS diff --git a/enviro/helpers.py b/enviro/helpers.py index fe51e55..a9ead63 100644 --- a/enviro/helpers.py +++ b/enviro/helpers.py @@ -1,5 +1,4 @@ from enviro.constants import * - import machine, os, time # miscellany @@ -50,35 +49,6 @@ def mkdir_safe(path): raise pass # directory already exists, this is fine -# TODO Keeping the below around for later comparisons with PHEW -""" -import machine, os, time, network, usocket, struct -from phew import logging -def update_rtc_from_ntp(max_attempts = 5): - logging.info("> fetching date and time from ntp server") - ntp_host = "pool.ntp.org" - attempt = 1 - while attempt < max_attempts: - try: - logging.info(" - synching rtc attempt", attempt) - query = bytearray(48) - query[0] = 0x1b - address = usocket.getaddrinfo(ntp_host, 123)[0][-1] - socket = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM) - socket.settimeout(30) - socket.sendto(query, address) - data = socket.recv(48) - socket.close() - local_epoch = 2208988800 # selected by Chris, by experiment. blame him. :-D - timestamp = struct.unpack("!I", data[40:44])[0] - local_epoch - t = time.gmtime(timestamp) - return t - except Exception as e: - logging.error(e) - - attempt += 1 - return False -""" def copy_file(source, target): with open(source, "rb") as infile: with open(target, "wb") as outfile: From 9d41d2d869d3711dc6ff0125f0e9103b40b4286b Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 15 Nov 2022 16:43:10 +0000 Subject: [PATCH 14/22] Attempt to log any unhandled exceptions --- enviro/__init__.py | 7 ++++ main.py | 91 ++++++++++++++++++++++++---------------------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index 3d6e85e..ef0c79d 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -204,6 +204,13 @@ def halt(message): warn_led(WARN_LED_BLINK) sleep() +# log the exception, blink the warning led, and go back to sleep +def exception(exc): + import sys, io + buf = io.StringIO() + sys.print_exception(exc, buf) + halt("! " + buf.getvalue()) + # returns True if we've used up 90% of the internal filesystem def low_disk_space(): if not phew.remote_mount: # os.statvfs doesn't exist on remote mounts diff --git a/main.py b/main.py index fe2cfb3..b0988e5 100644 --- a/main.py +++ b/main.py @@ -23,55 +23,60 @@ import enviro import os -# initialise enviro -enviro.startup() +try: + # initialise enviro + enviro.startup() -# if the clock isn't set... -if not enviro.is_clock_set(): - enviro.logging.info("> clock not set, synchronise from ntp server") - if not enviro.sync_clock_from_ntp(): - # failed to talk to ntp server go back to sleep for another cycle - enviro.halt("! failed to synchronise clock") + # if the clock isn't set... + if not enviro.is_clock_set(): + enviro.logging.info("> clock not set, synchronise from ntp server") + if not enviro.sync_clock_from_ntp(): + # failed to talk to ntp server go back to sleep for another cycle + enviro.halt("! failed to synchronise clock") -# check disk space... -if enviro.low_disk_space(): - # less than 10% of diskspace left, this probably means cached results - # are not getting uploaded so warn the user and halt with an error - enviro.halt("! low disk space") + # check disk space... + if enviro.low_disk_space(): + # less than 10% of diskspace left, this probably means cached results + # are not getting uploaded so warn the user and halt with an error + enviro.halt("! low disk space") -# TODO this seems to be useful to keep around? -filesystem_stats = os.statvfs(".") -enviro.logging.debug(f"> {filesystem_stats[3]} blocks free out of {filesystem_stats[2]}") + # TODO this seems to be useful to keep around? + filesystem_stats = os.statvfs(".") + enviro.logging.debug(f"> {filesystem_stats[3]} blocks free out of {filesystem_stats[2]}") -# TODO should the board auto take a reading when the timer has been set, or wait for the time? -# take a reading from the onboard sensors -enviro.logging.debug(f"> taking new reading") -reading = enviro.get_sensor_readings() + # TODO should the board auto take a reading when the timer has been set, or wait for the time? + # take a reading from the onboard sensors + enviro.logging.debug(f"> taking new reading") + reading = enviro.get_sensor_readings() -# here you can customise the sensor readings by adding extra information -# or removing readings that you don't want, for example: -# -# del readings["temperature"] # remove the temperature reading -# -# readings["custom"] = my_reading() # add my custom reading value + # here you can customise the sensor readings by adding extra information + # or removing readings that you don't want, for example: + # + # del readings["temperature"] # remove the temperature reading + # + # readings["custom"] = my_reading() # add my custom reading value -# is an upload destination set? -if enviro.config.destination: - # if so cache this reading for upload later - enviro.logging.debug(f"> caching reading for upload") - enviro.cache_upload(reading) + # is an upload destination set? + if enviro.config.destination: + # if so cache this reading for upload later + enviro.logging.debug(f"> caching reading for upload") + enviro.cache_upload(reading) - # if we have enough cached uploads... - if enviro.is_upload_needed(): - enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) need uploading") - if not enviro.upload_readings(): - enviro.halt("! reading upload failed") + # if we have enough cached uploads... + if enviro.is_upload_needed(): + enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) need uploading") + if not enviro.upload_readings(): + enviro.halt("! reading upload failed") + else: + enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) not being uploaded. Waiting until there are {enviro.config.upload_frequency} file(s)") else: - enviro.logging.info(f"> {enviro.cached_upload_count()} cache file(s) not being uploaded. Waiting until there are {enviro.config.upload_frequency} file(s)") -else: - # otherwise save reading to local csv file (look in "/readings") - enviro.logging.debug(f"> saving reading locally") - enviro.save_reading(reading) + # otherwise save reading to local csv file (look in "/readings") + enviro.logging.debug(f"> saving reading locally") + enviro.save_reading(reading) + + # go to sleep until our next scheduled reading + enviro.sleep() -# go to sleep until our next scheduled reading -enviro.sleep() \ No newline at end of file +# handle any unexpected exception that has occurred +except Exception as exc: + enviro.exception(exc) From 250f72543007a25ed246557911338788d1e648ac Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 15 Nov 2022 17:21:53 +0000 Subject: [PATCH 15/22] Added option to adjust the level of logging --- main.py | 4 ++++ phew | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index b0988e5..352f700 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,10 @@ # # - the Pimoroni pirate crew +# uncomment the below two lines to change the amount of logging enviro will do +# from phew import logging +# logging.set_logging_types(logging.LOG_ERROR | logging.LOG_WARNING | logging.LOG_INFO) + # import enviro firmware, this will trigger provisioning if needed import enviro import os diff --git a/phew b/phew index bba8abc..74f07ad 160000 --- a/phew +++ b/phew @@ -1 +1 @@ -Subproject commit bba8abce70b51b61a4529ef8fe355ba28c52d231 +Subproject commit 74f07adc98365ce13038e9c84efacd6ebf724f13 From 7cff01b172948b31a3c9f3f647783eeaf1512019 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 15 Nov 2022 17:38:45 +0000 Subject: [PATCH 16/22] Made setting and clearing log types easier, and added an exception type --- enviro/__init__.py | 4 +++- main.py | 2 +- phew | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index ef0c79d..f3425e1 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -209,7 +209,9 @@ def exception(exc): import sys, io buf = io.StringIO() sys.print_exception(exc, buf) - halt("! " + buf.getvalue()) + logging.exception("! " + buf.getvalue()) + warn_led(WARN_LED_BLINK) + sleep() # returns True if we've used up 90% of the internal filesystem def low_disk_space(): diff --git a/main.py b/main.py index 352f700..13c17a0 100644 --- a/main.py +++ b/main.py @@ -21,7 +21,7 @@ # uncomment the below two lines to change the amount of logging enviro will do # from phew import logging -# logging.set_logging_types(logging.LOG_ERROR | logging.LOG_WARNING | logging.LOG_INFO) +# logging.disable_logging_types(logging.LOG_DEBUG) # import enviro firmware, this will trigger provisioning if needed import enviro diff --git a/phew b/phew index 74f07ad..775d78a 160000 --- a/phew +++ b/phew @@ -1 +1 @@ -Subproject commit 74f07adc98365ce13038e9c84efacd6ebf724f13 +Subproject commit 775d78afd3cec18711f05dd9204bf8b905632ac5 From 9ff72a9e5561dcda75835a6598970857c7240d01 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 22 Nov 2022 13:53:51 +0000 Subject: [PATCH 17/22] Fix for rain spamming when on USB --- enviro/boards/weather.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index a014aae..169b28a 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -23,8 +23,10 @@ wind_direction_pin = Analog(26) wind_speed_pin = Pin(9, Pin.IN, Pin.PULL_UP) rain_pin = Pin(10, Pin.IN, Pin.PULL_DOWN) +last_rain_trigger = False def startup(reason): + global last_rain_trigger import wakeup # check if rain sensor triggered wake @@ -50,6 +52,8 @@ def startup(reason): with open("rain.txt", "w") as rainfile: rainfile.write("\n".join(rain_entries)) + last_rain_trigger = True + # if we were woken by the RTC or a Poke continue with the startup return (reason is WAKE_REASON_RTC_ALARM or reason is WAKE_REASON_BUTTON_PRESS) @@ -58,9 +62,10 @@ def startup(reason): return True def check_trigger(): + global last_rain_trigger rain_sensor_trigger = rain_pin.value() - if rain_sensor_trigger: + if rain_sensor_trigger and not last_rain_trigger: activity_led(100) time.sleep(0.05) activity_led(0) @@ -84,6 +89,8 @@ def check_trigger(): with open("rain.txt", "w") as rainfile: rainfile.write("\n".join(rain_entries)) + last_rain_trigger = rain_sensor_trigger + def wind_speed(sample_time_ms=1000): # get initial sensor state state = wind_speed_pin.value() From 6bdf590be643ed93a4c50d4083174fc9d336f2b7 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 22 Nov 2022 13:55:03 +0000 Subject: [PATCH 18/22] Renamed vsys_present to vbus_present --- enviro/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enviro/__init__.py b/enviro/__init__.py index f3425e1..bd3587e 100644 --- a/enviro/__init__.py +++ b/enviro/__init__.py @@ -99,8 +99,8 @@ def stop_activity_led(): from pcf85063a import PCF85063A import enviro.helpers as helpers -# read the state of vsys to know if we were woken up by USB -vsys_present = Pin("WL_GPIO2", Pin.IN).value() +# read the state of vbus to know if we were woken up by USB +vbus_present = Pin("WL_GPIO2", Pin.IN).value() #BUG Temporarily disabling battery reading, as it seems to cause issues when connected to Thonny """ @@ -262,7 +262,7 @@ def get_wake_reason(): # TODO Temporarily removing this as false reporting on non-camera boards #elif not external_trigger_pin.value(): # wake_reason = WAKE_REASON_EXTERNAL_TRIGGER - elif vsys_present: + elif vbus_present: wake_reason = WAKE_REASON_USB_POWERED return wake_reason From 2173d091fa06eea32b852974e33751b95c390c4f Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Thu, 24 Nov 2022 13:01:53 +0000 Subject: [PATCH 19/22] Changed light reading to luminance, for consistency --- enviro/boards/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enviro/boards/weather.py b/enviro/boards/weather.py index 169b28a..3fc4b8e 100644 --- a/enviro/boards/weather.py +++ b/enviro/boards/weather.py @@ -195,7 +195,7 @@ def get_sensor_readings(seconds_since_last): "temperature": round(bme280_data[0], 2), "humidity": round(bme280_data[2], 2), "pressure": round(bme280_data[1] / 100.0, 2), - "light": round(ltr_data[BreakoutLTR559.LUX], 2), + "luminance": round(ltr_data[BreakoutLTR559.LUX], 2), "wind_speed": wind_speed(), "rain": rain, "rain_per_second": rain_per_second, From ed2496f172aa24300f9a7c2f78c946f67f2f9d8a Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Fri, 25 Nov 2022 14:49:49 +0000 Subject: [PATCH 20/22] Updated grow to v0.0.8 and added specific provisioning page --- enviro/boards/grow.py | 74 ++------------- enviro/config_template.py | 6 +- enviro/html/header.html | 2 +- enviro/html/provision-step-4-destination.html | 2 +- enviro/html/provision-step-grow-sensors.html | 90 +++++++++++++++++++ enviro/provisioning.py | 31 ++++++- phew | 2 +- 7 files changed, 132 insertions(+), 75 deletions(-) create mode 100644 enviro/html/provision-step-grow-sensors.html diff --git a/enviro/boards/grow.py b/enviro/boards/grow.py index ca93e69..1c5e564 100644 --- a/enviro/boards/grow.py +++ b/enviro/boards/grow.py @@ -3,7 +3,7 @@ from breakout_ltr559 import BreakoutLTR559 from machine import Pin, PWM from enviro import i2c -#from phew import logging # TODO from 0.0.8 +from phew import logging bme280 = BreakoutBME280(i2c, 0x77) ltr559 = BreakoutLTR559(i2c) @@ -16,66 +16,6 @@ Pin(13, Pin.IN, Pin.PULL_DOWN) ] -def moisture_readings(sample_time_ms=500): - # get initial sensor state - state = [sensor.value() for sensor in moisture_sensor_pins] - - # create an array for each sensor to log the times when the sensor state changed - # then we can use those values to calculate an average tick time for each sensor - changes = [[], [], []] - - start = time.ticks_ms() - while time.ticks_diff(time.ticks_ms(), start) <= sample_time_ms: - for i in range(0, len(state)): - now = moisture_sensor_pins[i].value() - if now != state[i]: # sensor output changed - # record the time of the change and update the state - changes[i].append(time.ticks_ms()) - state[i] = now - - # now we can average the readings for each sensor - results = [] - for i in range(0, len(changes)): - # if no sensor connected to change then we have no readings, skip - if len(changes[i]) < 2: - results.append(0) - continue - - # calculate the average tick between transitions in ms - average = time.ticks_diff(changes[i][-1], changes[i][0]) / (len(changes[i]) - 1) - - # scale the result to a 0...100 range where 0 is very dry - # and 100 is standing in water - # - # dry = 20ms per transition, wet = 60ms per transition - scaled = (min(40, max(0, average - 20)) / 40) * 100 - results.append(scaled) - - return results - -def get_sensor_readings(seconds_since_last): - # bme280 returns the register contents immediately and then starts a new reading - # we want the current reading so do a dummy read to discard register contents first - bme280.read() - time.sleep(0.1) - bme280_data = bme280.read() - - ltr_data = ltr559.get_reading() - - moisture_levels = moisture_readings(2000) - - from ucollections import OrderedDict - return OrderedDict({ - "temperature": round(bme280_data[0], 2), - "humidity": round(bme280_data[2], 2), - "pressure": round(bme280_data[1] / 100.0, 2), - "luminance": round(ltr_data[BreakoutLTR559.LUX], 2), - "moisture_1": round(moisture_levels[0], 2), - "moisture_2": round(moisture_levels[1], 2), - "moisture_3": round(moisture_levels[2], 2) - }) - -""" TODO from 0.0.8 pump_pins = [ Pin(12, Pin.OUT, value=0), Pin(11, Pin.OUT, value=0), @@ -133,9 +73,9 @@ def drip_noise(): def water(moisture_levels): from enviro import config targets = [ - config.moisture_target_1, - config.moisture_target_2, - config.moisture_target_3 + config.moisture_target_a, + config.moisture_target_b, + config.moisture_target_c ] for i in range(0, 3): @@ -153,7 +93,7 @@ def water(moisture_levels): drip_noise() time.sleep(0.5) -def get_sensor_readings(): +def get_sensor_readings(seconds_since_last): # bme280 returns the register contents immediately and then starts a new reading # we want the current reading so do a dummy read to discard register contents first bme280.read() @@ -163,11 +103,10 @@ def get_sensor_readings(): ltr_data = ltr559.get_reading() moisture_levels = moisture_readings() - + water(moisture_levels) # run pumps if needed from ucollections import OrderedDict - return OrderedDict({ "temperature": round(bme280_data[0], 2), "humidity": round(bme280_data[2], 2), @@ -177,7 +116,6 @@ def get_sensor_readings(): "moisture_2": round(moisture_levels[1], 2), "moisture_3": round(moisture_levels[2], 2) }) -""" def play_tone(frequency = None): if frequency: diff --git a/enviro/config_template.py b/enviro/config_template.py index 5ad2186..92d36d3 100644 --- a/enviro/config_template.py +++ b/enviro/config_template.py @@ -43,6 +43,6 @@ # grow specific settings auto_water = False -moisture_target_1 = 50 -moisture_target_2 = 50 -moisture_target_3 = 50 \ No newline at end of file +moisture_target_a = 50 +moisture_target_b = 50 +moisture_target_c = 50 \ No newline at end of file diff --git a/enviro/html/header.html b/enviro/html/header.html index bdebc4e..070ca6b 100644 --- a/enviro/html/header.html +++ b/enviro/html/header.html @@ -43,7 +43,7 @@ select {appearance: none; font-family: var(--theme-font); outline: none; background-color: var(--black); color: var(--white); border-radius: var(--input-radius); padding: 1rem 2rem; margin: 1rem 0; border: 0; cursor: pointer; font-size: 1.6rem;} fieldset {display: grid;} fieldset input[type="text"] {box-shadow: none !important;} - input[type="text"], input[type="email"], input[type="password"] {appearance: none; font-family: var(--theme-font); outline: none; background-color: var(--black); color: var(--white); border-radius: var(--input-radius); padding: 1rem 2rem; margin: 1rem 0; border: 0; cursor: pointer; font-size: 1.6rem;} + input[type="text"], input[type="email"], input[type="password"], input[type="number"] {appearance: none; font-family: var(--theme-font); outline: none; background-color: var(--black); color: var(--white); border-radius: var(--input-radius); padding: 1rem 2rem; margin: 1rem 0; border: 0; cursor: pointer; font-size: 1.6rem;} input[type="checkbox"] {cursor: pointer; appearance: none;} input[type="checkbox"]::before {content: ""; color: var(--white); line-height: 0.9rem; font-size: 1.6rem; border: solid 1px var(--grey); width: 1rem; height: 1rem; margin: 0rem 0.1rem 0rem 0rem; vertical-align: middle; display: inline-block; position: relative; top: -0.1rem;} input[type="checkbox"]:checked::before {content: "×"; background-color: var(--black); border: solid 1px var(--black);} diff --git a/enviro/html/provision-step-4-destination.html b/enviro/html/provision-step-4-destination.html index fe28a73..afacdc4 100644 --- a/enviro/html/provision-step-4-destination.html +++ b/enviro/html/provision-step-4-destination.html @@ -140,7 +140,7 @@

HTTP authentication credentials (optional).

diff --git a/enviro/html/provision-step-grow-sensors.html b/enviro/html/provision-step-grow-sensors.html new file mode 100644 index 0000000..a06e8ac --- /dev/null +++ b/enviro/html/provision-step-grow-sensors.html @@ -0,0 +1,90 @@ +{{render_template("enviro/html/header.html")}} + +{{render_template("enviro/html/header-mini.html", board=board)}} + +
+
+

Let's set up your moisture sensors.

+

Every plant likes a different amount of moisture in its soil! We've provided some default percentage values below to start you off, but later on you might want to come back and adjust these levels.

+

When the soil moisture level gets below the target, Grow will chirp at you with three unique tones to let you know which plant needs watering.

+ + +
+ +

Channel A moisture target.

+
+ +
+ +

Channel B moisture target.

+
+ +
+ +

Channel C moisture target.

+
+ +
+ +
+ +

Do you want to Auto Water your plants?

+

If you have pumps connected to your Enviro Grow, you can enable and disabling auto-watering below. Grow will automatically water your pots until the soil reaches the target moisture level set in the previous step.

+ +
    +
  • + Yes +
  • +
  • + No +
  • +
+ + + +
+ + +
+ +{{render_template("enviro/html/footer.html")}} diff --git a/enviro/provisioning.py b/enviro/provisioning.py index e1c0896..1baaef2 100644 --- a/enviro/provisioning.py +++ b/enviro/provisioning.py @@ -126,11 +126,40 @@ def provision_step_4_destination(request): write_config() - return redirect(f"http://{DOMAIN}/provision-step-5-done") + if model == "grow": + return redirect(f"http://{DOMAIN}/provision-step-grow-sensors") + else: + return redirect(f"http://{DOMAIN}/provision-step-5-done") else: return render_template("enviro/html/provision-step-4-destination.html", board=model) +@server.route("/provision-step-grow-sensors", methods=["GET", "POST"]) +def provision_step_grow_sensors(request): + if request.method == "POST": + config.auto_water = (request.form["auto_water"] == 'True') + try: + config.moisture_target_a = int(request.form["moisture_target_a"]) + except ValueError: + pass + + try: + config.moisture_target_b = int(request.form["moisture_target_b"]) + except ValueError: + pass + + try: + config.moisture_target_c = int(request.form["moisture_target_c"]) + except ValueError: + pass + + write_config() + + return redirect(f"http://{DOMAIN}/provision-step-5-done") + else: + return render_template("enviro/html/provision-step-grow-sensors.html", board=model) + + @server.route("/provision-step-5-done", methods=["GET", "POST"]) def provision_step_5_done(request): config.provisioned = True diff --git a/phew b/phew index 775d78a..b33a44b 160000 --- a/phew +++ b/phew @@ -1 +1 @@ -Subproject commit 775d78afd3cec18711f05dd9204bf8b905632ac5 +Subproject commit b33a44bffeb1354b730b9572410232b10f6e62e9 From d976c25217be548d386e46f99aa73e827c2804d1 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Fri, 25 Nov 2022 14:53:10 +0000 Subject: [PATCH 21/22] Typo --- enviro/html/provision-step-grow-sensors.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enviro/html/provision-step-grow-sensors.html b/enviro/html/provision-step-grow-sensors.html index a06e8ac..1402025 100644 --- a/enviro/html/provision-step-grow-sensors.html +++ b/enviro/html/provision-step-grow-sensors.html @@ -6,7 +6,7 @@

Let's set up your moisture sensors.

Every plant likes a different amount of moisture in its soil! We've provided some default percentage values below to start you off, but later on you might want to come back and adjust these levels.

-

When the soil moisture level gets below the target, Grow will chirp at you with three unique tones to let you know which plant needs watering.

+

When the soil moisture level gets below the target, Grow will chirp at you with three unique tones to let you know which plant needs watering.

@@ -31,7 +31,7 @@

Channel C moisture target.


Do you want to Auto Water your plants?

-

If you have pumps connected to your Enviro Grow, you can enable and disabling auto-watering below. Grow will automatically water your pots until the soil reaches the target moisture level set in the previous step.

+

If you have pumps connected to your Enviro Grow, you can enable and disable auto-watering below. Grow will automatically water your pots until the soil reaches the target moisture level set in the previous step.

  • From bebb3b5712492da4fb66b34409677eb9260e1ded Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Fri, 25 Nov 2022 15:13:48 +0000 Subject: [PATCH 22/22] Added more logging for grow --- enviro/boards/grow.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/enviro/boards/grow.py b/enviro/boards/grow.py index 1c5e564..48158cc 100644 --- a/enviro/boards/grow.py +++ b/enviro/boards/grow.py @@ -5,6 +5,8 @@ from enviro import i2c from phew import logging +CHANNEL_NAMES = ['A', 'B', 'C'] + bme280 = BreakoutBME280(i2c, 0x77) ltr559 = BreakoutLTR559(i2c) @@ -83,12 +85,15 @@ def water(moisture_levels): # determine a duration to run the pump for duration = round((targets[i] - moisture_levels[i]) / 25, 1) + logging.info(f"> sensor {CHANNEL_NAMES[i]} below moisture target {targets[i]} (currently at {int(moisture_levels[i])}).") + if config.auto_water: - logging.info(f"> running pump {i} for {duration} second (currently at {int(moisture_levels[i])}, target {targets[i]})") + logging.info(f" - running pump {CHANNEL_NAMES[i]} for {duration} second(s)") pump_pins[i].value(1) time.sleep(duration) pump_pins[i].value(0) else: + logging.info(f" - playing beep") for j in range(0, i + 1): drip_noise() time.sleep(0.5)