- Python 48.8%
- QML 39%
- Shell 12.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- dms-hrmd: bridge for the standard Bluetooth heart rate service (0x180D/0x2A37) that publishes samples to a small JSON file and reconnects with exponential backoff - heartRate: QML bar pill reading that file, with hero-zone colours, pulse animation and an option to hide while no device is connected - systemd user unit, install/uninstall scripts, zh_CN translations - 19 unit tests covering the HRS decoder and scan ordering |
||
| docs | ||
| heart-rate | ||
| systemd | ||
| tests | ||
| .gitignore | ||
| install.sh | ||
| README.md | ||
| uninstall.sh | ||
Heart Rate — DMS 状态栏心率部件
在 Dux Shell(DMS)状态栏显示实时心率,数据来自任何广播标准蓝牙心率服务(BLE Heart Rate Service,0x180D / 0x2A37)的设备:智能手表的心率广播、胸带、手机 App 都可以,不绑定具体型号。
已在 iQOO WATCH GT 上验证设计目标,同样适用于 Polar、Garmin、COROS、华为/小米手表等开启「心率广播」的设备。
实测记录
| 项目 | 结果 |
|---|---|
| 设备 | iQOO WATCH GT A07,34:05:57:68:FA:07,-46 dBm |
| 广播数据 | 标准 HRS,flags = 0x00:只有 uint8 心率值,没有接触检测和 RR 间期字段 |
| 桥接 | status: "live",每秒写入状态文件,bpm 74 / 75 / 76 连续变化 |
| 部件 | 状态栏实时显示,数值随数据跳动,CPU/内存部件同时刷新 |
| 隐藏/恢复 | 手表停止广播 → 约 10 秒后部件消失(空位归零,不留缝);手表恢复广播 → 按 30 秒上限退避重扫,约 38 秒后发现并自动重现 |
| 服务 | dms-hrmd.service active + enabled,开机自动重连 |
架构:为什么分成两层
┌─────────────────┐ BLE HRS ┌──────────────┐ JSON 文件 ┌──────────────────┐
│ 手表 / 心率带 │ ───────────► │ dms-hrmd │ ───────────► │ DMS 状态栏部件 │
│ (广播 0x2A37) │ 通知 ~1Hz │ (后台服务) │ 1Hz 轮询读取 │ heartRate (QML) │
└─────────────────┘ └──────────────┘ └──────────────────┘
dms-hrmd(Python + bleak):扫描并连接设备、订阅心率通知、解析协议、断线自动重连,把最新样本原子写入state.json。heartRate插件(纯 QML):只读那个 JSON 文件,完全不碰蓝牙。
这样拆的好处:蓝牙栈出问题不会连累 shell;cat 一下状态文件就能知道到底哪一环断了;部件本身也可以换成读任何别的心率源。
依赖
sudo pacman -S python-bleak bluez-utils
python-bleak— BLE 客户端库(走 D-Bus 访问 BlueZ,不需要 root)bluez-utils— 提供bluetoothctl,用于排障(部件本身不依赖它)
安装
./install.sh # 自动发现任何广播心率的设备
./install.sh --name "WATCH" # 按名称锁定设备(更稳,避免连到邻居的手表)
./install.sh --demo # 假数据,先用来看界面效果
脚本做的事:
- 把
heart-rate/软链到~/.config/dms/plugins/heartRate - 安装并启动用户服务
dms-hrmd.service - 重载插件
之后在 设置 → 插件 → Scan for Plugins 里启用 Heart Rate,再到 设置 → 状态栏 → 添加部件 把它放到状态栏。
没有 Scan 按钮可点时,
dms ipc call plugins list会显示No plugins available:DMS 依赖目录监听发现新插件,touch ~/.config/dms/plugins/heartRate/plugin.json也能触发,等价于点一次扫描。注意
barConfigs这个设置键无法通过dms ipc call settings set写入(会返回SETTINGS_SET_FAILURE),加部件请走设置界面。
服务安装后文件位置:
| 内容 | 路径 |
|---|---|
| 插件 | ~/.config/dms/plugins/heartRate → 本仓库 |
| 命令 | ~/.local/bin/dms-hrmd → 插件里的脚本,方便 --scan / --status |
| 服务 | ~/.config/systemd/user/dms-hrmd.service |
| 状态文件 | $XDG_RUNTIME_DIR/dms-hrm/state.json(默认,tmpfs,随会话消失) |
手动安装
mkdir -p ~/.config/dms/plugins
ln -sfn "$PWD/heart-rate" ~/.config/dms/plugins/heartRate
cp systemd/dms-hrmd.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now dms-hrmd
设备端要做什么
心率广播必须由设备主动开启,否则它不会出现在扫描结果里:
- iQOO / vivo 手表:设置 → 运动健康(或「心率广播」)→ 开启心率广播
- 华为 / 荣耀:设置 → 健康监测 → 心率广播(部分型号在「锻炼设置」里)
- Garmin:设置 → 传感器与配件 → 腕式心率 → 广播心率
- Polar / COROS:设备菜单里开启 Broadcast / 广播 HR
同一时间只能有一个客户端连接,开启广播后请关掉手表自己的手机 App 连接,否则会抢占连接。
验证设备确实在广播:
python3 heart-rate/helper/dms-hrmd.py --scan
会列出附近设备,心率设备前面标 HR:
HR C4:12:34:56:78:9A -52 WATCH GT A07
F0:11:22:33:44:55 -78 Some Speaker
使用与排障
systemctl --user status dms-hrmd # 服务是否在跑
journalctl --user -u dms-hrmd -f # 实时日志(扫描/连接/重连都在这)
python3 heart-rate/helper/dms-hrmd.py --status # 打印当前状态文件
python3 heart-rate/helper/dms-hrmd.py --demo # 不需要硬件,验证界面
状态文件的形状:
{
"v": 1,
"status": "live",
"connected": true,
"bpm": 72,
"ts": 1761900000.5,
"contact": true,
"rr": [819.3, 812.5],
"battery": 87,
"device": { "address": "C4:12:34:56:78:9A", "name": "WATCH GT A07" },
"error": null,
"helper": { "pid": 1234, "version": "1.0.0" }
}
status 取值:searching / connecting / live / disconnected / error / stopped。ts 是样本时间(epoch 秒),部件靠它判断数据是否已过期。
| 现象 | 原因与处理 |
|---|---|
| 状态栏只有灰色图标,没有数字 | 桥接没在跑:systemctl --user start dms-hrmd |
status: "searching" 一直不变 |
设备没开广播,或不在范围内;用 --scan 确认。找不到设备时按 1/2/4/8/15/30 秒退避重扫(避免蓝牙空转耗电),所以重新开启广播后最迟约 40 秒才会被发现;连接成功过再断线则会立即快速重试 |
status: "live" 但没有数值 |
已连接但收不到通知:在设备上重新开启广播;有些手表连接后需要几秒才开始推送 |
| 数值偶尔变灰 | 心跳数据超过「信号超时」秒数;正常掉线会自动重连 |
journalctl 里 BleakError: Not authorized |
该设备要求配对:给服务加 --pair 参数重启 |
插件设置
| 设置 | 说明 |
|---|---|
| 显示内容 | 图标 + 数值 / 仅数值 / 仅图标 |
| 状态栏图标 | monitor_heart、favorite、ecg_heart、cardiology |
| 显示单位 | 数值后加 bpm |
| 按心率区间着色 | 数值颜色随强度变化:静息 → 热身 → 有氧 → 无氧 |
| 最大心率 | 仅用于区间着色;220 - 年龄 是个粗略起点 |
| 信号超时 | 超过多少秒没有心跳就变灰(默认 15s) |
| 未连接时隐藏 | 桥接没连上设备时整个部件从状态栏消失(默认开启,见下) |
| 心跳动画 | 图标按实测心率缩放;关闭可省电 |
| 状态文件 | 留空即用默认路径,仅在桥接改了 --output 时才需要填 |
「未连接时隐藏」怎么判定
- 触发条件:状态文件里
status != "live"或connected != true(桥接没在跑、扫描中、连接中、掉线都算) - 8 秒宽限:掉线后不会立刻消失,避免 BLE 重连时部件闪一下就没
- 隐藏期间仍以 2 秒间隔轮询状态文件,设备一回来就自动重新出现;状态栏本身自动隐藏时轮询暂停,栏一出来立即恢复
- 连接正常但暂时没有心跳数据(比如广播开着却没戴)不会隐藏,只会按「信号超时」变灰 —— 那种情况信息本身有意义
实现上复用了 DMS 自己的 effectiveVisible(淡出 + 宽度归零),没有手动改几何尺寸,所以和 DMS 内置部件的隐藏动画一致。
点击部件会弹出面板:大号心率数字、强度区间、设备名、最后更新时间、桥接状态。数据过期时面板会显示原因和重启命令。
调整与移除
- 换位置:设置 → 状态栏 → 编辑,拖动部件;右键可移除
- 临时停用:设置 → 插件 → 关闭 Heart Rate(状态栏与设置项一并消失,配置保留)
- 彻底卸载:
./uninstall.sh(移除软链、服务、状态文件)
协议实现说明
0x2A37(Heart Rate Measurement)解析规则:
| Flags 位 | 含义 | 处理 |
|---|---|---|
| bit0 | 心率值宽度 | 0 = uint8,1 = uint16(小端) |
| bit1 | 接触检测 | 有 bit2 时才可信,否则记为 null |
| bit2 | 是否支持接触检测 | 决定 contact 是 true/false 还是 null |
| bit3 | 能量消耗 | uint16,可选 |
| bit4 | RR 间期 | uint16 数组,单位 1/1024 秒,换算成毫秒 |
可选字段被截断时只跳过该字段,不丢弃整个样本 —— 单个畸形包不会让数据流中断。
测试
python3 -m unittest discover -s tests -v # 13 个协议解析测试
卸载
./uninstall.sh
移除软链、服务和状态文件;插件设置保留在 ~/.config/dms/settings.json 里。
兼容性
- 需要 DMS ≥ 2.0.0(
surfaceLive、PopoverComponent、LayerShell命名空间等) - 任何实现标准 HRS 的设备都能用,不依赖厂商 SDK
- 部件读文件,所以桥接也可以用别的实现替换(只要写出同样格式的 JSON)
