|
|
|
@ -31,6 +31,9 @@ class BesBluetoothService extends GetxService { |
|
|
|
StreamSubscription? _statusSubscription; |
|
|
|
StreamSubscription? _cmdEventSubscription; |
|
|
|
|
|
|
|
/// 连接后补查电量的重试定时器,断开时取消(见 [_scheduleBatteryRetry]) |
|
|
|
Timer? _batteryRetryTimer; |
|
|
|
|
|
|
|
/// 0=未连接 1=连接中 2=已连接(与原生侧约定一致) |
|
|
|
final RxInt deviceStatus = 0.obs; |
|
|
|
final RxString connectedDeviceName = ''.obs; |
|
|
|
@ -167,6 +170,7 @@ class BesBluetoothService extends GetxService { |
|
|
|
void onClose() { |
|
|
|
_statusSubscription?.cancel(); |
|
|
|
_cmdEventSubscription?.cancel(); |
|
|
|
_batteryRetryTimer?.cancel(); |
|
|
|
super.onClose(); |
|
|
|
} |
|
|
|
|
|
|
|
@ -229,6 +233,7 @@ class BesBluetoothService extends GetxService { |
|
|
|
if (deviceStatus.value == 2) { |
|
|
|
await sendData([0xAA, 0x06]); |
|
|
|
await queryBattery(); |
|
|
|
_scheduleBatteryRetry(); |
|
|
|
// App 可能是在耳机已经通话中时才连上/重启回连的——0xD1/0xD2/0x02/0x03 |
|
|
|
// 都是推送事件,错过了就再也等不到,必须连上后主动查一次。 |
|
|
|
await queryCallState(); |
|
|
|
@ -255,6 +260,8 @@ class BesBluetoothService extends GetxService { |
|
|
|
Logger.e(_tag, '连接后握手失败(不致命): $e'); |
|
|
|
} |
|
|
|
} else if (event == 0) { |
|
|
|
_batteryRetryTimer?.cancel(); |
|
|
|
_batteryRetryTimer = null; |
|
|
|
connectedDeviceId.value = ''; |
|
|
|
connectedDeviceName.value = ''; |
|
|
|
deviceVersion.value = ''; |
|
|
|
@ -276,7 +283,10 @@ class BesBluetoothService extends GetxService { |
|
|
|
.map((b) => (b as int).toRadixString(16).padLeft(2, '0')) |
|
|
|
.join(' ') |
|
|
|
: '$raw'; |
|
|
|
Logger.d(_tag, '恒玄设备命令事件: type=${event['type']} raw=[$hex]'); |
|
|
|
// ⚠️ 用 warn 级:release 包的级别门槛就是 warning,写 debug/info 等于 |
|
|
|
// 在真机上什么都记不下来。这是定位「新固件上电量命令是哪个」的唯一 |
|
|
|
// 第一手材料,频率也低(只在耳机推帧时触发),留在正式包里是划算的。 |
|
|
|
Logger.w(_tag, '恒玄设备命令事件: type=${event['type']} raw=[$hex]'); |
|
|
|
final type = event['type']; |
|
|
|
if (type is String && type.isNotEmpty) lastCmdType.value = type; |
|
|
|
switch (type) { |
|
|
|
@ -315,19 +325,19 @@ class BesBluetoothService extends GetxService { |
|
|
|
// 哪些命令还活着,跳过它们就失去了对照组。 |
|
|
|
const skip = {0x51, 0x52, 0x56, 0x57, 0x58, 0x59, 0x5A, |
|
|
|
0x5E, 0x5F, 0x64, 0x65, 0x66, 0x67, 0x69, 0x6A}; |
|
|
|
Logger.i(_tag, |
|
|
|
Logger.w(_tag, |
|
|
|
'[探测] 开始 0x${from.toRadixString(16)}~0x${to.toRadixString(16)}'); |
|
|
|
for (var cmd = from; cmd <= to; cmd++) { |
|
|
|
if (cmd >= 0x51 && cmd <= 0x6A) continue; // 会切工作模式,别碰 |
|
|
|
if (skip.contains(cmd)) continue; |
|
|
|
if (deviceStatus.value != 2) return; |
|
|
|
Logger.i(_tag, '[探测] 发送 AA ${cmd.toRadixString(16).padLeft(2, '0')}'); |
|
|
|
Logger.w(_tag, '[探测] 发送 AA ${cmd.toRadixString(16).padLeft(2, '0')}'); |
|
|
|
try { |
|
|
|
await sendData([0xAA, cmd]); |
|
|
|
} catch (_) {} |
|
|
|
await Future.delayed(const Duration(milliseconds: 400)); |
|
|
|
} |
|
|
|
Logger.i(_tag, '[探测] 结束'); |
|
|
|
Logger.w(_tag, '[探测] 结束'); |
|
|
|
} |
|
|
|
|
|
|
|
/// 实测出来的命令号(2026-09-01,Echo-one 固件 0.0.2,`probeCommands` 扫 0x01~0x20 得到): |
|
|
|
@ -448,6 +458,39 @@ class BesBluetoothService extends GetxService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 连上之后电量迟迟没到就再补问几次。 |
|
|
|
/// |
|
|
|
/// 握手里只发一次 `AA 09` 是不够的,两个原因: |
|
|
|
/// 1. 那一刻 SPP 通道刚建好(还排在 `AA 06` 后面),命令可能被固件丢掉; |
|
|
|
/// 2. 协议 3.1.10 说这帧「响应查询**或者**状态变化时主动上报」——如果这版 |
|
|
|
/// 固件只走后者,连接那一瞬本来就不会有推送,等一等反而可能等到。 |
|
|
|
/// |
|
|
|
/// 间隔逐次拉长(3s / 8s / 20s),三次拿不到就彻底放弃:探测命令占着 SPP |
|
|
|
/// 会让用户点录音/翻译排队(2026-09-04 真机踩过),不能无限期轮询下去。 |
|
|
|
/// 中途任何一路(查询回包或耳机主动推)把电量填上了就立即停。 |
|
|
|
void _scheduleBatteryRetry() { |
|
|
|
_batteryRetryTimer?.cancel(); |
|
|
|
const delays = <Duration>[ |
|
|
|
Duration(seconds: 3), |
|
|
|
Duration(seconds: 8), |
|
|
|
Duration(seconds: 20), |
|
|
|
]; |
|
|
|
var attempt = 0; |
|
|
|
void next() { |
|
|
|
if (attempt >= delays.length) return; |
|
|
|
final d = delays[attempt++]; |
|
|
|
_batteryRetryTimer = Timer(d, () async { |
|
|
|
if (deviceStatus.value != 2) return; // 已断开 |
|
|
|
if (batteryLevel.value >= 0) return; // 已经拿到了,不再打扰通道 |
|
|
|
Logger.w(_tag, '电量仍未上报,第 $attempt 次补查 (AA 09)'); |
|
|
|
await queryBattery(); |
|
|
|
next(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
next(); |
|
|
|
} |
|
|
|
|
|
|
|
/// 请求上报通话状态(`AA 0E`),耳机回 `0x8E`: |
|
|
|
/// data[6] 0x01=来电 / 0x02=通话中 / 0x03=通话结束。 |
|
|
|
/// 原生侧已把 0x02/0x03 解析成 startMakeCall/stopMakeCall 事件,与 |
|
|
|
@ -495,13 +538,13 @@ class BesBluetoothService extends GetxService { |
|
|
|
// 连接后周期性上报,结构是 [len,attr,value] × 2(attr 0x01左 / 0x02右)。 |
|
|
|
// 实测值恒为 1,不像百分比;可能是等级或入耳状态。 |
|
|
|
// 固件确认它是电量后,把下面这行换成 _parseBattery(data) 即可。 |
|
|
|
Logger.d(_tag, '设备状态上报(0x18) 原始=$data'); |
|
|
|
Logger.w(_tag, '设备状态上报(0x18) 原始=$data'); |
|
|
|
break; |
|
|
|
default: |
|
|
|
// 没识别的回包打出来,方便按真实固件补协议(电量 CMD 就靠这个定位)。 |
|
|
|
// 用 INFO 级 + 十六进制:DEBUG 在某些构建里被过滤掉,而十进制的 |
|
|
|
// `[187, 134, 34, ...]` 对着协议文档看要一个个换算,很容易看错。 |
|
|
|
Logger.i( |
|
|
|
Logger.w( |
|
|
|
_tag, |
|
|
|
'未识别回包(设备主动上报) CMD=0x${data[1].toRadixString(16).padLeft(2, '0')} ' |
|
|
|
'原始=${data.map((e) => e.toRadixString(16).padLeft(2, '0').toUpperCase()).join(' ')}'); |
|
|
|
@ -586,7 +629,7 @@ class BesBluetoothService extends GetxService { |
|
|
|
// 只有盒子上报时(两只耳机都在盒内)退而显示盒电量 |
|
|
|
batteryLevel.value = batteryCase.value; |
|
|
|
} |
|
|
|
Logger.i( |
|
|
|
Logger.w( |
|
|
|
_tag, |
|
|
|
'恒玄耳机电量: ${batteryLevel.value}%' |
|
|
|
' (L=${batteryLeft.value}${chargingLeft.value ? "⚡" : ""}' |
|
|
|
@ -604,7 +647,7 @@ class BesBluetoothService extends GetxService { |
|
|
|
String mac(Iterable<int> b) => |
|
|
|
b.map((e) => e.toRadixString(16).padLeft(2, '0').toUpperCase()).join(':'); |
|
|
|
|
|
|
|
Logger.i(_tag, '[抓帧] BB0A 原始 = ${hex(data)}'); |
|
|
|
Logger.w(_tag, '[抓帧] BB0A 原始 = ${hex(data)}'); |
|
|
|
var p = 3; // 与 _parseBattery 同一起点:SOF + CMD + 1 字节长度 |
|
|
|
while (p + 1 < data.length) { |
|
|
|
final blockLen = data[p]; |
|
|
|
@ -617,7 +660,7 @@ class BesBluetoothService extends GetxService { |
|
|
|
'[3..8]=${body.length >= 9 ? mac(body.sublist(3, 9)) : "-"} ' |
|
|
|
'[6..11]=${body.length >= 12 ? mac(body.sublist(6, 12)) : "-"}' |
|
|
|
: (attr == 7 ? ' ← 蓝牙地址 = ${mac(body)}' : ''); |
|
|
|
Logger.i( |
|
|
|
Logger.w( |
|
|
|
_tag, |
|
|
|
'[抓帧] attr=$attr len=$blockLen body(${body.length}B)=' |
|
|
|
'${hex(body)}$note'); |
|
|
|
@ -654,7 +697,9 @@ class BesBluetoothService extends GetxService { |
|
|
|
final parsed = left ?? right ?? box; |
|
|
|
if (parsed != null) { |
|
|
|
deviceVersion.value = parsed; |
|
|
|
Logger.i(_tag, '恒玄耳机固件版本: $parsed'); |
|
|
|
// 固件版本决定这台机器该用哪套命令号(0.0.2 与 0.0.3 的电量命令不同), |
|
|
|
// 排查电量问题第一件事就是看它,所以要在 release 日志里留得住。 |
|
|
|
Logger.w(_tag, '恒玄耳机固件版本: $parsed'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|