Skip to content

Commit

Permalink
v2.6: OTA mSD updates, custom LoRa frames
Browse files Browse the repository at this point in the history
  • Loading branch information
StevenCellist committed Sep 1, 2022
1 parent 46f326d commit 0a9109a
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 306 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ De vermoedde accuduur is drie weken, waarbij het zonnepaneel buiten beschouwing

## Schema
Zie de figuur voor de opbouw van het circuit in de sensorkastjes.
![Schematic v2.5 15-08-2022](Schematic_Meet_je_leefomgeving_2022-08-15.svg)
![Schematic v2.5 15-08-2022](Schematic_Meet_je_leefomgeving_2022-09-01.svg)
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
226 changes: 99 additions & 127 deletions software/_main.py
Original file line number Diff line number Diff line change
@@ -1,99 +1,100 @@
#_main.py -- frozen into the firmware along all other modules except settings.py
import pycom

pycom.heartbeat(False)
pycom.heartbeat_on_boot(False)
pycom.wifi_on_boot(False)

import time
wake_time = time.ticks_ms()
start_time = time.ticks_ms() # save current boot time

import pycom
import machine
wake_reason = machine.wake_reason()[0] # tuple of (wake_reason, GPIO_list)

import settings
from lib.SSD1306 import SSD1306

i2c_bus = machine.I2C(0) # create I2C object
display = SSD1306(128, 64, i2c_bus) # initialize display (4.4 / 0.0 mA)
wake_reason = machine.wake_reason()[0] # tuple of (wake_reason, GPIO_list)

# on first boot, disable integrated LED and WiFi
if wake_reason == machine.PWRON_WAKE:
pycom.heartbeat_on_boot(False)
pycom.wifi_on_boot(False)

i2c_bus = machine.I2C(0) # create I2C object
display = SSD1306(128, 64, i2c_bus) # initialize display (4.4 / 0.0 mA)

# if button was pressed, check for firmware update
if wake_reason == machine.PIN_WAKE:
from lib.updateFW import check_update
update = check_update(display)
machine.sleep(1000) # show update result for 1s on display
if update:
machine.reset() # in case of an update, reboot the device

display.fill(0)
display.text("MJLO-" + settings.NODE, 1, 1)
display.text("Hello world!", 1, 11)
display.text("FW: v2.6", 1, 11)
display.show()

""" This part is only executed if DEBUG == True """
if settings.DEBUG == True:

PRINT = {
'volt' : lambda volt : print("Accu:", volt),
'temp' : lambda temp : print("Temp:", temp),
'pres' : lambda pres : print("Druk:", pres),
'humi' : lambda humi : print("Vocht:", humi),
'volu' : lambda volu : print("Volume:", volu),
'lx' : lambda lx : print("Licht:", lx),
'uv' : lambda uv : print("UV:", uv),
'voc' : lambda voc : print("VOC:", voc),
'co2' : lambda co2 : print("CO2:", co2),
'pm25' : lambda pm25 : print("PM2.5:", pm25),
'pm10' : lambda pm10 : print("PM10:", pm10),
'gps' : lambda lat, long, alt : print("NB", lat, "OL", long, "H", alt),
'perc' : lambda perc : print("Accu%:", perc)
}

if wake_reason == machine.PIN_WAKE: # if button is pressed in DEBUG mode, enable GPS
if wake_reason == machine.PIN_WAKE: # if button is pressed in DEBUG mode, enable GPS
from collect_gps import run_gps
loc = run_gps(timeout = 120)
PRINT['GPS'](loc['lat'], loc['long'], loc['alt'])
print("NB", loc['lat'], "OL", loc['long'], "H", loc['alt'])

from collect_sensors import run_collection
values = run_collection(i2c_bus = i2c_bus, all_sensors = True, t_wake = settings.T_WAKE)

for key in values:
PRINT[key](values[key])

awake_time = time.ticks_ms() - wake_time # time in seconds the program has been running
push_button = machine.Pin('P23', mode = machine.Pin.IN, pull = machine.Pin.PULL_DOWN) # initialize wake-up pin
machine.pin_sleep_wakeup(['P23'], mode = machine.WAKEUP_ANY_HIGH, enable_pull = True) # set wake-up pin as trigger
machine.deepsleep(settings.T_DEBUG * 1000 - awake_time) # deepsleep for remainder of the interval time
print("Temp: " + str(values['temp']) + " C")
print("Druk: " + str(values['pres']) + " hPa")
print("Vocht: " + str(values['humi']) + " %")
print("Licht: " + str(values['lx']) + " lx")
print("UV: " + str(values['uv']))
print("Accu: " + str(values['perc']) + " %")
print("Volume: " + str(values['volu']) + " dB")
print("VOC: " + str(values['voc']) + " Ohm")
print("CO2: " + str(values['co2']) + " ppm")
print("PM2.5: " + str(values['pm25']) + " ppm")
print("PM10: " + str(values['pm10']) + " ppm")

push_button = machine.Pin('P2', mode = machine.Pin.IN, pull = machine.Pin.PULL_DOWN) # initialize wake-up pin
machine.pin_sleep_wakeup(['P2'], mode = machine.WAKEUP_ANY_HIGH, enable_pull = True) # set wake-up pin as trigger
machine.deepsleep((settings.T_DEBUG - 30) * 1000) # deepsleep for remainder of the interval time

""" This part is only executed if DEBUG == False """
import network
import socket

from lib.cayenneLPP import CayenneLPP

lora = network.LoRa(mode = network.LoRa.LORAWAN, region = network.LoRa.EU868) # create LoRa object
LORA_CNT = 0 # default LoRa frame counter
LORA_FCNT = 0 # default LoRa frame count
if wake_reason == machine.RTC_WAKE or wake_reason == machine.PIN_WAKE: # if woken up from deepsleep (timer or button)..
lora.nvram_restore() # ..restore the LoRa information from nvRAM
try:
with open('/flash/counter.txt', 'r') as file:
LORA_CNT = int(file.readline()) # try to restore LoRa frame counter from deepsleep
except:
print("Failed to read counter file, reset counter to 0")
lora.nvram_restore() # ..restore LoRa information from nvRAM
LORA_FCNT = pycom.nvs_get('fcnt') # ..restore LoRa frame count from nvRAM

use_gps = False
# once a day, enable GPS (when rest(no. of messages * interval) / day < interval) (but not if the button was pressed)
if (LORA_CNT * settings.T_INTERVAL) % 86400 < settings.T_INTERVAL and wake_reason != machine.PIN_WAKE:
use_gps = True
frame = bytes([0]) # LoRa packet decoding type 0 (minimal)

all_sensors = False
# every FRACTION'th message or if the button was pushed, use all sensors (but not if GPS is used)
if (LORA_CNT % settings.FRACTION == 0 or wake_reason == machine.PIN_WAKE) and use_gps == False:
if LORA_FCNT % settings.FRACTION == 0 or wake_reason == machine.PIN_WAKE:
all_sensors = True
frame = bytes([1]) # LoRa packet decoding type 1 (all sensors)

use_gps = False
# once a day, enable GPS (but not if the button was pressed)
if LORA_FCNT % int(86400 / settings.T_INTERVAL) == 0 and wake_reason != machine.PIN_WAKE:
use_gps = True
all_sensors = False
frame = bytes([2]) # LoRa packet decoding type 2 (use GPS)

LORA_SF = settings.SF_LOW
# if GPS or all sensors are used, send on high SF (don't let precious power go to waste)
if use_gps == True or all_sensors == True:
LORA_SF = settings.SF_HIGH

lora.sf(LORA_SF) # set SF for this uplink
LORA_DR = 12 - LORA_SF # calculate DR for this SF
lora.sf(LORA_SF) # set SF for this uplink
LORA_DR = 12 - LORA_SF # calculate DR for this SF

s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # create a LoRa socket (blocking)
s.setsockopt(socket.SOL_LORA, socket.SO_DR, LORA_DR) # set the LoRaWAN data rate
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # create a LoRa socket (blocking)
s.setsockopt(socket.SOL_LORA, socket.SO_DR, LORA_DR) # set the LoRaWAN data rate

# join the network upon first-time wake
if LORA_CNT == 0:
if LORA_FCNT == 0:
import secret

if settings.LORA_MODE == 'OTAA':
Expand All @@ -102,93 +103,64 @@
mode = network.LoRa.ABP

lora.join(activation = mode, auth = secret.auth(settings.LORA_MODE, settings.NODE), dr = LORA_DR)
# don't need to wait for has_joined(): when joining the network, GPS is enabled next which takes much longer

# create Cayenne-formatted LoRa message (payload either 41 or 42 bytes so stick to 42 either way)
lpp = CayenneLPP(size = 42, sock = s)

LPP_ADD = { # routine for adding data to Cayenne message
'volt' : lambda volt : lpp.add_analog_input(volt, channel = 0), # 4 (2) bits
'temp' : lambda temp : lpp.add_temperature(temp, channel = 1), # 4 (2) bits
'pres' : lambda pres : lpp.add_barometric_pressure(pres, channel = 2), # 4 (2) bits
'humi' : lambda humi : lpp.add_relative_humidity(humi, channel = 3), # 3 (1) bits
'volu' : lambda volu : lpp.add_relative_humidity(volu, channel = 4), # 3 (1) bits
'lx' : lambda lx : lpp.add_luminosity(lx, channel = 5), # 4 (2) bits
'uv' : lambda uv : lpp.add_luminosity(uv, channel = 6), # 4 (2) bits
'voc' : lambda voc : lpp.add_luminosity(voc, channel = 7), # 4 (2) bits
'co2' : lambda co2 : lpp.add_luminosity(co2, channel = 8), # 4 (2) bits
'pm25' : lambda pm25 : lpp.add_barometric_pressure(pm25, channel = 9), # 4 (2) bits
'pm10' : lambda pm10 : lpp.add_barometric_pressure(pm10, channel = 10), # 4 (2) bits
'gps' : lambda lat, long, alt : lpp.add_gps(lat, long, alt, channel = 11), # 11 (3/3/3) bits
'perc' : lambda perc : lora.set_battery_level(perc), # set level for MAC command
}

DISPLAY_TEXT = { # routine for displaying text on OLED display
'volt' : lambda volt : None,
'temp' : lambda temp : display.text("Temp: " + str(round(temp, 1)) + " C", 1, 1),
'pres' : lambda pres : display.text("Druk: " + str(round(pres, 1)) + " hPa", 1, 11),
'humi' : lambda humi : display.text("Vocht: " + str(round(humi, 1)) + " %", 1, 21),
'lx' : lambda lx : display.text("Licht: " + str(int(lx)) + " lx", 1, 31),
'uv' : lambda uv : display.text("UV: " + str(int(uv)), 1, 41),

'volu' : lambda volu : display.text("Volume: " + str(int(volu)) + " dB", 1, 1),
'voc' : lambda voc : display.text("VOC: " + str(int(voc)) + " Ohm", 1, 11),
'co2' : lambda co2 : display.text("CO2: " + str(int(co2)) + " ppm", 1, 21),
'pm25' : lambda pm25 : display.text("PM2.5: " + str(pm25) + " ppm", 1, 31),
'pm10' : lambda pm10 : display.text("PM10: " + str(pm10) + " ppm", 1, 41),

'perc' : lambda perc : display.text("Accu: " + str(int(perc)) + " %", 1, 54),
}
# don't need to wait for has_joined(): GPS takes much longer to start

display.poweroff()
# run sensor routine
from collect_sensors import run_collection
values = run_collection(i2c_bus = i2c_bus, all_sensors = all_sensors, t_wake = settings.T_WAKE)

def pack(value, precision, size = 2):
value = int(value / precision) # round to precision
value = max(0, min(value, 2**(8*size))) # stay in range 0 .. int.max_size
return value.to_bytes(size, 'big') # pack to bytes

# run gps routine if enabled and add values to Cayenne message
if use_gps:
# add the sensor values that are always measured (frame is now 1 + 14 = 15 bytes)
frame += pack(values['volt'], 0.001) + pack(values['temp'] + 25, 0.01) + pack(values['pres'], 0.1) \
+ pack(values['humi'], 0.5, size = 1) + pack(values['volu'], 0.5, size = 1) \
+ pack(values['lx'], 1) + pack(values['uv'], 1) + pack(values['voc'], 1)

if all_sensors == True:
# add extra sensor values (frame is now 1 + 14 + 6 = 21 bytes)
frame += pack(values['co2'], 1) + pack(values['pm25'], 0.1) + pack(values['pm10'], 0.1)

if use_gps == True:
# run gps routine
from collect_gps import run_gps
loc = run_gps()
LPP_ADD['gps'](loc['lat'], loc['long'], loc['alt'])

# run sensor routine and add values to Cayenne message
from collect_sensors import run_collection
values = run_collection(i2c_bus = i2c_bus, all_sensors = all_sensors, t_wake = settings.T_WAKE)
for key in values:
LPP_ADD[key](values[key])

# send Cayenne message and reset payload
lpp.send(reset_payload = True)
# add gps values (frame is now 1 + 14 + 9 = 24 bytes)
frame += pack(loc['lat'] + 180, 0.0000001, size = 4) + pack(loc['long'] + 90, 0.0000001, size = 4) \
+ pack(loc['alt'], 0.1, size = 1)

# store LoRa context in non-volatile RAM (should be using wear leveling)
# send LoRa message and store LoRa context + frame count in non-volatile RAM (should be using wear leveling)
s.send(frame)
lora.nvram_save()

# store LoRa frame counter to flash (should be VERY r/w resistant >> RTC memory, millions of cycles)
with open('/flash/counter.txt', 'w') as file:
file.write(str(LORA_CNT + 1))
pycom.nvs_set('fcnt', LORA_FCNT)

# write all values to display in two series
display.poweron()
display.fill(0)
DISPLAY_TEXT['temp'](values['temp'])
DISPLAY_TEXT['pres'](values['pres'])
DISPLAY_TEXT['humi'](values['humi'])
DISPLAY_TEXT['lx'](values['lx'])
DISPLAY_TEXT['uv'](values['uv'])
DISPLAY_TEXT['perc'](values['perc'])
display.text("Temp: " + str(round(values['temp'], 1)) + " C", 1, 1),
display.text("Druk: " + str(round(values['pres'], 1)) + " hPa", 1, 11),
display.text("Vocht: " + str(round(values['humi'], 1)) + " %", 1, 21),
display.text("Licht: " + str(int(values['lx'])) + " lx", 1, 31),
display.text("UV: " + str(int(values['uv'])), 1, 41),
display.text("Accu: " + str(int(values['perc'])) + " %", 1, 54),
display.show()
machine.sleep(settings.T_DISPLAY * 1000)
display.fill(0)
DISPLAY_TEXT['volu'](values['volu'])
DISPLAY_TEXT['voc'](values['voc'])
DISPLAY_TEXT['perc'](values['perc'])
display.text("Volume: " + str(int(values['volu'])) + " dB", 1, 1),
display.text("VOC: " + str(int(values['voc'])) + " Ohm", 1, 11),
if all_sensors == True:
DISPLAY_TEXT['co2'](values['co2'])
DISPLAY_TEXT['pm25'](values['pm25'])
DISPLAY_TEXT['pm10'](values['pm10'])
display.text("CO2: " + str(int(values['co2'])) + " ppm", 1, 21),
display.text("PM2.5: " + str(values['pm25']) + " ppm", 1, 31),
display.text("PM10: " + str(values['pm10']) + " ppm", 1, 41),
display.text("Accu: " + str(int(values['perc'])) + " %", 1, 54),
display.show()
machine.sleep(settings.T_DISPLAY * 1000)
display.poweroff()

# set up for deepsleep
awake_time = time.ticks_ms() - wake_time # time in seconds the program has been running
push_button = machine.Pin('P23', mode = machine.Pin.IN, pull = machine.Pin.PULL_DOWN) # initialize wake-up pin
machine.pin_sleep_wakeup(['P23'], mode = machine.WAKEUP_ANY_HIGH, enable_pull = True) # set wake-up pin as trigger
machine.deepsleep(settings.T_INTERVAL * 1000 - awake_time) # deepsleep for remainder of the interval time
awake_time = time.ticks_diff(time.ticks_ms(), start_time) # time in milliseconds the program has been running
push_button = machine.Pin('P2', mode = machine.Pin.IN, pull = machine.Pin.PULL_DOWN) # initialize wake-up pin
machine.pin_sleep_wakeup(['P2'], mode = machine.WAKEUP_ANY_HIGH, enable_pull = True) # set wake-up pin as trigger
machine.deepsleep(settings.T_INTERVAL * 1000 - awake_time) # deepsleep for remainder of the interval time
4 changes: 2 additions & 2 deletions software/collect_gps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def run_gps(timeout = 120):
gps_en.value(0) # enable GPS power
gps = MicropyGPS() # create GPS object

com = machine.UART(2, pins = ('P3', 'P4'), baudrate = 9600) # GPS communication
com = machine.UART(2, pins = ('P3', 'P11'), baudrate = 9600)# GPS communication

t1 = time.time()
while gps.latitude == gps.longitude == 0 and time.time() - t1 < timeout: # timeout if no fix after ..
Expand All @@ -26,4 +26,4 @@ def run_gps(timeout = 120):
values["lat"] = gps.latitude
values["long"] = gps.longitude
values["alt"] = gps.alt
return values
return values
15 changes: 6 additions & 9 deletions software/lib/SDS011.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
7 DATA6 ID byte 2
8 Checksum Low byte of sum of DATA bytes
9 Tail '\xab'
"""

import ustruct as struct
import struct

_SDS011_CMDS = {'SET': b'\x01',
'GET': b'\x00',
Expand Down Expand Up @@ -51,7 +50,7 @@ def make_command(self, cmd, mode, param):

def get_response(self, command_ID):
# try for 120 bytes (0.2) second to get a response from sensor (typical response time 12~33 bytes)
for _ in range(240):
for _ in range(120):
try:
header = self._uart.read(1)
if header == b'\xaa':
Expand All @@ -63,9 +62,9 @@ def get_response(self, command_ID):
self.process_measurement(packet)
return True
else:
print("Response did not match command ID", command_ID)
pass
except Exception as e:
print('Problem attempting to read:', e)
pass
return False

def wake(self):
Expand Down Expand Up @@ -94,13 +93,11 @@ def query(self):

def process_measurement(self, packet):
try:
*data, checksum, tail = struct.unpack('<HHBBBs', packet)
*data, _, _ = struct.unpack('<HHBBBs', packet)
self._pm25 = data[0]/10.0
self._pm10 = data[1]/10.0
checksum_OK = (checksum == (sum(data) % 256))
tail_OK = tail == b'\xab'
except Exception as e:
print('Problem decoding packet:', e)
pass

def read(self):
self.query() # query measurement
Expand Down
Loading

0 comments on commit 0a9109a

Please sign in to comment.