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

275 add co2 sensor support to smibhid #276

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
32 changes: 27 additions & 5 deletions smibhid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ Press the space_open or space_closed buttons to call the smib server endpoint ap
- API page that details API endpoints available and their usage
- Update page for performing over the air firmware updates and remote reset to apply them
- Pinger watchdog - Optionally ping an IP address and toggle a GPIO pin on ping failure. Useful for network device monitoring and reset.
- Extensible sensor module framework for async polling of I2C sensors (currently only writes to log out) and presentation of sensors and readings on the web API
- Supported sensors
- SGP30 (Equivalent CO2 and VOC)
- BME280
- SCD30

## Circuit diagram
### Pico W Connections
![Circuit diagram](images/SMIBHID%20circuit%20diagram.drawio.png)

### Pico W pinout
![Pico W pinout](images/pico_w_pinout.png)
### Pico 2 W pinout
![Pico 2 W pinout](images/pico_2_w_pinout.png)

### Example breadboard build
![Breadboard photo](images/breadboard.jpg)
Expand All @@ -41,20 +46,27 @@ Press the space_open or space_closed buttons to call the smib server endpoint ap


## Hardware
Below is a list of hardware ad links for my specific build:
Below is a list of hardware and links for my specific build:
- [Raspberry Pi Pico W](https://thepihut.com/products/raspberry-pi-pico-w?variant=41952994754755)
- [Prototype board](https://thepihut.com/products/pico-proto-pcb?variant=41359085568195)
- [LED push button switch - Red](https://thepihut.com/products/rugged-metal-pushbutton-with-red-led-ring?variant=27740444561)
- [LED push button switch - Green](https://thepihut.com/products/rugged-metal-pushbutton-with-green-led-ring?variant=27740444625)
- [JST connectors](https://www.amazon.co.uk/dp/B07449V33P)
- [2x16 Character I2C display](https://thepihut.com/products/lcd1602-i2c-module?variant=42422810083523)
- [SGP30 I2C sensor](https://thepihut.com/products/sgp30-air-quality-sensor-breakout)
- [BME280 sensor](https://thepihut.com/products/bme280-breakout-temperature-pressure-humidity-sensor)
- [SCD30 sensor](https://thepihut.com/products/adafruit-scd-30-ndir-co2-temperature-and-humidity-sensor)

## Deployment
Copy the files from the smibhib folder into the root of a Pico W running Micropython (minimum Pico W Micropython firmware v1.22.2 https://micropython.org/download/RPI_PICO_W/) and update values in config.py as necessary
Copy the files from the smibhib folder into the root of a Pico 2 W running Micropython (minimum Pico 2 W Micropython firmware v1.25.0-preview.365 https://micropython.org/download/RPI_PICO2_W/) and update values in config.py as necessary.

This project should work on a Pico W on recent firmware, but we have moved development, testing and our production SMIBHIDs to Pico 2 Ws.

### Configuration
- Ensure the pins for the space open/closed LEDs and buttons are correctly specified for your wiring
- Configure I2C pins for the display if using, display will detect automatically or disable if not found
- Configure I2C pins for the display and sensors if using, display will detect automatically or disable if not found
- Populate the display list with displays in use (must have appropriate driver module)
- Populate the sensors list with sensors in use (must have appropriate driver module)
- Populate Wifi SSID and password
- Configure the webserver hostname/IP and port as per your smib.webserver configuration
- Set the space state poll frequency in seconds (>= 5), set to 0 to disable the state poll
Expand Down Expand Up @@ -105,6 +117,16 @@ Use existing space state buttons, lights, slack API wrapper and watchers as an e
- Ensure the driver registers itself with the driver registry, use LCD1602 as an example
- Import the new driver module in display.py
- Update the config.py file to include the option for your new driver
- I2C Sensor boards can be added by providing a driver module that extends the SensorModule base class
- Copy an appropriate python driver module into the sensors sub folder
- Ensure the init method takes one mandatory parameter for the I2C interface
- Modify the driver module to extend SensorModule
- Provide a list of sensor names on this module to class super init
- Ensure the init method raises an error if device not found or has any configuration error to be caught by the sensors module driver load method
- Overload the get_reading() method to return a dictionary of sensor name - reading value pairs
- Update the config.py file to include the option for your new driver
- Add the module import to sensors.\_\_init\_\_.py
- Copy and adjust appropriately the try except block in sensors.\_\_init\_\_.load_modules method
- UIState machine
- A state machine exists and can be extended by various modules such as space_state to manage the state of the buttons and display output
- The current state instance is held in hid.ui_state_instance
Expand Down
4 changes: 4 additions & 0 deletions smibhid/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
SDA_PIN = 8
SCL_PIN = 9
I2C_ID = 0
I2C_FREQ = 400000

## Sensors - Populate driver list with connected sensor modules from this supported list: ["SGP30", "BME280", "SCD30"]
SENSOR_MODULES = []

## Displays - Populate driver list with connected displays from this supported list: ["LCD1602"]
DISPLAY_DRIVERS = ["LCD1602"]
Expand Down
53 changes: 41 additions & 12 deletions smibhid/http/website.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from lib.module_config import ModuleConfig
from json import dumps
import uasyncio
from lib.updater import Updater
from lib.updater import UpdateCore

class WebApp:

Expand All @@ -20,7 +20,8 @@ def __init__(self, module_config: ModuleConfig, hid: object) -> None:
self.hid = hid
self.wifi = module_config.get_wifi()
self.display = module_config.get_display()
self.updater = Updater()
self.sensors = module_config.get_sensors()
self.update_core = UpdateCore()
self.port = 80
self.running = False
self.create_style_css()
Expand All @@ -32,7 +33,7 @@ def __init__(self, module_config: ModuleConfig, hid: object) -> None:
def startup(self):
network_access = uasyncio.run(self.wifi.check_network_access())

if network_access == True:
if network_access:
self.log.info("Starting web server")
self.app.run(host='0.0.0.0', port=self.port, loop_forever=False)
self.log.info(f"Web server started: {self.wifi.get_ip()}:{self.port}")
Expand Down Expand Up @@ -67,8 +68,12 @@ async def api(request, response):

self.app.add_resource(WLANMAC, '/api/wlan/mac', wifi = self.wifi, logger = self.log)
self.app.add_resource(Version, '/api/version', hid = self.hid, logger = self.log)
self.app.add_resource(FirmwareFiles, '/api/firmware_files', updater = self.updater, logger = self.log)
self.app.add_resource(Reset, '/api/reset', updater = self.updater, logger = self.log)
self.app.add_resource(FirmwareFiles, '/api/firmware_files', update_core = self.update_core, logger = self.log)
self.app.add_resource(Reset, '/api/reset', update_core = self.update_core, logger = self.log)
self.app.add_resource(Modules, '/api/sensors/modules', sensors = self.sensors, logger = self.log)
self.app.add_resource(Sensors, '/api/sensors/<module>', sensors = self.sensors, logger = self.log)
self.app.add_resource(Readings, '/api/sensors/readings/<module>', sensors = self.sensors, logger = self.log)
self.app.add_resource(Readings, '/api/sensors/readings', module = "", sensors = self.sensors, logger = self.log)

class WLANMAC():

Expand All @@ -88,28 +93,52 @@ def get(self, data, hid, logger: uLogger) -> str:

class FirmwareFiles():

def get(self, data, updater: Updater, logger: uLogger) -> str:
def get(self, data, update_core: UpdateCore, logger: uLogger) -> str:
logger.info("API request - GET Firmware files")
html = dumps(updater.process_update_file())
html = dumps(update_core.process_update_file())
logger.info(f"Return value: {html}")
return html

def post(self, data, updater: Updater, logger: uLogger) -> str:
def post(self, data, update_core: UpdateCore, logger: uLogger) -> str:
logger.info("API request - POST Firmware files")
logger.info(f"Data: {data}")
if data["action"] == "add":
logger.info("Adding update - data: {data}")
html = updater.stage_update_url(data["url"])
html = update_core.stage_update_url(data["url"])
elif data["action"] == "remove":
logger.info("Removing update - data: {data}")
html = updater.unstage_update_url(data["url"])
html = update_core.unstage_update_url(data["url"])
else:
html = f"Invalid request: {data["action"]}"
return dumps(html)

class Reset():

def post(self, data, updater: Updater, logger: uLogger) -> None:
def post(self, data, update_core: UpdateCore, logger: uLogger) -> None:
logger.info("API request - reset")
updater.reset()
update_core.reset()
return

class Modules():

def get(self, data, sensors, logger: uLogger) -> str:
logger.info("API request - sensors/modules")
html = dumps(sensors.get_modules())
logger.info(f"Return value: {html}")
return html

class Sensors():

def get(self, data, module: str, sensors, logger: uLogger) -> str:
logger.info(f"API request - sensors/{module}")
html = dumps(sensors.get_sensors(module))
logger.info(f"Return value: {html}")
return html

class Readings():

def get(self, data, module: str, sensors, logger: uLogger) -> str:
logger.info(f"API request - sensors/readings - Module: {module}")
html = dumps(sensors.get_readings(module))
logger.info(f"Return value: {html}")
return html
4 changes: 4 additions & 0 deletions smibhid/http/www/api.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ <h2>Endpoints</h2>
<ul>
<li>MAC address (GET): <a href="/api/wlan/mac">/api/wlan/mac</a></li>
<li>Firmware version (GET): <a href="/api/version">/api/version</a></li>
<li>List sensor modules (GET): <a href="/api/sensors/modules">/api/sensors/modules</a></li>
<li>List module sensors (GET): <a href="/api/sensors/{module}">/api/sensors/{module}</a></li>
<li>Get latest module sensor readings (GET): <a href="/api/sensors/readings/{module}">/api/sensors/readings/{module}</a></li>
<li>Get latest sensor readings from all modules (GET): <a href="/api/sensors/readings">/api/sensors/readings</a></li>
<li>Firmware files (GET,POST): <a href="/api/firmware_files">/api/firmware_files - List, add or remove URLs staged for download and patch on reset</a>
<ul>
<li>List files: GET</li>
Expand Down
Binary file modified smibhid/images/SMIBHID circuit diagram.drawio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added smibhid/images/pico_2_w_pinout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed smibhid/images/pico_w_pinout.png
Binary file not shown.
4 changes: 2 additions & 2 deletions smibhid/lib/LCD1602.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
class LCD1602:
"""Driver for the LCD1602 16x2 character LED display"""

def __init__(self) -> None:
def __init__(self, i2c) -> None:
"""Configure and connect to display via I2C, throw error on connection issue."""
self.log = uLogger("LCD1602")
self.log.info("Init LCD1602 display driver")
Expand All @@ -60,7 +60,7 @@ def __init__(self) -> None:
self.spinner_task = None

try:
self.LCD1602_I2C = I2C(I2C_ID, sda = SDA_PIN, scl = SCL_PIN, freq = 400000)
self.LCD1602_I2C = i2c
self._showfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS
self._begin(self._row)
except BaseException:
Expand Down
4 changes: 4 additions & 0 deletions smibhid/lib/config/config_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
SDA_PIN = 8
SCL_PIN = 9
I2C_ID = 0
I2C_FREQ = 400000

## Sensors - Populate driver list with connected sensor modules from this supported list: ["SGP30", "BME280", "SCD30"]
SENSOR_MODULES = []

## Displays - Populate driver list with connected displays from this supported list: ["LCD1602"]
DISPLAY_DRIVERS = ["LCD1602"]
Expand Down
5 changes: 3 additions & 2 deletions smibhid/lib/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ class Display:
Example:
If an LCD1602 driver is configured to load, then issuing the command Display.print_startup() will render startup information appropriately on the 2x16 display if connected.
"""
def __init__(self) -> None:
def __init__(self, i2c) -> None:
self.log = uLogger("Display")
self.drivers = DISPLAY_DRIVERS
self.log.info("Init display")
self.enabled = False
self.i2c = i2c
self.screens = []
self._load_configured_drivers()
self.state = "Unknown"
Expand All @@ -30,7 +31,7 @@ def _load_configured_drivers(self) -> None:
if driver_class is None:
raise ValueError(f"Display driver class '{driver}' not registered.")

self.screens.append(driver_class())
self.screens.append(driver_class(self.i2c))

except Exception as e:
print(f"An error occurred while confguring display driver '{driver}': {e}")
Expand Down
11 changes: 8 additions & 3 deletions smibhid/lib/hid.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
from lib.display import Display
from lib.networking import WirelessNetwork
from lib.rfid.reader import RFIDReader
from config import RFID_ENABLED, CLOCK_FREQUENCY
from config import RFID_ENABLED, CLOCK_FREQUENCY, SDA_PIN, SCL_PIN, I2C_ID, I2C_FREQ
from lib.uistate import UIState
from lib.ui_log import UILog
from http.website import WebApp
from lib.pinger import Pinger
from machine import freq
from machine import freq, I2C
from lib.sensors import Sensors

class HID:

Expand All @@ -26,9 +27,11 @@ def __init__(self) -> None:
self.log.info("Setting CPU frequency to: " + str(CLOCK_FREQUENCY / 1000000) + "MHz")
freq(CLOCK_FREQUENCY)
self.loop_running = False
self.i2c = I2C(I2C_ID, sda = SDA_PIN, scl = SCL_PIN, freq = I2C_FREQ)
self.moduleConfig = ModuleConfig()
self.moduleConfig.register_display(Display())
self.moduleConfig.register_display(Display(self.i2c))
self.moduleConfig.register_wifi(WirelessNetwork())
self.moduleConfig.register_sensors(Sensors(self.i2c))
if RFID_ENABLED:
self.moduleConfig.register_rfid(RFIDReader(Event()))
self.display = self.moduleConfig.get_display()
Expand All @@ -38,6 +41,7 @@ def __init__(self) -> None:
self.ui_log = self.moduleConfig.get_ui_log()
self.space_state = SpaceState(self.moduleConfig, self)
self.pinger = Pinger(self.moduleConfig, self)
self.sensors = self.moduleConfig.get_sensors()
self.error_handler = ErrorHandler("HID")
self.error_handler.configure_display(self.display)
self.web_app = WebApp(self.moduleConfig, self)
Expand All @@ -60,6 +64,7 @@ def startup(self) -> None:
self.space_state.startup()
if self.reader:
self.reader.startup()
self.sensors.startup()
self.ui_log.startup()
self.web_app.startup()

Expand Down
11 changes: 10 additions & 1 deletion smibhid/lib/module_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def __init__(self) -> None:
self.wifi = None
self.reader = None
self.ui_log = None
self.sensors = None

def register_display(self, display: Display) -> None:
self.display = display
Expand All @@ -34,6 +35,9 @@ def register_rfid(self, reader: RFIDReader) -> None:

def register_ui_log(self, ui_log: UILog) -> None:
self.ui_log = ui_log

def register_sensors(self, sensors) -> None:
self.sensors = sensors

def get_display(self) -> Display:
if not self.display:
Expand All @@ -58,4 +62,9 @@ def get_ui_log(self) -> UILog:
self.log.warn("UI Log module not registered")
raise ModuleNotRegisteredError("UI Log")
return self.ui_log


def get_sensors(self):
if not self.sensors:
self.log.warn("Sensors module not registered")
raise ModuleNotRegisteredError("Sensors")
return self.sensors
Loading