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 @@
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.