No description
  • Python 48.8%
  • QML 39%
  • Shell 12.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
alvin 31bad45d91 feat: heart rate bar widget with BLE HRS bridge
- 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
2026-10-31 14:37:02 +08:00
docs feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00
heart-rate feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00
systemd feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00
tests feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00
.gitignore feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00
install.sh feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00
README.md feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00
uninstall.sh feat: heart rate bar widget with BLE HRS bridge 2026-10-31 14:37:02 +08:00

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          # 假数据,先用来看界面效果

脚本做的事:

  1. 把 heart-rate/ 软链到 ~/.config/dms/plugins/heartRate
  2. 安装并启动用户服务 dms-hrmd.service
  3. 重载插件

之后在 设置 → 插件 → 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)