案例:一个精确到"恰好 60 秒"的 WiFi 掉线
这个排查过程比较能说明这条项目线上问题定位的方法论,值得完整记一遍。
现象:连上 AP 后约 60 秒,systemd-networkd 报 Lost carrier,同时 wpa_supplicant 报 REGDOM-CHANGE init=CORE type=WORLD。没有 wpa 的 DISCONNECTED 事件,dmesg 无相关日志,NetworkManager 仍显示 connected 但路由标记为 dead,不会自愈,nmcli con up 立刻能连回来但又是 60 秒后掉。
首轮排查假设(后被推翻):怀疑是无线网卡(NXP IW612/SDIO)的省电模式导致。证据:iw dev wlan0 set power_save off 后连续 7 分钟以上零掉线,看起来对上了。这个假设当时被记录为"高置信度"并推到了部署环节。
真凶(后续排查推翻首轮结论):用 iw event 抓包发现掉线事件实际是 disconnected (local request)——不是外部干扰,是本机主动断开的。完整时序是:
连上 AP → wpa: REGDOM-CHANGE init=COUNTRY_IE alpha2=CN
→ (恰好 60s 后) Lost carrier + REGDOM-CHANGE init=CORE type=WORLD60 秒是内核 cfg80211 的 REG_ENFORCE_GRACE_MS 常量:regdomain 变化后有 60 秒宽限期,宽限期结束后内核会主动断开落在"新规则不允许信道"上的连接。设备自带的 /lib/firmware/regulatory.db 是 reMarkable 的精简版本(约 2KB,带厂商签名,内核校验证书链,无法替换),iw reg get 显示 CN 的允许频段只有 2402–2472MHz 和 5735–5835MHz 两段,不含 5150–5350MHz。而路由器的 5GHz 用的是信道 36(5180MHz)——落在不允许的频段里。所以链路是:连上 CN regdomain 允许该信道(初始按 country IE) → 60 秒宽限期结束、regdomain 收紧到 WORLD 判定,5180 又变合法 → 重连 → country IE 再次设为 CN → 60 秒后再断,无限循环。之前的省电模式假设之所以"看起来验证通过",只是因为当时测试窗口凑巧短于下一次断线周期,属于伪相关。
修复:
# 方案一:设备侧锁 2.4GHz(2437MHz 在允许频段内),代价是速率下降
nmcli con modify <SSID> 802-11-wireless.band bg
# 方案二:路由器 5GHz 改用 149-165 信道(5735-5835MHz 段内合法)
nmcli con modify <SSID> 802-11-wireless.band a这个案例的方法论价值:第一个"验证通过"的假设不一定是真凶,尤其是当验证窗口短于真实的复现周期时。后来把这条也写进了排查规范——用 iw event 这类能看到内核决策原因(而不只是现象)的工具去确认因果链,而不是靠"改一个参数、观察几分钟没复现"就下结论。
手写识别:为什么最后收敛到"逐槽裁图"
手写批注转写这个功能,核心难点不是"模型能不能认出字",而是转写结果必须能确定性地对应回原文的哪一行——如果对应关系错了,转写结果读起来通顺但其实是在编造内容,比认错字更危险。
试过两版都没走通:
- 几何反推:用
.rm笔画数据里的坐标反推行位置。撞墙点是 Group 结构没有可靠的anchor_origin_y字段,几何数据不足以稳定推出行号。 - 整页空间分割交给视觉模型:让多模态模型自己判断一页里的内容分区。出现"串区"(相邻两行被模型脑补拼到一起)和幻觉问题——模型在做空间推理时不如在做纯识字任务时可靠。
最终方案:把"识字"和"定位"彻底分离。行位置由 .rm 文件里的确定性 anchor 结构(cardanchor::CardPage)导出,不经过模型;每一行按预期行高单独裁成窄横条图片,逐条喂给视觉模型,模型的任务收窄到"这条横条里写的是什么字",不再需要做空间判断。真机测试 6 槽零串槽。
这条经验概括成一句可复用的原则:凡是能用确定性数据结构解决的对应关系问题,就不要交给模型做空间推理——模型的错误模式(幻觉、脑补)在"认字"这类窄任务上很少出现,但在"判断这段内容属于哪个区域"这类需要隐式空间推理的任务上会明显放大。
还有一个通知机制的坑值得记录:转写完成的提示最初接的是阅读器(reader)内部的 showNotification,上线后发现如果用户在转写过程中已经退出笔记回到书库列表,这条通知永远不会显示——因为 reader 局部的通知队列在退出 reader 之后就不再被渲染。修复是改接 MainView 全局的 notificationQueue.enqueue,在任何界面下都能正确入队显示。这类"只在特定界面路径下测过、换个路径就失效"的问题,后来被写成了一条通用检查项:涉及 UI 反馈的改动,验证时要覆盖"用户在流程中途切换界面"这种非线性路径,不能只测最短路径。
2026-09-11:仓库级重构
到 9 月上旬,仓库内早期做的中文输入法、完整阅读管线、PKM、手写识别这几条功能线(连同支撑它们的逆向工程产物)已经积累了较深的相互耦合,维护成本上升。9 月 11 号执行了一次仓库级重构:这几条目录整体移出 git 版本控制,迁到本机一个不随仓库分发的目录。
需要精确说明的是这次操作的边界:移动的是源码的版本控制归属,不是运行时状态。已编译部署到设备上的 .so 和相关运行时数据不受影响,继续作为设备上的现役进程运行;只是这部分源码不再纳入当前仓库的持续集成和文档维护范围。
重构后收敛出的当前仓库结构是七条独立顶层项目线(shelf 书架 / notes 笔记 / enhance 系统增强单点工具 / gateway Web 前端 / rmsvc-core 服务基座 / defw 固件逆向产物 / packaging 安装器),架构原则也同步升级:一律遵循 XDG 目录规范、明确的设计模式分层、支持可插拔多服务注册、不对接已废弃的旧路径;旧代码只允许"抽离出独立 crate + 原路径 re-export",不允许静默改行为。
安装器新增了一道固件安全门,装机前做 sha256 比对:
REMOTE_HASH="$(ssh "root@$HOST" 'sha256sum /usr/bin/xochitl' | awk '{print $1}')"
# 命中 firmware-allowlist.txt 才继续;不在白名单里默认拒装
# --force 会强装,同时自动把这个哈希追加进白名单留痕这个设计反映了架构原则上的一个转变:早期是"先跑起来,出问题再救"(配合上面提到的备份+回滚流程),现在更倾向于把已知的危险操作前置校验掉——不再假设目标设备的固件版本和当前调试环境一致。
现状
七条线中 shelf 和 notes 目前迭代最深,各自经过多轮基于真机反馈的架构调整(notes 的"整理页"交互设计迭代过三期开发 + 八轮用户反馈)。9 月 11 号之后另外维护了一份不含开发历史的公开仓库 rm-tweak(Apache-2.0),只含当前七条线代码和精简文档;这个私有仓库继续保留全量开发记录,两边靠人工比对同步,不做自动化整包导出。