Files
feature-next-arch/lib/core/bluetooth/ble_protocol_decoder.dart
2026-08-07 08:49:29 +08:00

723 lines
24 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>[];
try {
final text = utf8.decode(data);
if (text.contains(',')) {
final parts = text.split(',');
return BleDecodedResult(fields: _parseStatusFields(parts), rawHex: hex);
}
} catch (e) {
try {
final text = latin1.decode(data);
if (text.contains(',')) {
final parts = text.split(',');
return BleDecodedResult(
fields: _parseStatusFields(parts),
rawHex: hex,
);
}
} catch (_) {}
}
if (data.isNotEmpty) {
final status = data[0];
fields.add(
BleField(
label: '状态码',
value: '0x${status.toRadixString(16).toUpperCase().padLeft(2, '0')}',
),
);
}
if (data.length >= 2) {
final mode = data[1];
const modes = {0x00: '待机', 0x01: '遥控', 0x02: '自动', 0x03: '急停'};
fields.add(
BleField(
label: '控制模式',
value: modes[mode] ?? '未知(0x${mode.toRadixString(16)})',
),
);
}
if (data.length >= 3)
fields.add(BleField(label: '电量', value: '${data[2]}%'));
if (data.length >= 4) {
final fault = data[3];
fields.add(
BleField(
label: '故障状态',
value: fault == 0
? '无故障'
: '故障码: 0x${fault.toRadixString(16).toUpperCase().padLeft(2, '0')}',
),
);
}
if (data.length >= 6) {
final speed = (data[5] << 8) | data[4];
fields.add(BleField(label: '速度', value: '$speed'));
}
if (data.length > 8) {
try {
final text = String.fromCharCodes(data.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('');
}
String controlModeText(String v) {
return switch (v) {
'0' => '待机',
'1' => '遥控',
'2' => '自动',
'3' => '急停',
_ => v.isNotEmpty ? v : '--',
};
}
String qualText(String v) {
final q = int.tryParse(v) ?? -1;
return switch (q) {
0 => '无效',
1 => '单点定位',
2 => '差分定位',
4 => '固定解',
5 => '浮点解',
_ => 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) {
final o = int.tryParse(v) ?? -1;
return switch (o) {
0 => '无障碍',
1 => '有障碍',
_ => v.isNotEmpty ? 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(' ');
}
}