年前买了一个MAX30102模块,在家无聊做了这个demo对一些相关的知识进行学习。
主要学习的内容:
实现的效果:

实现的思路:
心率基本上正确,血氧图一乐。
ESP-IDF平台的学习记录可以参考 ESP32学习专栏
文章目录
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
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).
LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L
目录一、ESP32简单介绍二、ESP32Wi-Fi模块介绍三、ESP32Wi-Fi编程模型四、ESP32Wi-Fi事件处理流程 五、ESP32Wi-Fi开发环境六、ESP32Wi-Fi具体代码七、ESP32Wi-Fi代码解读6.1主程序app_main7.2自定义代码wifi_init_sta()八、ESP32Wi-Fi连接验证8.1测试方法8.2服务器模拟工具sscom58.3测试代码8.4测试结果前言为了开发一款亚马逊物联网产品,开始入手ESP32模块。为了能够记录自己的学习过程,特记录如下操作过程。一、ESP32简单介绍ESP32是一套Wi-Fi(2.4GHz)和蓝牙(4.2)双模解决方
有道无术,术尚可求,有术无道,止于术。本系列SpringBoot版本3.0.4本系列SpringSecurity版本6.0.2本系列SpringAuthorizationServer版本1.0.2源码地址:https://gitee.com/pearl-organization/study-spring-security-demo文章目录前言1.OAuth2AuthorizationServerMetadataEndpointFilter2.OAuth2AuthorizationEndpointFilter3.OidcProviderConfigurationEndpointFilter4.N
在我的代码中,我需要使用各种算法(包括CRC32)对文件进行哈希处理。因为我还在Digest系列中使用其他加密哈希函数,所以我认为为它们维护一个一致的接口(interface)会很好。为了记录,我确实找到了digest-crc,一颗完全符合我要求的gem。问题是,Zlib是标准库的一部分,并且有一个我想重用的CRC32工作实现。此外,它是用C编写的,因此它应该提供与digest-crc相关的卓越性能,后者是纯ruby实现。实现Digest::CRC32一开始看起来非常简单:%w(digestzlib).each{|f|requiref}classDigest::CRC32一切正常:
我正在尝试在我的机器上安装win32-apigem,但在构建native扩展时我遇到了一些问题:$geminstallwin32-api--no-ri--rdocTemporarilyenhancingPATHtoincludeDevKit...Buildingnativeextensions.Thiscouldtakeawhile...C:\Programs\dev_kit\bin\make.exe:***Couldn'treservespaceforcygwin'sheap,Win32error0ERROR:Errorinstallingwin32-api:ERROR:Failed
我在Windows上运行ruby1.9.2并试图移植在Ruby1.8中工作的代码。该代码使用以前运行良好的Open4.popen4。对于1.9.2,我做了以下事情:通过geminstallPOpen4安装了POpen4需要POpen4通过require'popen4'尝试像这样使用POpen4:Open4.popen4("cmd"){|io_in,io_out,io_er|...}当我这样做时,我得到了错误:nosuchfiletoload--win32/open3如果我尝试安装win32-open3(geminstallwin32-open3),我会收到错误消息:win32-op
DellInspiron5488加内存32G 原装内置内存仅仅8G,目前看,真的太小了! 1.内存型号Dell5488内存型号:DDR42666。笔记本有两个内存插槽,原装占了一个,还能扩展一个。 2.买内存如果买Dell原装笔记本内存,8G就得500块左右。 我咨询了一下,三星的笔记本内存,可以兼容。16G,299块(2023年2月23日,京东价) Dell5488内存组合,最多只能插两根16G内存。 我于是买了两根三星16G内存。装上,很爽😄 跑国产系统统信UOS,再也看不到用交换区了,32G内存,爽!
本文代码使用HAL库。文章目录前言一、MCP4017的重要特性二、MCP4017计算RBW阻值三、MCP4017地址四、MCP4017读写函数五、CubeMX创建工程(利用ADC测量MCP4017电压)、对应代码:总结前言一、MCP4017的重要特性蓝桥杯板子上的是MCP4017T-104ELT,如图1。MCP4017是一个可编程电阻,通过写入的数值可以改变电阻的大小。重点在于6引脚(W),5引脚(B