# 面对面翻译 · 独立 BLE 链路协议(草案 v0.1) > 状态:**待固件确认**。本文只定义协议,不含实现。 > 固件与 App 双方按本文对齐后再各自写代码;有异议的条目在 §14 逐条回复,改动记进 §16 变更记录。 > > 配套阅读:[bes_earphone_protocol.md](bes_earphone_protocol.md)(现有 SPP 协议,本文与它并存,不替代) --- ## 0. 为什么要单开一条 BLE 面对面翻译要求手机同时做四件事:**收左耳麦、收右耳麦、往左耳放译文、往右耳放译文**,四条流内容各不相同。 现有恒玄链路是**一条经典蓝牙 SPP**(RFCOMM,UUID `aeaf5241-5453-7070-7353-445542594c46`), 所有指令与音频挤在这一条上,上行靠 `mode=0x03` 交织、下行靠 `AA 56` 的 84 字节双槽区分方向。 这套结构服务「通话翻译」够用(两路的语义是固定的:本端 mic / 对端 spk), 但面对面的两路是**两个人**,需要: - 两路上行**同时并发**且可独立起停(A 说话时 B 插话,不能排队); - 两路下行**可独立寻址**(A 说的话只能进 B 的耳朵); - 每路带**独立序号**,好判丢包、好做打断。 在 SPP 的定长双槽结构上硬塞这些语义,会把现有通话翻译的包格式一起改掉。 所以本功能**另起一条 BLE 链路**,与 SPP 完全隔离:进面对面时建链,退出时拆链并恢复原状。 --- ## 1. 设计边界(明确不动什么) | 不动 | 说明 | |---|---| | 现有 SPP 全部指令与音频格式 | `AA 51/52/56/57/58/59/5A/5E/5F/65/69/6A`、`BB D1/D2/D9/DA/E5/E6/E7/64/55/68/60/5B` 一律不改 | | 现有 `AA 66` / `AA 67` 面对面 | 那是**旧版**面对面(手机麦上行 + `AA 69` 单声道下行),保留可用,新模式用新命令号,两者互斥但互不修改 | | 通话翻译 / AI 对话 / 录音 / OTA | 全部保持现状;面对面 BLE 与它们**互斥**,不并行 | | 经典蓝牙配对关系 | 面对面结束后必须回到进入前的状态(见 §11) | App 侧对应载体:[face_to_face_ble_service.dart](../lib/data/services/face_to_face_ble_service.dart) (接口已固化,本文补的是它 `TODO(固件)` 里缺的那部分)。 业务层 [translation_controller.dart](../lib/modules/translation/controllers/translation_controller.dart) 的 `FaceToFaceTrigger` / `activeSpeaker` 语义不变。 --- ## 2. 总体拓扑:两档,协议同一套 固件按自身能力选一档,**App 侧接口与业务逻辑完全相同**,靠握手里的 `TOPO` 字段自适应。 ### 档位 A —— 单链路(主耳中转)· 建议先落地 ``` BLE (1 条) 手机 ────────────────► 主耳 ──耳间私有链路──► 副耳 控制/双路音频 按 EAR 字段分发 ``` - 只有主耳做 GATT peripheral,两路音频在同一条 BLE 上用帧头的 `EAR` 字段区分; - 与现有 SPP 的双路交织同构,固件改动量最小,TWS 主从关系不用拆; - 代价:副耳多一跳转发(实测应 < 20ms,需固件给实测值);耳间链路带宽要能承载一路上行 + 一路下行。 ### 档位 B —— 双链路(左右耳各自独立)· 目标形态 ``` BLE (2 条,互相独立) 手机 ═══════════════► 左耳 ═══════════════► 右耳 ``` - 左右耳各自广播、各自被 App 连接,各跑一条 GATT; - 时空壶 WT2 Edge 走的就是这条路(其耳机不出现在系统蓝牙列表,全靠 App 内分别连 L/R); - 延迟最低、两路彻底解耦;代价是要拆 TWS 主从,A2DP/HFP 期间必然中断(见 §14 Q1/Q2)。 > **协议帧在两档下完全一致**。档位 B 时每条链路只应出现自己耳位的 `EAR` 值, > 收到不属于本耳的包直接丢弃并上报 `ERR_EAR_MISMATCH`。 --- ## 3. 完整时序 ``` App 耳机(主耳) 副耳 │ │ │ │ ① SPP: AA 6B │ │ │───────────────────────────────────────►│ │ │ │ 切工作模式:停A2DP/HFP│ │ ② SPP: BB EB ... │ 起 BLE 广播(带 token) │ │◄───────────────────────────────────────│ │ │ │ │ │ ③ BLE 扫描 → 按 token 过滤 → 连接 │ │ │ ④ MTU 交换 / 2M PHY / 订阅通知 │ │ │───────────────────────────────────────►│ │ │ │ │ │ ⑤ BLE: AA 01 HELLO │ │ │───────────────────────────────────────►│ │ │ ⑥ BLE: BB 81 HELLO_ACK ... │ │ │◄───────────────────────────────────────│ │ │ ⑦ BLE: AA 02 SESSION_CFG │ │ │───────────────────────────────────────►│ │ │ ⑧ BLE: AA 03 START_STREAM │ │ │───────────────────────────────────────►│ 开双耳拾音 │ │ │ │ │ ⑨ BLE: BB 90 上行音频(EAR=L, seq++) │ │ │◄═══════════════════════════════════════│◄─────────────────────│ │ ⑩ 云端 ASR→翻译→TTS │ │ │ ⑪ BLE: AA 91 下行音频(EAR=R, seq++) │ │ │═══════════════════════════════════════►│─────────────────────►│ 播放 │ │ │ │ ⑫ BLE: AA 05 KEEPALIVE (1s 周期) │ │ │◄──────────────────────────────────────►│ │ │ │ │ │ ⑬ BLE: AA 04 STOP_STREAM │ │ │───────────────────────────────────────►│ │ │ ⑭ BLE 主动断开 │ │ │ ⑮ SPP: AA 6C 退出面对面 │ │ │───────────────────────────────────────►│ 停广播/恢复TWS/恢复A2DP│ │ ⑯ SPP: BB EC │ │ │◄───────────────────────────────────────│ │ ``` **为什么入口放在 SPP 而不是直接扫 BLE**:耳机平时不广播这个服务(省电、也避免被无关 App 扫到), 只有收到 `AA 6B` 才开始广播,且广播里带上 App 给的 token。 这样「扫描时连到隔壁桌同型号耳机」这个问题在协议层就不成立(见 §5)。 --- ## 4. 阶段一 · SPP 侧模式切换 沿用现有 SPP 帧格式:`AA CMD [LEN] [payload]` / `BB CMD LEN [payload]`。 命令号选在模式控制段(`0x5x`/`0x6x`)内的空位,响应码遵守本机固件 `响应 = 请求 | 0x80` 的规律。 > 已占用下行:`06 09 0B 0C 0E 0F 10 20 51 52 56 57 58 59 5A 5E 5F 65 66 67 69 6A` > 已占用上行:`02 03 04 07 0A 18 55 5B 60 61 64 68 86 89 8C 8E 90 D1 D2 D6 D9 DA DE DF E5 E6 E7 E9` > 本协议新增 `6B/6C` ↔ `EB/EC`,与上表均无冲突。 > > ⚠️ App 侧的 `BesBluetoothService.probeCommands()` 目前跳过的是 `0x51~0x6A`(会切工作模式的段), > **落地本协议时必须把跳过区间扩到 `0x6C`** —— 否则扫命令时会把耳机扫进面对面模式, > 且扫描进程不会发 `AA 6C` 退出,只能靠看门狗兜底。 ### 4.1 `AA 6B` 进入面对面 BLE 模式 ``` AA 6B 07 ``` | 字段 | 长度 | 取值 | |---|---|---| | `PROTO_VER` | 1 | 本文档版本,首版 `0x01` | | `CAP` | 1 | App 支持的编码位图:bit0 G.722、bit1 mSBC、bit2 Opus(预留)。首版固定 `0x01` | | `TOPO_REQ` | 1 | `0x00`=由耳机决定(推荐)、`0x01`=要求单链路、`0x02`=要求双链路 | | `TOKEN` | 4 | App 生成的随机会话标识,每次进入重新生成,用于广播过滤 | ### 4.2 `BB EB` 响应 ``` BB EB 08 ``` | 字段 | 取值 | |---|---| | `RESULT` | 见 §12 错误码,`0x00`=已进入并开始广播 | | `TOPO` | `0x01`=单链路(仅主耳广播)、`0x02`=双链路(左右耳各自广播) | | `EAR_MASK` | bit0=左耳可用、bit1=右耳可用。**单耳在盒时只置一位**,App 据此提示用户 | | `CODEC` | 实际采用的编码,`0x01`=G.722 | | `FRAME_MS` | 每包承载的音频时长,`20` 或 `40`(建议 40) | | `FW_MINOR` | 固件的本协议实现小版本,便于后续差异化兼容 | ### 4.3 `AA 6C` 退出 ``` AA 6C → BB EC 01 ``` ⚠️ **必须幂等**:不在面对面模式时收到 `AA 6C` 也要回 `BB EC 00`,不能报错。 App 在异常恢复路径上会无条件补发一次(见 §11)。 --- ## 5. 阶段二 · BLE 广播与发现 ### 5.1 广播内容 | 字段 | 内容 | |---|---| | Flags | LE General Discoverable + BR/EDR Not Supported | | Local Name | `<产品名>-F2F`,左右耳**可以同名**(靠 MSD 区分,不靠名字) | | Service UUID (16B) | 见 §6.1 | | Manufacturer Specific Data | 见下 | **Manufacturer Specific Data(AD Type `0xFF`,共 11 字节)** ``` | CompanyID 2B (LE) | MAGIC 0xF2 0xF2 | VER 1B | EAR 1B | TOKEN 4B | BATT 1B | ``` | 字段 | 说明 | |---|---| | `MAGIC` | 固定 `F2 F2`,快速筛掉无关广播 | | `VER` | 协议版本,与 `AA 6B` 的 `PROTO_VER` 一致 | | `EAR` | `0x01`=左耳、`0x02`=右耳、`0x03`=主耳(单链路档,代表双耳) | | `TOKEN` | **原样回显 `AA 6B` 里那 4 字节** | | `BATT` | 该耳电量 0~100,低 7 位;最高位为充电位(与 `BB 0A` 同口径) | ### 5.2 App 侧扫描规则(强制) 1. 只接受 `MAGIC` 命中的广播; 2. **`TOKEN` 必须与本次 `AA 6B` 下发的完全一致,否则一律忽略** —— 这是防止连到旁边同型号耳机的唯一手段,会场/展会场景必然会同时扫到多副; 3. 档位 B 下需集齐 `EAR=0x01` 与 `0x02` 两个广播才算就绪;只集到一个时按 §14 Q9 的降级策略处理; 4. 扫描超时 **15s**:超时后 App 发 `AA 6C` 回滚,并向用户报「耳机未就绪」。 ### 5.3 广播参数与耳机侧超时 - 建链期快速广播:间隔 30~50ms,持续 30s; - **30s 内无人连接,耳机自动退出面对面模式并恢复常态**(第一道看门狗,见 §11); - 连接建立后立即停止广播(档位 B 下各耳独立停)。 ### 5.4 iOS 注意 iOS 拿不到 BLE 设备的 MAC(CoreBluetooth 只给每台手机各不相同的 peripheral UUID), 所以**设备身份判定只能靠 TOKEN + EAR,不能靠地址**。这也是 §5.2 第 2 条必须强制的原因。 (同一问题在设备确权上已经踩过,见 CLAUDE.md「设备确权」一节。) --- ## 6. 阶段三 · GATT 服务与特征 ### 6.1 UUID(**待固件分配,下表为占位**) 沿用现有 SPP UUID 的 128-bit 风格,基址建议:`AEAF5F32-XXXX-4C4B-A100-000000000000` | 项 | 占位 UUID | 属性 | 方向 | |---|---|---|---| | Service | `AEAF5F32-0001-4C4B-A100-000000000001` | — | — | | `CTRL_TX` | `AEAF5F32-0002-4C4B-A100-000000000001` | Write / Write Without Response | App → 耳机(控制) | | `CTRL_RX` | `AEAF5F32-0003-4C4B-A100-000000000001` | Notify | 耳机 → App(控制/事件) | | `AUDIO_UP` | `AEAF5F32-0004-4C4B-A100-000000000001` | Notify | 耳机 → App(音频) | | `AUDIO_DOWN` | `AEAF5F32-0005-4C4B-A100-000000000001` | Write Without Response | App → 耳机(音频) | **控制与音频分开成 4 个特征,不合并**:音频是每 40ms 一包的高频流, 与控制帧共用一个特征时,控制消息(尤其是「打断」和 `STOP_STREAM`)会排在音频队列后面, 表现为「点了停止还在播」。分开后两条队列各自独立。 ### 6.2 链路参数要求 | 项 | 要求 | 理由 | |---|---|---| | ATT MTU | ≥ 185,建议 247 | 40ms 聚合包 = 6B 头 + 80B 音频 = 86B;留余量给未来的 Opus/更长聚合 | | PHY | 优先 2M,回退 1M | 四路并发时降低空口占用 | | 连接间隔 | 15~30ms | 大于 40ms 会让下行音频出现可闻的断续 | | Slave latency | 0 | 音频流不允许休眠跳包 | | 数据长度扩展 (DLE) | 开 | 否则 86B 包会被拆成多个 27B 空口包,吞吐骤降 | --- ## 7. 阶段四 · BLE 链路握手 BLE 链路上的操作码空间**与 SPP 独立**(不同链路,不会混淆)。 帧格式同样是 `AA ` / `BB `,`LEN` 为 payload 字节数。 | 步骤 | 帧 | payload | 说明 | |---|---|---|---| | ① HELLO | `AA 01` | `` | `ROLE`:`0x01`=App。token 再校验一次,防止连错 | | ② HELLO_ACK | `BB 81` | `` | `EAR` 必须与广播里一致;`MTU_OK`=0 时 App 降级到 20ms 单帧包 | | ③ SESSION_CFG | `AA 02` | `` | 见下 | | ④ CFG_ACK | `BB 82` | `` | | | ⑤ START_STREAM | `AA 03` | `` | 指定开哪几路上行;档位 B 下固定为本耳 | | ⑥ START_ACK | `BB 83` | `` | 回 `0x00` 后耳机开始推 `BB 90` | | ⑦ STOP_STREAM | `AA 04` | — | 停上行,链路保留(切换语言/暂停时用,不必拆链) | | ⑧ KEEPALIVE | `AA 05` | `` | App 每 **1s** 发一次 | | ⑨ KEEPALIVE_ACK | `BB 85` | `` | `LOSS`=耳机侧统计的下行丢包率(%),用于诊断 | **`SESSION_CFG` 字段** | 字段 | 取值 | |---|---| | `UP_ENABLE` | bit0=左耳上行、bit1=右耳上行 | | `VAD_MODE` | `0x00`=**常发**(不做静音抑制,默认)、`0x01`=静音抑制(耳机侧 VAD 判静音则不发包) | | `DOWN_GAIN` | 下行播放增益 0~100,默认 80 | | `TRIGGER` | `0x00`=免按键(连续拾音)、`0x01`=按住说话(按键事件驱动,对应 App 的 `FaceToFaceTrigger.device`) | > ⚠️ `VAD_MODE` 默认 `0x00` 是刻意的:断句策略在 App/云端侧,固件 VAD 一旦判早就会把一句话切碎, > 每段各翻一次,效果比多传点静音差得多。同传模式下已经踩过这个坑 > (见 CLAUDE.md「面对面 / 同声翻译:延迟构成」第 1 条)。省电需求确认后再开 `0x01`。 --- ## 8. 音频上行(耳机 → App) ``` BB 90 ``` | 字段 | 说明 | |---|---| | `EAR` | 低 4 位:`0x1`=左耳、`0x2`=右耳 | | `FLAGS` | 高 4 位:bit4 `VAD_ACTIVE`(本包含人声)、bit5 `SEG_BEGIN`(一段发声起始)、bit6 `SEG_END`(结束)、bit7 保留 | | `SEQ` | **每耳独立**自增,回绕 `0xFFFF`。App 据此算丢包,不做重传 | | `PAYLOAD` | `N × 40B` G.722 码流。`FRAME_MS=40` 时 N=2,`=20` 时 N=1 | - 编码:**G.722**,16kHz / 16bit / 单声道,一帧 640B PCM(320 样本 = 20ms)→ 40B 码流。 选它是因为现有原生插件的 `G722Codec` 编解码器两端都已落地、已验证,不引入新依赖。 - `SEG_BEGIN` / `SEG_END` 是**耳机给的提示**,不是命令:App 可以用它抢跑一次断句, 但最终断句仍以云端 ASR 为准(`VAD_MODE=0x00` 时耳机照发不误)。 - 丢包**不重传**:语音流重传只会加大延迟。App 检测到 seq 跳变时补静音帧维持时基。 --- ## 9. 音频下行(App → 耳机) ``` AA 91 ``` | 字段 | 说明 | |---|---| | `EAR` | 低 4 位:这包该播给哪只耳。档位 A 由主耳按此转发;档位 B 下与本链路耳位不符则丢弃并回 `ERR_EAR_MISMATCH` | | `FLAGS` | bit4 `PLAY_BEGIN`(一段译文开始,耳机可据此压低本地音/给提示音)、bit5 `PLAY_END`(结束,可恢复)、bit6 `FLUSH`(**打断**:丢弃本耳未播完的缓冲,立即播本包) | | `SEQ` | 每耳独立自增 | | `PAYLOAD` | 同上行 | - **打断(`FLUSH`)是面对面必需的**:对方还在听上一句译文时新一句已经翻好, 没有 flush 就会越积越多、译文与对话彻底脱节。App 在每段译文首包置 `PLAY_BEGIN`, 需要抢播时同时置 `FLUSH`。 - **不需要静音填充**。这点与 SPP 的 `AA 56` 84 字节双槽不同:那是定长双槽包,空槽必须填静音帧; BLE 是包交换,没数据就不发包,耳机侧靠 `SEQ` 与到达时间维持时基即可。 - **节流**:App 按 `FRAME_MS` 的实时速率发送。耳机可用 `BB 86 ` 请求改节奏 (语义对齐现有 SPP 的 `0xD6/0xE9` 调速),合法值 `16 / 20 / 25 / 40`。 - 缓冲:耳机侧建议 60~120ms 抖动缓冲;溢出时**丢最旧的**并上报 `EVT_BUF_OVERFLOW`,不要阻塞。 --- ## 10. 事件上报 ``` BB 87 ``` | `EVENT` | 含义 | PARAM | App 侧对应 | |---|---|---|---| | `0x01` | 按键 | ``,ACTION:`0x01`按下 / `0x02`抬起 / `0x03`单击 / `0x04`双击 | **按住说话**:左耳按下→`activeSpeaker=1`、右耳按下→`=2`、抬起→`=0`。语义已在 `translation_controller` 落地,固件只需如实上报 | | `0x02` | 佩戴状态 | ``,STATE:`0x00`摘下 / `0x01`戴上 / `0x02`入盒 | 摘下/入盒 → App 暂停该路,UI 提示 | | `0x03` | 电量 | ``,最高位为充电位 | 与 `BB 0A` 同口径 | | `0x04` | 链路统计 | `` | 每 10s 一次,仅用于诊断日志 | | `0x05` | 异常 | ``,见 §12 | 缓冲溢出、编码失败等 | | `0x06` | 耳机请求退出 | `` | 用户在耳机上操作或电量过低;App 收到后走正常退出流程 | --- ## 11. 退出与恢复(重点) 功能结束后**必须完整回到进入前的状态**。这一节的每一条都是验收项。 ### 11.1 三层看门狗 | 层 | 条件 | 动作 | 责任方 | |---|---|---|---| | 1 | 开始广播后 **30s** 无 BLE 连接 | 停广播、退出面对面模式、恢复常态、SPP 主动上报 `BB EC 00` | 固件 | | 2 | BLE 已连接但 **5s** 未收到 `AA 05` KEEPALIVE | 断 BLE、退出面对面模式、恢复常态 | 固件 | | 3 | BLE 意外断开 / App 侧异常 | 立即补发 SPP `AA 6C`;SPP 也不可用时,**下次 SPP 连上后先发一次 `AA 6C`** 清状态 | App | > 第 2 层是本协议里最重要的一条。App 被系统杀掉、崩溃、用户强退时, > 没有任何机会发退出指令;缺了它耳机会永远卡在面对面模式,用户表现为 > 「耳机连着但放不了音乐、也接不了电话」,且看不出原因。 > (同类教训见 CLAUDE.md 里 BES OTA 的 90s 看门狗一节。) ### 11.2 固件的恢复清单 收到 `AA 6C`、或任一看门狗触发时,**逐项恢复**: 1. 停止 BLE 广播,断开本协议的 GATT 连接; 2. 恢复 TWS 主从与耳间同步(档位 B 拆过主从时); 3. 恢复 A2DP / HFP —— 音乐能播、来电能接; 4. 恢复按键功能映射到用户原设置(面对面期间若临时改过键位); 5. 恢复 ANC / 通透模式到进入前的档位; 6. 恢复麦克风通路与增益; 7. SPP 回 `BB EC 00`(看门狗触发时为主动上报)。 ### 11.3 App 的恢复动作 - 退出 BLE 模式后不重连 SPP:SPP 链路全程未断,`BesBluetoothService` 状态不受影响; - 恢复完成前不允许再次进入面对面(防止半状态叠加); - UI 上「结束」按钮必须**先返回、后台跑收尾**,不要 await 整条退出链 (录音页踩过这个坑,见 CLAUDE.md「录音页退不出去」)。 ### 11.4 互斥 面对面 BLE 模式与下列功能**同一时刻只能有一个**,冲突时 `AA 6B` 回 `ERR_BUSY`: 通话翻译(`AA 51`)、AI 对话(`0x64`)、主麦录音(`AA 59`)、媒体录音(`AA 5E`)、OTA、旧版面对面(`AA 66`)。 --- ## 12. 错误码(`RESULT` / `CODE` 共用一张表) | 值 | 名称 | 含义 | App 侧表现 | |---|---|---|---| | `0x00` | `OK` | 成功 | — | | `0x01` | `ERR_UNSUPPORTED` | 固件不支持本协议或版本过低 | 提示「耳机固件需升级」,引导 OTA | | `0x02` | `ERR_BUSY` | 正在通话/AI/录音/OTA | 提示「请先结束当前功能」 | | `0x03` | `ERR_EAR_UNAVAILABLE` | 单耳或双耳不可用(在盒/未佩戴) | 提示「请取出并佩戴双耳」 | | `0x04` | `ERR_TOKEN_MISMATCH` | token 校验失败 | 静默重试一次,仍失败则回滚 | | `0x05` | `ERR_BLE_RESOURCE` | BLE 资源不足(连接数/内存) | 提示重试 | | `0x06` | `ERR_CODEC` | 编码不支持或初始化失败 | 记日志,回滚 | | `0x07` | `ERR_TIMEOUT` | 阶段超时 | 回滚并提示 | | `0x08` | `ERR_EAR_MISMATCH` | 下行包耳位与链路不符 | 记日志(属 App 侧 bug) | | `0x09` | `EVT_BUF_OVERFLOW` | 下行缓冲溢出 | 记日志,App 降速 | | `0x0A` | `ERR_INTERNAL` | 固件内部错误 | 回滚并提示 | --- ## 13. 带宽与延迟预算 **带宽**(G.722 @16kHz = 16 kbps/路) | 流 | 路数 | 小计 | |---|---|---| | 上行(左耳 + 右耳) | 2 | 32 kbps | | 下行(左耳 + 右耳) | 2 | 32 kbps | | 控制 + keepalive | — | < 2 kbps | | **合计** | | **≈ 66 kbps** | 40ms 聚合时每包 86 字节,四条流合计每 40ms 4 包 ≈ 8.6 KB/s。 BLE 1M PHY 在 30ms 连接间隔下的实测可用吞吐通常在 100~300 kbps,**余量充足**; 开 2M PHY + DLE 后更宽松。档位 B 下两条链路各承担一半,压力更小。 > 对照:若直接传 16k/16bit 裸 PCM,单路即 256 kbps,四路 1 Mbps —— BLE 上不可行。 > **所以下行/上行都必须是编码后码流,不能传裸 PCM。** **延迟预算**(单向,从 A 开口到 B 听到) | 段 | 预估 | 备注 | |---|---|---| | 耳机采集 + 编码 | 20~40ms | 一个聚合包 | | BLE 上行 | 30~60ms | 含连接间隔与重传 | | 档位 A 的耳间转发 | +10~20ms | 仅档位 A、且仅下行侧 | | 云端 ASR→翻译→TTS | **800~2500ms** | 大头。端到端模型可压到 ~800ms,三段串行则 2s+ | | BLE 下行 | 30~60ms | | | 解码 + 抖动缓冲 + 播放 | 60~120ms | | | **合计** | **≈ 1.0~2.8s** | | > 蓝牙链路只占其中 100~250ms。**优化重心在云端那一段**,不要在协议上过度抠 10ms。 --- ## 14. 待固件确认清单 请逐条回复「支持 / 不支持 / 需改为」: | # | 问题 | 影响 | |---|---|---| | Q1 | 能否做**档位 B(双链路)**?拆开 TWS 主从后,两只耳机还能各自独立跑 GATT 并保持同步吗? | 决定最终形态;不能则先做档位 A | | Q2 | 面对面模式下 **A2DP / HFP 是否必须中断**?若中断,来电时如何处理(自动退出面对面?) | 决定 §11 恢复清单与用户提示文案 | | Q3 | §6.1 的 **GATT UUID 由谁分配**?固件方是否已有预留的 128-bit 基址? | 阻塞两端编码 | | Q4 | 支持 **MTU 247 / 2M PHY / DLE** 吗?最大支持的 MTU 是多少? | 决定聚合帧长(40ms vs 20ms) | | Q5 | **G.722 能否直接用在 BLE 通道**上(现有实现在 SPP 上)?编解码器是否可复用? | 不能则要换 Opus,两端都要加依赖 | | Q6 | 是否有**入耳检测 / 骨传导 / VPU** 可用来判定「是佩戴者本人在说话」?能否随上行包给出该标志? | 面对面串音抑制的关键;没有的话只能靠云端,效果差一档 | | Q7 | 面对面模式下**哪些按键可用**、键位码如何编排?长按/抬起事件能否上报? | 决定「按住说话」能否做 | | Q8 | 广播里的 **TOKEN 回显**能实现吗?广播内容能否在运行时动态改? | 不能则无法防止连错耳机,需另想办法 | | Q9 | **单耳在盒 / 只戴一只**时的降级行为?是拒绝进入,还是以单耳模式进入? | 决定 `EAR_MASK` 语义与 App 提示 | | Q10 | §11.1 的三层**看门狗超时值**(30s / 5s)是否合适?固件侧能否实现第 1、2 层? | 直接决定「App 崩了耳机会不会卡死」 | | Q11 | 与 **OTA / AI 对话 / 通话**的互斥由固件拦(回 `ERR_BUSY`)还是 App 自律? | 建议固件拦,App 也拦,双保险 | | Q12 | 档位 B 下两只耳机的**上行时间戳是否需要对齐**?耳间有共同时钟吗? | 影响能否做跨耳的回声/串音抑制 | | Q13 | 单包最大长度、单位时间最大包数有无限制? | 影响聚合策略 | | Q14 | 耳机侧**下行抖动缓冲**多大?溢出策略是丢旧还是丢新? | 影响打断(FLUSH)的实际效果 | --- ## 15. 联调验收用例 | # | 场景 | 期望 | |---|---|---| | T01 | 正常进入 → 双耳广播 → 连接 → 握手 → 上下行通 | 全流程 < 5s,双耳都能收发 | | T02 | 左耳说话 | 只有右耳听到译文;左耳自己听不到 | | T03 | 双方同时说话 | 两路各自独立上行,互不排队,两路译文分别到达对侧 | | T04 | 打断:A 的第二句在第一句译文播完前到达 | 置 `FLUSH` 后立即切到新句,不排队堆积 | | T05 | 扫描期旁边有另一副同型号耳机(也在面对面模式) | **绝不连错**:token 不匹配的广播被忽略 | | T06 | 正常退出 | 音乐可播、来电可接、按键功能恢复、ANC 恢复 | | T07 | **App 进程被杀** | ≤ 5s 后耳机自行退出并恢复常态(看门狗 2) | | T08 | 进入模式后一直不连(App 卡住) | ≤ 30s 后耳机自行退出(看门狗 1) | | T09 | 面对面期间来电 | 按 Q2 的结论执行,且结束后状态一致 | | T10 | 面对面期间摘下一只耳 / 放回盒 | 上报 `EVT 0x02`,App 暂停该路并提示,戴回后恢复 | | T11 | 走出距离边缘导致丢包 | 无崩溃、无杂音,seq 补洞,恢复后自愈 | | T12 | 连续进出 20 次 | 无资源泄漏,第 20 次与第 1 次耗时相当 | | T13 | 重复发 `AA 6C` 三次 | 三次都回 `BB EC 00`(幂等) | | T14 | 面对面期间尝试进 AI / 通话翻译 | 回 `ERR_BUSY`,两边状态都不被破坏 | --- ## 16. 变更记录 | 版本 | 日期 | 变更 | |---|---|---| | v0.1 | 2026-09-05 | 首版草案,待固件确认 §14 |