Пульсоксиметр – это медицинский прибор, который позволяет не инвазивно и безболезненно измерять уровень сатурации кислорода в крови, то есть степень ее насыщенности кислородом. Во время текущей пандемии коронавируса Covid-19 спрос на пульсоксиметры резко возрос во всем мире. На нашем сайте вы можете посмотреть все проекты, связанные с борьбой с коронавирусом.
В данной статье мы рассмотрим создание пульсоксиметра на основе микроконтроллера ESP32 и датчика MAX30100, который будет измерять уровень кислорода в крови и передавать эти данные в сеть интернет, используя подключение к сети Wi-Fi, поэтому вы можете получить к ним доступ из любой точки земного шара. Таким образом, врач наблюдающий за пациентом, может удаленно следить за его состоянием здоровья, не вступая в ним непосредственный контакт и не подвергаясь опасности заражения коронавирусом. Ранее на нашем сайте мы уже рассматривали проект аналогичного пульсоксиметра на основе платы Arduino. Также у нас на сайте вы можете посмотреть все проекты, связанные с медициной.
Кроме анализа состояния больного коронавирусом данный пульсоксиметр можно также использовать при таких болезнях как астма, пневмония, анемия и проблемах с сердцем.
Но при создании данного проекта имейте ввиду, что датчик MAX30100 не имеет медицинского сертификата на измерение уровня кислорода в крови, поэтому применять описанный здесь проект пульсоксиметра следует с осторожностью, проверяя периодически его показания сертифицированным для этих целей медицинским прибором.
Датчик MAX30100
В датчик MAX30100 интегрированы модули для измерения уровня кислорода в крови и измерения частоты сердечного ритма (пульса). К микроконтроллерам его можно подключить по интерфейсу I2C.
Датчик содержит фотодетекторы и оптические элементы, в которых производится модуляция излучения красного или зеленого светодиодов импульсами. Ток светодиодов можно настроить в диапазоне от 0 до 50mA. На следующем рисунке представлен внешний вид датчика MAX30100.
Датчик работает с уровнями напряжений от 1.8V до 5.5V. Подтягивающие резисторы для интерфейса I2C включены в состав модуля.
Необходимые компоненты
- Модуль ESP32 (купить на AliExpress).
- Датчик MAX30100 (купить на AliExpress).
- Аккаунт на Adafruit IO.
- Источник питания 5V с поддерживаемым током не менее 1A.
- Кабель с Micro USB на USBA.
- Arduino IDE.
Схема проекта
Схема пульсоксиметра на модуле ESP32 и датчике MAX30100 представлена на следующем рисунке.
Как видите, схема очень проста. Контакты 21 и 22 модуля ESP32 подключены к контактам SDA и SCL датчика MAX30100. Запитывается вся схема от напряжения 5V.
Внешний вид собранной на макетной плате конструкции пульсоксиметра представлен на следующем рисунке.
Настройка сервиса Adafruit IO
Платформа Adafruit IO весьма удобна для реализации различных проектов тематики интернета вещей (IoT). Она предоставляет панель инструментом с колоссальным функционалом.
Для создания панели инструментов в Adafruit IO для работы с проектом нашего пульсоксиметра выполните следующую последовательность шагов:
Шаг 1. Создайте себе аккаунт на сервисе Adafruit IO, указав там свое имя, фамилию, адрес email, имя пользователя и пароль.
Шаг 2. После того как вы завершите свой процесс регистрации на сервисе Adafruit IO, у вас откроется шаблон панели инструментов. Нам на основе этого шаблона необходимо создать свою панель инструментов, введя ее имя и описание к ней.
Шаг 3. После заполнения первоначальных данных необходимо в панели создать инструмент для построения графиков и секцию управления датчиком.
Выберите блок переключателя. С его помощью мы будем включать и выключать наш пульсоксиметр.
Шаг 4. Введите имя для созданного блока переключателя. Аналогичным образом выберите блок для построения графиков.
Этот блок нам необходимо выбрать два раза потому что в нашем проекте мы будем строить два графика: один для отображения частоты сердечного ритма, а второй – для отображения сатурации кислорода в крови (SpO2).
Шаг 5. На заключительном шаге нам необходимо получить ключ adafruit – в дальнейшем он нам понадобится в программе.
Объяснение программы для модуля ESP32
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.
В данном проекте мы будем использовать достаточно много библиотек и все они будут нам необходимы. Первым делом подключим все эти библиотеки в программе.
1 2 3 4 5 6 |
#include <stdint.h> #include <Wire.h> #include <WiFi.h> #include "Adafruit_MQTT.h" #include "Adafruit_MQTT_Client.h" #include "MAX30100_PulseOximeter.h" //used arduino builtin MAX30100 lib (https://github.com/oxullo/Arduino-MAX30100) |
Далее укажем параметры подключения к сети WiFi – ее имя и пароль.
1 2 |
#define WLAN_SSID "xxxxxxxxx" #define WLAN_PASS "2581xxxxx2" |
Затем укажем параметры, необходимые для работы с сервисом Adafruit io.
1 2 3 4 5 |
#define AIO_UPDATE_RATE_SEC 5 #define AIO_SERVER "io.adafruit.com" #define AIO_SERVERPORT 1883 #define AIO_USERNAME "xxxxxxxxxxxxx" #define AIO_KEY "abcdefgh" |
Данные, считываемые пульсоксиметром, будут обновляться каждые 5 секунд и передаваться на сервер io.adafruit.com на порт 1883. Имя пользователя (username) и пароль (password) должны быть сгенерированы в панели инструментов (dashboard) сервиса adafruit IO.
Затем укажем контакты модуля ESP32, по которым будет осуществляться взаимодействие по интерфейсу I2C.
1 2 |
#define I2C_SDA 21 #define I2C_SCL 22 |
Следующие три переменные будут использоваться для хранения последнего сообщения и значений bpm (частота сердечного ритма) и spo2 (уровень кислорода в крови).
1 2 3 |
uint32_t tsLastReport = 0; float bpm_dt=0; float spo2_dt = 0; |
Протокол MQTT работает по принципу издатель-подписчик. В нашем проекте устройство, которое передает данные на сервер Adafruit, выступает в качестве издателя, а сам сервер Adafruit IO выступает в роли подписчика. Таким образом, всегда когда устройство публикует новые данные, сервер, поскольку он подписан на их получение, принимает эти данные и выполняет необходимые действия. Более подробно о работе протокола MQTT вы можете прочитать в этой статье.
Аналогичные процессы происходят если сервер публикует данные, а устройство подписано на их получение. В нашем проекте устройство передает данные SPO2 и BPM на сервер, он публикует их, также устройство считывает состояние переключателя ON-OFF с сервера, таким образом, оно подписано на получение этих данных о состоянии переключателя.
Вся эта модель издатель-подписчик запрограммирована в следующих строках кода:
1 2 3 4 5 6 |
WiFiClient client; Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY); Adafruit_MQTT_Subscribe sw_sub = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/switch"); // Notice MQTT paths for AIO follow the form: <username>/feeds/<feedname> Adafruit_MQTT_Publish bpm_pub = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/bpm"); Adafruit_MQTT_Publish spo2_pub = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/SpO2"); |
В функции void setup() мы инициализируем последовательную связь со скоростью 115200, интерфейс I2C, соединимся с сетью WiFi в соответствии с ранее указанными параметрами доступа к ней (имя и пароль), начнем процесс подписки протокола MQTT на данные о состоянии переключателя (ранее созданный нами переключатель на панели инструментов Adafruit IO).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
void setup() { Serial.begin(115200); Wire.begin(I2C_SDA, I2C_SCL); WiFi.begin(WLAN_SSID, WLAN_PASS); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); mqtt.subscribe(&sw_sub); Serial.print("Initializing pulse oximeter.."); // Initialize the PulseOximeter instance // Failures are generally due to an improper I2C wiring, missing power supply // or wrong target chip if (!pox.begin()) { Serial.println("FAILED"); for(;;); } else { Serial.println("SUCCESS"); } // The default current for the IR LED is 50mA and it could be changed // by uncommenting the following line. Check MAX30100_Registers.h for all the // available options. pox.setIRLedCurrent(MAX30100_LED_CURR_7_6MA); // Register a callback for the beat detection pox.setOnBeatDetectedCallback(onBeatDetected); stopReadPOX(); } |
После этого датчик max30100 начнет работать с установленным током светодиодов. Данный ток можно настраивать в заголовочных файлах (header files) датчика MAX30100. Также начинает работу функция измерения частоты сердечного ритма. После всех этих сделанных настроек работа датчика MAX30100 останавливается.э
В функции void loop() открывается соединение MQTT и модель издатель-подписчик проверяется каждые 5000 миллисекунд. В данном случае, если переключатель включен, то наш пульсоксиметр начинает измерять данные частоты сердечного ритма и уровня кислорода в крови и публиковать их на сервере. Если переключатель выключен, то все задачи, выполняемые нашим пульсоксиметром, временно приостанавливаются.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
void loop() { MQTT_connect(); Adafruit_MQTT_Subscribe *subscription; while ((subscription = mqtt.readSubscription(5000))) { if (subscription == &sw_sub) { Serial.print(F("Got: ")); Serial.println((char *)sw_sub.lastread); if (!strcmp((char*) sw_sub.lastread, "ON")) { Serial.print(("Starting POX... ")); startReadPOX(); BaseType_t xReturned; if(poxReadTaskHld == NULL){ xReturned = xTaskCreate( poxReadTask, /* Function that implements the task. */ "pox_read", /* Text name for the task. */ 1024*3, /* Stack size in words, not bytes. */ NULL, /* Parameter passed into the task. */ 2,/* Priority at which the task is created. */ &poxReadTaskHld ); /* Used to pass out the created task's handle. */ } delay(100); if(mqttPubTaskHld == NULL){ xReturned = xTaskCreate( mqttPubTask, /* Function that implements the task. */ "mqttPub", /* Text name for the task. */ 1024*3, /* Stack size in words, not bytes. */ NULL, /* Parameter passed into the task. */ 2,/* Priority at which the task is created. */ &mqttPubTaskHld ); /* Used to pass out the created task's handle. */ } } else { Serial.print(("Stoping POX... ")); // Detele POX read task if(poxReadTaskHld != NULL) vTaskDelete(poxReadTaskHld); poxReadTaskHld = NULL; } // Delete the MQTT Pub Task if(mqttPubTaskHld != NULL){ vTaskDelete(mqttPubTaskHld); mqttPubTaskHld = NULL; } stopReadPOX(); } } } } |
Тестирование работы пульсоксиметра
После того как схема собрана и программа загружена в модуль ESP32, можно приступать к тестированию работы проекта. Убедитесь в том, что в программе вы изменили настройки для входа в сеть Wi-Fi и для доступа к сервису Adafruit на свои.
После загрузки кода программы в модуль пульсоксиметр должен начать свою работу. Как видим на рисунке ниже, уровень кислорода в крови составляет 96%, а частота сердечного ритма колеблется в диапазоне от 78 до 81 удара в минуту. Также отображается время когда были считаны эти данные.
Более подробно работу проекта вы можете посмотреть на видео, приведенном в конце статьи.
Исходный код программы (скетча)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
#include <stdint.h> #include <Wire.h> #include <WiFi.h> #include "Adafruit_MQTT.h" #include "Adafruit_MQTT_Client.h" #include "MAX30100_PulseOximeter.h" //used arduino builtin MAX30100 lib (https://github.com/oxullo/Arduino-MAX30100) #define WLAN_SSID "xxxxxxxxx" #define WLAN_PASS "2581xxxxx2" #define AIO_UPDATE_RATE_SEC 5 #define AIO_SERVER "io.adafruit.com" #define AIO_SERVERPORT 1883 #define AIO_USERNAME "xxxxxx" #define AIO_KEY "abcdefgh" #define I2C_SDA 21 #define I2C_SCL 22 TaskHandle_t poxReadTaskHld = NULL; TaskHandle_t mqttPubTaskHld = NULL; // PulseOximeter is the higher-level interface to the sensor PulseOximeter pox; uint32_t tsLastReport = 0; float bpm_dt=0; float spo2_dt = 0; WiFiClient client; Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY); Adafruit_MQTT_Subscribe sw_sub = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/switch"); // Notice MQTT paths for AIO follow the form: <username>/feeds/<feedname> Adafruit_MQTT_Publish bpm_pub = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/bpm"); Adafruit_MQTT_Publish spo2_pub = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/SpO2"); // функция вызывается когда происходит обнаружение пульса void onBeatDetected() { Serial.println("Beat!") } /******************************************* функция чтобы приостановить считывание данных датчиком MAX30100 **************************************************/ void stopReadPOX(){ pox.shutdown(); } /******************************************* функция чтобы начать считывание данных датчиком MAX30100 **************************************************/ void startReadPOX(){ pox.resume(); } /******************************************* MAX30100 Read task **************************************************/ void poxReadTask( void * param ) { while(1){ // Make sure to call update as fast as possible pox.update(); vTaskDelay( 1 / portTICK_PERIOD_MS ); } poxReadTaskHld = NULL; vTaskDelete(NULL); // задача "убивает" саму себя } /******************************************* задача публикации данных в протоколе MQTT **************************************************/ void mqttPubTask( void * param ) { uint8_t sec_count=0; while(1){ Serial.print("Heart rate:"); float bpm_dt = pox.getHeartRate(); Serial.print(bpm_dt); Serial.print("bpm / SpO2:"); float spo2_dt = pox.getSpO2(); Serial.print(spo2_dt); Serial.println("%"); if(sec_count >= AIO_UPDATE_RATE_SEC){ if (! bpm_pub.publish(bpm_dt)) { Serial.println(F("Failed to publish bmp..")); } else { Serial.println(F("bmp publish OK!")); } if (! spo2_pub.publish(spo2_dt)) { Serial.println(F("Failed to publish SpO2..")); } else { Serial.println(F("SpO2 publish OK!")); } sec_count=0; } vTaskDelay( 1000 / portTICK_PERIOD_MS ); sec_count++; } mqttPubTaskHld = NULL; vTaskDelete(NULL); // kill itself } /******************************************** функция соединения с MQTT *******************************************************/ // Function to connect and reconnect as necessary to the MQTT server. void MQTT_connect() { int8_t ret; if (mqtt.connected()) { return; } Serial.print("Connecting to MQTT... "); uint8_t retries = 3; while ((ret = mqtt.connect()) != 0) { Serial.println(mqtt.connectErrorString(ret)); Serial.println("Retrying MQTT connection in 5 seconds..."); mqtt.disconnect(); delay(5000); retries--; if (retries == 0) { while (1); } } Serial.println("MQTT Connected!"); } /**************************************************************************************************/ void setup() { Serial.begin(115200); Wire.begin(I2C_SDA, I2C_SCL); WiFi.begin(WLAN_SSID, WLAN_PASS); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); mqtt.subscribe(&sw_sub); Serial.print("Initializing pulse oximeter.."); // Initialize the PulseOximeter instance // Failures are generally due to an improper I2C wiring, missing power supply // or wrong target chip if (!pox.begin()) { Serial.println("FAILED"); for(;;); } else { Serial.println("SUCCESS"); } // ток по умолчанию для инфракрасного светодиода (IR LED) датчика MAX30100 равен 50mA, но его можно изменить // раскомментировав следующую строку. Посмотрите файл MAX30100_Registers.h на предмет всех возможных настроек pox.setIRLedCurrent(MAX30100_LED_CURR_7_6MA); // Register a callback for the beat detection pox.setOnBeatDetectedCallback(onBeatDetected); stopReadPOX(); } void loop() { MQTT_connect(); Adafruit_MQTT_Subscribe *subscription; while ((subscription = mqtt.readSubscription(5000))) { if (subscription == &sw_sub) { Serial.print(F("Got: ")); Serial.println((char *)sw_sub.lastread); if (!strcmp((char*) sw_sub.lastread, "ON")) { Serial.print(("Starting POX... ")); startReadPOX(); BaseType_t xReturned; if(poxReadTaskHld == NULL){ xReturned = xTaskCreate( poxReadTask, /* функция, которая выполняет задачу. */ "pox_read", /* текстовое имя для задачи. */ 1024*3, /* размер стека в словах (не в байтах) */ NULL, /* Parameter passed into the task. */ 2,/* приоритет с которым создается задача */ &poxReadTaskHld ); /* Used to pass out the created task's handle. */ } delay(100); if(mqttPubTaskHld == NULL){ xReturned = xTaskCreate( mqttPubTask, /* Function that implements the task. */ "mqttPub", /* Text name for the task. */ 1024*3, /* Stack size in words, not bytes. */ NULL, /* Parameter passed into the task. */ 2,/* Priority at which the task is created. */ &mqttPubTaskHld ); /* Used to pass out the created task's handle. */ } } else { Serial.print(("Stoping POX... ")); // Detele POX read task if(poxReadTaskHld != NULL){ vTaskDelete(poxReadTaskHld); poxReadTaskHld = NULL; } // Delete the MQTT Pub Task if(mqttPubTaskHld != NULL){ vTaskDelete(mqttPubTaskHld); mqttPubTaskHld = NULL; } stopReadPOX(); } } } } |