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.
 
 
 
 
 
 

154 lines
4.4 KiB

import 'dart:io';
import 'package:device_jieli/device_jieli.dart';
import '../../core/utils/logger.dart';
/// 杰理设备广播包解析结果:从扫描到的 [JieliDevice.rawAdv] 中提取出
/// 服务端绑定接口需要的 PID / MAC / 设备授权码 (code)。
class JieliBindInfo {
/// 产品 ID(小端字节解析)
final int pid;
/// 设备真实 MAC(XOR 0x5A 解码后),格式 "AA:BB:CC:DD:EE:FF"
final String mac;
/// 设备授权码 (license / SN),hex 大写
final String code;
/// 解析得到的 PID 是否与扫描结果上的 BLE/EDR 地址一致
/// (仅用于日志,不强制要求一致)
final bool macMatchesAddress;
const JieliBindInfo({
required this.pid,
required this.mac,
required this.code,
required this.macMatchesAddress,
});
@override
String toString() =>
'JieliBindInfo(pid=0x${pid.toRadixString(16).toUpperCase()}, mac=$mac, code=$code, match=$macMatchesAddress)';
}
/// 杰理 BLE 广播解析。
///
/// 字节布局(与旧版一致,基于扫到的 raw scanRecord 字节):
/// Android:
/// - bytes[6..7] → PID
/// - bytes[9..14] → MAC(每字节 XOR 0x5A)
/// - bytes[15..24] → 授权码 code
/// iOS:
/// - bytes[4..5] → PID
/// - bytes[7..12] → MAC(每字节 XOR 0x5A)
/// - bytes[13..22] → 授权码 code
class JieliAdvParser {
static const String _tag = 'JieliAdvParser';
/// 用 [device.rawAdv] 直接解析。若 raw 数据不完整或 device 未携带广播包,返回 null。
static JieliBindInfo? parse(JieliDevice device) {
final raw = device.rawAdv;
if (raw == null || raw.isEmpty) {
Logger.w(_tag, '设备 ${device.address} 没有 rawAdv,无法解析绑定信息');
return null;
}
return parseHex(raw, deviceAddress: device.address);
}
/// 直接对 hex 字符串解析,便于测试。
static JieliBindInfo? parseHex(String hex, {String? deviceAddress}) {
final bytes = _hexToBytes(hex);
if (bytes == null) {
Logger.w(_tag, 'rawAdv 不是合法 hex: $hex');
return null;
}
final int pidStart;
final int pidEnd;
final int macStart;
final int macEnd;
final int codeStart;
final int codeEnd;
if (Platform.isIOS) {
pidStart = 4;
pidEnd = 5;
macStart = 7;
macEnd = 12;
codeStart = 13;
codeEnd = 22;
} else {
// Android 默认;其他平台沿用 Android 偏移。
pidStart = 6;
pidEnd = 7;
macStart = 9;
macEnd = 14;
codeStart = 15;
codeEnd = 24;
}
if (bytes.length <= codeEnd) {
Logger.w(_tag,
'rawAdv 长度不足 ${codeEnd + 1} 字节(实际 ${bytes.length}),跳过解析');
return null;
}
final pid = _bytesToIntBigEndian(bytes, pidStart, pidEnd);
final mac = _decodeMac(bytes, macStart, macEnd);
final code = _bytesToHexReversed(bytes, codeStart, codeEnd);
bool match = false;
if (deviceAddress != null && deviceAddress.isNotEmpty) {
final a = deviceAddress.replaceAll(':', '').toUpperCase();
final b = mac.replaceAll(':', '').toUpperCase();
match = a == b;
}
final info = JieliBindInfo(
pid: pid,
mac: mac,
code: code,
macMatchesAddress: match,
);
Logger.i(_tag, '解析广播: $info (deviceAddress=$deviceAddress)');
return info;
}
static List<int>? _hexToBytes(String hex) {
final s = hex.startsWith('0x') || hex.startsWith('0X') ? hex.substring(2) : hex;
if (s.length.isOdd) return null;
final out = <int>[];
for (int i = 0; i < s.length; i += 2) {
final v = int.tryParse(s.substring(i, i + 2), radix: 16);
if (v == null) return null;
out.add(v);
}
return out;
}
static int _bytesToIntBigEndian(List<int> bytes, int start, int end) {
int result = 0;
for (int i = start; i <= end; i++) {
result = (result << 8) | (bytes[i] & 0xFF);
}
return result;
}
static String _bytesToHexReversed(List<int> bytes, int start, int end) {
final slice = bytes.sublist(start, end + 1);
return slice
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
}
static String _decodeMac(List<int> bytes, int start, int end) {
final out = <String>[];
for (int i = start; i <= end; i++) {
final decoded = bytes[i] ^ 0x5A;
out.add(decoded.toRadixString(16).padLeft(2, '0').toUpperCase());
}
return out.join(':');
}
}