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? _hexToBytes(String hex) { final s = hex.startsWith('0x') || hex.startsWith('0X') ? hex.substring(2) : hex; if (s.length.isOdd) return null; final out = []; 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 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 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 bytes, int start, int end) { final out = []; for (int i = start; i <= end; i++) { final decoded = bytes[i] ^ 0x5A; out.add(decoded.toRadixString(16).padLeft(2, '0').toUpperCase()); } return out.join(':'); } }