XiaoHongKeyFlow 小鸿键盘配置工具:FRB、星闪 SSAP 与原子配置实战
小鸿键盘的 v1 不是“用 Flutter 发一串文本命令”这么简单。它同时存在两条完全独立的链路:按键输入仍由 WS63 通过星闪发给 BS21E Dongle,再由 USB HID 进入电脑;配置工具则通过 HarmonyOS App 连接 WS63 的自定义 SSAP 服务。
这两个状态必须分开描述。App 显示“控制连接成功”,不等于无 Dongle 的系统级星闪键盘已经完成。
真机演示
小鸿键盘配置工具
配套演示视频约 107 秒,完整记录了设备扫描连接、键位选择、宏配置、参数同步以及设备端响应过程。静态封面用于快速查看最终界面,协议回读和物理按键结果则按后文的验收边界分别说明。
这段视频由实际设备操作录制,画面包含控制 App、键盘屏幕和完整操作过程。某一次灯光变化、某一次按键输入是否通过,还要和串口日志、App ACK 以及目标文本框结果对齐;107 秒的视频也不能替代 20 次重启持久化统计。
仓库怎么读才不会把两条链路混掉
XiaoHongKeyFlow/
|-- apps/xh_keyflow/lib/ Flutter 工作台和 NearLink 适配
|-- apps/xh_keyflow/rust/src/ FRB API、协议和本地状态
|-- apps/xh_keyflow/ohos/ HarmonyOS ArkTS 宿主
|-- firmware/ws63/ 键盘端 WS63 固件
|-- firmware/releases/ 经核验的发布固件
|-- docs/sle-keyboard-protocol.md SSAP 控制协议
|-- docs/REAL_DEVICE_E2E_VALIDATION.md
| 真机复验步骤和结论
`-- artifacts/validation/ 日志、构建信息和逐项结果
apps/xh_keyflow/README.md 开头已经写得很明确:改键和屏幕设置走控制 App,快捷键输入仍经 USB 接收器。这个前提必须先读,否则后面很容易把 SSAP 通知误写成系统键盘输入。
四层架构,而不是一条 FFI
FRB 没有直接操作 NearLink,因为扫描、配对和 SSAP 连接属于 HarmonyOS 系统能力,由 ArkTS 宿主更合适。Rust 的职责是协议和一致性:键位枚举、HID 宏、版本冲突、校验和、配置档案和设备事件。
FRB API 只暴露稳定语义
配置仍然是标准的三项:
rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust
Rust 初始化并维护统一设备状态:
#[flutter_rust_bridge::frb(init)]
pub fn init_app() {
flutter_rust_bridge::setup_default_user_utils();
store::ensure_initialized();
}
#[flutter_rust_bridge::frb(sync)]
pub fn encode_set_key_command(
key_id: String,
steps: Vec<KeyStep>,
) -> Result<String, String> {
let pairs: Vec<_> = steps
.iter()
.map(|step| (step.modifier, step.keycode))
.collect();
crate::protocol::encode_set_key(&key_id, &pairs)
}
#[flutter_rust_bridge::frb(sync)]
pub fn ingest_sle_line(
device_id: String,
line: String,
) -> Option<DeviceEvent> {
store::ingest_sle_line(&device_id, &line)
}
API 的输入是 key_id 和结构化 KeyStep,不是让 Dart 自己拼协议。以 O 键映射为 Ctrl+C、再输入 V 为例,Rust 会生成:
SET_KEY:2:2:01,06;00,19
具体编码来自真实实现:
pub fn encode_set_key(
key_id: &str,
steps: &[(u8, u8)],
) -> Result<String, String> {
let ekey = id_to_ekey(key_id)
.ok_or_else(|| format!("XHKB_INVALID_KEYMAP: 未知键位 {key_id}"))?;
if steps.len() > MACRO_MAX_STEPS {
return Err("XHKB_INVALID_KEYMAP: 超过 6 步宏".into());
}
let mut body = format!("SET_KEY:{ekey}:{}:", steps.len());
for (index, (modifier, keycode)) in steps.iter().enumerate() {
if index > 0 { body.push(';'); }
body.push_str(&format!("{modifier:02X},{keycode:02X}"));
}
Ok(body)
}
最多 6 步、合法键位、保留键不可编辑等规则都在 Rust 校验,UI 无法绕过。
NearLink 请求必须先监听再写
设备回复可能很快。如果 write() 完成后才订阅事件,会偶发丢掉 ACK。Dart 侧的 NearlinkBridge.request 因此先建立订阅:
Future<String> request(
String text, {
required bool Function(String payload) matches,
Duration timeout = const Duration(seconds: 2),
}) async {
final completer = Completer<String>();
late final StreamSubscription<String> subscription;
subscription = events.listen((raw) {
final payload = payloadFromEvent(raw);
if (payload.startsWith('ERR:')) {
if (!completer.isCompleted) {
completer.completeError(StateError(payload));
}
} else if (matches(payload) && !completer.isCompleted) {
completer.complete(payload);
}
});
try {
await write(text);
return await completer.future.timeout(timeout);
} finally {
await subscription.cancel();
}
}
matches 不能写成“任意 ACK 都算成功”。同一连接上可能还有心跳、按键通知或上一条命令的迟到响应,必须匹配本次命令、版本号和阶段。
一次改键为什么要做事务
若逐键写入 NV,第三个键失败时,设备上会留下半份新配置。项目采用 BEGIN / STAGE / COMMIT:
配置校验和使用 FNV-1a,把 schema、键位 ID、步数、modifier 和 keycode 全部纳入计算。提交前发现设备版本与编辑基线不同,就返回 XHKB_CONFLICT,而不是悄悄覆盖另一端的修改。
可以复现的实操路径
- 插入 BS21E Dongle,让输入链路保持原工作方式。
- 启动 HarmonyOS App,扫描并连接
XH-KB控制服务。 - 读取
INFO、当前键位表和设置,确认协议主版本兼容。 - 在工作台选择可编辑的 O 键,配置最多 6 步 HID 宏。
- 同步配置,等待精确的事务 ACK,再回读版本和 checksum。
- 主动断开、重新扫描连接,确认控制通道恢复。
软件侧验证命令:
cd XiaoHongKeyFlow/apps/xh_keyflow
flutter pub get
flutter_rust_bridge_codegen generate
cargo test --manifest-path rust/Cargo.toml
flutter test
cd ../..
python3 tools/verify_ws63_releases.py
烧录属于另一个风险级别。只能给键盘本体 WS63 烧写已核验的 load-only 包,不能把 WS63 与 BS21E Dongle 混为一个目标,也不能用 all 包覆盖 NV、Bootloader、校准和配对数据。
把一次 O 键改键拆开看
假设当前配置版本是 2,要把 O 键改成两步宏。App 不能先在本地把卡片显示成“已同步”,正确顺序应该是:
1. GET_KEYMAP
<- KM:START:2
<- KM:KEY:...
<- KM:END:<checksum>
2. BEGIN_KEYMAP:2
<- ACK:BEGIN_KEYMAP:2
3. STAGE_KEY:2:2:01,06;00,19
<- ACK:STAGE_KEY:2
4. COMMIT_KEYMAP
<- ACK:COMMIT_KEYMAP:3
5. 再次 GET_KEYMAP
<- 版本 3,内容和 checksum 与提交结果一致
这里至少有三次校验。第一次校验编辑基线没有落后;第二次校验每个 staged 键合法;第三次在提交后回读整表。只收到 ACK:STAGE_KEY 不能说明 NV 已写入,只收到 ACK:COMMIT_KEYMAP 也不应省掉回读。
如果两台 App 同时编辑,后提交的一台拿着旧版本 2 发 BEGIN_KEYMAP:2,设备应直接返回冲突。正确处理是刷新远端配置,让用户重新确认,而不是自动用新版本号重试并覆盖别人的修改。
真机联调按三路证据对时
硬件联调时,我会同时保留三类记录:
| 时间线 | 记录内容 | 用来回答的问题 |
|---|---|---|
| App 日志 | 扫描、连接、发送命令、收到通知 | HarmonyOS 控制端做了什么 |
| WS63 串口 | SSAP RX/TX、事务、NV 返回值、复位原因 | 固件是否收到并执行 |
| 物理画面/输入框 | 屏幕、灯珠、真实按键输出 | 最终效果是否发生 |
三路最好使用同一轮测试的时间戳或递增序号。否则很容易拿上一轮成功的串口日志去解释下一轮失败的画面。对于 PING/PONG,记录里已经做过连续 20 次逐序号匹配;对于改键事务,也应保存 BEGIN、STAGE、COMMIT 和回读的完整一组,而不是只截最后一行。
常见故障怎么定位
能扫描、不能连接。 先确认扫描到的是 XH-KB 控制广播且 connectable=true,再看配对状态。不要去系统设置里把它当标准键盘连接。
连接后收不到通知。 核对 Service 0x2222、Property 0x2323 和 CCCD 写入结果。服务发现成功不等于通知订阅成功。
事务一直超时。 检查请求监听是否在 write 前建立,并核对返回 ACK 的阶段和版本;心跳不能误完成事务的 Completer。
提交成功但重启丢失。 看 COMMIT 的 NV 返回值和重启后的完整回读。运行时配置生效并不等于持久化成功。
App 显示控制已连接但按键没输入。 转去检查 WS63 到 BS21E Dongle 再到 USB HID 的输入链路。继续重连 SSAP 对这个问题没有帮助。
新旧固件兼容不要靠猜
协议读回若包含 KM:START:<version>,App 才能启用 BEGIN/STAGE/COMMIT;旧固件只返回 KM:START 时,可以降级为旧的逐条写入,但页面必须明确提示“非原子模式”。不能因为第一条新命令超时,就盲目按新旧两套协议各发一次,那会产生重复写入和难以解释的 ACK。
握手阶段建议记录协议主版本、配置 schema、固件版本和能力位。主版本不兼容时直接阻止编辑;新增可选设置则通过能力位隐藏,而不是只比较版本字符串。这样将来加入更多灯光场景或宏动作,旧 App 不会把未知值解析成默认 0 后再写回设备。
固件升级后第一件事也不是修改配置,而是读取整份快照并验证 checksum。确认新固件能理解旧 NV schema 后,才允许进入编辑工作台;迁移失败则保留原始数据和诊断,不应自动写一份默认配置覆盖现场键位。
把 PASS 和 PENDING 放在同一张表
| 验收项 | 当前结论 | 说明 |
|---|---|---|
| HAP 安装、扫描和连接 | PASS | 真机已连接产品控制服务 |
| Service/Property 严格匹配 | PASS | 0x2222 / 0x2323 |
| 20/20 PING/PONG | PASS | 序号匹配,无丢失重复 |
| 实体按键通知门槛 | PASS | Probe 证据超过 20 个唯一事件 |
| BEGIN/STAGE/COMMIT 改键 | PASS | 版本递增并回读一致 |
| 亮度、休眠、旋转、灯光设置回读 | PASS | 协议链路已验证 |
| 单次重启 NV 回读 | PASS | 只能证明这一轮 |
| 20 次重启保持 | PENDING | NFR-06 尚未执行 |
| 屏幕和灯珠最终物理效果 | PENDING | 需现场逐档肉眼确认 |
| USB Dongle 实际打字 | PENDING | 需目标输入框计数 |
| 无 Dongle 系统 SLE HID | NO-GO / v2 | 当前 SDK 缺少可核验能力 |
这张表看起来没有一句“全部完成”痛快,但它能保护验收:已经通过的内容有证据,未完成的内容也不会被一段演示视频悄悄盖过去。
为什么我一直强调这是两条链路
键盘项目最容易出现的误会,是看到 App 已连接就认为键盘已经可以输入。控制链路和输入链路虽然都与 WS63 有关,目标却完全不同。控制链路传的是配置、状态和 ACK,最终落到 HarmonyOS 应用;输入链路传的是按键报告,经过 BS21E Dongle 后以 USB HID 身份进入系统。前者成功,只能说明 App 能管理键盘,不能推出后者正常。
这种区分不只是文章措辞。界面状态也应分别展示“控制已连接”和“输入接收器状态”,日志中使用不同事件名称,验收表更要分开签字。否则现场出现“改键成功但按键没输出”时,所有人只会围绕一个绿色连接图标反复排查,真正的 Dongle、配对或 HID 问题反而被忽略。
无 Dongle 的系统级星闪 HID 是另一项能力。设备能在系统设置中被发现,也不等于它实现了标准键盘服务。自定义 SSAP 有自己的 UUID 和通知格式,系统不会因为设备名称里写了 Keyboard 就把它当成 HID。把这项标为 v2/No-Go,不是降低目标,而是避免在缺少厂商接口和标准报告链路时给出无法兑现的承诺。
原子事务解决的是“半份配置”问题
用户在页面上一次点击同步,心理预期是整套键位要么全部生效,要么保持原样。若 App 逐条发送,前几项写成功、后几项因断连失败,设备就处在页面没有展示过的混合状态。更麻烦的是,这个混合状态如果已经写入 NV,重启后仍会保留,很难判断该回滚到哪一版。
BEGIN 阶段先比较版本,确认编辑基线没有过期;STAGE 只写入临时区域并逐项校验;COMMIT 再一次性替换运行时配置和 NV 数据。事务期间任何错误都丢弃 staged 内容,旧配置继续工作。这样 App 重试时面对的是一个已知版本,而不是猜测前面究竟成功了几条。
checksum 的意义也不只是防传输损坏。回读时版本号可能正确,但内容若因固件缺陷或存储问题发生变化,checksum 能快速暴露不一致。它必须覆盖 schema、键 ID、步骤数量和每个 modifier/keycode,不能只对可见字符串做校验。App 与固件使用同一套规范,才能把“看起来一样”变成可比较的结果。
真机调试比纯软件多出来的麻烦
硬件链路有大量软件测试看不到的时间因素。扫描窗口太短可能偶尔找不到广播;刚连接就写属性,服务发现或通知订阅可能还没完成;设备复位后重新广播需要时间;串口输出过多又可能改变任务调度。一次操作成功不能说明时序已经稳定,所以 Probe 阶段才会做连续 PING、按键通知和断开重连。
调试时我更信任递增序号,而不是日志的先后位置。App、WS63 和系统日志来自不同缓冲区,最终导出后时间可能有轻微偏差。请求序号能把一次 write、一次固件处理和一次 notify 串起来。若收到迟到 ACK,必须根据序号和阶段判断归属,不能因为它恰好出现在当前等待窗口里就算作成功。
板端重启是另一个不能轻描淡写的问题。Probe 期间虽然功能门通过,但曾观察到无法解释的复位。只要复位原因还没定位,产品化稳定性就不能直接写 PASS。后续需要记录复位寄存器、看门狗状态、任务栈和发生前的通信负载,区分供电、内存越界、断言和看门狗超时。功能能跑通与设备能长期稳定运行,是两张不同的验收单。
从视频到验收记录,中间还差一步整理
演示视频适合让读者快速理解产品:选择键位、修改宏、同步设置,键盘端画面随操作变化。但正式验收不能要求观看者自己从视频里猜结论。每一项操作都应对应起止时间、配置版本、命令序号、ACK、回读值和最终物理观察。这样即使后来需要复核,也能准确定位到视频中的一段和日志中的一组记录。
对屏幕亮度和灯光场景,协议回读只能证明固件接受并保存了数值。屏幕实际亮度是否变化、灯珠颜色是否正确,还需要现场画面或测量。相反,画面看起来变化了,也不能替代重启后的 NV 回读。物理效果和数据持久化互为补充,任何一方都不能代表另一方。
USB Dongle 打字验收则应选择一个干净文本框,清空输入法组合状态,按固定键固定次数,保存最终文本和计数。若宏包含 Ctrl+C 等快捷键,还要准备可重复的源文本与目标文本,避免剪贴板旧内容干扰。验收步骤越具体,结果越容易复现,也越不容易在现场争论“刚才是不是按漏了”。
做硬件项目时,诚实标记未完成反而更省时间
把 PENDING 写出来并不意味着已有工作没有价值。当前控制通道、原子改键、设置回读和单次重启已经形成可用基础;剩余三项也因此能够明确安排,而不是重新判断整个系统到底做到哪。真正危险的是用一个“整体完成”标签覆盖所有细节,等验收人员随机抽到未测项才发现证据不存在。
对于后续版本,建议每次固件、HAP 或硬件连接方式变化都重新建立候选版本号和证据目录。不要把不同日期、不同固件的成功日志拼成一个结果。只有同一候选在同一轮中完成控制、输入、重启和物理效果,才能把相应 PENDING 逐项转成 PASS。这种做法看起来慢,却能减少最后阶段返工。
当前真实状态
现场参数不能脱离固件版本保存
键位配置虽然看起来只是几个按键和宏,实际上它与固件支持的键码表、最大步骤数、修饰键规则和校验算法绑定。如果 App 只保存一份没有 schema 信息的 JSON,升级固件后再下发旧配置,就可能遇到同一个数值已经代表不同含义的情况。配置文件应记录 schema 版本、目标设备型号、固件版本范围和生成时间;导入时先做兼容性检查,再允许进入 STAGE。
兼容不代表所有字段都必须完全一致。新增可选字段可以由旧配置采用默认值,删除或改义的字段则必须走显式迁移。迁移结果应先在页面展示差异,例如某个旧宏超过新固件限制、某个键码已停用,让用户确认后再形成新版本。直接静默截断最容易留下“同步成功但行为不对”的现场争议。
多台键盘同时调试时,还要把设备身份写进证据。广播名可以重复,连接地址也可能在重启后变化,验收记录至少需要稳定设备 ID、硬件批次和固件版本。一次 ACK 属于哪台设备、一次回读是否来自刚才配置的样机,都应能从日志中确认。否则两台设备摆在桌上时,很容易出现页面连着 A、操作人员却在按 B 的情况。
这也是为什么文章把控制链路和输入链路分开描述:配置事务保证发送给目标设备的数据完整,却不自动证明 USB Dongle 最终向电脑产生了正确键盘输入。正式验收仍需把设备身份、配置版本、回读校验和实际按键结果放在同一轮记录中,才能形成闭环。
仓库记录的 v1 状态是:HarmonyOS App 扫描连接、SSAP 控制、键位原子事务、亮度/休眠/旋转/灯光设置、主动断开与重连、单次重启配置回读均为 PASS。
仍为 PENDING 的项目包括 20 次重启持久化、屏幕和灯珠物理效果肉眼确认、USB Dongle 在目标输入框中的实际打字验收。无 Dongle 的系统级 SLE HID 属于 v2,当前受设备侧 HID 资料限制,不能因为系统设置里看到了设备名就写成已经实现。
这个项目里,FRB 最合适的位置并不是“接管所有平台能力”,而是守住协议边界。ArkTS 连接设备,Rust 判定一条配置是否有效,Flutter 负责让用户看懂两条链路的真实状态。这样出了问题,至少不会把“控制连上了”和“键盘能打字”混成一句模糊的成功提示。
更多推荐



所有评论(0)