Files

14 KiB
Raw Permalink Blame History

奇妙小屏幕 BLE 控制协议 1.0

WiFi 通道扩展 1.2

能力 wifi_transport。加密 BLE transport.offer 返回 channel_id(16 随机字节小写 hex)、secret(32 随机字节 Base64)、device_id、host(当前 WiFi IPv4)、port=8080、path=/ws/mobile、expires_in_ms=10000。每次 offer 撤销前一候选;活动通道须先 close。BLE transport.close 在串行门禁内撤销候选与活动通道,作为回退执行屏障。session.、wifi.、task.get 与通道协商只走 BLE。

KDF:T=SHA256(ASCII(channel_id));HKDF-SHA256(secret,salt=T,info=ASCII("QMS-WIFI-1")||T,length=72)。密钥及 nonce prefix 布局沿用 BLE Cipher,AAD 标签为 QMS-WIFI-1;双向序号独立从 0 开始,不共享 BLE 计数。最大加密记录 65536 字节。

WebSocket 首个二进制消息为 ASCII(channel_id) 的 32 字节加加密 JSON {device_id},5 秒内送达;设备验证凭据、BLE 会话及身份后一次消费,返回加密 {device_id,ready:true}。之后每条二进制消息为加密 RPC JSON,格式沿用原协议;WiFi 请求编号独立递增。transport.ping 是 WiFi 唯一通道级方法,每 5 秒调用、5 秒超时,不延长 BLE 活跃时间。设备 10 秒无有效 WiFi 消息撤销通道。

会话锁串行业务和撤销;BLE 断开使 WiFi 失效。只读失败回退后可重取;写入结果不明不重放,回读并提示。旧凭据、错误身份、篡改、重放、非二进制、超长消息拒绝;凭据不进 URL/日志/磁盘。未认证连接不得踢掉合法通道。WiFi 建立不增加 mobile.generation;mobile.active_transport 为 ble/wifi/null。

本文是首版实现契约。Python/Kotlin 加密黄金向量、分片和 Android 13 真实 BLE 已有通过记录;各业务验收范围见测试结果归档,不能据此推断所有场景通过。命令只能进入设备公共业务服务,不调用网页 DOM,不提供 shell、任意路径或原生驱动地址。变化登记到跨端兼容记录。

1. GATT 与广播

项目 固定 UUID/行为
Service 9f57a001-6c31-4c58-bc22-1f728b641001
RX 9f57a002-6c31-4c58-bc22-1f728b641001,Write with response;手机到设备
TX 9f57a003-6c31-4c58-bc22-1f728b641001,Notify;设备到手机

使用传统可连接 BLE 广播,包含服务 UUID;扫描响应提供 QMS- 加短编号。完整昵称与持久 UUID 在握手后取得并缓存;首次扫描尚无昵称时显示产品名与短编号。改名不改变 ID。持久 UUID 在设备首次启动生成,放 /var/lib/matrix-screen-controller/mobile/identity.json,镜像构建不得携带已生成身份。短编号为 UUID 去连字符后的前 8 位,仅展示用。

禁用经典蓝牙配对及不必要的音频/文件传输 profile;GATT 不要求系统 bonding。不能保证其他工具完全无法建立物理连接;保证未完成本协议握手不得执行控制。手机先订阅 TX,再发送握手。连接后停止广播;服务端另外检查并拒绝第二个连接,不以“不广播”代替互斥。

10 秒内未完成握手释放连接。活动会话最多间隔 5 秒发送业务请求或 ping;当前 Android 前台每 2 秒轮询状态,后台释放前每 2 秒 ping。设备连续 20 秒未收到有效消息释放。单次普通命令超时 10 秒,WiFi 扫描 20 秒;耗时切网作为任务立即返回。设备检测断开后目标 5 秒内恢复广播,蓝牙硬件不可用时显示故障并有界退避重试,不阻止显示服务。

2. 分片与消息

所有整数使用网络字节序。每条 RX/TX 特征值是一个分片:magic:uint16=0x514d, wire:uint8=1, kind:uint8, message_id:uint32, index:uint16, count:uint16, total_length:uint32, payload:bytes。头长 16 字节,payload 上限为 ATT_MTU - 3 - 16;必须在 MTU 23 时也可工作,不能强制大 MTU。

kind:1 为 client hello,2 为 server hello,3 为加密记录,4 为握手拒绝。message_id 在当前方向逐条递增,重连归零。每方向仅允许一个重组消息,index 从 0 连续递增,count 与 total_length 必须一致;总消息不超过 65536 字节。重复、乱序、超长、类型未知或 15 秒未完成重组均丢弃并关闭会话,不允许部分消息触发业务。两端操作队列串行;已开始的帧消息完整发完,控制在下一条消息优先,缩略图和预览不得积压。

业务载荷为 UTF-8 JSON 对象。请求:{"id":"1","method":"status.get","params":{}};成功:{"id":"1","ok":true,"result":{}};失败:{"id":"1","ok":false,"error":{"code":"CONFLICT","message":"内容已改变,请刷新","retryable":false}}。id 为当前会话递增的十进制字符串,最大 64 字符;服务端只接受新的请求编号,重复编号返回错误而不再次执行变更。连接中断后不得自动重放变更;先回读状态,必要时请用户重试。

通用错误:BAD_REQUEST、NOT_READY、BUSY、UNSUPPORTED_VERSION、UNSUPPORTED_CAPABILITY、NOT_FOUND、CONFLICT、TIMEOUT、DEVICE_ERROR。message 不包含密码、内部路径、命令行、异常堆栈或设备调试身份。握手拒绝只含固定错误码。

3. 自动加密

传输上限补充:ServerHello 在未建立会话时一律按 MTU 23 分片。session.open 可携带整数 receive_mtu(23~517,缺省 23),表示手机当前实际可接收的 ATT MTU;后续下行取该值与 BlueZ 写入回调 MTU 的较小值。此字段只限制分片大小,不影响加密参数。针对实际手机报告 247、BlueZ 回调报告 517 的不一致情况,禁止仅依据 BlueZ 数值发送大通知;断线重置上限为 23。

使用成熟库的 P-256 ECDH、HKDF-SHA256 与 AES-256-GCM,不手写椭圆曲线/AES。客户端和服务端每次连接都生成新的临时密钥和 32 字节随机数,公钥使用 65 字节 SEC1 非压缩编码。JSON 内二进制为标准 Base64,包含必要 padding。

ClientHello 字段:protocol_major:1, protocol_minor:0, public_key, random。ServerHello 字段相同。协商拒绝未知主版本,不建立降级明文会话。握手 JSON 不包含手机名称或 WiFi 信息。

设 C、S 为收到的两个完整原始 hello JSON UTF-8 字节(不重新序列化),T=SHA256(uint32(len(C)) || C || uint32(len(S)) || S)。salt 为 SHA256(client_random || server_random),IKM 为 ECDH 共享秘密,info 为 ASCII QMS-BLE-1 加 T。HKDF 输出 72 字节:前 32 为 c2s key,接着 32 为 s2c key,最后两个 4 字节分别为 c2s/s2c nonce prefix。

加密记录为 sequence:uint64 || ciphertext || tag:16bytes。每方向 sequence 从 0 开始且必须等于期望值;nonce 为该方向 4 字节 prefix 加 sequence;AAD 为 ASCII QMS-BLE-1、T、方向字节(c2s=0,s2c=1)和 sequence。序号不回绕;鉴别失败、重放、跳序、非法公钥立即释放会话。会话密钥只存内存,断开销毁。

第一条加密请求必须为 session.open,带手机昵称;设备成功回复才算控制会话建立、才通知网页。应用层加密防被动窃听,不认证正版 App,不抵御主动中间人;UUID、昵称和 APK 内置常量都不是认证凭据。

4. 业务接口

响应包含协议主次版本与 capabilities。客户端忽略未知可选字段;缺少必需字段为协议错误。不存在某能力时隐藏或禁用相关操作并解释,不猜测默认支持。时间、大小和单位固定在字段名及模型中;不把网页内部 config 整包当作移动端契约。

method params result 与业务边界
session.open client_name device_id, device_name, short_id, protocol_major, protocol_minor, capabilities, limits;名称规则见需求。
session.ping 空 alive:true;刷新有效会话活跃时间。
session.rename client_name 回读当前名称并同步网页连接状态。
device.rename name 更新持久设备昵称,返回身份元数据,不改变 UUID。
status.get 空 当前结构复用设备状态:state, screen, service, power, resources, network, frp, ui, system, mobile 等字段;手机使用的路径列于下文。不可用原因沿用各子对象定义,不能假设所有子对象均有 available 字段。
storage.get 空 设备与软件容量统计,复用现有有界缓存,单位 bytes。
settings.get 空 当前设置、可写字段、允许值及设置修订;不得带 WiFi 密码。
settings.patch expected_revision, changes 仅允许亮度、方向、刷新率、性能模式、网络提示延迟;校验冲突后复用现有业务并回读。
wifi.scan 空 networks:[{ssid,security,signal_percent,connected}], scanned_at_ms;时间为 Unix 毫秒;同 SSID/安全类型去重取最强信号,隐藏 SSID 用手动输入。
wifi.get 空 已保存配置(不含密码)、password_configured、实际连接状态、待生效状态。
wifi.set expected_revision, ssid, security, password_action, password?, ipv4_mode, address?, prefix?, gateway?, dns_servers, activation password_action 为 keep/replace/none;none 仅开放网络;activation 为 immediate/next_boot;返回配置修订与可选 task_id。
task.get task_id state:pending/running/succeeded/failed, stage, error?;只允许查询本协议产生的有界任务,不提供任意系统任务入口。
library.list cursor?, limit 默认 20、最大 50;返回库修订、条目和下一游标。条目含 type、id、revision、name、is_demo、playable、thumbnail_revision。翻页库变化返回 CONFLICT。
library.summary 空 返回 library_revision,item_count;与 library.list 使用同一目录视图和修订,供新版 App 判断缓存是否可复用。
library.preview type,id,revision 返回 mime:image/png, data_base64,16×16 静态预览;动画使用首帧。
library.thumbnail type,id,revision 返回 mime:image/png, data_base64,64×64 静态缩略图;动画使用首帧。
content.play type,id,revision 使用既有内容解析与显示服务;过期拒绝,不能顺便修改默认内容。
content.default.get 空 当前默认内容元数据。
content.default.set type,id,revision 保存并立即播放;失败按既有默认内容事务回滚,返回最终状态。
playback.patch session_id,paused?,position_ms?,speed? 操作既有动画会话;过期会话 CONFLICT,速度限设备已有枚举。
frame.get known_revision? 未变返回 unchanged:true;变化返回逻辑 RGB 帧的 PNG Base64、frame_revision、采样时间。不传完整动图。

settings 与 wifi 的 revision 为设备生成的不透明字符串,相关状态变化即更新;网页修改也必须更新同一修订。配置检查与提交在公共控制服务内串行执行,防止“检查通过后被另一个客户端覆盖”。错误不得回传旧密码或完整输入。

5. 生命周期与兼容

系统状态每 2 秒读取,容量进入设备页时读取,库按进入/刷新读取,缩略图仅为可见项排队且按设备 ID、条目 ID、修订缓存。后台立即停预览,30 秒关闭会话。重新连接获取完整状态,不把本地缓存写回设备。

BLE 只接收白名单方法。旧客户端遇新能力忽略;旧设备缺某能力时客户端明确提示。不兼容主版本停止控制并返回列表。蓝牙关闭、主服务重启、OTA 维护或适配器消失时失效会话不可复用;恢复后重新握手,不恢复旧密钥。

协议 1.1 增加 library_progressive capability;仅声明该能力的设备可调用 library.summary 和 library.preview。协议 1.0 的 library.list、library.thumbnail 和当前帧接口保持原义;旧 App 忽略新能力,新 App 遇旧设备读取完整目录并沿用 64×64 图。内容缓存不参与设备设置或控制写入。

6. 契约测试要求

共享固定的测试私钥、hello 原始字节、HKDF 输出、双向密文与分片向量;测试密钥明确仅为公开夹具,不能来自设备。Python 与 Kotlin 双向互验,覆盖 MTU 23、大 MTU、边界分片、非 ASCII 昵称、错误公钥、篡改、重放、断线、库翻页冲突、任务失败和主版本拒绝。接口最终字段模型及黄金向量必须随实现提交,未经测试不能写成协议验收通过。

7. 当前状态模型的维护边界

status.get 当前复用设备只读状态构造函数,不依赖 REST URL 或网页控件;它仍与设备状态字段存在结构耦合,不能宣称只有驱动改变才需要检查手机兼容。移动端当前使用 state.animation_playback(active、session_id、paused、position_ms、total_duration_ms、speed、supported_speeds);其余状态作为详情展示。任何这些字段的删除、重命名、类型或含义变化必须按协议变更登记,不可随网页重构破坏旧 App。

frame.get 变化时提供 sampled_at_ms;unchanged 回复仅有 unchanged 与 frame_revision。App 用本机成功收到回复的时间表示新鲜度,避免设备与手机时钟偏差。缩略图缓存按当前连接隔离、按 type/id/revision 标识;断线清理,不把手机缓存写回设备。

2026-09-26 兼容扩展

library.list 的用户条目遵循设备持久混合顺序,顺序变化影响 library_revision 和游标。演示条目固定在用户条目之前。 task.get 可附加脱敏 error_code(AUTH_FAILED、NETWORK_UNAVAILABLE、IP_CONFIG_FAILED、UNKNOWN);旧客户端可忽略。没有可靠原因时用 UNKNOWN。wifi.get 仍不含密码。协议主版本保持 1。

兼容说明:原 mobile.transport 为 BLE 统计对象,保持原义;实际链路新增 active_transport,不改变旧字段类型。

Android 动画浏览(2026-10-10)

协议 1.3 新增 library_animation capability。library.animation.info 参数 type/id/revision、offset(默认0)、limit(默认50,1..50),返回 revision、frame_count、frames(index/duration_ms)、next_offset。library.animation.chunk 参数 type/id/revision、tier(preview/full)、frame_index、offset;返回 revision、tier、frame_index、offset、total_bytes、sha256、data_base64、next_offset,每块原始 PNG 字节最多4096。帧维持16×16/64×64、源顺序与时长;错误使用现有 BAD_REQUEST/CONFLICT/NOT_FOUND。原接口与加密消息格式不变。