前后端数据契约
PipeMonitor 的 Flutter 应用与 STM32 固件之间通过云端 API 交换数据。数据契约的核心是 Measurement 模型,它与固件 uplink_encoder 生成的 JSON 帧字段一一对应:
{"t":"tele","ts":1712345678,"seq":123,"flow":12.3,"total":1234.5,
"v":0.85,"pres":0.52,"temp":[23.1,23.2,22.9,23.0,22.8,23.1,23.4],
"heart_count":10,"valid":63}
Measurement 模型
Measurement 类封装了所有遥测字段,同时提供了几个关键设计:
有效值判断:不信任位图
/// 判断指定通道的温度是否可用:要求设备上报了一个有限数值。
/// 这里**不**信任 `valid` 位图——当前 STM32 固件即使接了 T3–T6
/// 传感器也不会在位图里置位,光看位图会漏显有效数据。
bool temperatureValid(int i) {
if (i < 0 || i >= temperatures.length) return false;
final v = temperatures[i];
return v != null && v.isFinite;
}
固件的 valid 位图有时不可靠,所以 Flutter 端直接用 isFinite 判断,避免位图标错导致有效数据被误隐藏。
缺值补齐
传感器偶尔会丢帧——某次轮询中流量计响应了但压力变送器超时了。如果直接显示这一帧,压力栏会闪成 -- 再恢复,用户体验很差。
/// 用上一帧有效测量值补齐本帧缺失字段,仍保留本帧的 seq / 心跳 / 时间。
Measurement retainMissingValuesFrom(Measurement previous) {
return Measurement(
timestamp: timestamp,
seq: seq,
flow: _finiteOrFallback(flow, previous.flow),
total: _finiteOrFallback(total, previous.total),
velocity: _finiteOrFallback(velocity, previous.velocity),
pressure: _finiteOrFallback(pressure, previous.pressure),
temperatures: List.generate(maxTempLength, (i) {
final current = i < temperatures.length ? temperatures[i] : null;
final fallback = i < previous.temperatures.length ? previous.temperatures[i] : null;
return _finiteOrFallback(current, fallback);
}),
heartCount: heartCount,
validBits: validBits,
);
}
只有 heartCount(心跳)的帧说明 MCU 采集线程还活着,但传感器本轮无效——UI 用上一帧的有效值补齐,避免整屏刷成 --。
服务端时间戳单调守卫
云端返回的数据带有 serverReceivedAt(服务端 wall-clock),Flutter 端用它做单调守卫:
factory Measurement.fromServerEnvelope(Map<String, dynamic> envelope) {
final payload = envelope['payload'];
return Measurement.fromJson(
payload,
serverReceivedAt: parseServerReceivedAt(envelope['receivedAt']),
);
}
WS 重连时服务端 latest 缓存可能被回放,但 serverReceivedAt 比已展示的实时帧旧——仓库层直接丢弃过期帧,避免 UI 显示”倒退”。
API 服务抽象
abstract interface class ApiService {
Future<String> login({required String username, required String password});
void setAuthToken(String? token);
Future<Measurement> getLatest();
Future<List<HistoryPoint>> getHistory({...});
Future<List<Alarm>> getAlarms({...});
Future<DeviceStatus> getStatus();
Future<Command> sendReboot();
Future<Command> sendUploadPeriod({required int seconds});
}
使用 Dart 的 abstract interface class 定义契约,仓库层只依赖这个抽象接口。这样可以在开发阶段无缝替换为 Mock 实现,测试时也不需要真实后端。
CSV 导出
Flutter 端支持将历史数据导出为 CSV 文件,方便用户在 PC 上做离线分析。导出逻辑封装在 CsvExporter 中,支持 Web 端下载和移动端文件保存。
跨平台文件保存通过条件导入实现:
import 'file_saver_mobile.dart'
if (dart.library.html) 'file_saver_web.dart';
Web 端使用 HTML Anchor 下载,移动端使用平台通道写入本地文件。同一个接口,两个实现,编译时自动选择。
时间戳兼容层
STM32 固件上报的 ts 可能是秒或毫秒,取决于固件版本。Flutter 端做兼容处理:
DateTime _parseDeviceTimestamp(Object? raw) {
final value = (raw as num?)?.toInt() ?? 0;
// 小于 1e12 视为 Unix 秒,否则按毫秒处理
final millis = value.abs() < 1000000000000 ? value * 1000 : value;
return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true).toLocal();
}
小结
Measurement模型与固件 JSON 帧字段一一对应,数据契约清晰- 不信任位图,用
isFinite兜底有效值判断 - 缺值补齐避免传感器偶尔丢帧时 UI 闪烁
serverReceivedAt单调守卫防止 WS 重连回放过期数据- API 服务抽象层让 Mock 和真实实现无缝切换
- CSV 导出通过条件导入实现 Web/移动端跨平台