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.
 
 
 
 
 
 

12 KiB

恒玄(BES)耳机 SPP 协议梳理 —— 来自 deepvoice 项目

来源:/Users/yunyanzhineng/work/src/deepvoice 链路:经典蓝牙 RFCOMM,SPP UUID aeaf5241-5453-7070-7353-445542594c46 本项目对应封装:lib/data/services/bes_bluetooth_service.dart + local_plugins/bluetooth_manager (这套 native 插件 eaimar 已经完整拉过来了,两边的 BluetoothManager.kt 一致, 所以下行指令发送、上行事件解析、G.722 / mSBC 编解码全部现成可用。)


一、帧格式

方向 SOF 结构
App → 耳机 0xAA AA CMD [len] [payload…],短指令只有 AA CMD 两字节
耳机 → App 0xBB BB CMD len(1~2) [payload…]

native 侧 ConnectedThread.run() 按 0xBB / 0xCC 拆帧,handleCommand() 按 data[1] 分发;未识别的 CMD 一律以 type: "unknown" + 原始字节抛给 Dart,由 Dart 二级解析 (本项目 BesBluetoothService._handleRawFrame 就是这么处理固件版本回包的)。


二、App → 耳机(下行指令)

1. 设备信息 / 设置

指令 含义 回包 deepvoice 出处
AA 06 查询固件版本 BB 07 主动上报 / BB 86 响应 bluetooth_service.dart:57 已接入本项目
AA 10 查询 EQ 配置 BB 90 eq_settings_controller.dart:76
AA 0C 查询按键功能配置 BB 8C eq_settings_controller.dart:78
AA 0F 06 02 01 <value> 设置 EQ — eq_settings_controller.dart:148
AA 0B 06 02 <attr> <value> 设置按键功能(attr=按键位,value=功能码) — eq_settings_controller.dart:192
AA 0E 进入同声翻译 / 会议助手页面前下发。代码里没有注释说明用途,推测是通知耳机切到「APP 功能模式」,需向固件确认 — home_view.dart:239,259

2. 通话翻译(双向,G.722 双声道)

指令 含义 出处
AA 51 进入通话翻译/通话模式(耳机开始上传双路 G.722) CallTranslationHelper.start()
AA 52 退出通话模式 CallTranslationHelper.stop()
AA 57 通知耳机「ASR 已开启」(收到耳机 0xD1 后发) startRecognition()
AA 58 通知耳机「ASR 已关闭」(收到耳机 0xD2 后发) stopRecognition()
AA 56 54 03 [40B spk][40B mic] 下行 TTS 音频:84 字节定长包,len=84,mode=0x03(双声道),右耳=对端译文、左耳=己方译文;空槽填静音帧 sendAudioData()

3. 面对面翻译(手机麦上行,耳机放译文)

指令 含义 出处
AA 66 开始面对面翻译 FaceToFaceTranslationHelper.start()
AA 67 结束面对面翻译 stop()
AA 69 2C 01 [40B G.722] 下行 TTS 音频,44 字节包,mode=0x01(单声道) sendAudioData()

4. 现场录音 / 同声翻译(耳机主麦,mSBC)

指令 含义 出处
AA 59 开始主麦录音(耳机开始上传 mSBC 流) meeting_record_controller._startBtLiveRecord()、SimultaneousTranslationHelper.start()
AA 5A 停止主麦录音 _stopBtLiveRecord()、SimultaneousTranslationHelper.stop()

同声翻译的 BT 路径直接复用现场录音的 59/5A 指令,只是把 PCM 喂给 ASR 而不是写文件。

5. 媒体(音视频)录音

指令 含义
AA 5E 开始媒体录音(audioType=1,录耳机播放的音频)
AA 5F 停止媒体录音

6. 通话录音

meeting_record_controller 里 audioType=2(通话录音)用的就是 AA 51 / AA 52, 和通话翻译同一组指令——耳机侧只有「进入/退出通话双路音频模式」这一个开关, 录音还是翻译取决于 App 拿到 spkData/micData 之后怎么处理。

7. AI 实时对话

指令 含义 出处
AA 6A 3 秒内回应耳机的 0x64,确认真正进入 AI 对话,否则耳机停止传音频 AgentSessionCoordinator.startSession()
AA 65 请求退出 AI 模式(正常路径等耳机回 0xE5,2s 未回则强制结束) requestStop()
AA 69 … 下行 AI 语音(同面对面翻译的包格式,mode=0x01) HeadsetAudioLink.sendOnePacket()

三、耳机 → App(上行事件)

native handleCommand() 的完整分发表(BluetoothManager.kt:180):

CMD 事件 type 分发去向 含义
0xD1 startCall translation + cmd 通话开始(用户接通)
0xD2 stopCall translation + cmd 通话结束
0x02 startMakeCall cmd 去电开始
0x03 / 0x04 stopMakeCall(同时发 stopCall 给翻译) cmd 去电结束
0x8E(data[6]=02/03) startMakeCall / stopMakeCall cmd 通话状态的另一种上报格式
0xD9 startMicRecording translation + cmd 主麦录音已开始(响应 AA 59)
0xDA stopMicRecording translation + cmd 主麦录音已停止(响应 AA 5A)
0xDE startMusic cmd 媒体播放/录音开始(响应 AA 5E)
0xDF / 0x61 stopMusic cmd 媒体停止
0xE6 / 0xE7 startFaceToFace / stopFaceToFace translation 面对面翻译开/关(耳机按键触发)
0x64 / 0xE5 startAI / stopAI ai AI 对话唤醒 / 结束
0xD6 / 0xE9 speedAdjustment translation + ai 下行发包节奏调整:data[6] = 01→16ms / 02→20ms / 03→25ms
0x55 / 0x68 / 0x60 —(走音频解码) — G.722 音频流,见下
0x5B —(走音频解码) — mSBC 音频流,见下
其它 unknown + 原始字节 cmd Dart 侧二级解析(固件版本 0x07/0x86 就在这里)

音频流解码(native 已实现,直接可用)

G.722(0x55/0x68/0x60) — data[3] & 0x0F 是声道模式:

mode 含义 解码后回调
0x01 单路:本端麦克风 micData
0x02 单路:对端/耳机麦 spkData(同时喂给 AI 链路,AI 模式下耳机麦实测是按 spk 标记上行的)
0x03 双路交织,每 40 字节一块交替 交替 spkData / micData

mSBC(0x5B) — 每 57 字节一帧,交替解出:mainMicData(主麦)/ ffMicData(前馈麦)。

两种解码输出统一是 16kHz / 16bit / 单声道 PCM,并且:

  • 全部通过 onDeviceDataReceived()(EventChannel bluetooth_manager/read)发给 Dart;
  • 同时按类型发给 native 的 translationCallback / aiCallback 槽。

四、和本项目功能的对应关系

eaimar 的翻译/ASR 栈在 Dart 侧(azure_speech / volcano_speech / bytedance_speech), deepvoice 的在 native 插件(asr_tr_tts_manager / agent_manager,eaimar 没拉这两个)。 所以对接方式是:指令用 BesBluetoothService.sendData() 发,音频从 onDeviceDataReceived() 取 PCM 喂现有 Dart ASR, 不需要把那两个 native 插件搬过来。

本项目功能 入口 需要的下行指令 需要监听的上行事件 音频来源 备注
通话翻译(AI 页) HomeController.openTranslationFeature('call') → Routes.translation 进页 AA 51,退页 AA 52;收到 0xD1 后 AA 57,收到 0xD2 后 AA 58 startCall(0xD1) / stopCall(0xD2) / speedAdjustment G.722 mode=0x03 双路 → spkData=对端、micData=本端 译文要回放到耳机需要按 AA 56 84字节包上行,G.722 编码用 native 已有的 G722Codec.encodeSync(当前 Dart 侧没有暴露编码接口,要加一个 method channel)
面对面翻译 Routes.translation (faceToFace) 开始 AA 66,结束 AA 67 startFaceToFace(0xE6) / stopFaceToFace(0xE7)(耳机按键也能触发) 己方=手机麦;对端=耳机 spkData 译文下行 AA 69 44 字节包,mode=0x01
同声翻译 Routes.translation (simultaneous) 连了耳机走 AA 59 / AA 5A;没连走手机麦 startMicRecording(0xD9) / stopMicRecording(0xDA) mSBC → mainMicData 与现场录音共用指令,只是 PCM 去向不同
通话录音 HomeController.openCallRecording() → CallRecordingController AA 51 / AA 52 startCall / stopCall 同通话翻译:spkData=右声道、micData=左声道 deepvoice 存的是双声道 WAV(左=本端麦、右=对端),本项目要照这个存
现场录音 HomeController.openFieldRecording() AA 59 / AA 5A startMicRecording / stopMicRecording mSBC → mainMicData(隔帧取主麦) 注意:meeting_record_controller._mainMicBuilderAdd 是隔一帧取一帧(主麦/前馈麦交替上行)
AI 实时对话 lib/modules/agent 收到 0x64 3 秒内必须回 AA 6A;退出发 AA 65 startAI(0x64) / stopAI(0xE5) / speedAdjustment(0xE9) 耳机麦以 G.722 mode=0x02 上行 → spkData 会话只能由耳机长按发起,App 侧只能确认/停止
固件升级页 Routes.OTA_UPGRADE AA 06 0x07 / 0x86 — 版本解析已在 BesBluetoothService._parseFirmwareVersion 落地,设备页右下角已显示
EQ / 按键设置 lib/modules/eq_settings AA 10、AA 0C 查询;AA 0F…、AA 0B… 设置 0x90(EQ 回包)、0x8C(按键回包) — 目前 eq_settings 模块走的是杰理链路,接恒玄要另加一路

已经就位 / 还缺的

已就位(native 插件拉过来时一并带入,不用再写):

  • 帧拆包、handleCommand 全量事件分发
  • G.722 / mSBC 解码 → 16k PCM,走 onDeviceDataReceived()
  • RFCOMM 连接、ACL 广播自动回连
  • sendData() 下行通道、固件版本查询与解析

还缺(要在本项目补的):

  1. 下行音频编码接口:native 有 G722Codec.encodeSync,但 bluetooth_manager 的 method channel 没暴露, 通话翻译 / 面对面翻译 / AI 的 TTS 回放需要加一个 encodeG722 方法,或者把打包逻辑 (AA 56 / AA 69)直接做进 native,Dart 只丢 PCM。
  2. PCM 分流器:onDeviceDataReceived() 只给一条裸 PCM 流,分不清 spk/mic/mainMic。 deepvoice 靠 native 的 translationCallback 分类型。本项目建议在 bluetooth_manager 的 cmd EventChannel 上补一个带 type 的音频事件,或者按 deepvoice 的做法在 Dart 侧靠交替计数分声道 (_callBuilderAdd / _mainMicBuilderAdd 就是这个套路)。
  3. 三条链路的互斥:deepvoice 里 translation 与 AI 共用 0x68 上行 / AA 69 下行,靠 translationCallback != null 做守卫。本项目接进来后同样要保证「通话翻译 / 面对面翻译 / AI 对话」 同一时刻只有一个在跑。
  4. 按键事件落业务:目前 BesBluetoothService.lastCmdType 只记录不处理, 需要把 startMicRecording / stopMicRecording / startCall / stopCall / startFaceToFace / startAI 接到对应页面的控制器上(参考支架那条链路的 HolderRecordingService 常驻服务写法)。

五、注意事项(实测)

  • TWS 双地址:DEEPVOICE 在系统配对列表里是 F0:A9:68:94:C1:C4, 但能跑 SPP 的是 F0:A9:68:94:BE:F8。连接时要按 「原生当前已连接地址 → 上次成功地址 → 配对列表地址」依次尝试 (bes_scan_view.dart 已按此实现)。
  • AI 的 3 秒窗口:0x64 之后必须在 3s 内发 AA 6A,deepvoice 是在建 WebSocket 之前先发, 防止弱网握手把窗口耗光。
  • 调速指令:0xD6/0xE9 的 data[6] 决定下行发包间隔(16/20/25ms), 下行音频一旦要做,这个必须跟着变,否则耳机侧会缓冲溢出或断续。
  • AI PEN 不是这条链路:meeting_connect_controller.dart 里那组 0x5A 01 xx 指令 (0x0d 查新文件、0x87 开热点、0x0b/0x0c 取 SSID/密码、0x03~0x06 取 FTP 参数) 走的是 BLE(FFF0/FFF1/FFF2)+ WiFi + FTP 的录音笔导文件流程,和恒玄耳机 SPP 无关。