diff --git a/preload/apps/spo2/__init__.py b/preload/apps/spo2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..152cf15131c6f58d13810a8a316df943e78c5694 --- /dev/null +++ b/preload/apps/spo2/__init__.py @@ -0,0 +1,96 @@ +import max86150 +import display + + +class SPO2: + def __init__(self): + self.sensor = None + self.RATE = 128 + self.HISTORY_MAX = self.RATE * 4 + self.history = [] + self.update_screen = 0 + self.disp = display.open() + self.DRAW_AFTER_SAMPLES = 5 + self.histogram_offset = 0 + self.WIDTH = 160 + self.SCALE_FACTOR = 30 + self.OFFSET_Y = 49 + self.COLOR_BACKGROUND = [0, 0, 0] + self.avg = [0] * 10 + self.avg_pos = 0 + self.last_sample = 0.0 + self.filtered_value = 0.0 + + def open(self): + def callback(datasets): + self.update_screen += len(datasets) + + self.update_history(datasets) + + # don't update on every callback + if self.update_screen >= self.DRAW_AFTER_SAMPLES: + self.draw_histogram() + + self.sensor = max86150.MAX86150(callback) + while True: + pass + + def update_history(self, datasets): + for val in datasets: + # get red value (first in tuple) + self.avg[self.avg_pos] = val[0] + if self.avg_pos < 9: + self.avg_pos += 1 + else: + self.avg_pos = 0 + + avg_data = sum(self.avg) / 10 + # DC offset removal + self.filtered_value = 0.9 * ( + self.filtered_value + avg_data - self.last_sample + ) + self.last_sample = avg_data + self.history.append(self.filtered_value) + + # trim old elements + self.history = self.history[-self.HISTORY_MAX :] + + def draw_histogram(self): + self.disp.clear(self.COLOR_BACKGROUND) + + # offset in pause_histogram mode + window_end = len(self.history) - self.histogram_offset + s_start = max(0, window_end - (self.RATE * 2)) + s_end = max(0, window_end) + s_draw = max(0, s_end - self.WIDTH) + + # get max value and calc scale + value_max = max(abs(x) for x in self.history[s_start:s_end]) + scale = self.SCALE_FACTOR / (value_max if value_max > 0 else 1) + + # draw histogram + draw_points = ( + int(x * scale + self.OFFSET_Y) for x in self.history[s_draw:s_end] + ) + + prev = next(draw_points) + for x, value in enumerate(draw_points): + self.disp.line(x, prev, x + 1, value) + prev = value + + self.disp.update() + self.update_screen = 0 + + def close(self): + if self.self is not None: + self.sensor.close() + self.sensor = None + + +if __name__ == "__main__": + sensor = SPO2() + try: + sensor.open() + except KeyboardInterrupt as e: + sensor.close() + raise e diff --git a/preload/apps/spo2/metadata.json b/preload/apps/spo2/metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..427dc08a2177561ff74eb114fc76b48e9cb77977 --- /dev/null +++ b/preload/apps/spo2/metadata.json @@ -0,0 +1 @@ +{"name":"SPO2","description":"A simple oximetetry monitor.","category":"hardware","author":"card10 contributors","revision":-1,"source":"preload"} diff --git a/pycardium/meson.build b/pycardium/meson.build index 3007f7b0d2855813c15fc327e1eca1d329bc28d3..931662c251b4c52bc3cb6305351bb9f90fb67aad 100644 --- a/pycardium/meson.build +++ b/pycardium/meson.build @@ -9,6 +9,7 @@ modsrc = files( 'modules/interrupt.c', 'modules/light_sensor.c', 'modules/max30001-sys.c', + 'modules/max86150.c', 'modules/os.c', 'modules/personal_state.c', 'modules/power.c', diff --git a/pycardium/modules/interrupt.c b/pycardium/modules/interrupt.c index 65e3ce63cd2b6bfdd8b9d0393581435d44dfb1aa..ad3b86d73424c91bfe6dde576f92dd4a842da968 100644 --- a/pycardium/modules/interrupt.c +++ b/pycardium/modules/interrupt.c @@ -97,6 +97,8 @@ static const mp_rom_map_elem_t interrupt_module_globals_table[] = { MP_OBJ_NEW_SMALL_INT(EPIC_INT_BHI160_GYROSCOPE) }, { MP_ROM_QSTR(MP_QSTR_MAX30001_ECG), MP_OBJ_NEW_SMALL_INT(EPIC_INT_MAX30001_ECG) }, + { MP_ROM_QSTR(MP_QSTR_MAX86150), + MP_OBJ_NEW_SMALL_INT(EPIC_INT_MAX86150) }, }; static MP_DEFINE_CONST_DICT( diff --git a/pycardium/modules/max86150.c b/pycardium/modules/max86150.c new file mode 100644 index 0000000000000000000000000000000000000000..511bfa2b1a427e07e71e095acb1f741b267e826b --- /dev/null +++ b/pycardium/modules/max86150.c @@ -0,0 +1,89 @@ +#include "py/obj.h" +#include "py/objlist.h" +#include "py/runtime.h" +#include "py/builtin.h" +#include "api/common.h" +#include "mphalport.h" + +#include "epicardium.h" + +STATIC mp_obj_t mp_max86150_enable_sensor(size_t n_args, const mp_obj_t *args) +{ + struct max86150_sensor_config cfg = { 0 }; + cfg.sample_buffer_len = mp_obj_get_int(args[0]); + cfg.ppg_sample_rate = mp_obj_get_int(args[1]); + + int stream_id = epic_max86150_enable_sensor(&cfg, sizeof(cfg)); + + if (stream_id < 0) { + mp_raise_OSError(-stream_id); + } + + return MP_OBJ_NEW_SMALL_INT(stream_id); +} + +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN( + mp_max86150_enable_sensor_obj, 2, 2, mp_max86150_enable_sensor +); + +STATIC mp_obj_t mp_max86150_read_sensor(mp_obj_t stream_id_in) +{ + struct max86150_sensor_data buf[256]; + int stream_id = mp_obj_get_int(stream_id_in); + + int n = epic_stream_read(stream_id, buf, sizeof(buf)); + + mp_obj_list_t *list = mp_obj_new_list(0, NULL); + for (int i = 0; i < n; i++) { + mp_obj_t tuple[3]; + + tuple[0] = mp_obj_new_int(buf[i].red); + tuple[1] = mp_obj_new_int(buf[i].ir); + tuple[2] = mp_obj_new_int(buf[i].ecg); + + mp_obj_list_append(list, mp_obj_new_tuple(3, tuple)); + } + + return MP_OBJ_FROM_PTR(list); +} + +STATIC MP_DEFINE_CONST_FUN_OBJ_1( + mp_max86150_read_sensor_obj, mp_max86150_read_sensor +); + +STATIC mp_obj_t mp_max86150_disable_sensor(void) +{ + int ret = epic_max86150_disable_sensor(); + + if (ret != 0) { + mp_raise_OSError(-ret); + } + + return mp_const_none; +} + +STATIC MP_DEFINE_CONST_FUN_OBJ_0( + mp_max86150_disable_sensor_obj, mp_max86150_disable_sensor +); + +STATIC const mp_rom_map_elem_t max86150_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_max86150) }, + { MP_ROM_QSTR(MP_QSTR_enable_sensor), + MP_ROM_PTR(&mp_max86150_enable_sensor_obj) }, + { MP_ROM_QSTR(MP_QSTR_read_sensor), + MP_ROM_PTR(&mp_max86150_read_sensor_obj) }, + { MP_ROM_QSTR(MP_QSTR_disable_sensor), + MP_ROM_PTR(&mp_max86150_disable_sensor_obj) }, +}; +STATIC MP_DEFINE_CONST_DICT( + max86150_module_globals, max86150_module_globals_table +); + +const mp_obj_module_t max86150_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&max86150_module_globals, +}; + +/* clang-format off */ +/* Register the module to make it available in Python */ +MP_REGISTER_MODULE(MP_QSTR_sys_max86150, max86150_module, MODULE_MAX86150_ENABLED); diff --git a/pycardium/modules/py/max86150.py b/pycardium/modules/py/max86150.py new file mode 100644 index 0000000000000000000000000000000000000000..08fdecc1582aafae3105fb05b1424bdbde7814d3 --- /dev/null +++ b/pycardium/modules/py/max86150.py @@ -0,0 +1,77 @@ +import sys_max86150 +import uerrno +import interrupt + + +class MAX86150: + """ + The MAX86150 class provides a stram interface to the MAX86150 PPG and ECG. + + .. code-block:: python + + import MAX86150 + m = max86150.MAX86150() + m.read() + m.close() + """ + + def __init__(self, callback=None, sample_buffer_len=128, sample_rate=200): + """ + Initializes the MAX86150 (if it is not already running). + + :param callback: If not None: A callback which is called with the data when ever new data is available + + """ + self.active = False + self.stream_id = -uerrno.ENODEV + self.interrupt_id = interrupt.MAX86150 + self._callback = callback + self.sample_rate = sample_rate + self.sample_buffer_len = sample_buffer_len + self.enable_sensor() + + def enable_sensor(self): + """ + Enables the sensor. Automatically called by __init__. + """ + interrupt.disable_callback(self.interrupt_id) + interrupt.set_callback(self.interrupt_id, self._interrupt) + self.stream_id = sys_max86150.enable_sensor( + self.sample_buffer_len, self.sample_rate + ) + + self.active = True + + if self._callback: + interrupt.enable_callback(self.interrupt_id) + + def __enter__(self): + return self + + def __exit__(self, _et, _ev, _t): + self.close() + + def close(self): + """ + Close the currently open connection to the sensor. + """ + + if self.active: + self.active = False + sys_max86150.disable_sensor() + + interrupt.disable_callback(self.interrupt_id) + interrupt.set_callback(self.interrupt_id, None) + + def read(self): + """ + Read as many samples (signed integer) as currently available. + """ + assert self.active, "Sensor is inactive" + return sys_max86150.read_sensor(self.stream_id) + + def _interrupt(self, _): + if self.active: + data = self.read() + if self._callback: + self._callback(data) diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build index df93ce38da16d1908dd86ac5e57c49839a4f4220..fe26c64e2c87640a580e70f535d46d5a5b1b961f 100644 --- a/pycardium/modules/py/meson.build +++ b/pycardium/modules/py/meson.build @@ -7,6 +7,7 @@ python_modules = files( 'ledfx.py', 'leds.py', 'max30001.py', + 'max86150.py', 'pride.py', 'simple_menu.py', diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h index ba31836e8e470da58c9fe1e6a56e43f48f4c9236..c2c1fdace490c618807adca14bca281276714741 100644 --- a/pycardium/modules/qstrdefs.h +++ b/pycardium/modules/qstrdefs.h @@ -181,7 +181,9 @@ Q(CHAOS) Q(COMMUNICATION) Q(CAMP) +/* required for interrupts */ Q(MAX30001_ECG) +Q(MAX86150) /* ws2812 */ Q(ws2812) diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h index 03fd3d3ff9a087cfc5522fb371d5c453dfddeb79..3d6ae86af0bb255b111f1c04cdb0c6a2c837414a 100644 --- a/pycardium/mpconfigport.h +++ b/pycardium/mpconfigport.h @@ -49,6 +49,7 @@ int mp_hal_trng_read_int(void); /* Modules */ #define MODULE_BHI160_ENABLED (1) +#define MODULE_MAX86150_ENABLED (1) #define MODULE_BME680_ENABLED (1) #define MODULE_BUTTONS_ENABLED (1) #define MODULE_DISPLAY_ENABLED (1)