Flutter HMI 上位机的完整技术架构:从串口硬件抽象到协议帧解析,再到控制器状态管理和 UI 交互。覆盖串口层、协议层、控制器层的全链路设计。
一、整体架构:双串口隔离
HMI 上位机与 MCU 通过两个物理串口通信,采用分治隔离架构:
flowchart TB
subgraph SERIAL[串口抽象层]
S1[SerialTransport 接口]
S2[DesktopSerial]
S3[AndroidUsbSerial]
end
subgraph PROTO[协议层]
P1[HmiFrame 编解码]
P2[HmiSessionFrame]
P3[Session 状态管理]
end
subgraph CTRL[控制器层]
C1[HmiController]
C2[参数管理]
C3[状态同步]
end
subgraph UI[UI 层]
U1[仪表盘]
U2[配置面板]
end
SERIAL --> PROTO --> CTRL --> UI
style SERIAL fill:transparent,stroke:#8dc7ff,color:#eaf4ff
style PROTO fill:transparent,stroke:#8dc7ff,color:#eaf4ff
style CTRL fill:transparent,stroke:#8dc7ff,color:#eaf4ff
style UI fill:transparent,stroke:#8dc7ff,color:#eaf4ff| 端口 | 物理层 | 协议 | 用途 |
|---|---|---|---|
| Port A | USART3 | 20B 固定帧 (HmiFrame) | 主控制、实时参数读写 |
| Port B | USART1 | 变长会话帧 (HmiSessionFrame) | 配置、固件升级、数据导出 |
设计原则:Port A 的短帧(20B)保证确定性延迟,适合实时控制回路。Port B 的变长帧(HDLC 风格)承载大块数据,两者物理隔离,互不阻塞。
模块划分
lib/
├── core/
│ ├── protocol/ # 协议层:帧定义、编解码、CRC
│ │ ├── hmi_frame.dart # 20B 固定帧
│ │ ├── hmi_session_frame.dart # HMIS 变长帧
│ │ ├── decoder/
│ │ │ ├── hmi_frame_decoder.dart
│ │ │ └── hmi_session_frame_decoder.dart
│ │ └── crc/
│ │ ├── crc16_modbus.dart
│ │ └── crc16_dgus.dart
│ └── serial/ # 串口抽象层
│ ├── serial_transport.dart # 抽象接口
│ ├── desktop_serial_transport.dart # flutter_libserialport
│ └── android_usb_serial_transport.dart # USB 串口
└── features/
└── hmi/ # 控制器与 UI
├── controllers/
├── providers/
└── widgets/
二、跨平台串口抽象(SerialTransport)
2.1 抽象接口
abstract class SerialTransport {
/// 枚举可用端口
static Future<List<PortInfo>> enumeratePorts() async;
/// 连接
Future<bool> connect(PortConfig config);
/// 断开
Future<void> disconnect();
/// 写入(send 方法,线程安全)
Future<void> send(List<int> data);
/// 数据流(listen 模式)
Stream<List<int>> get dataStream;
/// 连接状态流
Stream<ConnectionState> get connectionState;
}
2.2 桌面端实现(flutter_libserialport)
class DesktopSerialTransport implements SerialTransport {
final _transport = SerialPortTransport(); // 包装 libserialport
bool _disposed = false;
Future<bool> connect(PortConfig config) async {
await _transport.connect(
portName: config.portName,
baudRate: config.baudRate,
dataBits: 8,
stopBits: 1,
parity: Parity.none,
);
// 监听原生串口事件(errno 本地化)
_transport.stream.listen(_onData, onError: _onError);
return true;
}
Future<void> send(List<int> data) async {
if (_disposed) return;
await _transport.write(data);
}
}
2.3 Android 端实现(MethodChannel + EventChannel)
class AndroidUsbSerialTransport implements SerialTransport {
static const _channel = MethodChannel('com.hmi/serial');
static const _eventChannel = EventChannel('com.hmi/serial_events');
final int transportId; // 多实例隔离
/// writeAsync 防止 TX/RX 冲突
Future<void> send(List<int> data) async {
await _channel.invokeMethod('send', {
'transportId': transportId,
'data': data,
});
}
/// _markDisconnected 防重入守卫
void _markDisconnected() {
if (_disconnected) return;
_disconnected = true;
_channel.invokeMethod('disconnect', {'transportId': transportId});
}
}
三、协议层
3.1 Port A — 20B 固定帧(HmiFrame)
class HmiFrame {
final int address; // 设备地址 (1B)
final int functionCode; // 功能码 (1B)
final List<int> data; // 数据域 (16B)
final int crcHigh; // CRC16 高字节
final int crcLow; // CRC16 低字节
// 帧总长 20B
static const int frameLength = 20;
static const int dataLength = 16;
List<int> toBytes() {
return [address, functionCode, ...data, crcHigh, crcLow];
}
}
3.2 Port B — 变长帧(HmiSessionFrame)
class HmiSessionFrame {
static const int sof = 0x7E; // 帧起始
static const int maxPayload = 255; // 最大负载
final int frameType; // 帧类型
final int sequenceNo; // 序列号
final List<int> payload; // 变长负载
final int crc; // CRC
List<int> toBytes() {
return [sof, frameType, sequenceNo, payload.length, ...payload, crc >> 8, crc & 0xFF];
}
}
3.3 CRC 算法
Port A 和 Port B 使用不同的 CRC 多项式:
| Port A (Modbus) | Port B (DGUS) | |
|---|---|---|
| 多项式 | 0x8005 | 0x8005(相同) |
| 初值 | 0xFFFF | 0x0000 |
| 结果异或 | 0x0000 | 0xFFFF |
3.4 帧解码状态机(HmiSessionFrameDecoder)
class HmiSessionFrameDecoder {
enum State { waitingSOF, readingHeader, readingPayload, checkingCRC }
State _state = State.waitingSOF;
final List<int> _buffer = [];
int _expectedPayloadLength = 0;
/// 输入一个字节,如果有完整帧则返回
DecodeResult? feed(int byte) {
switch (_state) {
case State.waitingSOF:
if (byte == 0x7E) {
_state = State.readingHeader;
_buffer.clear();
_buffer.add(byte);
}
break;
case State.readingHeader:
_buffer.add(byte);
if (_buffer.length == 4) { // SOF + type + seq + len
_expectedPayloadLength = _buffer[3];
_state = _expectedPayloadLength > 0
? State.readingPayload
: State.checkingCRC;
}
break;
case State.readingPayload:
_buffer.add(byte);
if (_buffer.length - 4 == _expectedPayloadLength) {
_state = State.checkingCRC;
}
break;
case State.checkingCRC:
_buffer.add(byte);
if (_buffer.length == _expectedPayloadLength + 6) {
final frame = _parseFrame();
_state = State.waitingSOF;
return frame;
}
break;
}
return null;
}
}
四、控制器架构与状态管理
4.1 双端口控制器
class HmiController {
final SerialTransport _portA; // USART3 — 20B 固定帧
final SerialTransport _portB; // USART1 — HMIS 会话
// _txChain — 串行化写事务,防止帧交错
final _txQueue = StreamController<List<int>>();
bool _txBusy = false;
Future<void> sendOnPortA(HmiFrame frame) async {
await _txQueue.add(frame.toBytes());
_processQueue();
}
}
4.2 会话状态机(session state machine)
enum SessionState {
disconnected,
connecting,
subscribed, // 正常订阅状态
reconnecting,
error,
}
class SessionManager {
SessionState _state = SessionState.disconnected;
int _sessionEpoch = 0; // 版本门控
Future<void> subscribe() async {
_state = SessionState.connecting;
final epoch = ++_sessionEpoch;
// 发送订阅请求
await _sendSubscribeFrame();
// 版本门控:如果 subscribe 完成时 _sessionEpoch 已变,丢弃结果
if (_sessionEpoch != epoch) return;
_state = SessionState.subscribed;
}
void onDisconnected() {
_sessionEpoch++; // 递增 epoch,失效所有未完成回调
_state = SessionState.reconnecting;
_scheduleReconnect();
}
}
4.3 _sessionEpoch 版本门控
核心设计:每次断连/重连时递增 _sessionEpoch。所有异步回调(超时、数据到达)在触发前检查 _sessionEpoch 是否匹配,不匹配则丢弃。这防止了断连后残留的异步回调污染新会话。
五、监控与调试
- 环形缓冲区日志 — 保留最后 128 条串口事件,供故障排查
- Stack Watermark — 监控 Flutter isolate 栈使用
- Dashboard — 暗色面板显示连接状态、帧统计、错误计数
Comments NOTHING