Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qwst sensor modules, including BME688 and SCD41 #226

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
99 changes: 99 additions & 0 deletions documentation/developer-guide.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,104 @@

## Tips if you want to modify the code
### Adding data points to the returned readings
#### Simple data mangling
You can customise the sensor readings to be saved and uploaded by adjusting the "reading" dictionary; Adding extra information or removing data points that you don't want. Make any adustments after the line populating the reading dictionary:
```
reading = enviro.get_sensor_readings()
```

For example:
```
reading = enviro.get_sensor_readings()

del reading["temperature"] # remove the temperature data
reading["custom"] = my_reading() # add my custom reading value
```

#### Adding a QW/ST globally supported module
You can add one or more module boards connected over the QW/ST connector and provide a global configuration for collecting and returning the data to the readings alongside the enviro board specific items.

##### Method (with examples)
Add a line in config_template.py in the appropriate section to allow users to enable that board and specify an address on the I2C bus.

Example:
```
# QW/ST modules
# These are modules supported out of the box, provide the I2C address if
# connected or otherwise leave as None
bme688_address = None
```

The template should have the address set to None, config files should be manually adjusted by the user to an address value after provisioning has completed for any boards connected.

Ensure your new configuration line has a default configuration in the config_defaults.py file to ensure firmware updates don't break older config files.

Example:
```
DEFAULT_BME688_ADDRESS = None

def add_missing_config_settings():
# <...existing default value blocks...>

try:
config.bme688_address
except AttributeError:
warn_missing_config_setting("bme688_address")
config.bme688_address = DEFAULT_BME688_ADDRESS
```

Alternatively, if the module can only use a single address, you can simply add the address to constants.py

Create a new python module in the enviro/qwst_modules/ directory that defines how your custom board should collect and return data to the reading dictionary. The readings must be performed in a function called get_readings that takes positional arguments i2c and address. The return of this function must be an OrderedDict{} and should contain key value pairs of reading names to values, with reading names that are likely to be unique to this board to ensure they do not overwrite other readings on upload.

Example:

Create file enviro/qwst_modules/bme688.py containing:
```
from breakout_bme68x import BreakoutBME68X
from ucollections import OrderedDict
from phew import logging

def get_readings(i2c, address, seconds_since_last):
bme688 = BreakoutBME68X(i2c)
bme688_data = bme688.read()

readings = OrderedDict({
"temperature_bme688": round(bme688_data[0], 2),
"humidity_bme688": round(bme688_data[2], 2),
"pressure_bme688": round(bme688_data[1] / 100.0, 2),
"gas_resistance_bme688": round(bme688_data[3], 2)
})

for reading in readings:
name_and_value = reading + " : " + str(readings[reading])
logging.info(f" - {name_and_value}")

return readings
```

Modify the function get_qwst_modules() in enviro/\_\_init\_\_.py to include an entry for your new board. This should use an if statement to check the address from the config file is visible on the I2C bus, reference your new module file path from the section above in the import statement and yield a dictionary that has keys for "name", "include" and "address" to the caller. The include key has a value of your imported module and the address is the I2C address from the config file.

Example:
```
def get_qwst_modules():
modules = []
# <...existing module configurations...>

if config.bme688_address in i2c_devices:
try:
import enviro.qwst_modules.bme688 as bme688
yield {"name": "BME688", "include": bme688, "address": config.bme688_address}
except RuntimeError:
pass

return modules
```

To complete this example, adjust your config.py to have bme688_address = 118 with a factory address BME688 board connected via the QW/ST connector.

#### Modifying specific board sensor collections
If the existing readings from a specific board require adjustment, for example adding a sea level adjusted value for atmospheric pressure readings. This should be done in the in board specific file in the boards directory, modifying the necessary lines in the get_sensor_readings() function.

### Code structure

Expand Down
35 changes: 31 additions & 4 deletions enviro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# ===========================================================================
from enviro.constants import *
from machine import Pin
import config
hold_vsys_en_pin = Pin(HOLD_VSYS_EN_PIN, Pin.OUT, value=True)

# detect board model based on devices on the i2c bus and pin state
Expand All @@ -10,9 +11,9 @@
i2c = PimoroniI2C(I2C_SDA_PIN, I2C_SCL_PIN, 100000)
i2c_devices = i2c.scan()
model = None
if 56 in i2c_devices: # 56 = colour / light sensor and only present on Indoor
if I2C_ADDR_BH1745 in i2c_devices: # colour / light sensor and only present on Indoor
model = "indoor"
elif 35 in i2c_devices: # 35 = ltr-599 on grow & weather
elif I2C_ADDR_LTR559 in i2c_devices: # ltr-599 on grow & weather
pump3_pin = Pin(12, Pin.IN, Pin.PULL_UP)
model = "grow" if pump3_pin.value() == False else "weather"
pump3_pin.init(pull=None)
Expand All @@ -30,7 +31,24 @@ def get_board():
if model == "urban":
import enviro.boards.urban as board
return board


# return any additional sensors connected with Qw/ST
def get_qwst_modules():
if I2C_ADDR_SCD41 in i2c_devices:
try:
import enviro.qwst_modules.scd41 as scd41
yield {"name": "SCD41", "include": scd41, "address": I2C_ADDR_SCD41}
except RuntimeError:
# Likely another device present on the SCD41 address
pass

if config.bme688_address in i2c_devices:
try:
import enviro.qwst_modules.bme688 as bme688
yield {"name": "BME688", "include": bme688, "address": config.bme688_address}
except RuntimeError:
pass

# set up the activity led
# ===========================================================================
from machine import PWM, Timer
Expand Down Expand Up @@ -409,14 +427,23 @@ def get_sensor_readings():


readings = get_board().get_sensor_readings(seconds_since_last, vbus_present)
module_readings = get_qwst_modules_readings(seconds_since_last)
readings = readings | module_readings
# 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)
timefile.write(now_str)

return readings

def get_qwst_modules_readings(seconds_since_last):
module_readings = {}
for module in get_qwst_modules():
logging.info(f" - getting readings from module: {module['name']}")
module_readings = module_readings | module["include"].get_readings(i2c, module["address"], seconds_since_last)
return module_readings

# save the provided readings into a todays readings data file
def save_reading(readings):
# open todays reading file and save readings
Expand Down
8 changes: 8 additions & 0 deletions enviro/config_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from phew import logging

DEFAULT_USB_POWER_TEMPERATURE_OFFSET = 4.5
DEFAULT_BME688_ADDRESS = None


def add_missing_config_settings():
Expand All @@ -24,5 +25,12 @@ def add_missing_config_settings():
warn_missing_config_setting("wifi_country")
config.wifi_country = "GB"

try:
config.bme688_address
except AttributeError:
warn_missing_config_setting("bme688_address")
config.bme688_address = DEFAULT_BME688_ADDRESS


def warn_missing_config_setting(setting):
logging.warn(f"> config setting '{setting}' missing, please add it to config.py")
5 changes: 5 additions & 0 deletions enviro/config_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@

# compensate for usb power
usb_power_temperature_offset = 4.5

# QW/ST modules
# These are modules supported out of the box, provide the I2C address if
# connected or otherwise leave as None
bme688_address = None
5 changes: 5 additions & 0 deletions enviro/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@
WATER_VAPOR_SPECIFIC_GAS_CONSTANT = 461.5
CRITICAL_WATER_TEMPERATURE = 647.096
CRITICAL_WATER_PRESSURE = 22064000

# I2C addresses
I2C_ADDR_BH1745 = 0x38
I2C_ADDR_LTR559 = 0x23
I2C_ADDR_SCD41 = 0x62
20 changes: 20 additions & 0 deletions enviro/qwst_modules/bme688.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from breakout_bme68x import BreakoutBME68X
from ucollections import OrderedDict
from phew import logging

def get_readings(i2c, address, seconds_since_last):
bme688 = BreakoutBME68X(i2c)
bme688_data = bme688.read()

readings = OrderedDict({
"temperature_bme688": round(bme688_data[0], 2),
"humidity_bme688": round(bme688_data[2], 2),
"pressure_bme688": round(bme688_data[1] / 100.0, 2),
"gas_resistance_bme688": round(bme688_data[3], 2)
})

for reading in readings:
name_and_value = reading + " : " + str(readings[reading])
logging.info(f" - {name_and_value}")

return readings
24 changes: 24 additions & 0 deletions enviro/qwst_modules/scd41.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import time

import breakout_scd41

def get_readings(i2c, address, seconds_since_last):
breakout_scd41.init(i2c)
breakout_scd41.start()

retries = 25
while retries > 0 and not breakout_scd41.ready():
time.sleep(0.2)
retries -= 1

if retries == 0:
return {}

scd_co2, scd_temp, scd_humidity = breakout_scd41.measure()

from ucollections import OrderedDict
return OrderedDict({
"scd_co2": scd_co2,
"scd_temperature": scd_temp,
"scd_humidity": scd_humidity
})
13 changes: 7 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@
# 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")

# Take a reading from the configured boards sensors and any configured qw/st
# modules, returns a dictionary of reading name and value pairs
# e.g. reading = {"temperature" : 19.1, "humidity" : 64,...}
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 returned date, adding or removing data points
# Refer to the documentation for more information:
# https://github.com/pimoroni/enviro/blob/main/documentation/developer-guide.md

# is an upload destination set?
if enviro.config.destination:
Expand Down