应用架构

Flutter 客户端(包名 com.varka.mill)采用 Provider + ChangeNotifier 状态管理,五层依赖注入树:

SecureStorageService          ← Token 持久化(flutter_secure_storage)

RealtimeService (WebSocket) ──┐
    │                         │
ApiService (REST / Dio)       │
    │                         │
Auth / Measurement / Alarm / Command Repositories

MultiProvider → FlowmeterApp

关键 Provider 注册

Provider类型职责
SecureStorageServiceProvider安全存储 Token
ThemeModeControllerChangeNotifierProvider深色/浅色主题切换
ApiServiceProviderREST 抽象(HttpApiServiceMockApiService
RealtimeServiceProviderWebSocket 抽象(WsRealtimeServiceMockRealtimeService
AuthRepositoryChangeNotifierProvider登录/登出生命周期
MeasurementRepositoryProvider遥测 + 历史 + 状态
AlarmRepositoryProvider告警聚合
CommandRepositoryProvider命令生命周期跟踪

所有可配置项通过 --dart-define 注入,包括 API 地址、WebSocket URL、设备 ID、离线超时等。

实时数据页

WebSocket 连接

WsRealtimeService 连接到 wss://mill-api.varka.cn/ws/live?token=<JWT>,处理三种帧:

  • hello:连接建立时服务端推送初始状态(最新数据 + 统计信息)
  • message:实时推送,按 kind 区分:tele(遥测)、alarm(告警)、ack(命令确认)
  • 心跳:30 秒 ping/pong,超时自动断开

断线重连采用指数退避策略:初始 1 秒,每次翻倍,上限 30 秒。

遥测数据展示

MeasurementRepository 管理实时遥测流,核心逻辑:

  • 离线检测:可配置超时(默认 45 秒),基于定时器,App 进入后台时暂停
  • HTTP 轮询兜底:WebSocket 看似无数据时,每 15 秒通过 REST 检查状态
  • 时间戳回退:设备 ts → 服务端 receivedAt → 本地 DateTime.now()
  • App 生命周期WidgetsBindingObserver.didChangeAppLifecycleState 监听前台/后台切换,后台暂停定时器,前台恢复并重新同步

数据模型

Measurement 模型包含:timestamp、seq、flow、total、weight、4 通道温度、relayDo、relayDi、statusBits、heartCount、validBits。使用智能 getter 做有效性判断:

bool get flowValid => flow != null && flow!.isFinite;
bool get autoMode => (statusBits & 0x08) != 0;
bool temperatureValid(int i) => temperatures[i] != null && temperatures[i]!.isFinite;

历史趋势图

数据获取

MeasurementRepository.getHistory() 通过 REST API 分页拉取历史数据:

  • GET /api/history?dev=FM002&limit=5000&before_id=<cursor>
  • 使用游标分页(before_id),单页最多 5000 条,总上限 50000 条
  • 本地缓存:合并远程数据与本地缓存,支持离线查看

温度异常值过滤

历史数据中对温度做跳变异常值过滤:如果相邻两点温差超过阈值,将异常点标记为 NaN,图表中自动断线。

图表渲染

使用 fl_chart 库绘制时间序列图,支持:

  • 多视图切换HistoryView 枚举分组——流量(flow)、累计(total)、重量(weight)、温度(4 通道)、继电器输出(relayDo)、继电器输入(relayDi)
  • 16+ 字段HistoryField 枚举覆盖所有可绘制字段
  • NaN 哨兵值:缺失数据用 NaN 表示,图表自动断线而非插值
  • 时间范围选择:用户可自定义查询的时间窗口

视口缩放与锚点保持

图表支持手势缩放和拖拽,缩放时保持当前视口锚点不跳变。

告警管理

告警来源

AlarmRepository 聚合两类告警:

  1. 服务端持久化告警:通过 WebSocket 和 REST API 接收
  2. 客户端生成告警DEVICE_OFFLINE / DEVICE_ONLINE 状态转换,存储在 SharedPreferences 中

告警模型

enum AlarmSeverity { info, warn, critical }

中文显示标题映射:OVER_FLOW(溢流)、UNDER_FLOW(欠流)、OVER_PRESSURE(超压)、OVER_TEMP(超温)、SENSOR_FAULT(传感器故障)、MCU_RESTART(MCU 重启)、GATEWAY_OFFLINE(网关离线)、DEVICE_OFFLINE(设备离线)等。

未读计数

未读告警计数跨重启持久化(SharedPreferences),本地最多保留 50 条告警记录。

继电器控制

命令生命周期

CommandRepository 跟踪命令的完整生命周期:

POST /api/commands/relay-set
  → pending(等待发送)
  → sent(已发送到服务端)
  → acked(收到设备 ACK)/ failed(超时)/ superseded(被新命令取代)

超级替换机制

新的 relay_set 命令会取代同一位图模式的旧命令(避免用户快速点击时产生大量过期命令)。

遥测确认

即使没有收到显式 ACK,如果最新遥测数据中的 relay_do 与目标位图匹配,命令也被视为已确认。ACK 超时 10 秒,超时后清理定时器。

乐观更新

发送命令后立即更新 UI(乐观更新),不等待设备确认。如果最终失败,回滚 UI 状态。

防抖

用户操作有防抖保护,避免短时间内发送过多命令。

五页签导航

使用 GoRouter + StatefulShellRoute.indexedStack 实现五页签导航,每个页签独立的 ViewModel 保持页面状态:

路径页面功能
/Dashboard实时遥测卡片 + 状态横幅
/historyHistory历史趋势图(fl_chart)+ 时间范围选择
/controlControl16 通道继电器位图控制
/alarmAlarm告警列表 + 严重程度筛选 + 未读标记
/userUser登录表单 / 设置面板

App 生命周期管理:前台恢复时重新连接 WebSocket 并刷新快照,后台暂停定时器以节省电量。