应用架构
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 | 类型 | 职责 |
|---|---|---|
SecureStorageService | Provider | 安全存储 Token |
ThemeModeController | ChangeNotifierProvider | 深色/浅色主题切换 |
ApiService | Provider | REST 抽象(HttpApiService 或 MockApiService) |
RealtimeService | Provider | WebSocket 抽象(WsRealtimeService 或 MockRealtimeService) |
AuthRepository | ChangeNotifierProvider | 登录/登出生命周期 |
MeasurementRepository | Provider | 遥测 + 历史 + 状态 |
AlarmRepository | Provider | 告警聚合 |
CommandRepository | Provider | 命令生命周期跟踪 |
所有可配置项通过 --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 聚合两类告警:
- 服务端持久化告警:通过 WebSocket 和 REST API 接收
- 客户端生成告警:
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 | 实时遥测卡片 + 状态横幅 |
/history | History | 历史趋势图(fl_chart)+ 时间范围选择 |
/control | Control | 16 通道继电器位图控制 |
/alarm | Alarm | 告警列表 + 严重程度筛选 + 未读标记 |
/user | User | 登录表单 / 设置面板 |
App 生命周期管理:前台恢复时重新连接 WebSocket 并刷新快照,后台暂停定时器以节省电量。