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

745 lines
26 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
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]'), '');
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',
);
}
return BleDecodedResult(fields: _parseStatusFields(parts), rawHex: hex);
}
} catch (_) {}
if (body.isNotEmpty) {
final status = body[0];
fields.add(
BleField(
label: '状态码',
value: '0x${status.toRadixString(16).toUpperCase().padLeft(2, '0')}',
),
);
}
if (body.length >= 2) {
final mode = body[1];
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];
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];
fields.add(BleField(label: '速度', value: '$speed'));
}
if (body.length > 8) {
try {
final text = String.fromCharCodes(body.sublist(8));
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),避免被误当成有效模式
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)',
};
}
// 协议定义 定位质量: 0无效 1GPS单点 2DGPS差分/SBAS 4RTK固定解 5RTK浮点解 7手动输入
String qualText(String v) {
final q = int.tryParse(v) ?? -1;
return switch (q) {
0 => '无效',
1 => 'GPS 单点定位',
2 => 'DGPS 差分/SBAS',
4 => 'RTK 固定解',
5 => 'RTK 浮点解',
7 => '手动输入模式',
_ => 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)!)) {
0 => '无障碍',
1 => '有障碍',
_ => v,
};
}
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(' ');
}
}