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>[];
|
|
|
|
|
|
|
2026-09-11 09:06:20 +08:00
|
|
|
|
// 🔥 关键修复:协议帧为 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 {
|
2026-09-11 09:06:20 +08:00
|
|
|
|
// 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(',');
|
2026-09-11 09:06:20 +08:00
|
|
|
|
// 诊断:仅当控制模式槽位(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
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-11 09:06:20 +08:00
|
|
|
|
return BleDecodedResult(fields: _parseStatusFields(parts), rawHex: hex);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {}
|
2026-08-07 08:49:29 +08:00
|
|
|
|
|
2026-09-11 09:06:20 +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')}',
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-11 09:06:20 +08:00
|
|
|
|
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)})',
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-11 09:06:20 +08:00
|
|
|
|
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')}',
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-11 09:06:20 +08:00
|
|
|
|
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'));
|
|
|
|
|
|
}
|
2026-09-11 09:06:20 +08:00
|
|
|
|
if (body.length > 8) {
|
2026-08-07 08:49:29 +08:00
|
|
|
|
try {
|
2026-09-11 09:06:20 +08:00
|
|
|
|
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('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-11 09:06:20 +08:00
|
|
|
|
// 协议定义 控制模式: 0无控制 1本地遥控 2蓝牙 3TCP 4其他接口;
|
|
|
|
|
|
// 先按整数解析(容忍前后空格),有映射的一律显示对应文字;
|
|
|
|
|
|
// 超出协议范围的码(如固件扩展值)显示为 未知(n),避免被误当成有效模式
|
2026-08-07 08:49:29 +08:00
|
|
|
|
String controlModeText(String v) {
|
2026-09-11 09:06:20 +08:00
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-11 09:06:20 +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 => '无效',
|
2026-09-11 09:06:20 +08:00
|
|
|
|
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) {
|
2026-09-11 09:06:20 +08:00
|
|
|
|
// 障碍物标志位为单个数字 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 => '有障碍',
|
2026-09-11 09:06:20 +08:00
|
|
|
|
_ => 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(' ');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|