毫米波雷达+AI+微信小程序:生命体征监测系统完整技术方案
2026/9/16 20:47:43 网站建设 项目流程

简介:一份基于毫米波与人工智能的生命体征监测与情绪识别微信小程序完整源码包,面向计算机、人工智能、通信、物联网等相关专业的学生及开发者。项目涵盖小程序端页面交互、数据展示与业务逻辑,适合用于课程设计、毕业设计或初期项目立项演示,也便于新手按模块拆解学习。源码包共848个文件,以JS、TS、WXSS、WXML、JSON文件为主,辅以WXS工具脚本及PNG/SVG图片素材,整体仅1.31MB,结构紧凑、目录清晰。已有196人学习下载。整体功能经过测试运行成功,可直接导入微信开发者工具查看;封面图片与echarts.js等第三方库的引入方式、小程序组件化目录划分,均具有较高借鉴价值,也可作为毕业设计初版方案,并帮助初学者理解毫米波数据在前端可视化与情绪识别场景中的落地流程。

1. 为什么是毫米波、AI 和微信小程序这三样东西拼在一起

一套「毫米波 + AI + 微信小程序」的生命体征监测系统,拆开看分别是三种不同领域的事:雷达信号处理、边缘 AI 推理、蓝牙低功耗数据链路。把它们拼在一起,解决的是同一个刚需——不用接触人体、不侵犯隐私、又能持续拿到呼吸和心跳数据,再把结果送到每个人手机里。养老院夜间看护、独居老人居家监测、新生儿监护这类场景,摄像头有隐私争议,接触式腕带又容易在睡眠中被摘掉,毫米波雷达是当前少有的能同时绕开这两个问题的传感器。

但很多人拿到这类「完整源码 + 说明」的工程包后,卡住的不是 AI 模型,而是整条链路没打通:雷达原始数据怎么变成呼吸率心率,情绪识别到底识别什么特征,微信小程序怎么稳定收蓝牙数据。这篇文章就按「雷达信号处理 → 情绪特征与模型 → 小程序 BLE 数据链路 → 部署验证」的顺序,把每个环节的参数、代码和坑位铺开。适合正在做毫米波生命体征项目、或准备接手类似源码包的嵌入式、前端和大模型方向的工程师。

2. 毫米波雷达信号怎么变成呼吸和心率

2.1 FMCW 单帧测距与慢时间序列的构建

常见做法是使用 TI 的 IWR1443/IWR6843 或英飞凌的 BGT60TR13C 这类 60GHz 频段 FMCW 雷达。FMCW 雷达发射线性调频连续波,回波与发射信号混频后得到中频信号,频率正比于目标距离。做生命体征监测时,雷达发射一帧包含多个 chirp 的波形,先对单个 chirp 做 FFT,得到距离维频谱;再在同一个距离门(Range Bin)上观察多个 chirp 之间的相位变化。人体胸腔的起伏只有几毫米,但 FMCW 雷达对相位极其敏感,漫反射回来的波在同一个距离门上形成可检测的微多普勒与微位移信号。

把每个 chirp 的峰值相位按时间顺序排列,就得到慢时间序列(Slow-time Sample),这个序列的采样率等于 chirp 重复频率,一般在 20~50Hz 之间。帧结构上我一般会让每帧包含 32~64 个 chirp,帧率控制在 20fps,既能覆盖 0.8~3Hz 的心跳频段,又不至于让数据量过大。下面这段代码展示最常见的处理流程:先在距离维上找到胸腔所在的距离门,再取该距离门上的相位序列。

import numpy as np from scipy import signal # radar_data: 单帧距离维FFT结果,shape = (num_chirps, num_range_bins) # 假设距离门0.05m,目标在1.2m附近,对应bin索引约24 range_resolution = 0.05 target_distance = 1.2 target_bin = int(target_distance / range_resolution) # 取该距离门上的复数序列(I/Q信号) slow_time_complex = radar_data[:, target_bin] # 提取相位并解缠绕,得到胸腔微动位移信号 phase = np.unwrap(np.angle(slow_time_complex))

代码里的np.unwrap是关键。相位在 -π~π 之间跳变,如果不先解缠,后续滤波和 FFT 会出现大量假峰。target_bin的选择不是固定的,可以用能量最大法自动选门:计算每个距离门在慢时间维上的能量,选能量最高的那个门作为胸腔位置。常见错误是把所有距离门的数据叠加,这会同时混入呼吸谐波和身体抖动,信噪比明显下降。

2.2 相位解缠与带通滤波:把微动从相位里抠出来

拿到了相位序列,接下来的任务是分离呼吸和心跳。呼吸引起的胸腔位移约 4~12mm,对应 0.1~0.5Hz 的信号;心跳引起的位移只有 0.2~0.5mm,对应 0.8~3Hz。两者的幅度差接近一个数量级,所以不能简单用一个带通滤波器直接滤出心跳,必须先做呼吸谐波抑制。

我的处理顺序是:先去掉直流和线性趋势(用一阶差分或多项式拟合),再做一次呼吸频带的带通滤波,从原始信号中减去重建的呼吸信号,最后才对残差做心跳频带滤波。这种「减法式」滤波比直接让心跳信号通过高通滤波器更有效,因为呼吸的二次谐波会落在 0.2~1.0Hz,正好与心跳频带重叠。

fs = 20.0 # 慢时间采样率 20Hz # 设计0.1~0.5Hz呼吸带通滤波器,4阶巴特沃斯 b_breathe, a_breathe = signal.butter(4, [0.1/(fs/2), 0.5/(fs/2)], btype='bandpass') breathe = signal.filtfilt(b_breathe, a_breathe, phase) # 设计0.8~3Hz心跳带通滤波器 b_heart, a_heart = signal.butter(4, [0.8/(fs/2), 3.0/(fs/2)], btype='bandpass') residual = phase - breathe heartbeat = signal.filtfilt(b_heart, a_heart, residual)

滤波器的阶数选择有讲究。阶数太高会带来群延迟和振铃效应,阶数太低则阻带衰减不够。filtfilt做零相位滤波,保证滤完后的信号峰值位置不偏移,这对后续计算准确的 R 峰位置很重要;实际嵌入式环境里用lfilter替代,但要接受几个采样点的相位延迟,在做呼吸率/心率计算时补偿掉这个延迟。

2.3 呼吸率与心率估计:从频谱峰值到时域修正

滤波完成后,用短时傅里叶变换(STFT)估计频率,比直接对整个序列做 FFT 更适合生命体征场景——人的呼吸和心率在几十秒内就会有波动,整段 FFT 把时间信息全丢了。常见设置是:窗口长度 30 秒、窗口重叠 25 秒、FFT 点数 512,频率分辨率约 0.03Hz,可以区分 18 与 19 次/分钟的呼吸差异。

# 30秒窗、25秒重叠,计算呼吸频谱图 f, t, Zxx = signal.stft(breathe, fs=fs, nperseg=fs*30, noverlap=fs*25, nfft=512) # 在每个时间窗内找0.1~0.5Hz的峰值 resp_rate = [] for col in Zxx.T: idx = np.where((f >= 0.1) & (f <= 0.5))[0] peak_idx = idx[np.argmax(np.abs(col[idx]))] resp_rate.append(f[peak_idx] * 60) # 转换为 bpm

时域上还有一道校验:呼吸波形是类似正弦的周期信号,可以用过零检测辅助确认频率峰值不是谐波或噪声。心率估计同理,但要在 0.8~3Hz 频带内搜索峰值,并额外加上一个 5 秒滑动中值滤波,防止单次误检把心率从 70 跳到 140。这个中值滤波的窗口不能太长,否则心率突变(比如从睡眠进入清醒)反应太慢。

2.4 信号质量评估:什么时候该信数据,什么时候该丢

生成了呼吸率和心率,并不代表数值可信。毫米波雷达对目标的体动极其敏感:人翻身、手挡住胸口、离开雷达覆盖区域,相位信号会瞬间饱和或产生跳变。我一般会在算法输出前加一个质量门控,计算滑动窗口内的三个指标:

指标计算方式阈值建议
信号幅度30秒窗口内相位波形的标准差低于 0.005 rad 视为丢目标
频谱纯度峰值能量 / 频带总能量低于 0.4 视为多干扰源
时频一致性相邻两秒频率差超过 0.15Hz 视为体动干扰

当三个指标任一不合格时,直接把当前窗口的数据标记为无效,而不是强行输出一个人为平滑后的数值。这是生命体征设备最容易被忽略的一点:监测量是给人看的,错误的数据比没数据危害更大。实际部署时建议在协议层保留这个质量位,微信小程序端根据质量位决定是否显示数值,而不是让前端再做一套滤波逻辑。

3. AI 情绪识别的特征与轻量模型落地方案

3.1 情绪识别不是猜表情:HRV 与呼吸特征的生理依据

毫米波雷达做情绪识别,不是去识别人脸表情,而是通过心肺系统的自主神经反应间接推断情绪状态。当人紧张或焦虑时,交感神经兴奋,心率升高、心率变异性(HRV)降低、呼吸频率上升且变得不规则;平静或放松时,副交感神经主导,HRV 升高,呼吸波形更平滑。这些生理信号完全可以通过毫米波雷达测到的心跳间隔(IBI,Inter-Beat Interval)和呼吸波提取。

所以一套合理的情绪识别系统,输入不是原始波形,而是从波形中提取的时序特征。常见做法是把情绪状态划分为平静、紧张、疲劳、愉悦四类,或者更保守地只分「放松 / 紧张」二分类,避免在真实部署里对过多情绪类别下结论。这里要明确一点:用雷达做情绪识别,准确率天花板受限于生理信号的间接性,目标不是读心,而是识别用户当前的压力水平区间。

3.2 特征工程:从 30 秒窗口里算出的 12 个可解释特征

我通常在一个 30 秒滑窗内计算 12 个特征,窗移 5 秒。频域特征用上一章得到的呼吸率、心率和频谱纯度;时域特征全部从 IBI 序列计算。关键的一个前处理是 IBI 的去伪:相邻两次心跳间隔的差值如果超过 20%,记为异常,用线性插值替换,否则 HRV 特征会被单个噪声点污染。

def compute_hrv_features(ibi_series): # ibi_series: 单位秒,心跳间隔序列 diff = np.abs(np.diff(ibi_series)) # 去伪:差值超过20%的点用前后均值插值 for i in range(len(diff)): if diff[i] > 0.2 * ibi_series[i]: ibi_series[i+1] = (ibi_series[i] + ibi_series[i+2]) / 2 sdnn = np.std(ibi_series) # 时域:所有间隔标准差 rmssd = np.sqrt(np.mean(np.square(np.diff(ibi_series)))) # 相邻差均方根 # 频域:用 lomb-scargle 或插值后FFT,取LF(0.04-0.15Hz)和HF(0.15-0.4Hz) from scipy.signal import lombscargle freqs = np.linspace(0.04, 0.4, 100) pgram = lombscargle(np.cumsum(ibi_series), ibi_series, freqs) lf = pgram[(freqs >= 0.04) & (freqs < 0.15)].sum() hf = pgram[(freqs >= 0.15) & (freqs <= 0.4)].sum() return sdnn, rmssd, lf / (hf + 1e-6) # LF/HF比值

sdnn反映整体波动水平,rmssd主要反映副交感神经活性,LF/HF比值常被用来估计交感-副交感平衡。需要提醒的是,这些指标在不同年龄段和身体状态下的基线差异很大,单人的「相对变化」比跨人的「绝对数值」更有意义,所以模型输入中通常要拼接用户的个人基线偏移量,而不是直接喂原始 HRV 数值。

3.3 模型选型与 TFLite 部署边界

雷达端算力有限,情绪识别模型必须轻量。三类常用方案各有适用场景:

方案输入维度参数量适用场景帧率要求
SVM/RandomForest12 维特征几 KB时序无关,稳定状态识别5 秒一次
1D-CNN30 秒波形序列50~200KB需捕捉波形形态实时流式
LSTM/GRUIBI 或频谱序列500KB+趋势预测连续观测

我一般首推随机森林加 12 维特征,原因很实际:训练数据量通常只有几百条标注样本,树模型不容易过拟合,而且部署成 C 数组只需一个predict()函数;深度模型在样本量不足时反而容易学到数据集的个体差异。若确实需要捕捉情绪变化的连续过程,折中方案是随机森林在时间窗上的多数投票,用时间上下文替代序列模型,工程成本低很多。

模型训练完后转 TFLite,一个容易踩坑的点是量化。随机森林没有直接的 TFLite 转换路径,常见做法是把树的判定条件转成if-else规则,或直接用sklearn-porter转 C 代码。如果用了 1D-CNN,转 TFLite 时的默认 float32 模型在 MCU 上跑一次推理约 30~80ms,动态范围量化后降到 5~15ms,精度损失通常低于 2%。量化校准集至少用 1000 条真实采集的特征片段,不要拿训练集当校准集。

3.4 训练数据与窗口滑动的工程取舍

情绪识别系统最大的风险是数据泄露:训练集和测试集如果来自同一个人相邻时间段,模型会学到「当前状态」而不是「情绪特征」,表现为验证集准确率高达 90%,换一个陌生人直接掉到 60%。我建议严格按时间段划分数据集:比如前 70% 的采集时间做训练,后 30% 做测试,中间留出 10 分钟间隔,保证同一生理状态不在两个集合里重复出现。

窗口滑动策略上,30 秒窗对 HRV 计算足够,对情绪突变响应偏慢。如果产品需求是检测压力事件,可以把窗降到 15 秒,但特征方差会变大,模型误报率上升;我的做法是双窗并行:15 秒窗做快速预警,30 秒窗做状态确认,两个模型输出不一致时以状态确认结果为准。这样既保留了响应速度,又没有牺牲稳定性。

4. 微信小程序端的 BLE 数据链路与实时波形

4.1 从串口/蓝牙到小程序的字节流协议设计

雷达端的 AI 推理输出经过蓝牙透传模块(常见使用低功耗蓝牙 5.0 模块)发送到微信小程序。数据链路设计是整个系统里最容易被低估的部分:微信小程序的 BLE API 一次特征值写入最多 20 字节,且蓝牙底层的 MTU 协商结果影响单包长度,所以协议必须设计成分包传输、按帧重组。

我常用的协议格式是这样:每一帧固定为 16 字节,分为 4 个字段——帧头(2 字节)、消息类型(1 字节)、数据区(10 字节)、校验(1 字节)和帧尾(2 字节)。消息类型区分三类:状态包(呼吸率/心率/情绪结果)、波形包(心跳波形采样点)、调试包(原始信号质量参数)。波形包因为单点需要 2 字节,一帧正好放 5 个采样点,20Hz 波形率需要每秒发 4 帧,加上状态包每秒发 1 帧,总 BLE 吞吐量约 80~100 字节/秒,完全在低功耗蓝牙的可行范围内。

// 编码:状态包数据区 // byte0-1: 呼吸率 bpm,放大10倍存入 Uint16 // byte2-3: 心率 bpm,放大10倍存入 Uint16 // byte4: 情绪类别 0平静 1紧张 2疲劳 3愉悦 // byte5: 信号质量位 0无效 1有效 const buf = new ArrayBuffer(16); const view = new DataView(buf); view.setUint8(0, 0xAA); // 帧头 view.setUint8(1, 0x55); // 帧头 view.setUint8(2, 0x01); // 消息类型:状态包 view.setUint16(3, Math.round(respRate * 10), true); view.setUint16(5, Math.round(heartRate * 10), true); view.setUint8(7, emotionClass); view.setUint8(8, signalValid ? 1 : 0); view.setUint8(14, 0x0D); // 帧尾 view.setUint8(15, 0x0A); // 帧尾

把数值放大 10 倍再存,是为了避免传输浮点数。蓝牙透传如果用 ASCII 字符串,一帧只能传 5 个字符,效率太低;用定点数把一位小数变成整数,接收端再除以 10,是嵌入式端最通用也最稳妥的编码方式。

4.2 小程序 BLE 连接与特征值订阅的完整步骤

微信小程序端的 BLE 操作有固定的流程:初始化蓝牙适配器 → 开始扫描 → 获取设备 → 连接设备 → 获取服务 → 获取特征值 → 订阅通知 → 监听数据回调。每一步都是异步 API,常见的坑是回调嵌套过深后某个步骤静默失败,所以每步都要做超时保护。

// 扫描到设备后连接并订阅 connectBLEDevice(deviceId) { const that = this; wx.createBLEConnection({ deviceId, success: () => { wx.getBLEDeviceServices({ deviceId, success: (res) => { const service = res.services.find(s => s.uuid.indexOf('ffe0') >= 0); wx.getBLEDeviceCharacteristics({ deviceId, serviceId: service.uuid, success: (chrRes) => { const notifyChr = chrRes.characteristics.find(c => c.properties.notify && c.uuid.indexOf('ffe1') >= 0); wx.notifyBLECharacteristicValueChange({ deviceId, serviceId: service.uuid, characteristicId: notifyChr.uuid, state: true, success: () => { wx.onBLECharacteristicValueChange((data) => { this.parsePacket(data.value); // 分包重组 }); } }); } }); } }); }, fail: () => setTimeout(() => this.connectBLEDevice(deviceId), 1000) }); }

这里要特别处理一个微信官方文档一般不细说的点:onBLECharacteristicValueChange必须放在notifyBLECharacteristicValueChange成功回调之后注册,如果先注册再开启通知,可能丢失前几个包。另外,iOS 和安卓的蓝牙缓存策略不同,同一时刻只能有一个onBLECharacteristicValueChange监听,页面卸载时用wx.offBLECharacteristicValueChange释放,否则不同页面之间会串数据。

4.3 实时波形绘制与 FFT 显示的低功耗写法

小程序原生wx.createCanvasContext性能有限,绘制 20Hz 的滚动心电图必须用离屏 canvas 配合requestAnimationFrame。我的做法是维护一个固定长度的环形缓冲区(例如 1500 个点),每次收到新的波形包就把数据 push 进缓冲区,然后只重绘当前帧的变化区域,而不是清屏后全部重画。

// 环形缓冲区:收到5个新点后一次绘制 drawWave(containerSize, canvas) { const len = this.waveBuf.length; const step = containerSize.width / 1500; canvas.clearRect(0, 0, containerSize.width, containerSize.height); canvas.beginPath(); for (let i = 1; i < len; i++) { const x = (i - 1) * step; const y = containerSize.height / 2 - this.waveBuf[i] * 40; if (i === 1) canvas.moveTo(x, y); else canvas.lineTo(x, y); } canvas.stroke(); }

波形绘制还有一个精度细节:心跳波形(BCG 信号)的幅度本身就很小,直接映射到 canvas 高度后几乎是一条直线。我一般会先做一次实时归一化,取最近 5 秒窗口的峰峰值作为映射范围,而不是用固定比例尺。这样波形始终占满绘图区域,用户能看出心跳节律的变化,但要注意注释里标明「归一化显示」,避免用户把波形幅度误解为真实心跳强度。

4.4 分包、丢包与 DataView 解析的兼容性写法

BLE 的 MTU 协商不统一:安卓默认 23 字节,iOS 通常 185 字节。如果你的设备一次通知就发 20 字节,问题不大;但很多模块会按更大的 MTU 把多个逻辑帧拼在一个通知里发送。因此小程序端必须实现「流式解析」,把收到的字节先丢进接收缓冲区,再按帧头AA 55和帧尾0D 0A切帧。

parsePacket(data) { const bytes = new Uint8Array(data); for (let i = 0; i < bytes.length; i++) { this.rxBuffer.push(bytes[i]); } // 扫描所有帧头,截出完整帧 while (true) { const headIdx = this.rxBuffer.findIndex((v, idx) => v === 0xAA && this.rxBuffer[idx + 1] === 0x55); if (headIdx < 0) { this.rxBuffer = []; break; } if (headIdx > 0) this.rxBuffer.splice(0, headIdx); // 丢弃帧头前的乱数据 if (this.rxBuffer.length < 16) break; const frameEnd = this.rxBuffer.findIndex((v, idx) => v === 0x0D && this.rxBuffer[idx + 1] === 0x0A && idx > 0); if (frameEnd !== 14) { this.rxBuffer.splice(0, 1); continue; } const frame = this.rxBuffer.slice(0, 16); this.rxBuffer.splice(0, 16); this.decodeFrame(new DataView(new Uint8Array(frame).buffer)); } }

解析逻辑里最容易犯的错误是用数组索引当帧头偏移,但实际收到的字节流可能从任意位置开始。上面的代码先把多余头部数据切掉,再检查帧尾位置是否正好为 14,不满足就丢掉第一个字节继续找,保证任何丢包情况下都能自行恢复同步。如果用的是 uniapp 开发,onBLECharacteristicValueChange的 API 名完全一致,只是需要额外处理uni对象的 Promise 化,逻辑不变。

5. 部署验证与现场调试的检查清单

5.1 数据自检:用模拟器先跑通协议

拿到源码包后第一时间不是连真雷达,而是先做协议对测。方法是用串口工具或 Python 脚本模拟雷达端,按 4.1 节的帧格式定时发送数据,微信开发者工具里看有没有正确解析。这个步骤能隔离问题域:如果模拟器数据到达小程序端正常显示,说明问题出在雷达端/串口/BLE 模块;如果模拟器都不通,说明小程序代码有 bug,不需要硬件就能排查。

我习惯把模拟器脚本按「正常呼吸 → 呼吸暂停 15 秒 → 快速呼吸」三段编写,验证前端对无效数据(质量位=0)的显示逻辑是否正确。对比真机采集数据和模拟数据时,记得确认雷达端的时间基准:BLE 传输延迟通常只有几十毫秒,但小程序端的 setData 更新频率不要超过 10Hz,否则 CPU 占用升高导致页面卡顿,波形看起来像卡顿的假数据。

5.2 现场参数边界:遮挡、体位与多目标干扰

雷达安装高度和角度直接影响信号质量。60GHz 毫米波雷达的最佳覆盖区域是距离 0.5~3m、与雷达平面夹角 ±30° 的扇形区域。雷达安装过高,胸腔反射路径变长,信噪比下降;安装在床的正上方 1.5m 处常见效果最好,斜装时要额外做一次水平距离到垂直距离的几何校正。被子遮挡时呼吸幅值衰减明显但通常仍可检测,心跳信号衰减更严重,需要把 5.1 的信号质量门控阈值调低一档,或者干脆在 UI 上提示「检测信号弱」。

多目标场景(双人床)是硬伤,一个雷达同时看到两个人的呼吸,频谱上会出现两组峰值。源码包如果没做多目标分离,实际部署时必须限定单人场景。另有一个容易被忽略的参数:雷达的探测距离门设置。出厂默认可能覆盖 5~10m,过远的强反射物(墙体、衣柜)会淹没近处的人体信号,我在部署时会把距离门限制在 3m 以内,既降低计算量又减少杂波。

5.3 安装包与说明文档的工程化组织方式

这类源码包到手后建议按四个目录重新组织,方便迭代和回溯:

目录内容维护重点
radar_fw雷达固件工程(CCS/Keil)记录 mmWave SDK 版本与设备型号
ai_modelPython 训练与转换脚本固化数据划分随机种子,避免重训结果漂移
miniprogram微信小程序工程维护协议版本号,与固件一一对应
docs说明文档、协议表、现场部署记录记录每次现场的雷达高度、角度、异常现象

协议版本号这一点值得单独强调:很多问题表象是「数据不对」,实际是固件换了帧结构但小程序没同步更新。在状态包里加一个 1 字节协议版本字段,小程序端收到无法识别的版本号时直接提示「请升级小程序或固件」,比数据解析出错后排查半天高效得多。部署记录里建议把现场照片、雷达安装高度、被测者距离、床垫材质写清楚,后续回读数据时才有对照基准。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询