Flutter HMI 完整架构:从串口抽象到控制器状态管理

Babel36acl 方法与工具 无~ 11 次阅读 预计阅读时间: 10 分钟 发布于 1 天前 最后更新于 12 分钟前 2143 字


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 — 暗色面板显示连接状态、帧统计、错误计数

相关文章

此作者没有提供个人介绍。
最后更新于 2026-05-30