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.
 
 
 
 
 
 

267 lines
7.9 KiB

/// Echo-one 面对面 BLE 协议的纯函数编解码。
///
/// 字节布局以 `docs/face_to_face_ble_protocol.md` v0.2 为准,
/// 由 `test/devices/bes_f2f_protocol_test.dart` 钉死。原生组包必须与这里一致。
library;
class BesF2f {
BesF2f._();
static const int protoVer = 0x02;
static const int capG722 = 0x01;
static const int codecG722 = 0x01;
static const int earLeft = 0x01;
static const int earRight = 0x02;
static const int magic0 = 0xF2;
static const int magic1 = 0xF2;
/// 控制与音频分开的四个特征。UUID 按协议占位,固件已按此实现。
static const String serviceUuid = 'AEAF5F32-0001-4C4B-A100-000000000001';
static const String ctrlTxUuid = 'AEAF5F32-0002-4C4B-A100-000000000001';
static const String ctrlRxUuid = 'AEAF5F32-0003-4C4B-A100-000000000001';
static const String audioUpUuid = 'AEAF5F32-0004-4C4B-A100-000000000001';
static const String audioDownUuid = 'AEAF5F32-0005-4C4B-A100-000000000001';
static const int opHello = 0x01;
static const int opSessionCfg = 0x02;
static const int opStartStream = 0x03;
static const int opStopStream = 0x04;
static const int opKeepalive = 0x05;
static const int opExit = 0x06;
static const int opAudioUp = 0x90;
static const int opAudioDown = 0x91;
static const int opEvent = 0x87;
static const int opPace = 0x88;
static const int roleApp = 0x01;
static const int vadAlways = 0x00;
static const int triggerContinuous = 0x00;
static const int defaultGain = 80;
static const int flagVad = 0x10;
static const int flagSegBegin = 0x20;
static const int flagSegEnd = 0x40;
static const int flagOwnVoice = 0x80;
static const int flagPlayBegin = 0x10;
static const int flagPlayEnd = 0x20;
static const int flagFlush = 0x40;
static const int evtButton = 0x01;
static const int evtWear = 0x02;
static const int evtBattery = 0x03;
static const int evtStats = 0x04;
static const int evtFault = 0x05;
static const int evtExitRequest = 0x06;
static const int exitUser = 0x01;
static const int exitBattery = 0x02;
static const int exitIncomingCall = 0x03;
static const int exitPeerLost = 0x04;
static const int ok = 0x00;
static const int errUnsupported = 0x01;
static const int errBusy = 0x02;
static const int errEarUnavailable = 0x03;
static const int errToken = 0x04;
static const int errBleResource = 0x05;
static const int errCodec = 0x06;
static const int errTimeout = 0x07;
static const int errEarMismatch = 0x08;
static const int errOverflow = 0x09;
static const int errInternal = 0x0A;
static const int errTwsSplit = 0x0B;
/// `AA 6B 06 <ver> <cap> <token 4>`
static List<int> enterFrame(List<int> token) {
if (token.length != 4) {
throw ArgumentError('token must be 4 bytes');
}
return <int>[
0xAA, 0x6B, 0x06, protoVer, capG722,
token[0] & 0xFF, token[1] & 0xFF, token[2] & 0xFF, token[3] & 0xFF,
];
}
static List<int> exitFrame() => const <int>[0xAA, 0x6C];
/// 解析 `BB EB`。长度不够返回 null。
static BesF2fEnterAck? parseEnterAck(List<int> frame) {
if (frame.length < 10) return null;
if ((frame[0] & 0xFF) != 0xBB || (frame[1] & 0xFF) != 0xEB) return null;
return BesF2fEnterAck(
result: frame[3] & 0xFF,
earMask: frame[4] & 0xFF,
protoVer: frame[5] & 0xFF,
codec: frame[6] & 0xFF,
frameMs: frame[7] & 0xFF,
fwMinor: frame[8] & 0xFF,
classicKeep: (frame[9] & 0xFF) == 0x01,
);
}
static bool isExitAckOk(List<int> frame) {
if (frame.length < 4) return false;
return (frame[0] & 0xFF) == 0xBB &&
(frame[1] & 0xFF) == 0xEC &&
(frame[3] & 0xFF) == ok;
}
/// 广播里 Manufacturer Data 去掉 Company ID 之后的 9 字节。
static BesF2fAdvert? parseAdvertPayload(List<int> afterCompanyId) {
if (afterCompanyId.length < 9) return null;
if ((afterCompanyId[0] & 0xFF) != magic0 ||
(afterCompanyId[1] & 0xFF) != magic1) {
return null;
}
final ear = afterCompanyId[3] & 0xFF;
if (ear != earLeft && ear != earRight) return null;
return BesF2fAdvert(
version: afterCompanyId[2] & 0xFF,
ear: ear,
token: List<int>.unmodifiable(afterCompanyId.sublist(4, 8)),
battery: afterCompanyId[8] & 0x7F,
charging: (afterCompanyId[8] & 0x80) != 0,
);
}
static List<int> hello(List<int> token) => <int>[
0xAA, opHello, 0x06, protoVer,
token[0] & 0xFF, token[1] & 0xFF, token[2] & 0xFF, token[3] & 0xFF,
roleApp,
];
static List<int> sessionCfg() => <int>[
0xAA, opSessionCfg, 0x03, vadAlways, defaultGain, triggerContinuous,
];
static List<int> startStream() => <int>[0xAA, opStartStream, 0x00];
static List<int> stopStream() => <int>[0xAA, opStopStream, 0x00];
static List<int> keepalive(int seq) =>
<int>[0xAA, opKeepalive, 0x01, seq & 0xFF];
static List<int> exitEar() => <int>[0xAA, opExit, 0x00];
/// `AA 91` 下行音频。payload = flags(含耳位) + seq16le + g722。
static List<int> audioDown({
required int ear,
required int flags,
required int seq,
required List<int> g722,
}) {
final head = (ear & 0x0F) | (flags & 0xF0);
final body = <int>[head, seq & 0xFF, (seq >> 8) & 0xFF, ...g722];
return <int>[0xAA, opAudioDown, body.length & 0xFF, ...body];
}
/// 解析一条 BLE 通知帧。不是本协议返回 null。
static BesF2fBleFrame? parseBle(List<int> frame) {
if (frame.length < 3) return null;
final sof = frame[0] & 0xFF;
if (sof != 0xAA && sof != 0xBB) return null;
final op = frame[1] & 0xFF;
final len = frame[2] & 0xFF;
if (frame.length < 3 + len) return null;
return BesF2fBleFrame(op: op, payload: frame.sublist(3, 3 + len));
}
static BesF2fAudio? parseAudioUp(BesF2fBleFrame frame) {
if (frame.op != opAudioUp || frame.payload.length < 3) return null;
final b = frame.payload[0] & 0xFF;
return BesF2fAudio(
ear: b & 0x0F,
flags: b & 0xF0,
seq: (frame.payload[1] & 0xFF) | ((frame.payload[2] & 0xFF) << 8),
g722: List<int>.unmodifiable(frame.payload.sublist(3)),
);
}
/// 用户能看懂的失败文案键。未知码用超时。
static String messageKey(int code) {
switch (code) {
case errUnsupported:
return 'f2fFirmwareUpgrade';
case errBusy:
return 'f2fEndOtherFeature';
case errEarUnavailable:
return 'f2fWearBothEars';
case errTwsSplit:
return 'f2fPutBackInCase';
default:
return 'f2fEarNotReady';
}
}
}
class BesF2fEnterAck {
const BesF2fEnterAck({
required this.result,
required this.earMask,
required this.protoVer,
required this.codec,
required this.frameMs,
required this.fwMinor,
required this.classicKeep,
});
final int result;
final int earMask;
final int protoVer;
final int codec;
final int frameMs;
final int fwMinor;
final bool classicKeep;
bool get bothEars => (earMask & 0x03) == 0x03;
}
class BesF2fAdvert {
const BesF2fAdvert({
required this.version,
required this.ear,
required this.token,
required this.battery,
required this.charging,
});
final int version;
final int ear;
final List<int> token;
final int battery;
final bool charging;
bool tokenEquals(List<int> expected) {
if (token.length != expected.length) return false;
for (var i = 0; i < token.length; i++) {
if ((token[i] & 0xFF) != (expected[i] & 0xFF)) return false;
}
return true;
}
}
class BesF2fBleFrame {
const BesF2fBleFrame({required this.op, required this.payload});
final int op;
final List<int> payload;
int get result => payload.isEmpty ? 0xFF : payload[0] & 0xFF;
}
class BesF2fAudio {
const BesF2fAudio({
required this.ear,
required this.flags,
required this.seq,
required this.g722,
});
final int ear;
final int flags;
final int seq;
final List<int> g722;
bool get ownVoice => (flags & BesF2f.flagOwnVoice) != 0;
}