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

Merged
merged 16 commits into from
Mar 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
57 changes: 45 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,14 @@ 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/modules/<module>', sensors = self.sensors, logger = self.log)
#self.app.add_resource(Readings, '/api/sensors/modules/<module>/readings/latest', sensors = self.sensors, logger = self.log) #TODO: Fix tinyweb to allow for multiple parameters https://github.com/belyalov/tinyweb/pull/51
self.app.add_resource(Readings, '/api/sensors/readings/latest', module = "", sensors = self.sensors, logger = self.log)

class WLANMAC():

Expand All @@ -88,28 +95,54 @@ 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}")
sensor_list = sensors.get_sensors(module)
logger.info(f"Available sensors: {sensor_list}")
html = dumps(sensor_list)
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
74 changes: 62 additions & 12 deletions smibhid/http/www/api.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,68 @@
<body>
<h1>SMIBHID - API</h1>
<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>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>
<li>Add file: POST: Submit Value = "Add", URL = Str: id = "url" </li>
<li>Remove file: POST: Submit Value = "Remove", Str: URL </li>
</ul>
</li>
<li>Reset (POST): <a href="/api/reset">/api/reset - reset SMIBHID</a></li>
</ul>
<table>
<tr>
<th>Endpoint</th>
<th>Method</th>
<th>Options</th>
<th>Description</th>
</tr>
<tr>
<td><a href="/api/wlan/mac">/api/wlan/mac</a></td>
<td>GET</td>
<td></td>
<td>Get WLAN MAC address</td>
</tr>
<tr>
<td><a href="/api/version">/api/version</a></td>
<td>GET</td>
<td></td>
<td>Get firmware version</td>
</tr>
<tr>
<td>/api/firmware_files</td>
<td>GET, POST</td>
<td>
<ul>
<li>List files: GET</li>
<li>Add file: POST: Submit Value = "Add", URL = Str: id = "url" </li>
<li>Remove file: POST: Submit Value = "Remove", Str: URL </li>
</ul>
</td>
<td>List, add or remove URLs staged for download and patch on reset</td>
</tr>
<tr>
<td><a href="/api/reset">/api/reset</a></td>
<td>POST</td>
<td></td>
<td>Reset SMIBHID</td>
</tr>
<tr>
<td><a href="/api/sensors/modules">/api/sensors/modules</a></td>
<td>GET</td>
<td></td>
<td>List sensor modules</td>
</tr>
<tr>
<td>/api/sensors/modules/{module}</td>
<td>GET</td>
<td>Insert module name from module list in {module} parameter</td>
<td>List module sensors</td>
</tr>
<!-- <tr>
<td>/api/sensors/modules/{module}/readings/latest</td>
<td>GET</td>
<td></td>
<td>Get latest module sensor readings</td>
</tr> -->
<tr>
<td><a href="/api/sensors/readings/latest">/api/sensors/readings/latest</a></td>
<td>GET</td>
<td></td>
<td>Get latest sensor readings from all modules</td>
</tr>
</table>

<p />

Expand Down
28 changes: 28 additions & 0 deletions smibhid/http/www/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,32 @@ h2 {
a {
color: purple;
font-family: Arial, Helvetica, sans-serif;
}

table {
border-collapse: collapse;
border: 1px solid black;
width: 80%;
}

th {
color: darkgreen;
font-family: Arial, Helvetica, sans-serif;
text-align: left;
padding-top: 5px;
padding-left: 5px;
padding-right: 20px;
padding-bottom: 5px;
border: 1px solid black;
}

td {
color: darkslategray;
font-family: Arial, Helvetica, sans-serif;
text-align: left;
padding-top: 5px;
padding-left: 5px;
padding-right: 20px;
padding-bottom: 5px;
border: 1px solid black;
}
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
Loading