Files
flutterApp/lib/core/bluetooth/ble_protocol_decoder.dart

745 lines
26 KiB
Dart
Raw Normal View History

2026-08-07 08:49:29 +08:00
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:typed_data';
import '../protocol/machine_protocol_constants.dart';
import 'mc700_device_config.dart';
/// BLE 协议解析出的单个字段
class BleField {
final String label;
final String value;
const BleField({required this.label, required this.value});
}
/// BLE 协议解析结果
class BleDecodedResult {
final List<BleField> fields;
final String rawHex;
/// 0x05 读配置时携带的配置实体(可修改后回写)
final Mc700DeviceConfig? configEntity;
const BleDecodedResult({
required this.fields,
required this.rawHex,
this.configEntity,
});
bool get isEmpty => fields.isEmpty;
}
/// BLE 协议解析器
/// 与 TCP net_message_dispatcher 使用相同的协议格式:AB AA cmd [payload] ... AA AB
/// payload 通常为 UTF-8 逗号分隔文本,部分指令为二进制
class BleProtocolDecoder {
/// 入口:按命令类型分发解析
static BleDecodedResult decode(int command, Uint8List payload) {
final hex = _bytesToHex(payload);
try {
switch (command) {
case MachineProtocolConstants.cmdStatusInfo:
return _decodeStatusInfo(payload, hex);
case MachineProtocolConstants.cmdRemoteControl:
return _decodeRemoteControl(payload, hex);
case MachineProtocolConstants.cmdGetId:
return _decodeGetId(payload, hex);
case MachineProtocolConstants.cmdGetAuth:
return _decodeGetAuth(payload, hex);
case MachineProtocolConstants.cmdReadConfig:
return _decodeReadConfig(payload, hex);
case MachineProtocolConstants.cmdWriteConfig:
return _decodeWriteConfig(payload, hex);
case MachineProtocolConstants.cmdHeartbeat:
return _decodeHeartbeat(payload, hex);
case MachineProtocolConstants.cmdPathPlanning:
return _decodePathPlanning(payload, hex);
case MachineProtocolConstants.cmdObstacleAvoid:
return _decodeObstacleAvoid(payload, hex);
default:
return _decodeGeneric(command, payload, hex);
}
} catch (e) {
return BleDecodedResult(
fields: [const BleField(label: '解析错误', value: '异常')],
rawHex: hex,
);
}
}
// ─── 0x02 状态信息(与 TCP RunningStatusEntity.fromFields 一致,24字段逗号分隔) ───
static BleDecodedResult _decodeStatusInfo(Uint8List data, String hex) {
final fields = <BleField>[];
// 🔥 关键修复:协议帧为 AB AA 02 [24个逗号字段] CRC_lo CRC_hi AA AB。
// ProtocolParser 只剥离了帧尾 AA AB,payload 末尾仍带 2 字节 CRC16(小端)。
// 障碍物标志位是最后一个字段(fields[23]),若不先去掉 CRC,
// 这 2 字节会粘到障碍物字段上导致乱码。此处先剥离尾部 2 字节 CRC。
final Uint8List body =
data.length > 2 ? Uint8List.sublistView(data, 0, data.length - 2) : data;
2026-08-07 08:49:29 +08:00
try {
// allowMalformed 兜底:非法字节不再抛异常、不再落入 latin1 造成整段乱码
var text = utf8.decode(body, allowMalformed: true);
// 去掉控制字符(\0 \r \n 等)与替换符(U+FFFD):固件常在串尾补 \0,
// 或残留 CRC 被解码成非法字符,粘在最后一个字段(障碍物)上造成乱码
text = text.replaceAll(RegExp(r'[\u0000-\u001F\uFFFD]'), '');
2026-08-07 08:49:29 +08:00
if (text.contains(',')) {
final parts = text.split(',');
// 诊断:仅当控制模式槽位(parts[20])出现协议外码(如6)时,打印全字段索引对齐,
// 用于判断是固件扩展码还是字段错位;正常包不打印,避免刷屏
if (parts.length > 20 &&
!const ['0', '1', '2', '3', '4'].contains(parts[20].trim())) {
developer.log(
'[BLE-0x02] 字段对齐诊断: ' +
List.generate(parts.length, (i) => '[$i]=${parts[i]}')
.join(' '),
name: 'BleProtocolDecoder',
2026-08-07 08:49:29 +08:00
);
}
return BleDecodedResult(fields: _parseStatusFields(parts), rawHex: hex);
}
} catch (_) {}
2026-08-07 08:49:29 +08:00
if (body.isNotEmpty) {
final status = body[0];
2026-08-07 08:49:29 +08:00
fields.add(
BleField(
label: '状态码',
value: '0x${status.toRadixString(16).toUpperCase().padLeft(2, '0')}',
),
);
}
if (body.length >= 2) {
final mode = body[1];
2026-08-07 08:49:29 +08:00
const modes = {0x00: '待机', 0x01: '遥控', 0x02: '自动', 0x03: '急停'};
fields.add(
BleField(
label: '控制模式',
value: modes[mode] ?? '未知(0x${mode.toRadixString(16)})',
),
);
}
if (body.length >= 3)
fields.add(BleField(label: '电量', value: '${body[2]}%'));
if (body.length >= 4) {
final fault = body[3];
2026-08-07 08:49:29 +08:00
fields.add(
BleField(
label: '故障状态',
value: fault == 0
? '无故障'
: '故障码: 0x${fault.toRadixString(16).toUpperCase().padLeft(2, '0')}',
),
);
}
if (body.length >= 6) {
final speed = (body[5] << 8) | body[4];
2026-08-07 08:49:29 +08:00
fields.add(BleField(label: '速度', value: '$speed'));
}
if (body.length > 8) {
2026-08-07 08:49:29 +08:00
try {
final text = String.fromCharCodes(body.sublist(8));
2026-08-07 08:49:29 +08:00
if (text.isNotEmpty && !text.contains('\x00')) {
fields.add(BleField(label: '附加', value: text));
}
} catch (_) {}
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
/// 解析逗号分隔的 24 字段状态数据(与 RunningStatusEntity.fromFields 完全一致)
static List<BleField> _parseStatusFields(List<String> parts) {
while (parts.length < 24) {
parts.add('');
}
// 协议定义 控制模式: 0无控制 1本地遥控 2蓝牙 3TCP 4其他接口;
// 先按整数解析(容忍前后空格),有映射的一律显示对应文字;
// 超出协议范围的码(如固件扩展值)显示为 未知(n),避免被误当成有效模式
2026-08-07 08:49:29 +08:00
String controlModeText(String v) {
final n = int.tryParse(v.trim());
if (n == null) return v.isEmpty ? '--' : v;
return switch (n) {
0 => '无控制',
1 => '本地遥控',
2 => '蓝牙',
3 => 'TCP',
4 => '其他接口',
_ => '未知($n)',
2026-08-07 08:49:29 +08:00
};
}
// 协议定义 定位质量: 0无效 1GPS单点 2DGPS差分/SBAS 4RTK固定解 5RTK浮点解 7手动输入
2026-08-07 08:49:29 +08:00
String qualText(String v) {
final q = int.tryParse(v) ?? -1;
return switch (q) {
0 => '无效',
1 => 'GPS 单点定位',
2 => 'DGPS 差分/SBAS',
4 => 'RTK 固定解',
5 => 'RTK 浮点解',
7 => '手动输入模式',
2026-08-07 08:49:29 +08:00
_ => v.isNotEmpty ? v : '--',
};
}
String headingText(String v) {
final h = int.tryParse(v) ?? -1;
return switch (h) {
0 => '未初始化',
1 => '已初始化',
_ => v.isNotEmpty ? v : '--',
};
}
String obstacleText(String v) {
// 障碍物标志位为单个数字 0/1;字段尾部可能粘有 CRC/\0 等杂质,
// 取第一个数字字符作为标志,无数字则显示 '--',杜绝杂质乱码
final d = RegExp(r'[0-9]').firstMatch(v);
if (d == null) return '--';
return switch (int.parse(d.group(0)!)) {
2026-08-07 08:49:29 +08:00
0 => '无障碍',
1 => '有障碍',
_ => v,
2026-08-07 08:49:29 +08:00
};
}
return [
BleField(label: '电压', value: '${_tryParseDouble(parts[0])} V'),
BleField(label: '左目标速度', value: _tryParseDouble(parts[1])),
BleField(label: '右目标速度', value: _tryParseDouble(parts[2])),
BleField(label: '左实测速度', value: _tryParseDouble(parts[3])),
BleField(label: '右实测速度', value: _tryParseDouble(parts[4])),
BleField(label: '左电流', value: '${_tryParseDouble(parts[5])} A'),
BleField(label: '右电流', value: '${_tryParseDouble(parts[6])} A'),
BleField(label: '左电机温度', value: '${_tryParseDouble(parts[7])} ℃'),
BleField(label: '右电机温度', value: '${_tryParseDouble(parts[8])} ℃'),
BleField(label: '芯片温度', value: '${_tryParseDouble(parts[9])} ℃'),
BleField(label: '偏航角', value: '${_tryParseDouble(parts[10])} °'),
BleField(label: '俯仰角', value: '${_tryParseDouble(parts[11])} °'),
BleField(label: '翻滚角', value: '${_tryParseDouble(parts[12])} °'),
BleField(label: '卫星数量', value: parts[13].isNotEmpty ? parts[13] : '--'),
BleField(label: '定位质量', value: qualText(parts[14])),
BleField(label: '航向状态', value: headingText(parts[15])),
BleField(label: '纬度', value: parts[16].isNotEmpty ? parts[16] : '--'),
BleField(label: '经度', value: parts[17].isNotEmpty ? parts[17] : '--'),
BleField(label: '时间戳', value: parts[18].isNotEmpty ? parts[18] : '--'),
BleField(
label: '割刀速度',
value: parts[19].isNotEmpty ? '${parts[19]} rpm' : '--',
),
BleField(label: '控制模式', value: controlModeText(parts[20])),
BleField(
label: '电量',
value: parts[21].isNotEmpty ? '${parts[21]}%' : '--',
),
BleField(label: '工作面积', value: parts[22].isNotEmpty ? parts[22] : '--'),
BleField(label: '障碍物', value: obstacleText(parts[23])),
];
}
static String _tryParseDouble(String s) {
if (s.isEmpty) return '--';
final v = double.tryParse(s);
if (v == null) return s;
if (v == v.roundToDouble() && v.abs() < 10000) return v.toInt().toString();
return v.toStringAsFixed(2);
}
// ─── 0x00 远程遥控 ───
static BleDecodedResult _decodeRemoteControl(Uint8List data, String hex) {
final fields = <BleField>[];
try {
final text = utf8.decode(data, allowMalformed: true);
if (text.contains(',')) {
final parts = text.split(',');
while (parts.length < 5) parts.add('');
fields.add(
BleField(label: '左右速度', value: parts[0].isNotEmpty ? parts[0] : '--'),
);
fields.add(
BleField(label: '前后速度', value: parts[1].isNotEmpty ? parts[1] : '--'),
);
final modeStr = switch (parts[2]) {
'0' => '停止',
'1' => '遥控',
'2' => '自动',
_ => parts[2].isNotEmpty ? parts[2] : '--',
};
fields.add(BleField(label: '控制模式', value: modeStr));
if (parts.length > 3 && parts[3].isNotEmpty)
fields.add(BleField(label: '参数4', value: parts[3]));
if (parts.length > 4 && parts[4].isNotEmpty)
fields.add(BleField(label: '参数5', value: parts[4]));
return BleDecodedResult(fields: fields, rawHex: hex);
}
} catch (_) {}
if (data.length >= 5) {
fields.add(BleField(label: '左右速度', value: '${(data[1] << 8) | data[0]}'));
fields.add(BleField(label: '前后速度', value: '${(data[3] << 8) | data[2]}'));
const modes = {0: '停止', 1: '遥控', 2: '自动'};
fields.add(
BleField(label: '控制模式', value: modes[data[4]] ?? '未知(${data[4]})'),
);
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 0x03 查询ID ───
static BleDecodedResult _decodeGetId(Uint8List data, String hex) {
final fields = <BleField>[];
try {
final text = utf8.decode(data, allowMalformed: true);
if (text.replaceAll('\x00', '').trim().isNotEmpty) {
fields.add(
BleField(label: '设备ID', value: text.replaceAll('\x00', '').trim()),
);
return BleDecodedResult(fields: fields, rawHex: hex);
}
} catch (_) {}
try {
final ascii = String.fromCharCodes(data);
if (RegExp(r'^[\x20-\x7E]+$').hasMatch(ascii)) {
fields.add(BleField(label: '设备ID', value: ascii));
} else {
fields.add(BleField(label: '设备ID(Hex)', value: hex));
}
} catch (_) {
fields.add(BleField(label: '设备ID(Hex)', value: hex));
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 0x04 获取授权 ───
static BleDecodedResult _decodeGetAuth(Uint8List data, String hex) {
final fields = <BleField>[];
try {
final text = utf8.decode(data, allowMalformed: true);
final trimmed = text.replaceAll('\x00', '').trim();
if (trimmed == '1' || trimmed.toLowerCase() == 'true') {
fields.add(const BleField(label: '授权状态', value: '已授权'));
} else if (trimmed == '0' || trimmed.toLowerCase() == 'false') {
fields.add(const BleField(label: '授权状态', value: '未授权'));
} else if (trimmed.isNotEmpty) {
fields.add(BleField(label: '授权状态', value: trimmed));
}
if (fields.isNotEmpty)
return BleDecodedResult(fields: fields, rawHex: hex);
} catch (_) {}
if (data.isNotEmpty) {
if (data[0] == 0x01) {
fields.add(const BleField(label: '授权状态', value: '已授权'));
} else if (data[0] == 0x00) {
fields.add(const BleField(label: '授权状态', value: '未授权'));
} else {
fields.add(
BleField(label: '授权状态(Hex)', value: '0x${data[0].toRadixString(16)}'),
);
}
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 0x05 读配置(按嵌入式协议规范精确解析,171字节) ───
static BleDecodedResult _decodeReadConfig(Uint8List data, String hex) {
final fields = <BleField>[];
if (data.isEmpty) {
fields.add(const BleField(label: '状态', value: '空数据'));
return BleDecodedResult(fields: fields, rawHex: hex);
}
final bd = ByteData.sublistView(data);
final len = data.length;
// 1. UID固化标志 @payload[0] (帧字节3)
if (len >= 1) {
fields.add(
BleField(
label: 'UID固化标志',
value: data[0] == 1 ? '已固化' : '未固化(${data[0]})',
),
);
}
// 2. 芯片UID @payload[1..45] (帧字节4-48, 45字节)
if (len >= 46) {
fields.add(
BleField(label: '芯片UID', value: _readNullTermString(data, 1, 45)),
);
}
// 3. 遥控器通道配置 @payload[46..64] (帧字节49-67, 19字节)
if (len >= 65) {
fields.add(
BleField(label: '遥控器通道配置', value: _bytesToHex(data.sublist(46, 65))),
);
}
// 4. 字节68位域 @payload[65] (帧字节68)
if (len >= 66) {
final b = data[65];
fields.add(
BleField(
label: '割刀电机模式',
value: (b & 0x03) == 0 ? '纯电' : '油电(${b & 0x03})',
),
);
fields.add(
BleField(
label: '行走电机模式',
value: ((b >> 2) & 0x03) == 0 ? '轮式' : '履带(${(b >> 2) & 0x03})',
),
);
fields.add(
BleField(label: '左轮电机极性', value: (b & 0x10) == 0 ? '高极性' : '低极性(反取)'),
);
fields.add(
BleField(label: '右轮电机极性', value: (b & 0x20) == 0 ? '高极性' : '低极性(反取)'),
);
fields.add(
BleField(label: '左右轮通道交换', value: (b & 0x40) == 0 ? '不交换' : '通道互换'),
);
fields.add(
BleField(label: '联网目标', value: (b & 0x80) == 0 ? 'WiFi' : '4G'),
);
}
// 5. 前进速度限制 @payload[66..67] (帧字节69-70, uint16 LE)
if (len >= 68) {
fields.add(
BleField(label: '前进速度限制', value: '${bd.getUint16(66, Endian.little)}'),
);
}
// 6. 转向速度限制 @payload[68..69] (帧字节71-72, uint16 LE)
if (len >= 70) {
fields.add(
BleField(label: '转向速度限制', value: '${bd.getUint16(68, Endian.little)}'),
);
}
// 7. 字节73位域 @payload[70] (帧字节73)
if (len >= 71) {
final b = data[70];
fields.add(
BleField(label: '割刀通道极性', value: (b & 0x01) == 0 ? '高极性' : '低极性(反取)'),
);
fields.add(
BleField(label: '风门通道极性', value: (b & 0x02) == 0 ? '高极性' : '低极性(反取)'),
);
fields.add(
BleField(label: '油门通道极性', value: (b & 0x04) == 0 ? '高极性' : '低极性(反取)'),
);
fields.add(BleField(label: '底盘升降保护时间', value: '${(b >> 3) & 0x1F} 秒'));
fields.add(
BleField(label: 'RTK配置', value: (b & 0x80) == 0 ? '单天线' : '双天线'),
);
}
// 8. 字节74位域 @payload[71] (帧字节74)
if (len >= 72) {
final b = data[71];
fields.add(BleField(label: '割刀通道配置', value: '${b & 0x0F}'));
fields.add(BleField(label: '风门通道配置', value: '${(b >> 4) & 0x0F}'));
}
// 9. 字节75位域 @payload[72] (帧字节75)
if (len >= 73) {
final b = data[72];
fields.add(BleField(label: '油门通道配置', value: '${b & 0x0F}'));
fields.add(
BleField(
label: '遥控器类型',
value: (b >> 4) & 0x07 == 0 ? '飞控' : '自定义遥控器(${(b >> 4) & 0x07})',
),
);
fields.add(
BleField(label: '是否搭载继电器板', value: (b & 0x80) == 0 ? '未搭载' : '搭载'),
);
}
// 10. 字节76位域 @payload[73] (帧字节76)
if (len >= 74) {
final b = data[73];
fields.add(BleField(label: '底盘升降通道配置', value: '${b & 0x0F}'));
fields.add(BleField(label: '底盘通道配置', value: '${(b >> 4) & 0x0F}'));
}
// 11. 字节77位域 @payload[74] (帧字节77)
if (len >= 75) {
final b = data[74];
fields.add(BleField(label: '机械臂通道配置', value: '${b & 0x0F}'));
fields.add(BleField(label: '燃油泵通道配置', value: '${(b >> 4) & 0x0F}'));
}
// 12. WiFi名称 @payload[75..94] (帧字节78-97, 20字节)
if (len >= 95) {
fields.add(
BleField(label: 'WiFi名称', value: _readNullTermString(data, 75, 20)),
);
}
// 13. WiFi密码 @payload[95..114] (帧字节98-117, 20字节)
if (len >= 115) {
fields.add(
BleField(label: 'WiFi密码', value: _readNullTermString(data, 95, 20)),
);
}
// 14. 字节118位域 @payload[115] (帧字节118)
if (len >= 116) {
final b = data[115];
fields.add(
BleField(
label: '电池类型',
value: (b & 0x03) == 0 ? '铅酸' : '锂电(${b & 0x03})',
),
);
fields.add(
BleField(
label: '行走驱动配置',
value: switch ((b >> 2) & 0x03) {
0 => '风德控',
1 => '山东神澜BLDC',
2 => '山东神澜FOC',
_ => '未知(${(b >> 2) & 0x03})',
},
),
);
}
// 15. 转速比 @payload[116..119] (帧字节119-122, float LE)
if (len >= 120) {
final v = bd.getFloat32(116, Endian.little);
fields.add(BleField(label: '转速比', value: v.toStringAsFixed(2)));
}
// 16. 机器人长度 @payload[120..123] (帧字节123-126, float LE)
if (len >= 124) {
final v = bd.getFloat32(120, Endian.little);
fields.add(BleField(label: '机器人长度', value: v.toStringAsFixed(2)));
}
// 17. 机器人宽度 @payload[124..127] (帧字节127-130, float LE)
if (len >= 128) {
final v = bd.getFloat32(124, Endian.little);
fields.add(BleField(label: '机器人宽度', value: v.toStringAsFixed(2)));
}
// 18. 机器人高度 @payload[128..131] (帧字节131-134, float LE)
if (len >= 132) {
final v = bd.getFloat32(128, Endian.little);
fields.add(BleField(label: '机器人高度', value: v.toStringAsFixed(2)));
}
// 19. 割刀宽度 @payload[132..135] (帧字节135-138, float LE)
if (len >= 136) {
final v = bd.getFloat32(132, Endian.little);
fields.add(BleField(label: '割刀宽度', value: v.toStringAsFixed(2)));
}
// 20. 轮胎尺寸 @payload[136..139] (帧字节139-142, float LE)
if (len >= 140) {
final v = bd.getFloat32(136, Endian.little);
fields.add(BleField(label: '轮胎尺寸', value: v.toStringAsFixed(2)));
}
// 21. 左轮前进增益 @payload[140..143] (帧字节143-146, float LE)
if (len >= 144) {
final v = bd.getFloat32(140, Endian.little);
fields.add(BleField(label: '左轮前进增益', value: v.toStringAsFixed(2)));
}
// 22. 左轮后退增益 @payload[144..147] (帧字节147-150, float LE)
if (len >= 148) {
final v = bd.getFloat32(144, Endian.little);
fields.add(BleField(label: '左轮后退增益', value: v.toStringAsFixed(2)));
}
// 23. 右轮前进增益 @payload[148..151] (帧字节151-154, float LE)
if (len >= 152) {
final v = bd.getFloat32(148, Endian.little);
fields.add(BleField(label: '右轮前进增益', value: v.toStringAsFixed(2)));
}
// 24. 右轮后退增益 @payload[152..155] (帧字节155-158, float LE)
if (len >= 156) {
final v = bd.getFloat32(152, Endian.little);
fields.add(BleField(label: '右轮后退增益', value: v.toStringAsFixed(2)));
}
// 25. 固件版本 @payload[156..170] (帧字节159-173, 15字节)
if (len >= 171) {
fields.add(
BleField(label: '固件版本', value: _readNullTermString(data, 156, 15)),
);
}
// 构建配置实体(用于后续编辑和回写)
Mc700DeviceConfig? configEntity;
if (data.length >= 171) {
try {
configEntity = Mc700DeviceConfig.fromBytes(data);
} catch (e) {
developer.log('[BLE] 配置实体解析失败: $e', name: 'BleProtocolDecoder');
}
}
return BleDecodedResult(
fields: fields,
rawHex: hex,
configEntity: configEntity,
);
}
/// 读取 null 终止字符串(最多读取 [maxLen] 字节)
static String _readNullTermString(Uint8List data, int offset, int maxLen) {
final buffer = StringBuffer();
for (int i = 0; i < maxLen && offset + i < data.length; i++) {
final b = data[offset + i];
if (b == 0x00) break;
if (b >= 0x20 && b <= 0x7E) {
buffer.writeCharCode(b);
} else {
buffer.writeCharCode(0x2E);
}
}
return buffer.toString();
}
// ─── 0x06 写配置 ───
static BleDecodedResult _decodeWriteConfig(Uint8List data, String hex) {
final fields = <BleField>[];
if (data.isNotEmpty) {
final ok = data[0];
fields.add(
BleField(
label: '写入结果',
value: ok == 0x01
? '成功'
: ok == 0x00
? '失败'
: '0x${ok.toRadixString(16)}',
),
);
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 0xFF 心跳包 ───
static BleDecodedResult _decodeHeartbeat(Uint8List data, String hex) {
final fields = <BleField>[];
if (data.isEmpty) {
fields.add(const BleField(label: '心跳', value: '正常'));
} else if (data.length >= 2) {
final interval = (data[1] << 8) | data[0];
fields.add(BleField(label: '心跳间隔', value: '${interval}ms'));
} else {
fields.add(BleField(label: '心跳数据', value: hex));
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 0x01 路径规划 ───
static BleDecodedResult _decodePathPlanning(Uint8List data, String hex) {
final fields = <BleField>[];
try {
final text = utf8.decode(data, allowMalformed: true);
if (text.contains(',')) {
final parts = text.split(',');
if (parts.length >= 2) {
fields.add(BleField(label: '点编号', value: parts[0]));
fields.add(
BleField(
label: '状态',
value: switch (parts[1]) {
'1' => '已到达',
'2' => '收到指令',
_ => parts[1],
},
),
);
}
if (parts.length >= 4) {
fields.add(BleField(label: '纬度', value: parts[2]));
fields.add(BleField(label: '经度', value: parts[3]));
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
} catch (_) {}
if (data.length >= 3) {
fields.add(BleField(label: '点编号', value: '${data[0]}'));
final status = data.length > 1 ? data[1] : -1;
fields.add(
BleField(
label: '状态',
value: status == 0x01
? '已到达'
: status == 0x02
? '收到指令'
: '0x${status.toRadixString(16)}',
),
);
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 0x07 避障 ───
static BleDecodedResult _decodeObstacleAvoid(Uint8List data, String hex) {
final fields = <BleField>[];
if (data.isNotEmpty) {
final flag = data[0];
fields.add(
BleField(
label: '避障状态',
value: flag == 0x01
? '触发'
: flag == 0x00
? '正常'
: '0x${flag.toRadixString(16)}',
),
);
}
if (data.length >= 3) {
fields.add(BleField(label: '距离', value: '${(data[2] << 8) | data[1]}mm'));
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 通用/未知指令 ───
static BleDecodedResult _decodeGeneric(
int command,
Uint8List data,
String hex,
) {
final fields = <BleField>[];
try {
final text = utf8.decode(data, allowMalformed: true);
final trimmed = text.replaceAll('\x00', '').trim();
if (trimmed.isNotEmpty) {
fields.add(
BleField(
label: '文本内容',
value: trimmed.length > 50
? '${trimmed.substring(0, 50)}...'
: trimmed,
),
);
}
} catch (_) {}
if (fields.isEmpty) {
fields.add(BleField(label: '数据长度', value: '${data.length}B'));
}
return BleDecodedResult(fields: fields, rawHex: hex);
}
// ─── 工具方法 ───
static String _bytesToHex(List<int> bytes) {
if (bytes.isEmpty) return '';
return bytes
.map((b) => b.toRadixString(16).toUpperCase().padLeft(2, '0'))
.join(' ');
}
}