ESP32-DEVELOP

30分,水一个报告

实验报告

心率采集信息价值

心率采集是一种常见的生理指标监测方法,通过测量心脏每分钟搏动的次数来评估一个人的心脏功能状况。心率采集信息具有重要的价值,对于医学、运动科学和健康管理等领域都具有广泛的应用。以下是对心率采集信息价值的准确、科学的介绍。

首先,心率采集信息能够提供关于心脏健康状况的重要线索。正常的心率范围是60到100次/分钟,超出这个范围可能意味着心脏存在问题。通过监测心率的变化,医生可以判断是否存在心律不齐、心肌缺血、心肌梗死等心脏疾病,并及时采取相应的治疗措施。此外,心率变异性(HRV)分析也可以提供关于心脏健康状况的有价值信息。HRV指的是心率在不同时间点上的变化程度,较高的HRV通常与较好的心血管健康相关,而较低的HRV可能与心脏疾病风险增加有关。

其次,心率采集信息在运动科学中具有重要的应用。心率是评估运动强度和耐力的重要指标之一。通过监测运动时的心率变化,可以了解个体的心肺功能状况、运动耐力水平以及运动适应性。运动员、教练员和健身爱好者可以根据心率信息调整训练强度和持续时间,以实现更好的训练效果。此外,心率采集信息还可以用于监测运动后的恢复情况,有助于判断身体对运动的适应程度和疲劳程度,为合理安排训练计划提供依据。

此外,心率采集信息在健康管理中也具有重要作用。心率的变化可以反映人体对不同情绪、压力和环境的反应。通过监测日常生活中的心率变化,可以评估个体的应激水平和情绪状态。这对于识别和管理压力、焦虑和抑郁等心理健康问题具有帮助

设备

IMG_20230606_132505

IDE: Thonny

硬件: ESP32 、240x240 屏幕 、 USB数据线 、杜邦线-10cm母对母、MAX30102传感器

固件:

https://micropython.org/download/esp32/

image-20230530222745282

MAX30102传感器

介绍

MAX30102是一个集成的脉搏血氧仪和心率监测仪生物传感器的模块。

它集成了一个红光LED和一个红外光LED、光电检测器、光器件,以及带环境光抑制的低噪声电子电路。

MAX30102采用一个1.8V电源和一个独立的5.0V用于内部LED的电源,应用于可穿戴设备进行心率和血氧采集检测,佩戴于手指点耳垂和手腕处。

标准的I2C兼容的通信接口可以将采集到的数值传输给Arduino、KL25Z、STM32、STC51等单片机进行心率和血氧计算。

此外,该芯片还可以通过软件关断模块,待机电流接近为零,实现电源始终维持供电状态。

工作原理

光溶积法:利用人体组织在血管搏动时造成透光率不同来进行脉搏和血氧饱和度测量

光源:采用对动脉血中痒合血红蛋白(HbO2)和血红蛋白(Hb)有选择性的特定波长的发光二极管

透光率转化为电信号:动脉搏动充血容积变化导致这束光的透光率发送改变,此时由光电变换接收经人体组织反射光线,转变为电信号并将其放大输出。

在这里插入图片描述

电路解析

主要参数:

产品名称 MAX30102 心率模块
LED峰值波长器 660nm/880nm
LED供电电压 3.3 ~ 5V
检测信号类型 光反射信号(PPG)
输出信号接口 I2C接口
通信接口电压 1.8 ~ 3.3V ~ 5V(可选)
在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

引脚说明
| 名称 | 管教定义 |
| ——- | —————————————————— |
| VIN | 电源输入 1.6V-5.5V |
| 3位焊盘 | 选择总线的上拉电平,取决于引脚主控电压可选1.8v或者3.3v |
| SDA | IIC-SDA |
| SCL | IIC-SCL |
| GND | 地 |
| INT | INT 低电平有效中断(漏极开路)MAX30102 的中断引脚 |
| IRD | IR_DRV IR LED阴极和LED驱动器连接点 一般不接 |
| RD | R_DRV 红色LED阴极和LED驱动器连接点 一般不接 |

ESP32开发板

ESP32开发板是专为ESP32芯片设计的硬件平台,用于简化和加速基于ESP32的应用程序开发。它是一种集成了ESP32芯片、外围电路和连接器的单板电脑。

ESP32开发板具有丰富的硬件资源,包括数字和模拟GPIO引脚、UART、SPI、I2C接口、PWM输出以及ADC和DAC等功能。它还配备了Wi-Fi和蓝牙天线,使得无线通信变得更加方便。

ESP32开发板具有广泛的应用领域。由于其强大的无线通信功能,它常被用于物联网(IoT)应用开发,如智能家居、传感器网络、远程监控等。同时,其丰富的硬件接口和高性能处理器也使其成为机器人、自动化系统、嵌入式设备和工业控制等领域的理想选择。

ESP32开发板的优点还包括低功耗、高性价比和易于使用。它能够在低功耗模式下运行,适用于长时间运行的应用。此外,ESP32开发板的价格相对较低,对于学习和原型开发非常友好。其易于使用的开发环境和丰富的社区支持也为开发者提供了更多便利。

总之,ESP32开发板是一种功能强大且易于使用的硬件平台,为开发者提供了丰富的资源和灵活性,促进了基于ESP32芯片的应用程序开发和创新。

电路连接

1672988334039

检测心率

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
from machine import sleep, SoftI2C, Pin, Timer
from utime import ticks_diff, ticks_us
from max30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM


BEATS = 0 # 存储心率
FINGER_FLAG = False # 默认表示未检测到手指


def display_info(t):
# 如果没有检测到手指,那么就不显示
if FINGER_FLAG is False:
return

print('心率: ', BEATS)


def main():
global BEATS, FINGER_FLAG # 如果需要对全局变量修改,则需要global声明

# 创建I2C对象(检测MAX30102)
i2c = SoftI2C(sda=Pin(15), scl=Pin(2), freq=400000) # Fast: 400kHz, slow: 100kHz

# 创建传感器对象
sensor = MAX30102(i2c=i2c)

# 检测是否有传感器
if sensor.i2c_address not in i2c.scan():
print("没有找到传感器")
return
elif not (sensor.check_part_id()):
# 检查传感器是否兼容
print("检测到的I2C设备不是MAX30102或者MAX30105")
return
else:
print("传感器已识别到")

print("使用默认配置设置传感器")
sensor.setup_sensor()

# 对传感器进行设定
sensor.set_sample_rate(400)
sensor.set_fifo_average(8)
sensor.set_active_leds_amplitude(MAX30105_PULSE_AMP_MEDIUM)

t_start = ticks_us() # Starting time of the acquisition

MAX_HISTORY = 32
history = []
beats_history = []
beat = False

while True:
sensor.check()
if sensor.available():
# FIFO 先进先出,从队列中取数据。都是整形int
red_reading = sensor.pop_red_from_storage()
ir_reading = sensor.pop_ir_from_storage()

if red_reading < 1000:
print('No finger')
FINGER_FLAG = False # 表示没有放手指
continue
else:
FINGER_FLAG = True # 表示手指已放

# 计算心率
history.append(red_reading)

# 为了防止列表过大,这里取列表的后32个元素
history = history[-MAX_HISTORY:]

# 提取必要数据
minima, maxima = min(history), max(history)
threshold_on = (minima + maxima * 3) // 4 # 3/4
threshold_off = (minima + maxima) // 2 # 1/2

if not beat and red_reading > threshold_on:
beat = True
t_us = ticks_diff(ticks_us(), t_start)
t_s = t_us/1000000
f = 1/t_s
bpm = f * 60
if bpm < 500:
t_start = ticks_us()
beats_history.append(bpm)
beats_history = beats_history[-MAX_HISTORY:] # 只保留最大30个元素数据
BEATS = round(sum(beats_history)/len(beats_history), 2) # 四舍五入
if beat and red_reading < threshold_off:
beat = False


if __name__ == '__main__':
# 1. 创建定时器
timer = Timer(1)
# 2. 设置定时器的回调函数,每1秒钟调用1次display_info函数(用来显示数据)
timer.init(period=1000, mode=Timer.PERIODIC, callback=display_info)
# 3. 调用主程序,用来检测数据
main()

代码解释

该代码段主要通过与MAX30102传感器进行通信来检测心率。它使用了Python的machine模块中的一些功能,如定时器和I2C通信。MAX30102传感器是一种可穿戴式生物传感器,能够通过皮肤来检测心率。

代码的主要逻辑是在一个无限循环中不断检测传感器读数,并进行相关的心率计算。首先,代码会初始化I2C对象和传感器对象,以便与MAX30102传感器进行通信。然后,它会检查传感器是否被正确连接,并设置传感器的一些配置参数,如采样率和活跃LED的幅度。

在主循环中,代码会检查传感器是否有可用的数据。如果有可用数据,它会从传感器的缓存中读取红光和红外光的读数。然后,代码会判断读数是否超过一个阈值,以确定是否有手指放置在传感器上。如果读数低于阈值,则表示没有检测到手指;反之,表示手指已放置。

接下来,代码会将红光读数存储到一个历史列表中,并通过保留列表的最后32个元素来控制列表的长度。然后,代码会计算历史列表中的最小值和最大值,并根据这些值计算出心率的阈值。

代码会检查当前的心跳状态,并根据红光读数和阈值的比较来确定是否有心跳事件发生。如果检测到心跳开始,则计算心跳的频率和心率,并将计算结果添加到心率历史列表中。心率历史列表的长度被限制在最大30个元素,以保持数据的有效性。最后,代码会通过定时器每秒调用一次display_info函数来显示心率。

检测血氧、温度

检测血氧值代码模块

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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# -*-coding:utf-8

# 25 samples per second (in algorithm.h)
SAMPLE_FREQ = 25
# taking moving average of 4 samples when calculating HR
# in algorithm.h, "DONOT CHANGE" comment is attached
MA_SIZE = 4
# sampling frequency * 4 (in algorithm.h)
BUFFER_SIZE = 100


# this assumes ir_data and red_data as np.array
def calc_hr_and_spo2(ir_data, red_data):
"""
By detecting peaks of PPG cycle and corresponding AC/DC
of red/infra-red signal, the an_ratio for the SPO2 is computed.
"""
# get dc mean
ir_mean = int(sum(ir_data) / len(ir_data))

# remove DC mean and inver signal
# this lets peak detecter detect valley
x = [ir_mean - x for x in ir_data]

# 4 point moving average
# x is np.array with int values, so automatically casted to int
for i in range(len(x) - MA_SIZE):
x[i] = sum(x[i:i + MA_SIZE]) / MA_SIZE

# calculate threshold
n_th = int(sum(x) / len(x))
n_th = 30 if n_th < 30 else n_th # min allowed
n_th = 60 if n_th > 60 else n_th # max allowed

ir_valley_locs, n_peaks = find_peaks(x, BUFFER_SIZE, n_th, 4, 15)
# print(ir_valley_locs[:n_peaks], ",", end="")
peak_interval_sum = 0
if n_peaks >= 2:
for i in range(1, n_peaks):
peak_interval_sum += (ir_valley_locs[i] - ir_valley_locs[i - 1])
peak_interval_sum = int(peak_interval_sum / (n_peaks - 1))
hr = int(SAMPLE_FREQ * 60 / peak_interval_sum)
hr_valid = True
else:
hr = -999 # unable to calculate because # of peaks are too small
hr_valid = False

# ---------spo2---------

# find precise min near ir_valley_locs (???)
exact_ir_valley_locs_count = n_peaks

# find ir-red DC and ir-red AC for SPO2 calibration ratio
# find AC/DC maximum of raw

# FIXME: needed??
for i in range(exact_ir_valley_locs_count):
if ir_valley_locs[i] > BUFFER_SIZE:
spo2 = -999 # do not use SPO2 since valley loc is out of range
spo2_valid = False
return hr, hr_valid, spo2, spo2_valid

i_ratio_count = 0
ratio = []

# find max between two valley locations
# and use ratio between AC component of Ir and Red DC component of Ir and Red for SpO2
red_dc_max_index = -1
ir_dc_max_index = -1
for k in range(exact_ir_valley_locs_count - 1):
red_dc_max = -16777216
ir_dc_max = -16777216
if ir_valley_locs[k + 1] - ir_valley_locs[k] > 3:
for i in range(ir_valley_locs[k], ir_valley_locs[k + 1]):
if ir_data[i] > ir_dc_max:
ir_dc_max = ir_data[i]
ir_dc_max_index = i
if red_data[i] > red_dc_max:
red_dc_max = red_data[i]
red_dc_max_index = i

red_ac = int((red_data[ir_valley_locs[k + 1]] - red_data[ir_valley_locs[k]]) * (red_dc_max_index - ir_valley_locs[k]))
red_ac = red_data[ir_valley_locs[k]] + int(red_ac / (ir_valley_locs[k + 1] - ir_valley_locs[k]))
red_ac = red_data[red_dc_max_index] - red_ac # subtract linear DC components from raw

ir_ac = int((ir_data[ir_valley_locs[k + 1]] - ir_data[ir_valley_locs[k]]) * (ir_dc_max_index - ir_valley_locs[k]))
ir_ac = ir_data[ir_valley_locs[k]] + int(ir_ac / (ir_valley_locs[k + 1] - ir_valley_locs[k]))
ir_ac = ir_data[ir_dc_max_index] - ir_ac # subtract linear DC components from raw

nume = red_ac * ir_dc_max
denom = ir_ac * red_dc_max
if (denom > 0 and i_ratio_count < 5) and nume != 0:
# original cpp implementation uses overflow intentionally.
# but at 64-bit OS, Pyhthon 3.X uses 64-bit int and nume*100/denom does not trigger overflow
# so using bit operation ( &0xffffffff ) is needed
ratio.append(int(((nume * 100) & 0xffffffff) / denom))
i_ratio_count += 1

# choose median value since PPG signal may vary from beat to beat
ratio = sorted(ratio) # sort to ascending order
mid_index = int(i_ratio_count / 2)

ratio_ave = 0
if mid_index > 1:
ratio_ave = int((ratio[mid_index - 1] + ratio[mid_index]) / 2)
else:
if len(ratio) != 0:
ratio_ave = ratio[mid_index]

# why 184?
# print("ratio average: ", ratio_ave)
if ratio_ave > 2 and ratio_ave < 184:
# -45.060 * ratioAverage * ratioAverage / 10000 + 30.354 * ratioAverage / 100 + 94.845
spo2 = -45.060 * (ratio_ave ** 2) / 10000.0 + 30.054 * ratio_ave / 100.0 + 94.845
spo2_valid = True
else:
spo2 = -999
spo2_valid = False

return hr - 20, hr_valid, spo2, spo2_valid


def find_peaks(x, size, min_height, min_dist, max_num):
"""
Find at most MAX_NUM peaks above MIN_HEIGHT separated by at least MIN_DISTANCE
"""
ir_valley_locs, n_peaks = find_peaks_above_min_height(x, size, min_height, max_num)
ir_valley_locs, n_peaks = remove_close_peaks(n_peaks, ir_valley_locs, x, min_dist)

n_peaks = min([n_peaks, max_num])

return ir_valley_locs, n_peaks


def find_peaks_above_min_height(x, size, min_height, max_num):
"""
Find all peaks above MIN_HEIGHT
"""

i = 0
n_peaks = 0
ir_valley_locs = [] # [0 for i in range(max_num)]
while i < size - 1:
if x[i] > min_height and x[i] > x[i - 1]: # find the left edge of potential peaks
n_width = 1
# original condition i+n_width < size may cause IndexError
# so I changed the condition to i+n_width < size - 1
while i + n_width < size - 1 and x[i] == x[i + n_width]: # find flat peaks
n_width += 1
if x[i] > x[i + n_width] and n_peaks < max_num: # find the right edge of peaks
# ir_valley_locs[n_peaks] = i
ir_valley_locs.append(i)
n_peaks += 1 # original uses post increment
i += n_width + 1
else:
i += n_width
else:
i += 1

return ir_valley_locs, n_peaks


def remove_close_peaks(n_peaks, ir_valley_locs, x, min_dist):
"""
Remove peaks separated by less than MIN_DISTANCE
"""

# should be equal to maxim_sort_indices_descend
# order peaks from large to small
# should ignore index:0
sorted_indices = sorted(ir_valley_locs, key=lambda i: x[i])
sorted_indices.reverse()

# this "for" loop expression does not check finish condition
# for i in range(-1, n_peaks):
i = -1
while i < n_peaks:
old_n_peaks = n_peaks
n_peaks = i + 1
# this "for" loop expression does not check finish condition
# for j in (i + 1, old_n_peaks):
j = i + 1
while j < old_n_peaks:
n_dist = (sorted_indices[j] - sorted_indices[i]) if i != -1 else (sorted_indices[j] + 1) # lag-zero peak of autocorr is at index -1
if n_dist > min_dist or n_dist < -1 * min_dist:
sorted_indices[n_peaks] = sorted_indices[j]
n_peaks += 1 # original uses post increment
j += 1
i += 1

sorted_indices[:n_peaks] = sorted(sorted_indices[:n_peaks])

return sorted_indices, n_peaks


if __name__ == "__main__":
hr, hrb, sp, spb = calc_hr_and_spo2([12853, 15573, 15580, 15586, 15587, 15567, 15520, 15480, 15464, 15460, 15462, 15466, 15473, 15479, 15485, 15490, 15495, 15503, 15512, 15518, 15521, 15521, 15518, 15517, 15522, 15527, 15536, 15547, 15558, 15568, 15577, 15587, 15594, 15604, 15610, 15616, 15620, 15624, 15625, 15615, 15576, 15531, 15508, 15500, 15502, 15509, 15516, 15523, 15528, 15533, 15538, 15547, 15556, 15564, 15564, 15560, 15556, 15556, 15559, 15564, 15570, 15579, 15588, 15599, 15610, 15619, 15628, 15635, 15642, 15649, 15655, 15662, 15669, 15672, 15661, 15621, 15571, 15546, 15537, 15538, 15545, 15553, 15560, 15565, 15570, 15577, 15585, 15593, 15600, 15601, 15597, 15592, 15591, 15594, 15600, 15608, 15617, 15626, 15633, 15640], [12258, 14318, 14322, 14324, 14326, 14317, 14299, 14284, 14280, 14279, 14280, 14283, 14285, 14288, 14292, 14294, 14297, 14299, 14302, 14304, 14305, 14305, 14304, 14304, 14306, 14308, 14311, 14316, 14321, 14325, 14329, 14333, 14329, 14329, 14332, 14335, 14336, 14338, 14338, 14333, 14315, 14295, 14286, 14283, 14285, 14288, 14292, 14295, 14297, 14298, 14301, 14305, 14309, 14312, 14312, 14310, 14308, 14308, 14309, 14312, 14315, 14318, 14322, 14327, 14332, 14336, 14341, 14344, 14347, 14350, 14351, 14354, 14357, 14359, 14353, 14335, 14313, 14304, 14300, 14302, 14305, 14309, 14312, 14314, 14316, 14319, 14323, 14326, 14329, 14329, 14326, 14325, 14324, 14326, 14328, 14332, 14336, 14341, 14345, 14349])

print("hr detected:", hrb)
print("sp detected:", spb)

if (hrb == True and hr != -999):
hr2 = int(hr)
print("Heart Rate : ", hr2)
if (spb == True and sp != -999):
sp2 = int(sp)
print("SPO2 : ", sp2)

代码解释

这段代码实现了计算心率(Heart Rate)和血氧饱和度(SpO2)的算法。它接受红外(IR)信号和红色(Red)信号作为输入,通过检测心率周期的峰值和对应的红外和红色信号的交流/直流成分,计算出血氧饱和度的比值(SPO2)。以下是代码的主要步骤:

  1. 计算红外信号的直流均值。
  2. 对红外信号进行去直流均值并翻转信号,以便检测谷值。
  3. 使用4点移动平均对信号进行平滑处理。
  4. 计算阈值,阈值的值为信号的平均值,限制在30和60之间。
  5. 使用峰值检测算法找到红外信号的谷值位置。
  6. 计算心率,通过计算峰值之间的间隔时间来估算心率。
  7. 寻找与红外信号谷值位置相邻的最大值,以及计算红色信号的直流和交流成分,用于血氧饱和度校准比率的计算。
  8. 计算血氧饱和度的值,使用血氧饱和度校准比率和峰值比率的公式计算。
  9. 返回心率、心率有效性、血氧饱和度和血氧饱和度有效性。

血氧和温度检测代码

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
from machine import sleep, SoftI2C, Pin, Timer
from utime import ticks_diff, ticks_us
from max30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM
from hrcalc import calc_hr_and_spo2

BEATS = 0 # 存储心率
FINGER_FLAG = False # 默认表示未检测到手指
SPO2 = 0 # 存储血氧
TEMPERATURE = 0 # 存储温度


def display_info(t):
# 如果没有检测到手指,那么就不显示
if FINGER_FLAG is False:
return
print('心率: ', BEATS, " 血氧:", SPO2, " 温度:", TEMPERATURE)


def main():
global BEATS, FINGER_FLAG, SPO2, TEMPERATURE # 如果需要对全局变量修改,则需要global声明

# 创建I2C对象(检测MAX30102)
i2c = SoftI2C(sda=Pin(15), scl=Pin(2), freq=400000) # Fast: 400kHz, slow: 100kHz

# 创建传感器对象
sensor = MAX30102(i2c=i2c)

# 检测是否有传感器
if sensor.i2c_address not in i2c.scan():
print("没有找到传感器")
return
elif not (sensor.check_part_id()):
# 检查传感器是否兼容
print("检测到的I2C设备不是MAX30102或者MAX30105")
return
else:
print("传感器已识别到")

# 配置
sensor.setup_sensor()
sensor.set_sample_rate(400)
sensor.set_fifo_average(8)
sensor.set_active_leds_amplitude(MAX30105_PULSE_AMP_MEDIUM)

t_start = ticks_us() # Starting time of the acquisition

MAX_HISTORY = 32
history = []
beats_history = []
beat = False
red_list = []
ir_list = []

while True:
sensor.check()
if sensor.available():
# FIFO 先进先出,从队列中取数据。都是整形int
red_reading = sensor.pop_red_from_storage()
ir_reading = sensor.pop_ir_from_storage()

if red_reading < 1000:
print('No finger')
FINGER_FLAG = False # 表示没有放手指
continue
else:
FINGER_FLAG = True # 表示手指已放

# 计算心率
history.append(red_reading)

# 为了防止列表过大,这里取列表的后32个元素
history = history[-MAX_HISTORY:]

# 提取必要数据
minima, maxima = min(history), max(history)
threshold_on = (minima + maxima * 3) // 4 # 3/4
threshold_off = (minima + maxima) // 2 # 1/2

if not beat and red_reading > threshold_on:
beat = True
t_us = ticks_diff(ticks_us(), t_start)
t_s = t_us/1000000
f = 1/t_s
bpm = f * 60
if bpm < 500:
t_start = ticks_us()
beats_history.append(bpm)
beats_history = beats_history[-MAX_HISTORY:] # 只保留最大30个元素数据
BEATS = round(sum(beats_history)/len(beats_history), 2) # 四舍五入
if beat and red_reading < threshold_off:
beat = False
# 计算血氧
red_list.append(red_reading)
ir_list.append(ir_reading)
# 最多 只保留最新的100个
red_list = red_list[-100:]
ir_list = ir_list[-100:]
# 计算血氧值
if len(red_list) == 100 and len(ir_list) == 100:
hr, hrb, sp, spb = calc_hr_and_spo2(red_list, ir_list)
if hrb is True and spb is True:
if sp != -999:
SPO2 = int(sp)

# 计算温度
TEMPERATURE = sensor.read_temperature()


if __name__ == '__main__':
# 1. 创建定时器
timer = Timer(1)
# 2. 设置定时器的回调函数,每1秒钟调用1次display_info函数(用来显示数据)
timer.init(period=1000, mode=Timer.PERIODIC, callback=display_info)
# 3. 调用主程序,用来检测数据
main()

代码解释

代码的主要逻辑是在一个无限循环中不断检测传感器读数,并进行相关的计算。首先,代码会初始化I2C对象和传感器对象,以便与MAX30102传感器进行通信。然后,它会检查传感器是否被正确连接,并设置传感器的一些配置参数,如采样率和活跃LED的幅度。

在主循环中,代码会检查传感器是否有可用的数据。如果有可用数据,它会从传感器的缓存中读取红光和红外光的读数,并判断是否有手指放置在传感器上。如果没有检测到手指,代码会继续下一次循环;否则,会进行心率和血氧的计算。

对于心率的计算,代码会将红光读数存储到一个历史列表中,并通过保留列表的最后32个元素来控制列表的长度。然后,代码会计算历史列表中的最小值和最大值,并根据这些值计算出心率的阈值。通过比较当前读数和阈值,代码可以检测到心跳的开始和结束,并计算出心率值。

对于血氧的计算,代码会将红光和红外光的读数存储到相应的列表中,并限制列表的长度为最新的100个元素。然后,代码会调用calc_hr_and_spo2函数来计算血氧值。如果计算成功,将血氧值存储在SPO2变量中。

对于温度的读取,代码使用传感器的read_temperature方法获取环境温度,并将其存储在TEMPERATURE变量中。

最后,通过定时器每秒调用一次display_info函数,将心率、血氧和温度显示出来。

OLED屏幕驱动image-20230606133118700

1673072933980

代码实现

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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
from machine import sleep, SoftI2C, Pin, Timer
from utime import ticks_diff, ticks_us
from max30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM
from hrcalc import calc_hr_and_spo2
# ------------ 添加新代码 -----------
import ssd1306


BEATS = 0 # 存储心率
FINGER_FLAG = False # 默认表示未检测到手指
SPO2 = 0 # 存储血氧
TEMPERATURE = 0 # 存储温度
HREAT_LIST = [] # 存储心率的最新30次数据
SPO2_LIST = [] # 存储血氧的最新10次数据
TEMP_LIST = [] # 存储温度的最新10次数据
# ------------ 添加新代码 -----------
OLED = None


# ------------ 添加新代码 -----------
def create_oled():
global OLED

# 创建I2C对象(驱动屏幕)
i2c_oled = SoftI2C(scl=Pin(12), sda=Pin(13))
# 宽度高度
oled_width = 128
oled_height = 64
# 创建oled屏幕对象
OLED = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c_oled)

OLED.fill(0)
OLED.text('Put your finger', 0, 30)
OLED.show()


def display_info(t):
# ------------ 修改代码 -----------
global HREAT_LIST, SPO2_LIST, TEMP_LIST, OLED, BEATS, SPO2, TEMPERATURE

# 如果没有检测到手指,那么就不显示
if FINGER_FLAG is False:
HREAT_LIST.clear()
SPO2_LIST.clear()
TEMP_LIST.clear()
# ------------ 添加新代码 -----------
BEATS = SPO2 = TEMPERATURE = 0
OLED.fill(0)
OLED.text('Put your finger', 0, 30) # OLED屏幕显示 放手指
OLED.show()
return

# ------------ 修改代码 将下方代码注释 -----------
"""
print('心率: ', BEATS, " 血氧:", SPO2, " 温度:", TEMPERATURE)

if len(HREAT_LIST) < 30:
HREAT_LIST.append(BEATS)
HREAT_LIST = HREAT_LIST[-30:] # 保证列表最多30个元素
print("正在检测【心率】...", 30 - len(HREAT_LIST))
elif len(SPO2_LIST) < 10:
SPO2_LIST.append(SPO2)
SPO2_LIST = SPO2_LIST[-10:] # 保证列表最多10个元素
print("正在检测【血氧】...", 10 - len(SPO2_LIST))
elif len(TEMP_LIST) < 10:
TEMP_LIST.append(TEMPERATURE)
TEMP_LIST = TEMP_LIST[-10:] # 保证列表最多10个元素
print("正在检测【温度】...", 10 - len(TEMP_LIST))
else:
print("----------已完成检测----------")
print('心率: ', sum(HREAT_LIST[20:])/len(HREAT_LIST[20:]), " 血氧:", sum(SPO2_LIST[5:])/len(SPO2_LIST[5:]), " 温度:", sum(TEMP_LIST[5:])/len(TEMP_LIST[5:]))
"""

# ------------ 添加新代码 -----------
if len(HREAT_LIST) < 30:
bmp_str = 'BPM :%3d' % int(BEATS)
else:
bmp_str = 'BPM :%3d' % int(sum(HREAT_LIST[20:])/len(HREAT_LIST[20:]))

if len(SPO2_LIST) < 10:
spo2_str = 'SPO2:%3d' % int(SPO2)
else:
spo2_str = 'SPO2:%3d' % int(sum(SPO2_LIST[5:])/len(SPO2_LIST[5:]))

if len(TEMP_LIST) < 10:
temp_str = 'TEMP:%3d' % int(TEMPERATURE)
else:
temp_str = 'TEMP:%3d' % int(sum(TEMP_LIST[5:])/len(TEMP_LIST[5:]))

if len(HREAT_LIST) < 30:
HREAT_LIST.append(BEATS)
HREAT_LIST = HREAT_LIST[-30:] # 保证列表最多30个元素
bmp_str += " <-- %d" % (30 - len(HREAT_LIST))
elif len(SPO2_LIST) < 10:
SPO2_LIST.append(SPO2)
SPO2_LIST = SPO2_LIST[-10:] # 保证列表最多10个元素
spo2_str += " <-- %d" % (10 - len(SPO2_LIST))
elif len(TEMP_LIST) < 10:
TEMP_LIST.append(TEMPERATURE)
TEMP_LIST = TEMP_LIST[-10:] # 保证列表最多10个元素
temp_str += " <-- %d" % (10 - len(TEMP_LIST))

OLED.fill(0)
OLED.text(bmp_str, 0, 10)
OLED.text(spo2_str, 0, 30)
OLED.text(temp_str, 0, 50)
OLED.show()


def main():
global BEATS, FINGER_FLAG, SPO2, TEMPERATURE # 如果需要对全局变量修改,则需要global声明

# 创建I2C对象(检测MAX30102)
i2c = SoftI2C(sda=Pin(15), scl=Pin(2), freq=400000) # Fast: 400kHz, slow: 100kHz

# 创建传感器对象
sensor = MAX30102(i2c=i2c)

# 检测是否有传感器
if sensor.i2c_address not in i2c.scan():
print("没有找到传感器")
return
elif not (sensor.check_part_id()):
# 检查传感器是否兼容
print("检测到的I2C设备不是MAX30102或者MAX30105")
return
else:
print("传感器已识别到")

# 配置
sensor.setup_sensor()
sensor.set_sample_rate(400)
sensor.set_fifo_average(8)
sensor.set_active_leds_amplitude(MAX30105_PULSE_AMP_MEDIUM)

t_start = ticks_us() # Starting time of the acquisition

MAX_HISTORY = 32
history = []
beats_history = []
beat = False
red_list = []
ir_list = []

while True:
sensor.check()
if sensor.available():
# FIFO 先进先出,从队列中取数据。都是整形int
red_reading = sensor.pop_red_from_storage()
ir_reading = sensor.pop_ir_from_storage()

if red_reading < 1000:
print('No finger')
FINGER_FLAG = False # 表示没有放手指
continue
else:
FINGER_FLAG = True # 表示手指已放

# ------------ 修改代码 -----------
if len(HREAT_LIST) < 30:
# 计算心率
history.append(red_reading)

# 为了防止列表过大,这里取列表的后32个元素
history = history[-MAX_HISTORY:]

# 提取必要数据
minima, maxima = min(history), max(history)
threshold_on = (minima + maxima * 3) // 4 # 3/4
threshold_off = (minima + maxima) // 2 # 1/2

if not beat and red_reading > threshold_on:
beat = True
t_us = ticks_diff(ticks_us(), t_start)
t_s = t_us/1000000
f = 1/t_s
bpm = f * 60
if bpm < 500:
t_start = ticks_us()
beats_history.append(bpm)
beats_history = beats_history[-MAX_HISTORY:] # 只保留最大30个元素数据
BEATS = round(sum(beats_history)/len(beats_history), 2) # 四舍五入
if beat and red_reading < threshold_off:
beat = False
# ------------ 修改代码 -----------
elif len(SPO2_LIST) < 10:
# 计算血氧
red_list.append(red_reading)
ir_list.append(ir_reading)
# 最多 只保留最新的100个
red_list = red_list[-100:]
ir_list = ir_list[-100:]
# 计算血氧值
if len(red_list) == 100 and len(ir_list) == 100:
hr, hrb, sp, spb = calc_hr_and_spo2(red_list, ir_list)
if hrb is True and spb is True:
if sp != -999:
SPO2 = int(sp)
# ------------ 修改代码 -----------
elif len(TEMP_LIST) < 10:
# 计算温度
TEMPERATURE = sensor.read_temperature()


if __name__ == '__main__':
# 1. 创建定时器
timer = Timer(1)
# 2. 设置定时器的回调函数,每1秒钟调用1次display_info函数(用来显示数据)
timer.init(period=1000, mode=Timer.PERIODIC, callback=display_info)
# ------------ 添加新代码 -----------
# 3. 创建OLED屏幕
create_oled()
# 4. 调用主程序,用来检测数据
main()

实验总结和感悟

在我使用ESP32开发板和MAX30102传感器设计的可穿戴设备的实验中,我成功地实现了心率、血氧和温度的测量功能。以下是我对这次实验的心得体会。

首先,我深刻认识到了传感器在物联网和可穿戴设备领域的重要性。MAX30102传感器能够准确测量心率和血氧水平。通过与ESP32开发板的结合,我能够实时获取传感器采集到的数据,并进行处理和展示。这使我对传感器的原理和工作方式有了更深入的理解。

其次,我学会了如何使用ESP32开发板进行数据采集和处理。ESP32是一款功能强大的微控制器,具有丰富的通信接口和处理能力。通过编写适当的代码,我能够将传感器的数据读取到ESP32上,并使用串口或者其他通信方式将数据传输到计算机或移动设备上。这为数据的后续处理和分析提供了便利。

在实验过程中,我遇到了一些挑战。首先是传感器的校准和信号处理。传感器的准确性和稳定性对于测量结果至关重要。我花了一些时间研究和调整代码,确保测量结果的准确性。