From 6d6803612a0e3d2aa499ebb8e0dc06dea09c20b4 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 23 Jul 2021 16:37:21 +0100 Subject: [PATCH] Add SCD41 MicroPython bindings --- .../examples/breakout_scd41/scd41_demo.py | 15 ++ micropython/modules/breakout_scd41/README.md | 39 ++++ .../modules/breakout_scd41/breakout_scd41.c | 54 ++++++ .../modules/breakout_scd41/breakout_scd41.cpp | 177 ++++++++++++++++++ .../modules/breakout_scd41/breakout_scd41.h | 15 ++ .../modules/breakout_scd41/micropython.cmake | 32 ++++ micropython/modules/micropython.cmake | 1 + 7 files changed, 333 insertions(+) create mode 100644 micropython/examples/breakout_scd41/scd41_demo.py create mode 100644 micropython/modules/breakout_scd41/README.md create mode 100755 micropython/modules/breakout_scd41/breakout_scd41.c create mode 100644 micropython/modules/breakout_scd41/breakout_scd41.cpp create mode 100644 micropython/modules/breakout_scd41/breakout_scd41.h create mode 100644 micropython/modules/breakout_scd41/micropython.cmake diff --git a/micropython/examples/breakout_scd41/scd41_demo.py b/micropython/examples/breakout_scd41/scd41_demo.py new file mode 100644 index 00000000..cdcadb8c --- /dev/null +++ b/micropython/examples/breakout_scd41/scd41_demo.py @@ -0,0 +1,15 @@ +import time + +import pimoroni_i2c +import breakout_scd41 + +i2c = pimoroni_i2c.PimoroniI2C(4, 5) + +breakout_scd41.init(i2c) +breakout_scd41.start() + +while True: + if breakout_scd41.ready(): + co2, temperature, humidity = breakout_scd41.measure() + print(co2, temperature, humidity) + time.sleep(1.0) diff --git a/micropython/modules/breakout_scd41/README.md b/micropython/modules/breakout_scd41/README.md new file mode 100644 index 00000000..278d6086 --- /dev/null +++ b/micropython/modules/breakout_scd41/README.md @@ -0,0 +1,39 @@ +# SCD41 CO2 Sensor Driver + +## Getting Started + +Construct a new PimoroniI2C instance for your specific board. Breakout Garden uses pins 4 & 5 and Pico Explorer uses pins 20 & 21. + +Since SCD41 has a fixed I2C address and the Sensirion SCD4x library is used under the hood, it's wrapped up as a module for Python. + +Import the `breakout_scd41` and call `init` to set up I2C: + +```python +import time + +import pimoroni_i2c +import breakout_scd41 + +i2c = pimoroni_i2c.PimoroniI2C(4, 5) + +breakout_scd41.init(i2c) +``` + +## Taking Measurements + +Before taking a measurement you must start periodic measurement by calling `start()`. + +Poll on `ready()` and use `measure()` to read the result when it's `True`: + +```python +breakout_scd41.start() + +while True: + if breakout_scd41.ready(): + co2, temperature, humidity = breakout_scd41.measure() + print(co2, temperature, humidity) + time.sleep(1.0) +``` + +The `measure()` method will return a Tuple containing the CO2 reading, temperature in degrees C and humidity. + diff --git a/micropython/modules/breakout_scd41/breakout_scd41.c b/micropython/modules/breakout_scd41/breakout_scd41.c new file mode 100755 index 00000000..d8a78021 --- /dev/null +++ b/micropython/modules/breakout_scd41/breakout_scd41.c @@ -0,0 +1,54 @@ +#include "breakout_scd41.h" + +/***** Constants *****/ + + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// SCD41 Module +//////////////////////////////////////////////////////////////////////////////////////////////////// + +/***** Module Functions *****/ +// Init, optionally (though you really should supply it) accepts a PimoroniI2C instance +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(scd41_init_obj, 0, scd41_init); + +// Start/Stop measurement, no args (module-level, so no "self") +STATIC MP_DEFINE_CONST_FUN_OBJ_0(scd41_start_periodic_measurement_obj, scd41_start_periodic_measurement); +STATIC MP_DEFINE_CONST_FUN_OBJ_0(scd41_stop_periodic_measurement_obj, scd41_stop_periodic_measurement); +STATIC MP_DEFINE_CONST_FUN_OBJ_0(scd41_get_data_ready_obj, scd41_get_data_ready); + +STATIC MP_DEFINE_CONST_FUN_OBJ_1(scd41_set_temperature_offset_obj, scd41_set_temperature_offset); +STATIC MP_DEFINE_CONST_FUN_OBJ_0(scd41_get_temperature_offset_obj, scd41_get_temperature_offset); + +STATIC MP_DEFINE_CONST_FUN_OBJ_1(scd41_set_sensor_altitude_obj, scd41_set_sensor_altitude); +STATIC MP_DEFINE_CONST_FUN_OBJ_1(scd41_set_ambient_pressure_obj, scd41_set_ambient_pressure); + +// No args here, either, we're home free! +STATIC MP_DEFINE_CONST_FUN_OBJ_0(scd41_read_measurement_obj, scd41_read_measurement); + +/***** Globals Table *****/ +STATIC const mp_map_elem_t scd41_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_breakout_scd41) }, + { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&scd41_init_obj) }, + { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&scd41_start_periodic_measurement_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&scd41_stop_periodic_measurement_obj) }, + { MP_ROM_QSTR(MP_QSTR_measure), MP_ROM_PTR(&scd41_read_measurement_obj) }, + { MP_ROM_QSTR(MP_QSTR_ready), MP_ROM_PTR(&scd41_get_data_ready_obj) }, + + { MP_ROM_QSTR(MP_QSTR_set_temperature_offset), MP_ROM_PTR(&scd41_set_temperature_offset_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_temperature_offset), MP_ROM_PTR(&scd41_get_temperature_offset_obj) }, + + { MP_ROM_QSTR(MP_QSTR_set_sensor_altitude), MP_ROM_PTR(&scd41_set_sensor_altitude_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_ambient_pressure), MP_ROM_PTR(&scd41_set_ambient_pressure_obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(mp_module_scd41_globals, scd41_globals_table); + +/***** Module Definition *****/ +const mp_obj_module_t scd41_user_cmodule = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t*)&mp_module_scd41_globals, +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +MP_REGISTER_MODULE(MP_QSTR_breakout_scd41, scd41_user_cmodule, MODULE_BREAKOUT_SCD41_ENABLED); +//////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/micropython/modules/breakout_scd41/breakout_scd41.cpp b/micropython/modules/breakout_scd41/breakout_scd41.cpp new file mode 100644 index 00000000..b15d3503 --- /dev/null +++ b/micropython/modules/breakout_scd41/breakout_scd41.cpp @@ -0,0 +1,177 @@ +#include "hardware/spi.h" +#include "hardware/sync.h" +#include "pico/binary_info.h" + +#include "scd4x_i2c.h" +#include "sensirion_common.h" +#include "sensirion_i2c_hal.h" +#include "common/pimoroni_i2c.hpp" + +using namespace pimoroni; + +bool scd41_initialised = false; + + +extern "C" { +#include "breakout_scd41.h" +#include "pimoroni_i2c.h" + +/***** I2C Struct *****/ +typedef struct _PimoroniI2C_obj_t { + mp_obj_base_t base; + I2C *i2c; +} _PimoroniI2C_obj_t; + +#define NOT_INITIALISED_MSG "SCD41: Not initialised. Call scd41.init() first." +#define READ_FAIL_MSG "SCD41: Reading failed." +#define FAIL_MSG "SCD41: Error." +#define SAMPLE_FAIL_MSG "SCD41: Read invalid sample." + +mp_obj_t scd41_init(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + + enum { ARG_i2c }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_i2c, MP_ARG_OBJ, {.u_obj = nullptr} } + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Perform the I2C type checking incantations + if(!MP_OBJ_IS_TYPE(args[ARG_i2c].u_obj, &PimoroniI2C_type)) { + mp_raise_ValueError(MP_ERROR_TEXT("SCD41: Bad i2C object")); + return mp_const_none; + } + + _PimoroniI2C_obj_t *i2c = (_PimoroniI2C_obj_t *)MP_OBJ_TO_PTR(args[ARG_i2c].u_obj); + + sensirion_i2c_hal_init(i2c->i2c); + scd41_initialised = true; + + return mp_const_none; +} + +mp_obj_t scd41_stop_periodic_measurement() { + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + scd4x_stop_periodic_measurement(); + return mp_const_none; +} + +mp_obj_t scd41_start_periodic_measurement() { + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + int error = scd4x_start_periodic_measurement(); + if(error) { + mp_raise_msg(&mp_type_RuntimeError, FAIL_MSG); + } + + return mp_const_none; +} + +mp_obj_t scd41_get_data_ready() { + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + uint16_t data_ready = 0; + int error = scd4x_get_data_ready_status(&data_ready); + if(error) { + mp_raise_msg(&mp_type_RuntimeError, READ_FAIL_MSG); + return mp_const_none; + } + // The datasheet doesn't really say *which* bit might be 1 if data is ready... + // so check if the least significant eleven bits are != 0 + return (data_ready & 0x7ff) ? mp_const_true : mp_const_false; +} + +mp_obj_t scd41_set_temperature_offset(mp_obj_t offset) { + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + float o = mp_obj_get_float(offset); + int error = scd4x_set_temperature_offset(o); + if(error) { + mp_raise_msg(&mp_type_RuntimeError, FAIL_MSG); + } + + return mp_const_none; +} + +mp_obj_t scd41_get_temperature_offset() { + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + + int32_t t_offset; + int error = scd4x_get_temperature_offset(&t_offset); + if(error) { + mp_raise_msg(&mp_type_RuntimeError, FAIL_MSG); + return mp_const_none; + } + + return mp_obj_new_int(t_offset); +} + +mp_obj_t scd41_set_sensor_altitude(mp_obj_t altitude) { + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + int a = mp_obj_get_int(altitude); + int error = scd4x_set_sensor_altitude(a); + if(error) { + mp_raise_msg(&mp_type_RuntimeError, FAIL_MSG); + } + + return mp_const_none; +} + +mp_obj_t scd41_set_ambient_pressure(mp_obj_t pressure) { + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + int p = mp_obj_get_int(pressure); + int error = scd4x_set_ambient_pressure(p); + if(error) { + mp_raise_msg(&mp_type_RuntimeError, FAIL_MSG); + } + + return mp_const_none; +} + + +mp_obj_t scd41_read_measurement() { + uint16_t co2; + int32_t temperature; + int32_t humidity; + if(!scd41_initialised) { + mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); + return mp_const_none; + } + int error = scd4x_read_measurement(&co2, &temperature, &humidity); + if(error) { + mp_raise_msg(&mp_type_RuntimeError, READ_FAIL_MSG); + return mp_const_none; + } + + if(co2 == 0) { + mp_raise_msg(&mp_type_RuntimeError, SAMPLE_FAIL_MSG); + return mp_const_none; + } + + mp_obj_t tuple[3]; + tuple[0] = mp_obj_new_float(co2); + tuple[1] = mp_obj_new_float(temperature / 1000.0f); + tuple[2] = mp_obj_new_float(humidity / 1000.0f); + return mp_obj_new_tuple(3, tuple); +} +} diff --git a/micropython/modules/breakout_scd41/breakout_scd41.h b/micropython/modules/breakout_scd41/breakout_scd41.h new file mode 100644 index 00000000..a02c2448 --- /dev/null +++ b/micropython/modules/breakout_scd41/breakout_scd41.h @@ -0,0 +1,15 @@ +// Include MicroPython API. +#include "py/runtime.h" +#include "py/objstr.h" + +// Declare the functions we'll make available in Python +extern mp_obj_t scd41_init(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t scd41_start_periodic_measurement(); +extern mp_obj_t scd41_stop_periodic_measurement(); +extern mp_obj_t scd41_read_measurement(); +extern mp_obj_t scd41_get_data_ready(); + +extern mp_obj_t scd41_set_temperature_offset(mp_obj_t offset); +extern mp_obj_t scd41_get_temperature_offset(); +extern mp_obj_t scd41_set_sensor_altitude(mp_obj_t altitude); +extern mp_obj_t scd41_set_ambient_pressure(mp_obj_t pressure); diff --git a/micropython/modules/breakout_scd41/micropython.cmake b/micropython/modules/breakout_scd41/micropython.cmake new file mode 100644 index 00000000..45488031 --- /dev/null +++ b/micropython/modules/breakout_scd41/micropython.cmake @@ -0,0 +1,32 @@ +set(MOD_NAME breakout_scd41) +set(DRIVER ${CMAKE_CURRENT_LIST_DIR}/../../../drivers/scd4x) +string(TOUPPER ${MOD_NAME} MOD_NAME_UPPER) +add_library(usermod_${MOD_NAME} INTERFACE) + +target_sources(usermod_${MOD_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c + ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.cpp + ${DRIVER}/src/scd4x_i2c.c + ${DRIVER}/src/sensirion_common.c + ${DRIVER}/src/sensirion_i2c.c + ${DRIVER}/i2c_hal.cpp + ${DRIVER}/scd4x.cpp +) + +target_include_directories(usermod_${MOD_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR} + ${DRIVER} + ${DRIVER}/src +) + +target_compile_definitions(usermod_${MOD_NAME} INTERFACE + -DMODULE_${MOD_NAME_UPPER}_ENABLED=1 +) + +target_link_libraries(usermod INTERFACE usermod_${MOD_NAME}) + +set_source_files_properties( + ${CMAKE_CURRENT_LIST_DIR}/breakout_scd41.c + PROPERTIES COMPILE_FLAGS + "-Wno-discarded-qualifiers -Wno-implicit-int" +) \ No newline at end of file diff --git a/micropython/modules/micropython.cmake b/micropython/modules/micropython.cmake index ea44e0a1..b920d109 100644 --- a/micropython/modules/micropython.cmake +++ b/micropython/modules/micropython.cmake @@ -28,6 +28,7 @@ include(breakout_bme68x/micropython) include(breakout_bme280/micropython) include(breakout_bmp280/micropython) include(breakout_icp10125/micropython) +include(breakout_scd41/micropython) include(pico_scroll/micropython) include(pico_rgb_keypad/micropython)