赞
踩
年前买了一个MAX30102模块,在家无聊做了这个demo对一些相关的知识进行学习。
主要学习的内容:
实现的效果:
实现的思路:
心率基本上正确,血氧图一乐。
LED发出光→心脏泵送血液、呼吸、体温等因素影响光的投射/反射→光电二极管采集光量转换成电信号→ADC采集得到PPG
PPG名义上仅用于确定心率,,可以通过透射吸收(如在指尖)或反射(如在前额)获得。
PPG波形的DC分量对应于来自组织的透射或反射光信号,并且取决于组织的结构以及动脉( artery )和静脉血液( venous blood )的平均体积。直流分量随呼吸而缓慢变化,而交流分量随心跳周期收缩期和舒张期之间的血容量变化而波动。交流分量的基频取决于心率(HR),并叠加在直流分量上。
脱氧血红蛋白Deoxy(RHb)、氧性血红蛋白Oxy(O2Hb)、羧性血红蛋白(COHb)和高铁血红蛋白(MetHb)在不同波长下的光吸收。
PPG信号会受到光的波长、测量位置、接触力度、运动伪影、环境光强和环境温度的影响。
统计出脉冲间隔就可以计算出心率。
血氧的概念
血红蛋白可分为正常血红蛋白和异常血红蛋白,正常血红蛋白能结合氧气,而异常血红蛋白不能结合氧气。正常血红蛋白包括RHb和O2Hb,而异常血红蛋白包括羧基血红蛋白(COHb)、高铁血红蛋白(MetHb)和硫血红蛋白(SHb)(图1)。总血红蛋白浓度(tHb)表示为:
t H b = O 2 H b + R H b + M e t H b + C O H b + S H b tHb = O2Hb + RHb + Met Hb + COHb + SHb tHb=O2Hb+RHb+MetHb+COHb+SHb
SHb很少,可以从计算中省略。在正常情况下,只考虑能携带氧气的血红蛋白。因此,血红蛋白氧饱和度S为:
S = O 2 H b ∕ ( O 2 H b + R H b ) S = O2Hb∕(O2Hb + RHb) S=O2Hb∕(O2Hb+RHb)
血氧计算公式由来
用660nm (Red)和940nm(IR)的光测量动脉血液的传输信号振幅。
光吸收A为:
A ≡ l o g ( I 0 I ) = E ∗ C ∗ D A \equiv log(\frac{I_0}{I}) = E*C*D A≡log(II0)=E∗C∗D
I
0
I_0
I0: 入射光强度;
I
I
I: 透射光强度
E
E
E: 吸光系数(dL/g/cm)
C
C
C: 浓度(g / dL)
D
D
D: 厚度(cm)
透射光强度差 Δ I \Delta I ΔI为:
Δ
A
≡
l
o
g
(
I
I
−
Δ
I
)
=
E
h
∗
H
b
∗
Δ
D
\Delta A \equiv log(\frac{I}{I-\Delta I}) = Eh*Hb*\Delta D
ΔA≡log(I−ΔII)=Eh∗Hb∗ΔD
=
Δ
I
I
−
Δ
I
2
=
A
C
D
C
=\frac{\Delta I}{I - \frac{\Delta I}{2}} = \frac{AC}{DC}
=I−2ΔIΔI=DCAC
H
b
Hb
Hb: 血红蛋白浓度(g/dL)
E
h
Eh
Eh: Hb的吸光系数 (dL/g/cm)
Δ
D
\Delta D
ΔD: 动脉血液厚度的变化(cm)
在用于脉搏血氧计测量的660和940nm处,除了氧和脱氧血红蛋白(Oxy 和 Deoxy)外,血液中其他组织的吸光度被忽略
660nm 吸收系数 Deoxy(约4) > Oxy(约0)
940nm 吸收系数 Oxy(约1.2) > Deoxy(约0.7)
所以Red和IR的吸光度差比值为
Φ = Δ A R e d Δ A I R = A C R e d / D C R e d A C I R / D C I R \Phi = \frac{\Delta A_{Red}}{\Delta A_{IR}}=\frac{AC_{Red}/DC_{Red}}{AC_{IR}/DC_{IR}} Φ=ΔAIRΔARed=ACIR/DCIRACRed/DCRed
这个等式清楚地表达了脉搏血氧计的特点,它测量的是动脉血液,而不考虑血液脉动或血红蛋白浓度的变化。
总血红蛋白浓度 tHb (RHb + O2Hb) 的吸光系数Eh可计算为Eo和Er的加权平均,对应于浓度比:
E h = ( E o ∗ S + E r ∗ ( 1 − S ) ) Eh = (Eo*S + Er*(1-S)) Eh=(Eo∗S+Er∗(1−S))
E
o
Eo
Eo: 氧血红蛋白O2Hb吸光系数
E
r
Er
Er: 脱氧血红蛋白RHb吸光系数
又有
S
=
O
2
H
b
∕
(
O
2
H
b
+
R
H
b
)
S = O2Hb∕(O2Hb + RHb)
S=O2Hb∕(O2Hb+RHb)
O
2
H
b
+
R
H
b
=
1
O2Hb + RHb=1
O2Hb+RHb=1
所以有
Φ = Δ A R e d Δ A I R = A C R e d / D C R e d A C I R / D C I R = E o R e d ∗ S + E r R e d ∗ ( 1 − S ) E o I R ∗ S + E r I R ∗ ( 1 − S ) \Phi = \frac{\Delta A_{Red}}{\Delta A_{IR}}=\frac{AC_{Red}/DC_{Red}}{AC_{IR}/DC_{IR}}=\frac{Eo_{Red}*S + Er_{Red}*(1-S)}{Eo_{IR}*S + Er_{IR}*(1-S)} Φ=ΔAIRΔARed=ACIR/DCIRACRed/DCRed=EoIR∗S+ErIR∗(1−S)EoRed∗S+ErRed∗(1−S)
E o R e d Eo_{Red} EoRed、 E r R e d Er_{Red} ErRed、 E o I R Eo_{IR} EoIR、 E r I R Er_{IR} ErIR是常数,所以可以使用 Φ \Phi Φ根据S对标准曲线进行校准,这样就可以从 Φ \Phi Φ计算出S,可以测量S。
而MAX30102的厂家美信公司拟合的曲线为:
S = − 45.060 ∗ Φ ∗ Φ + 30.354 ∗ Φ + 94.845 S = -45.060 * \Phi * \Phi + 30.354 * \Phi + 94.845 S=−45.060∗Φ∗Φ+30.354∗Φ+94.845
这就是采用这个公式计算的原因。
网上MAX30102 模块的型号还是挺多的。
我都买的时候看着第一型设计的好像比较合理,将稳压电阻电容等元件都放在了PCB背面,正面只有一个传感器。所以选择了这个型号,几款型号的原理都是差不多的,都是几个稳压电路得到元件需要的5v和1.8v电压,然后引出传感器的IIC接口与一个中断信号引脚。
从上面的芯片结构图可以看到,MAX30102 分别有一个 红光RED 和 红外IR 发光二极管,按照一定的时序顺序的点亮这两个LED,投过手指后通过可见光+红外光光电二极管完成光强的采集,并且将光电信号通过一个ADC完成模数转换,另外温度数据也可以采集。这些数据采集好后按一定的时序通过IIC接口传输到控制器。
MAX30102 有三种工作模式:
数据手册给出了血氧SpO2模式下的工作时序,这个是每次FIFO将满就读取一次数据的时序,后面具体实现的时候我用的是每次完成采样就读取一次的时序。
Die Temperature Config
的 TEMP_EN
字段使能一次温度采集。Interrupt Status 2
的 DIE_TEMP_RDY
中断标志字段被芯片拉高。Interrupt Status 2
的 DIE_TEMP_RDY
中断标志字段本demo设计的工作时序如下图所示:
Die Temperature Config
的 TEMP_EN
字段使能一次温度采集。即每隔10s获取一次温度数据,但是为了简化中断信号的处理,所以温度信息的采集不使用中断信号进行触发,而是间隔足够的时间(100ms)后直接读取温度数据。并初始化FIFO指针开始采集光电数据,后续每次收到中断信号,都代表本次数据采集完成待读取。
为什么要这样处理呢,因为如果需要每隔一段时间获取温度数据的话,配置温度采集使能的过程可能还会收到上一次光电数据采集完成的中断信号。使得中断信号不好区分。
介绍几个常用的中断:
FIFO_A_FULL[3:0]
配置的值,就会触发此中断标志,通过读取寄存器Interrupt Status 1(0x00)
可以清除此中断标志。Interrupt Status 1(0x00)
或读取FIFO数据可以清除此中断标志。Interrupt Status 2(0x01)
或读取温度数据寄存器Die Temp Fraction(0x20)
可以清除此中断标志。值得注意的是,当通过配置寄存器Die Temperature Config(0x21)
中的TEMP_EN
字段使能一次温度采样后,DIE_TEMP_RDY中断会覆盖PPG_RDY中断。这样能确保开启温度采集后下一个中断表示的是温度数据采集完成。
数据手册说 MAX30102 的 IIC 接口最高支持 400kHz 速率。但我实测设为100KHz都工作异常,具体是工作时序还是数据传输时序问题没有深入研究。本demo设置为50kHz。
器件地址:
所以驱动中定义 MAX30102 的7bit器件地址为0x57。
写数据时序:
标准IIC时序,调用IDF平台的API函数i2c_master_write_to_device
即可
esp_err_t i2c_master_write_to_device(i2c_port_t i2c_num, uint8_t device_address, const uint8_t *write_buffer, size_t write_size, TickType_t ticks_to_wait)
参数:
i2c_num – I2C port number to perform the transfer on
device_address – I2C device's 7-bit address
write_buffer – Bytes to send on the bus
write_size – Size, in bytes, of the write buffer
ticks_to_wait – Maximum ticks to wait before issuing a timeout.
读数据时序:
标准IIC时序,调用IDF平台的API函数i2c_master_write_read_device
即可
esp_err_t i2c_master_write_read_device(i2c_port_t i2c_num, uint8_t device_address, const uint8_t *write_buffer, size_t write_size, uint8_t *read_buffer, size_t read_size, TickType_t ticks_to_wait)
参数:
i2c_num – I2C port number to perform the transfer on
device_address – I2C device's 7-bit address
write_buffer – Bytes to send on the bus
write_size – Size, in bytes, of the write buffer
read_buffer – Buffer to store the bytes received on the bus
read_size – Size, in bytes, of the read buffer
ticks_to_wait – Maximum ticks to wait before issuing a timeout.
ESP32硬件主要包含两个部分:IIC驱动 与 GPIO中断。
IIC驱动器初始化
配置IIC为 I2C_MODE_MASTER 模式。设置好SCL与SDA对应的GPIO后,就可以调用官方API i2c_param_config
进行配置,配置好后使用i2c_driver_install
安装驱动。
IIC驱动初始化函数:
#define MAX30102_I2C_SCL 33 // GPIO number used for I2C master clock #define MAX30102_I2C_SDA 32 // GPIO number used for I2C master data #define MAX30102_I2C_NUM 0 // I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip #define MAX30102_I2C_FREQ_HZ 50000 // I2C master clock frequency #define MAX30102_I2C_TX_BUF_DISABLE 0 // I2C master doesn't need buffer #define MAX30102_I2C_RX_BUF_DISABLE 0 #define MAX30102_I2C_TIMEOUT_MS 1000 /** * @brief init the i2c port for MAX30102 */ static esp_err_t max30102_i2c_init() { int i2c_master_port = MAX30102_I2C_NUM; i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = MAX30102_I2C_SDA, .scl_io_num = MAX30102_I2C_SCL, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = MAX30102_I2C_FREQ_HZ, }; i2c_param_config(i2c_master_port, &conf); return i2c_driver_install(i2c_master_port, conf.mode, MAX30102_I2C_RX_BUF_DISABLE, MAX30102_I2C_TX_BUF_DISABLE, 0); }
GPIO初始化
gpio_config_t
,配置中断类型为下降沿触发GPIO_INTR_NEGEDGE
,gpio模式为输入模式GPIO_MODE_INPUT
,并且使能上拉功能。gpio_config
初始化GPIO。gpio_evt_queue
用于处理gpio中断事件。gpio_intr_task
作为中断处理函数。gpio_isr_handler_add
为特定的gpio 引脚挂载isr处理程序参考代码:
void gpio_intr_task() { uint8_t byte[6]; int data[2]; uint8_t io_num; for(;;) { if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) { ESP_ERROR_CHECK(max30102_register_read(0x07, &byte, 6)); data[0] = ((byte[0]<<16 | byte[1]<<8 | byte[2]) & 0x03ffff); data[1] = ((byte[3]<<16 | byte[4]<<8 | byte[5]) & 0x03ffff); printf("Red: %d, IR: %d\n", data[0], data[1]); sample_cnt += 1; } } } static void IRAM_ATTR gpio_isr_handler(void* arg) { uint32_t gpio_num = (uint32_t) arg; xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL); } /** * @brief init the gpio intr for MAX30102 */ static esp_err_t max30102_gpio_intr_init() { gpio_config_t io_conf = {}; io_conf.intr_type = GPIO_INTR_NEGEDGE; io_conf.mode = GPIO_MODE_INPUT; io_conf.pin_bit_mask = (1ULL<<MAX30102_GPIO_INT); io_conf.pull_down_en = 0; io_conf.pull_up_en = 1; ESP_ERROR_CHECK(gpio_config(&io_conf)); //create a queue to handle gpio event from isr gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t)); //start gpio task xTaskCreate(gpio_intr_task, "gpio_intr_task", 2048, NULL, 10, NULL); //install gpio isr service gpio_install_isr_service(0); //hook isr handler for specific gpio pin gpio_isr_handler_add(MAX30102_GPIO_INT, gpio_isr_handler, (void*) MAX30102_GPIO_INT); return ESP_OK; }
其中中断处理函数读取寄存器 FIFO Data Register(0x07)
7个字节的数据,即一个采样点的数据,按照协议前三个字节为Red channel,后3个字节为 IR channel。一般IR通道交流分量与直流分量的比例更大,但是不知道是我的传感器有问题还是其他方面的原因,我的实测结果是反的。
完成硬件初始化后,需要对传感器进行初始化配置。主要流程:
复位(可选)
寄存器0x02~0x03,使能需要的中断。我这里使能了A_FULL_EN
、PPG_RDY_EN
、DIE_TEMP_RDY_EN
,所以寄存器Interrupt Enable 1(0x02)
配置为0xc0,寄存器Interrupt Enable 2(0x03)
配置为0x02,
寄存器0x04~0x06,清除FIFO指针。寄存器FIFO Write Pointer(0x04)
、FIFO Overflow Counter(0x05)
、FIFO Read Pointer(0x06)
配置为0x00。
寄存器0x08,配置FIFO工作参数。寄存器FIFO Configuration (0x08)
的SMP_AVE[2:0]
字段表示对采样值取平均,这里不做平均处理,所以配置为0b000;FIFO_ROLLOVER_EN
字段控制如果数据满了需不需要循环填充,我这里配置为禁用;FIFO_A_FULL
字段配置当触发FIFO满中断A_FULL
时FIFO剩余空闲采样点的个数,这里配置为0xf,即还有15个空闲就触发满。所以寄存器配置为0x0f。
寄存器0x09,配置工作模式。寄存器Mode Configuration [0x09]
的MODE[2:0]
字段配置为0b011,表示SpO2 模式。所以寄存器配置为 0x03
寄存器0x0a,配置SpO2 模式参数。寄存器SpO2 Configuration (0x0A)
的SPO2_ADC_RGE[1:0]
字段控制ADC采样范围,LED_PW[1:0]
字段配置LED发光脉冲长度与ADC采样精度,以及后面的LED电流大小这几个指标是相互权衡的。更高的精度需要更长的转换时间,所以对应的脉冲宽度需要更长,所以采样率需要小一点。如果LED电流更大,则采集到的光电信号范围会更大,所以需要实测来确定,这里配置ADC范围为8192nA,脉冲宽度为411.75us,对应ADC精度为18bit。SPO2_SR[2:0]
字段配置采样率,这里设置为100次/秒。因此寄存器配置为0x47。可以设置的几套参数为:
寄存器0x0C~0x0D,设置LED脉冲电流值。这里设置为0x50,对应电流值为 16mA。(没有实测最佳值,根据ADC采样范围实测了一个合适的值)。
最后需要清空一下数据各种中断标志位,因为在上面还没有配置完成就已经开始数据采集了,如果不清除后面时序不好处理。
参考代码:
void max30102_init() { ESP_ERROR_CHECK(max30102_i2c_init()); ESP_LOGI(TAG, "MAX30102 I2C initialized successfully"); max30102_gpio_intr_init(); ESP_LOGI(TAG, "MAX30102 GPIO INTR initialized successfully"); // reset ESP_ERROR_CHECK(max30102_register_write_byte(0x09, 0x40)); vTaskDelay(100 / portTICK_RATE_MS); // Interrupt Enable ESP_ERROR_CHECK(max30102_register_write_byte(0x02, 0xc0)); // enable interrupts: A_FULL: FIFO Almost Full Flag and PPG_RDY: New FIFO Data Ready ESP_ERROR_CHECK(max30102_register_write_byte(0x03, 0x02)); // enable interrupt: DIE_TEMP_RDY: Internal Temperature Ready Flag // FIFO ESP_ERROR_CHECK(max30102_register_write_byte(0x04, 0x00)); // clear FIFO Write Pointer ESP_ERROR_CHECK(max30102_register_write_byte(0x05, 0x00)); // clear FIFO Overflow Counter ESP_ERROR_CHECK(max30102_register_write_byte(0x06, 0x00)); // clear FIFO Read Pointer // FIFO Configuration ESP_ERROR_CHECK(max30102_register_write_byte(0x08, 0x0f)); // SMP_AVE = 0b000: 1 averaging, FIFO_ROLLOVER_EN = 0, FIFO_A_FULL = 0xf // Mode Configuration ESP_ERROR_CHECK(max30102_register_write_byte(0x09, 0x03)); // MODE = 0b011: SpO2 mode // SpO2 Configuration ESP_ERROR_CHECK(max30102_register_write_byte(0x0a, 0x47)); // SPO2_ADC_RGE = 0b10: 8192, SPO2_SR = 0b001: 100 SAMPLES PER SECOND, // LED_PW = 0b11: PULSE WIDTH 411, ADC RESOLUTION 18 // LED Pulse Amplitude ESP_ERROR_CHECK(max30102_register_write_byte(0x0c, 0x50)); // LED1_PA(red) = 0x24, LED CURRENT 16mA ESP_ERROR_CHECK(max30102_register_write_byte(0x0d, 0x50)); // LED2_PA(IR) = 0x24, LED CURRENT 16mA // ESP_ERROR_CHECK(max30102_register_write_byte(0x10, 0x50)); // PILOT_PA = 0x24, LED CURRENT 16mA // clear PPG_RDY ! Cannot receive the first interrupt without clearing ! uint8_t data; ESP_ERROR_CHECK(max30102_register_read(0x00, &data, 1)); ESP_LOGI(TAG, "Interrupt Status 1: 0x%x", data); ESP_ERROR_CHECK(max30102_register_read(0x01, &data, 1)); ESP_LOGI(TAG, "Interrupt Status 2: 0x%x", data); }
不同ADC分辨率得到的数据格式如下图所示。
可以看到数据是左对齐的,因为血氧计算过程是线性的,所以全部当作18bit数据处理就行。
GPIO 中断服务函数:
void gpio_intr_task()
{
uint8_t byte[6];
int data[2];
uint8_t io_num;
for(;;) {
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
ESP_ERROR_CHECK(max30102_register_read(0x07, &byte, 6));
data[0] = ((byte[0]<<16 | byte[1]<<8 | byte[2]) & 0x03ffff);
data[1] = ((byte[3]<<16 | byte[4]<<8 | byte[5]) & 0x03ffff);
printf("Red: %d, IR: %d\n", data[1], data[0]);
}
}
}
我这个传感器测到的data[0]交流/直流比data[1]的小3倍左右,刚好与正确的RED与IR通道相反,而且实测过程中明显能看出data[0]受温度影响大,只有当传感器被手指加热到二十多度后数据才能稳定。所以我实际是把这两个通道调换了一下的,不过这样肯定是不对的,具体的原因不太清楚,需要学友做个实验和我对比下,排除一下传感器的问题。
值得注意的是添加QGraphicsView窗口后,需要右击提升为PlotWidget,这样配合 pyqtgraph 模块进行绘图比较方便。
由于处理算法非常暴力垃圾,用了一个简单的判断来过滤波峰与波谷的位置,然后通过一个简单的相邻比较来得到更大值对应得那个波峰与波谷的位置与采样值。所以处理的结果异常值还是比较多的,不过这里也主要是学习pyqtgraph模块。在数据比较稳定的时候还是能够测量出血氧与心率的。
上位机源码:
#!/usr/bin/python3 # -*- coding: utf-8 -*- import sys,os,math from ast import Try from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QColorDialog ,QMessageBox,QLabel from PyQt5.QtGui import QIcon, QImage, QPixmap, QColor from PyQt5.QtCore import QTimer, QDateTime import pyqtgraph as pg import numpy as np import serial from serial.tools import list_ports import re from ui.Ui_mainWindow import Ui_MainWindow class mainWindow(QMainWindow): def __init__(self): super().__init__() self.initUI() self.init() def initUI(self): self.ui = Ui_MainWindow() self.ui.setupUi(self) self.setWindowTitle('MAX30102') self.setWindowIcon(QIcon('logo.png')) self.ui.gv.setTitle("Red Channel") self.ui.gv1.setTitle("IR Channel") self.show() def init(self): self.ser = serial.Serial() self.receive_timer = QTimer(self) self.receive_timer.start(10) self.receive_timer.timeout.connect(self.dataReceive) self.initSerial() self.ui.btn_serial_scan.clicked.connect(self.initSerial) self.ui.btn_serial_ctrl.clicked.connect(self.serialCtrl) self.data_red = np.zeros(300) self.data_ir = np.zeros(300) self.time = 0 self.hr = 0 self.spo2 = 0 self.curve = self.ui.gv.plot(self.data_red) self.curve1 = self.ui.gv1.plot(self.data_ir) self.data_anal_timer = QTimer(self) self.data_anal_timer.start(1000) self.data_anal_timer.timeout.connect(self.dataAnalyse) self.ui.statusbar.showMessage(QDateTime.currentDateTime().toString('hh:mm:ss ')+'打开软件') def initSerial(self): self.ui.comboBox_port.clear() self.port_list = list(serial.tools.list_ports.comports()) for port in self.port_list: self.ui.comboBox_port.addItem(port[0]+':'+port[1]) def serialCtrl(self): if((self.ui.btn_serial_ctrl.text() == "打开串口") and (self.ser.is_open == False)): self.ser.port = self.port_list[self.ui.comboBox_port.currentIndex()][0] self.ser.baudrate = 115200 self.ser.timeout = 0.5 try: self.ser.open() self.ui.btn_serial_ctrl.setText('关闭串口') self.ui.btn_serial_scan.setDisabled(True) self.ui.statusbar.showMessage(QDateTime.currentDateTime().toString('hh:mm:ss ')+'打开串口:'+self.ser.port + ' baudrate = 115200') except serial.SerialException: self.ui.statusbar.showMessage(QDateTime.currentDateTime().toString('hh:mm:ss ')+'串口打开失败!') elif((self.ui.btn_serial_ctrl.text() == "关闭串口") and (self.ser.is_open == True)): try: self.ser.close() self.ui.btn_serial_ctrl.setText('打开串口') self.ui.btn_serial_scan.setDisabled(False) except serial.SerialException: self.ui.statusbar.showMessage(QDateTime.currentDateTime().toString('hh:mm:ss ')+'串口关闭失败!') def dataReceive(self): try: num = self.ser.inWaiting() #返回接收缓存中的字节数 except: pass else: if num > 0: data_read = self.ser.read(num) reveive_num = len(data_read) self.dataRepack(data_read) self.ui.statusbar.showMessage(QDateTime.currentDateTime().toString('hh:mm:ss ')+'接收 '+ str(reveive_num) + 'Bytes') def dataRepack(self, data): try: red_re = re.search(r'(?<=Red: )\d+', data.decode('utf-8')) ir_re = re.search(r'(?<=IR: )\d+', data.decode('utf-8')) temp_re = re.search(r'(?<=Temp: )((\-|\+)?\d+(\.\d+)?)+', data.decode('utf-8')) if(red_re != None): self.red = float(red_re.group(0)) if(ir_re != None): self.ir = float(ir_re.group(0)) if(temp_re != None): self.temp = float(temp_re.group()) self.dataDraw(self.red, self.ir) # print("red :", self.red, "ir :", self.ir) except Exception as se: print(str(se)) def dataDraw(self, red, ir): self.data_red[:-1] = self.data_red[1:] self.data_red[-1] = red self.data_ir[:-1] = self.data_ir[1:] self.data_ir[-1] = ir self.curve.setData(self.data_red) self.curve1.setData(self.data_ir) def dataAnalyse(self): data_red = self.data_red data_ir = self.data_ir hr_num = [] valley_red_index = [] valley_red_data = [] valley_ir_index = [] valley_ir_data = [] peak_red_index = [] peak_red_data = [] peak_ir_index = [] peak_ir_data = [] # red valley_pre = np.min(data_red) peak_pre = np.max(data_red) for i in range(3, 300-3): if((np.min(data_red[i-3:i-1]) >= data_red[i]) and (np.min(data_red[i+1:i+3]) > data_red[i])): valley = data_red[i] valley_gate = (valley + valley_pre) / 2 valley_pre = valley if(valley <= valley_gate): valley_red_index.append(i) valley_red_data.append(data_red[i]) if((np.max(data_red[i-3:i-1]) <= data_red[i]) and (np.max(data_red[i+1:i+3]) < data_red[i])): peak = data_red[i] peak_gate = (peak + peak_pre) / 2 peak_pre = peak if(peak >= peak_gate): peak_red_index.append(i) peak_red_data.append(data_red[i]) # ir valley_pre = np.min(data_ir) peak_pre = np.max(data_ir) for i in range(3, 300-3): if((np.min(data_ir[i-3:i-1]) >= data_ir[i]) and (np.min(data_ir[i+1:i+3]) > data_ir[i])): valley = data_ir[i] valley_gate = (valley + valley_pre) / 2 valley_pre = valley if(valley <= valley_gate): valley_ir_index.append(i) valley_ir_data.append(data_ir[i]) if((np.max(data_ir[i-3:i-1]) <= data_ir[i]) and (np.max(data_ir[i+1:i+3]) < data_ir[i])): peak = data_ir[i] peak_gate = (peak + peak_pre) / 2 peak_pre = peak if(peak >= peak_gate): peak_ir_index.append(i) peak_ir_data.append(data_ir[i]) # calc hr hr_num_mean = np.mean(np.diff(valley_ir_index)) self.hr = 60 / (hr_num_mean * (1/100) ) # calc spo2 ac_red = np.mean(peak_red_data) - np.mean(valley_red_data) dc_red = np.mean(peak_red_data) - ac_red / 2 ac_ir = np.mean(peak_ir_data) - np.mean(valley_ir_data) dc_ir = np.mean(peak_ir_data) - ac_ir / 2 R = (ac_red / dc_red) / (ac_ir / dc_ir) # R = (ac_ir / dc_ir) / (ac_red / dc_red) self.spo2 = -45.060 * R * R + 30.354 * R + 94.845 # print("ACred %d, DCred: %d, ACir %d, DCir %d , spo2 %f \n" % (round(ac_red), round(dc_red), round(ac_ir), round(dc_ir), self.spo2)) if((hr_num_mean >= 30) and (hr_num_mean <= 120)): self.ui.label_hr.setText('心率:'+str(round(self.hr, 1)) + ' BPM') else: self.ui.label_hr.setText('心率:') if((hr_num_mean >= 30) and (hr_num_mean <= 120) and (self.spo2 >= 0) and (self.spo2 <= 100)): self.ui.label_spo2.setText('血氧:'+str(round(self.spo2, 1)) + ' %') else: self.ui.label_spo2.setText('血氧:') self.ui.label_temp.setText('温度:' + str(round(self.temp, 1)) + ' ℃') def closeEvent(self, a0: QtGui.QCloseEvent) -> None: reply = QMessageBox.question(self, 'Message', "确认退出?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: a0.accept() else: a0.ignore() if __name__ == '__main__': app = QApplication(sys.argv) main_window = mainWindow() sys.exit(app.exec_())
ESP32( IDF平台)+MAX30102 配合Pyqt上位机实现PPG波形显示与心率计算
https://download.csdn.net/download/lum250/87397911
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。