You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

13 KiB

Echo-one 面对面翻译 · 双耳 BLE 协议(App 当前实现)

日期:2026-10-10
对象:固件
对应 App:已按本文组包、解析。请对照固件实现逐条核对。
更早的讨论稿见 face_to_face_ble_protocol.md(草案 v0.2)。以本文为准,不一致的地方按本文改固件或回我。


1. 形态

两个人各戴一只耳机。TWS 拆开后,左耳、右耳各自一条 BLE 连到同一部手机。手机不收音、不放音,只做字幕和转发。

谁在说 麦从哪来 云端 译文播到哪
戴左耳的人(源语言) 左耳 BB 90,EAR=1 A 路 右耳 AA 91,EAR=2
戴右耳的人(目标语言) 右耳 BB 90,EAR=2 B 路 左耳 AA 91,EAR=1

两条 BLE 互相独立,可以同时上行。经典蓝牙只用来进出这个模式(AA 6B / AA 6C),不传面对面的音频。

旧的面对面 AA 66 / AA 67 不动,和新模式互斥。通话翻译、AI、录音、OTA 也不并行。正在做这些事时,AA 6B 应回 ERR_BUSY (0x02)。


2. 两条 BLE 通道

左右耳的 GATT 完全一样。耳位不靠 UUID 区分,靠广播里的 EAR,以及音频帧低 4 位。

特征 UUID 属性 方向
Service AEAF5F32-0001-4C4B-A100-000000000001 — 扫描按这个 UUID 过滤
CTRL_TX AEAF5F32-0002-4C4B-A100-000000000001 Write Without Response App → 耳机,握手 / 配置 / 保活 / 退出
CTRL_RX AEAF5F32-0003-4C4B-A100-000000000001 Notify 耳机 → App,握手应答 / 事件
AUDIO_UP AEAF5F32-0004-4C4B-A100-000000000001 Notify 耳机 → App,本耳麦,BB 90
AUDIO_DOWN AEAF5F32-0005-4C4B-A100-000000000001 Write Without Response App → 耳机,译文,AA 91

控制和音频必须分开。停流、打断如果排在音频写后面,点结束会没反应。

链路要求(每条 LE 各自满足):

项 要求
写类型 CTRL_TX、AUDIO_DOWN 都认 Write Without Response
ATT MTU App 请求 247。40ms 包大约 86 字节,MTU 至少要装得下一包
PHY 优先 2M,不行再 1M
连接间隔 15~30ms。大于 40ms 译文会断
Slave latency 0
Supervision timeout 2~4s。App 被杀掉时来不及发退出,靠这条超时让固件自己恢复

BLE 帧格式(和经典蓝牙同形,操作码空间独立):

AA/BB | OP | LEN | payload

LEN 是 payload 的字节数,不含这 3 字节头。


3. 启动时序

耳机平时不要广播这个服务。只有收到经典链路上的 AA 6B 才拆 TWS、开始广播。

App                         经典蓝牙              左耳 BLE            右耳 BLE
 │  AA 6B                    │                    │                  │
 │──────────────────────────►│ 拆 TWS,两耳广播    │                  │
 │  BB EB                    │                    │                  │
 │◄──────────────────────────│                    │                  │
 │  扫描,TOKEN 对上,集齐左右耳后分别连接          │                  │
 │  订阅 CTRL_RX、AUDIO_UP   │                    │                  │
 │───────────────────────────────────────────────►│─────────────────►│
 │  HELLO → CFG → START(每只耳朵各一遍)          │                  │
 │  BB 90 左耳麦             │                    │                  │
 │◄═══════════════════════════════════════════════│                  │
 │  AA 91 译文给右耳         │                    │                  │
 │══════════════════════════════════════════════════════════════════►│
 │  BB 90 右耳麦 / AA 91 译文给左耳,方向相反       │                  │

两只耳朵的握手互不等待。两只都回了 BB 83 才开始推音频。 12 秒内没握完,App 整场退出。


4. 经典蓝牙:进入 / 退出

走现有经典链路(Android SPP,iOS GATT over BR/EDR)。不是 BLE。

4.1 进入 AA 6B

App 每次进入重新生成 4 字节随机 TOKEN。

AA 6B 06 02 01 <TOKEN 4 字节>
字节 值 含义
PROTO_VER 0x02 本协议版本
CAP 0x01 只要 G.722

App 等 BB EB 的上限是 3 秒。超时就放弃,并补发 AA 6C。

4.2 应答 BB EB

整帧 10 字节:

BB EB 07 <RESULT> <EAR_MASK> <PROTO_VER> <CODEC> <FRAME_MS> <FW_MINOR> <CLASSIC_KEEP>
字段 App 怎么用
RESULT 0x00 才继续扫 BLE。非 0 按第 8 节提示用户,并退出
EAR_MASK bit0 = 左耳可用,bit1 = 右耳可用。必须是 0x03。缺一只不进入,请直接回 0x03,不要拆了一半
CODEC 0x01 = G.722
FRAME_MS 20 或 40。决定一包里几帧。其它值 App 按 40 处理
FW_MINOR 固件小版本,App 记下来,暂不分支
CLASSIC_KEEP 0x01 = 拆 TWS 后经典链路还在;0x00 = 经典链路会断

CLASSIC_KEEP=0 时,App 不当成用户把耳机断开。退出命令改走每只耳朵的 AA 06。下次经典蓝牙再连上,App 会先补发一次 AA 6C。

4.3 退出 AA 6C

AA 6C            →    BB EC 01 <RESULT>

AA 6C 没有长度字节。

必须幂等:当前不在面对面模式,也要回 BB EC 01 00,不要报错。App 在异常恢复时会无条件补发。

RESULT=0x00 表示已重新组对,A2DP / HFP 恢复。


5. 广播与扫描

收到 AA 6B 且 RESULT=0、双耳都可用之后,左右耳各自广播。

厂商数据(AD Type 0xFF)。Company ID 2 字节之后是下面 9 字节:

F2 F2 | VER | EAR | TOKEN 4 字节 | BATT
字段 要求
MAGIC 固定 F2 F2
VER 0x02,和 AA 6B 的 PROTO_VER 一致
EAR 0x01 左耳,0x02 右耳。两只耳机的名字可以相同
TOKEN 原样回显这次 AA 6B 的 4 字节。对不上 App 直接忽略
BATT 低 7 位电量 0~100,最高位充电标志

App 扫描规则:

  1. 只扫上面的 Service UUID。
  2. TOKEN 必须和这次 AA 6B 逐字节相同。旁边另一副同型号耳机不能连上。
  3. 必须同时看到 EAR=1 和 EAR=2 才连接。 15 秒没集齐就发 AA 6C 回滚。
  4. iOS 拿不到 BLE MAC,身份只认 TOKEN + EAR。
  5. 扫描在 App 前台完成。建议建链期广播间隔 30~50ms。
  6. 30 秒内没被连上,该耳自己退出面对面;两耳都退出后重新组对,并在经典链路上报 BB EC 01 00。

连上之后该耳停止广播。


6. 每只耳朵的握手

GATT 连上后 App 会:请求 MTU 247、连接优先级拉高、优先 2M PHY。
通知顺序:先 CTRL_RX,再 AUDIO_UP。两个通知都开了才发 HELLO。

左右耳各走一遍,内容相同,TOKEN 相同。

步骤 App 发出 耳机应答 说明
1 AA 01 06 02 <TOKEN 4B> 01 BB 81 … HELLO。最后一字节 01 表示角色是 App。TOKEN 再对一次
2 AA 02 03 00 50 00 BB 82 … 配置。见下表
3 AA 03 00 BB 83 … 开流。应答成功后开始推 BB 90
4 AA 05 01 <SEQ> 可不回 两只都 BB 83 之后,每 5 秒一次。只做诊断,不要因为没收到保活就断链

AA 02 的 3 字节 payload:

字节 当前固定值 含义
VAD_MODE 0x00 常发。静音也发,不要在耳机上切句
DOWN_GAIN 0x50(80) 下行播放增益 0~100
TRIGGER 0x00 免按键,连续拾音

HELLO 应答 BB 81:App 只看 RESULT 是不是 0。RESULT 非 0,这只耳朵判失败,整场退出。
FRAME_MS 以 BB EB 里那个字节为准,不从 BB 81 再改一回。

BB 82 / BB 83 的 RESULT 非 0,同样整场退出。

开流之后如果要暂停而不断开,用 AA 04 00(STOP_STREAM)。当前正常结束不发这条,直接走第 7 节退出。


7. 退出

顺序:

  1. 每只还连着的耳朵,CTRL_TX 发 AA 06 00。耳机停流、断开这条 BLE、准备重新组对。
  2. 经典链路还在,再发 AA 6C,等 BB EC 01 00。
  3. 经典链路已经断了(CLASSIC_KEEP=0):这次只发 AA 06。App 记下标记,下次经典蓝牙一连上就补发 AA 6C。

固件收到 AA 06、AA 6C,或者下面任一条件,都要自己恢复,不能停在面对面模式:

条件 谁负责
广播 30 秒没人连 固件
LE 断连(supervision timeout 2~4s,含 App 被杀) 固件。这是 App 被杀掉时的唯一恢复手段
App 主动结束 App 发 AA 06,再视情况发 AA 6C

恢复要做完这些:停广播、断这条 GATT、两耳重新组对、A2DP / HFP 能用、按键和降噪回到进入前的状态。CLASSIC_KEEP=0 的固件,组对完成后要主动把经典链路连回手机。

单耳 BLE 断开:App 结束整场,不再单耳继续翻。
来电:耳机在 CTRL_RX 上报第 9 节的退出事件,原因 0x03。App 结束会话并提示用户。固件恢复 HFP,把电话交给系统。


8. 音频

编码固定 G.722。16 kHz、16 bit、单声道。20ms = 640 字节 PCM = 40 字节码流。

BB EB 的 FRAME_MS=40:一包 2 帧,payload 80 字节。
FRAME_MS=20:一包 1 帧,payload 40 字节。

8.1 上行 AUDIO_UP(耳机 → App)

BB 90 <LEN> <EAR|FLAGS> <SEQ 小端 2 字节> <G.722…>

8.2 下行 AUDIO_DOWN(App → 耳机)

AA 91 <LEN> <EAR|FLAGS> <SEQ 小端 2 字节> <G.722…>

LEN = 1(耳位和标志)+ 2(序号)+ G.722 字节数。
40ms 包:LEN = 0x53(83),整帧 86 字节。
20ms 包:LEN = 0x2B(43),整帧 46 字节。

低 4 位是耳位,必须等于这条链路自己的耳朵。对不上就丢,并可回 ERR_EAR_MISMATCH (0x08)。
下行的耳位是「播给谁」:左耳麦的译文写到右耳那条链路,帧里 EAR=2。

高 4 位:

位 掩码 上行(耳机 → App) 下行(App → 耳机)
bit4 0x10 本包含人声 一句译文开始
bit5 0x20 一段开始 播放结束
bit6 0x40 一段结束 打断:丢掉本耳还没播完的缓冲,从本包播
bit7 0x80 佩戴者本人在说话 不用

SEQ 每耳独立,从 0 递增,到 0xFFFF 回绕。丢包不重传。

OWN_VOICE(bit7):没有骨传导 / VPU 时这一位请一直发 0。
App 只有在这只耳朵曾经出现过 bit7=1 之后,才丢掉 bit7=0 的包。一直是 0 的话,整路都会送去识别,不会被静音。

下行节奏:没音频时 App 会先垫几包编码静音再送真声音,一句结束后再补一段静音。耳机用 BB 88 告诉 App 缓冲松紧(写在 CTRL_RX):

BB 88 <LEN> <ADJ>
ADJ 含义
+1 (0x01) 缓冲少一包,马上再补一包
-1 (0xFF) 缓冲多一包,下一拍停发一包
0 缓冲已经空,App 重新垫静音再起播

这是补包 / 扣包,不是把速率改成 1.25 倍。


9. 事件

写在 CTRL_RX:

BB 87 <LEN> <EVENT> <PARAM…>
EVENT 含义 PARAM App 行为
0x01 按键 <EAR> <KEY> <ACTION> ACTION 0x01 按下、0x02 抬起。当前是连续拾音,按键只更新界面上的左右标记
0x02 佩戴 <EAR> <STATE> 0x00 摘下,0x01 戴上,0x02 入盒。App 提示确认双耳都戴好
0x03 电量 <EAR> <LEVEL> 低 7 位电量,最高位充电。App 暂不展示
0x06 请求退出 <REASON> 0x03 来电:App 结束面对面。0x01 用户操作,0x02 电量过低,0x04 对耳失联

10. 错误码

BB EB 的 RESULT,以及握手应答里的 RESULT,用同一张表。

值 名称 App 提示
0x00 OK 继续
0x01 固件不支持 / 版本过低 耳机固件需升级
0x02 正在通话、AI、录音或 OTA 请先结束当前功能
0x03 有一只耳不可用 请确认双耳都已取出并佩戴
0x04 TOKEN 不一致 耳机未就绪
0x05 BLE 资源不足 耳机未就绪
0x06 编码失败 耳机未就绪
0x07 超时 耳机未就绪
0x08 下行耳位和这条链路不符 记日志
0x0A 固件内部错误 耳机未就绪
0x0B 拆 TWS 失败 请把双耳放回盒中再取出重试

任一阶段失败,App 都会发 AA 06(连上了的耳朵)和 AA 6C(经典链路还在时),把耳机退回普通状态。