diff --git a/smibhid/README.md b/smibhid/README.md index a1e9f57..b923c7d 100644 --- a/smibhid/README.md +++ b/smibhid/README.md @@ -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) @@ -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 @@ -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 diff --git a/smibhid/config.py b/smibhid/config.py index 741a7e1..a878af5 100644 --- a/smibhid/config.py +++ b/smibhid/config.py @@ -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"] diff --git a/smibhid/http/website.py b/smibhid/http/website.py index 32c9f93..b03e347 100644 --- a/smibhid/http/website.py +++ b/smibhid/http/website.py @@ -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: @@ -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() @@ -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}") @@ -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/', sensors = self.sensors, logger = self.log) + self.app.add_resource(Readings, '/api/sensors/readings/', sensors = self.sensors, logger = self.log) + self.app.add_resource(Readings, '/api/sensors/readings', module = "", sensors = self.sensors, logger = self.log) class WLANMAC(): @@ -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 diff --git a/smibhid/http/www/api.html b/smibhid/http/www/api.html index 6cae4b7..41795ac 100644 --- a/smibhid/http/www/api.html +++ b/smibhid/http/www/api.html @@ -10,6 +10,10 @@

Endpoints