lachlanmcalpine/colmi-ring
GitHub: lachlanmcalpine/colmi-ring
该项目通过逆向工程 Colmi R02 智能戒指固件并提取原始加速计数据流,构建了一个基于单枚戒指的打字解码器,探索廉价可穿戴设备能否为 AR 眼镜提供文本输入能力。
Stars: 1 | Forks: 0
# Colmi R02 —— 一款 20 美元的智能戒指到底能给你带来什么
*为廉价的 BLE 智能戒指打了字节补丁的固件,一个与它自身数据手册相矛盾的被测出的加速计上限,一个建立在其上的打字解码器,以及一次针对一个在自身硬件上撒谎的戒指进行的固件逆向工程工作。这里的大部分内容都是在具体的、有指导意义的方面犯错并修复它的记录。*
**核心结论:** 真正的独立采样上限是 **352 Hz 未滤波 / 270 Hz 已滤波** —— 这是使用传感器自身的 NEW-data 位确定的,而不是靠计算数据包得出的。数据手册暗示能达到 2 kHz。实际上并不存在,而这个代码仓库准确记录了原因。在这个数据流之上,一个全词解码器在单枚戒指的**连贯文本上达到了 95% 的 top-5 / 86% 的 top-1 单词准确率** —— 并且我可以准确地证明,剩下的误差是*传感器*的极限,而不是模型的极限。
## 为什么我要做这个
AR 眼镜存在一个至今无人能解的文本输入问题。目前的所有解决方案要么是重新外挂一个键盘 —— 这完全违背了不用携带设备的初衷 —— 要么就是让你在公共场合对着你的眼镜说话。我想知道是否可以用一枚戒指来代替:极致精简的外形,双手无需改变位置,不需要任何操作平面。
因此,最初引发这一切的问题非常具体。**一款 20 美元的智能戒指包含足够用来打字的信号吗?** 以下所有内容都是为了回答这个问题而做的工作。
## 问题所在
消费级可穿戴设备暴露的是经过大量滤波和严重下采样的涓涓细流般的运动数据,因为计步器需要的也就这么多。如果你想要任何依赖于*精细*动作的功能 —— 比如手势控制,或者读取手指敲击 —— 公开的接口就远远不够了,而厂商也没有理由为你提供更多。所以:硬件中究竟有多少信号,你能提取出多少,其中又有多少*足够*用来打字?
## 出了什么问题,以及我做了什么改变
这是最如实记录的部分,也是该项目的大部分内容。四个独立的事件脉络,每一个都是一个让人安心的假设掩盖了真实问题的案例。
### 1. 我把采样率搞错了三次
我报告过 936 Hz,然后是 1650 Hz,然后是 440 Hz。这三个都错了。每一次都是计算*数据包*或*回调*而不是*独立采样*所造成的假象 —— BLE walker 把同一个读数交给了我不止一次,而我还在勤奋地测量我自己的重复项。早先的一个“100 Hz”的数字是另一种 bug:缓慢的 FIFO 读取循环,而不是硬件限制。
**真正解决问题的是找到了一个传感器无法撒谎的真值。** 加速计在寄存器 `0x02` 的第 0 位暴露了一个 NEW-data 位。轮询*这个位*而不是相信数据包的到达,能给出一个不会被重复读取夸大的数字。这就是 352 Hz 的来源,也是我现在信任它而不信任之前三个数字的原因。这种单次发送的轮询也*证明*了上限,而不是仅仅观察它:如果硬件更快,循环会更快地找到它的十个新采样,更快触发,并传递更多数据。但它没有。
在四次修复失败后,我放弃了追逐 2 kHz。数据手册上说未滤波的 ADC 以 2 kHz 运行;STK8BA50 Linux 驱动独立暗示是 1792。两者都说速度摆在那里。我尝试了四种方法让 FIFO 进行累积 —— 每次它都保持在 count = 1 的旁路模式。我在笔记中写下*已得出结论 / 死胡同*并停止了尝试。**两份文档的共识并不能凌驾于对你手中零件的一次实际测量之上,而知道何时该停止为一个假设买单,是一项更有用的技能。**
### 2. Teacher forcing 掩盖了解码器中真正的障碍
在很长一段时间里,我都在追求端到端准确率,后来才意识到,在评估期间输入真值词边界会让分词看起来*已经被解决了*。事实并非如此 —— 大约 20% 的单词在错误的地方被切断,正是这个问题(而不是分类)限制了系统的上限。评估工具中的一种便利机制让我自我陶醉了好几个月。
### 3. 解码器的上限是*传感器*,而不是模型 —— 通过三种方式证明
在将连贯文本的 top-5 达到约 95% 之后,显而易见的下一步就是挤压 top-1(被默认采纳的单词)。我有三个软件理论。这三个都是错的,每一次受挫反而都收紧了相同的结论,而不是削弱它:
- **更好的语言模型无济于事。** 我将 2019 年代的 GPT-2 换成了现代的小型 LM (SmolLM2-360M),并在相同的交叉验证工具上对其进行了基准测试。top-1 的提升为零。解码受限于*戒指*,而不是受限于 LM —— 模型已经打破了它能打破的所有平局;剩下的都是传感器无法区分按键造成的。
- **你无法通过插入来摆脱困境。** 残余错误包括漏掉的短词,所以我构建了一个流程,用于插入句子“需要”的功能词。在每个阈值下都是负收益,而且上下文越多*越糟* —— 因为语言模型优化的是**流畅度,而不是保真度**。它很乐意插入读起来很顺但并没有被敲击出来的词。保真度必须来自敲击,绝不能来自模型。
- **解码参数已经是最优的了。** 为了 top-1(而不是 top-5)重新调整长度惩罚函数后发现,当前的设置在两者中都是胜出的 —— 敲击计数在*两个*方向上都有噪声,因此弹性窗口是承重部件,收紧它带来的破坏比修复的要多。
对侥幸逃脱的漏检进行逐次敲击的剖析印证了这一点:它们属于**检测幅度**失败 —— 思考停顿后的轻柔敲击,发生在物理上距离戒指较远的外侧手指上 —— 而不是模型失败。所以我停止了对模型的优化。
### 4. 在自身硬件上撒谎的变种
收到的一枚更贴手的戒指报告其硬件为 `RT12COL_V1.0` —— 我的任何一枚能用的戒指都没有使用过这个字符串。最直观的解读:新硅片,而且我的固件(针对 `RT02CR` 镜像打字节补丁)会把它变砖。我浪费了几个小时陷入获取固件的兔子洞,试图获取原厂镜像来进行逆向工程 —— 找到了厂商的 OTA 服务器(可预测的未授权 URL),在版本检查 API 上碰到了签名/token 验证墙,甚至反编译了厂商的 Android APK 来从它的字节码中读取端点。**这一切都是白费力气。**
让事情尘埃落定的是一个我一直想得太复杂的单次刷写测试。我的镜像是*完整的*固件,而不是补丁 —— 因此刷写一个镜像只是在问“这块芯片能运行 RT02CR 固件吗?”答案是肯定的。戒指重启后报告为 `RT02CR_V3.1`,与已知完好的戒指逐字节相同。**“RT12COL”仅仅是完全相同的 RTL8762E 硅片上的一个固件外观标签。** 直接的经验测试打败了精妙的变通方法 —— 我之前过于谨慎了,而“相同的戒指,不同的标签”的直觉是对的。
随后传感器无法进行流式传输 —— 开启原始运动返回的是一个错误帧,而不是数据。反汇编初始化代码(`fwdis.py`)表明这是一系列不带任何芯片 ID 校验的 I2C 寄存器写入 —— 它纯粹是因为缺少 ACK 而失败。这块板子上的加速计根本没有在固件期望的设备地址上做出响应,而将目标重定向到两个标准的 STK8321 地址也没有解决问题。所以这不仅仅是简单的地址替换问题;该零件处于一条完全不同的 I2C 路径上(供电、总线/引脚,或者是被换过的芯片)。
为了最终找到它,我在固件中编写了一个 I2C 地址扫描程序 —— 结果**把戒指变砖了**,因为我让探针在任何 BLE 连接建立之前的*启动阶段*就传输了它的报告,这破坏了 GATT 服务器,导致 OTA 无法恢复。这是一个真实的失败,并作为一个教训被记录下来:**注入操作绝不能在连接建立之前触发。** 这块板子本来就是买来并标记为专门用于这个实验的牺牲品,它完成了它的使命 —— 但错误是我犯的,而且启动安全的设计(存储扫描结果,将其搭载在命令回复上,使其仅在连接状态下传输)已经为下一次记录在案了。
## 架构,以及我做出的决策
**固件**
- **修补固件,不要和 App 较劲。** 厂商协议在速率方面是一个死胡同。`firmware/` 包含了反汇编笔记、可复现的补丁构建器(`build.py`)、一个验证器以及构建好的镜像。每个镜像在构建时都会断言自身已知的良好 SHA —— 我从不保留我无法重新生成的二进制文件。
- **优先选择 `DATA_SEL` 而不是 `PROTECT_DIS`。** 寄存器 `0x13` 的第 7 位选择未滤波路径,并带来了约 30% 的提升(270 → 352)。第 6 位禁用寄存器保护,*看起来*应该会有所帮助 —— 但它让情况变得更糟了(只有 211,且出现了读取撕裂)。之所以记录下来,是因为一个看似合理却失败的念头值得保留。
- **选择小批量通知而不是最大吞吐量。** 旗舰版构建每次通知发送 4 个采样(约 90 次通知/秒,约 11 毫秒延迟),并在 `pkt[63]` 处带有滚动丢包计数器,因此丢包是*可见的*,而不是在不知不觉中降低解码质量。可观察的延迟胜过一个无法验证的峰值数字。
- **Cortex-M0,所以没有 `movw`/`movt`/`add.w`。** RTL8762E 是 ARMv6-M 架构 —— 每个注入的常量都是由 `movs`/`lsls`/`adds` 构建的,并且构建器的 `m0_scan` 会自动拒绝任何对 M0 不安全的指令。正是这一个事实导致了早期的所有崩溃。
**解码器**
- **解码器是 LM 驱动并由戒指辅助的,而不是反过来。** 仅凭戒指信号只能达到 3–30% 的 top-1。坦诚对待这个比例,正是阻止项目过度吹嘘的关键 —— 并且这也正确地预测了更大的模型不会有帮助。
- **全词 DP 对齐解码。** 它根据 5 万词的字典对敲击序列进行评分,因此它在*结构上就不会出现拼写错误* —— 错误属于选词错误,绝不会是乱码。分割和弱空格合并恢复流程修复了作为真正天花板的错误切割边界。
- **经过校准的置信度分数才是产品。** 既然残余误差是传感器造成的,而不是模型造成的,那么解决方案就不是一个更好的解码器 —— 而是由置信度驱动的、类似自动纠正的用户体验(UX):有把握时默默采纳,不确定时提供 top-5 选项,非常没把握时标记为“需要检查”。我验证了该分数是经过校准的 —— 准确率随其单调上升 —— 因此那 4–5% 一直出错的词可以被*标记出来*,而不是默默地错下去。
- **通用与个人化。** 解码器清晰地分开了:语言模型可以跨用户和操作平面免费迁移(并且占据主导地位),而起始点检测和按键识别则是高度个人化且依赖于操作平面的。这种分解正是整个商业化故事的核心 —— 一个基础模型加上几分钟的用户校准,而且用户越多,入门成本就越低。
*Claude 编写了这里的大部分代码。而工程判断是我的 —— 信任传感器的 NEW-data 位胜过信任传输层,知道何时该停止为 2 kHz 和单词插入假设买单,通过硬件字符串解读出“相同的戒指,不同的标签”,以及在证明上限是传感器之后,选择围绕置信度分数进行重新设计。*
## 结果
**采样率 —— 我声称的与实际情况对比:**
| 声称 | 方法 | 结论 |
|---|---|---|
| 936 Hz | 统计回调 | 错误 —— BLE walker 重复读取 |
| 1650 Hz | 统计数据包 | 错误 —— 存在重复项 |
| 440 Hz | 数据包计时 | 错误 —— 相同原因 |
| 100 Hz | FIFO 读取循环 | 错误 —— 循环缓慢,并非硬件限制 |
| **270 Hz** | NEW-data 位,已滤波路径 | **正确** |
| **352 Hz** | NEW-data 位,`DATA_SEL` 未滤波 | **正确 —— 真正的上限** |
**打字解码 —— 端到端 top-5,每一个层级都是一次独立且经过测量的改变:**
| 步骤 | @5 |
|---|---|
| 基线 | 64.5 |
| 正确分割 + 5 折 CV | 70.7 |
| 数据清洗 | 73.5 |
| 弱空格合并 | 76.7 |
| 长度 (λ=4) | 79.0 |
| 按键识别 CNN 调优 | 81.3 |
| 双向句子重排序 | **85.0** |
在所有单词上进行报告时,最终配置的结果是 **@1 72 / @5 85 / @10 87**(3 折 CV,219 个词)。但测试文本中约 28% 是故意敲出的乱码(姓名、脱离上下文的词);而在**连贯**文本上 —— 即你实际会写的内容 —— 结果是 **@1 86 / @5 95.5 / @10 95.5**。这个连贯文本的数据才是与实际部署相关的,而且 top-5 == top-10(差距为*零*)证明了残余的漏检完全是戒指根本没有提出的词 —— 这是检测失败,而不是排序失败。
速率提升确实有帮助,但并不均匀:25 Hz 提升至 352 Hz 使按键识别精度提升了 43%,手指识别提升了 34%,但几乎完全没有改变行/手部准确率。**速率被归档为必要条件,但并不充分。**
## 我学到了什么,以及下一步计划
1. **测量事物本身,而不是事物的代理指标。** 四个错误的速率数字都是因为计算了交付量而不是独立采样数。
2. **两份文档达成一致并不能凌驾于一次实际测量之上。** 数据手册和内核驱动程序都与 2 kHz 一致,但对我手指上的零件来说都不相关。
3. **评估中的一个便利机制可能会隐藏你数月之久的真正瓶颈。** Teacher forcing 让分割问题变得不可见。
4. **当三个修复方案都以同样的方式失败时,那就是答案。** 更大的模型、单词插入和重新调优都没有用 —— 因为这堵墙是传感器,而不是软件。富有成效的做法是停止优化,并围绕它重新设计。
5. **直接的经验测试通常胜过复杂的变通方法。** 一次简单的刷写测试“它能运行吗?”在几秒钟内回答了多小时的云逆向工程获取无法解决的问题。
6. **在你买来注定要使其失败的单元上失败,并把失败记录下来。** 变砖的戒指付出的代价没有超出它的使命 —— 而且启动安全的注入规则现在已经记录在案了。
下一步计划:
- **分割/检测** 才是阻碍因素,而不是分类或模型。其他一切都是次要的。
- **基于置信度分级的 UX**(采纳/提供/标记)是第二阶段的主干 —— 它将剩余 4–5% 的错误从静默错误转变为被标记的错误。
- **双戒指**,用于实现单枚戒指无法提供的手部模式分离。
- **自适应起始点检测**,用于恢复那些占漏检大部分比例的、停顿后的轻柔敲击。
- 姓名和不在词汇表中的词在软件层面无法修复;戒指太弱小,无法进行拼写 —— 一个专门的字符输入模式才是出路。
## 布局
```
firmware/ patch builder + reproducible images, disassembly notes (fwdis.py), verifier,
and the RT12COL variant tooling (build_addr.py, build_sweep.py — the sweep
sends at boot, kept as a documented cautionary example, do not reuse as-is)
typing/ the decoder: features, models, cross-validated evaluation, confidence work
cockpit/ tilt + gesture radial-menu controller
autocomplete/ LM-assisted word prediction from ring selects
gestures/ gesture capture + models
boxing/ a punch-detection side experiment
*.py (root) core modules — ring_reader, type_*, single_ring, signal_decomp — alongside a
large set of eval_*.py / reassess_*.py experiment scripts kept for the record
```
根目录同时存放了核心模块和实验脚本;将 `eval_*.py` 的工作拆分到它自己的包中已在待办事项列表中。
标签:云资产清单, 传感器, 固件分析, 打字解码, 智能戒指, 蓝牙低功耗(BLE), 逆向工具, 逆向工程