feat(device): 实现下位机 JSON 协议(data model 对齐)

按 docs/下位机交互数据模型.md 重构串口协议层:

协议层
- 新增 DeviceMessage 模型,对应 message_id/type/ack/need_ack/data
- 新增 JsonProtocolService,4 字节大端长度前缀 + UTF-8 JSON 帧
- 删除原二进制协议(serial_protocol.dart)

服务层
- 新增 DeviceMessageService,集中收发并按 type 分发
- 重写 SerialRunner 为 JsonSerialRunner,使用 create_task/control 消息

数据模型
- DeviceState 增加 doorStatus/lightStatus/taskStatus/lastInfoAt
- 新增 DeviceInfoNotifier 订阅 device_info 上行
- 灯光按钮接通 light_control 消息

测试
- 新增 device_protocol_test.dart(14 用例)
- 修复 models_test.dart 残留的 Step mixSpeed/blowSpeed 错误
This commit is contained in:
Developer
2026-06-04 13:00:21 +08:00
parent 5d28bf631b
commit 819889684f
13 changed files with 1689 additions and 122 deletions

View File

@@ -0,0 +1,123 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/device_state.dart';
import '../services/device_message.dart';
import '../services/device_message_service.dart';
import 'serial_provider.dart';
/// 设备信息通知器
///
/// 订阅 [DeviceMessageService] 中的 `device_info` 上行消息,
/// 维护门状态 / 灯状态 / 任务状态,供 UI 直接读取。
class DeviceInfoNotifier extends StateNotifier<DeviceState> {
final DeviceMessageService _msgService;
void Function()? _cancelSub;
DeviceInfoNotifier(this._msgService) : super(const DeviceState()) {
_cancelSub = _msgService.subscribe(
DeviceMessageType.deviceInfo,
_onDeviceInfo,
);
// 串口断开时无需主动清空 lastInfoAt —— infoStale 会根据
// 当前时间与 lastInfoAt 的差值自动判断为「通讯异常」。
}
void _onDeviceInfo(DeviceMessage msg) {
final data = msg.data;
final light = _parseLight(data['light_status']);
final door = _parseDoor(data['door_status']);
final task = _parseTask(data['task_status']);
state = state.copyWith(
lightStatus: light,
doorStatus: door,
taskStatus: task,
lastInfoAt: DateTime.now(),
);
}
/// 主动查询设备信息(发送 need_ack=true 的 create_task 之外的 device_info 请求,
/// 具体取决于下位机协议约定;目前仅作占位)。
Future<void> queryDeviceInfo() async {
if (!_msgService.canSend) return;
final id = _msgService.nextId();
await _msgService.send(DeviceMessage.request(
messageId: id,
type: DeviceMessageType.deviceInfo,
data: const <String, dynamic>{},
needAck: true,
));
}
/// 发送灯光控制消息
///
/// 主动切换后,下位机通常会通过 device_info 上报新的 light_status
/// 覆盖本地状态。
Future<bool> toggleLight() async {
if (!_msgService.canSend) return false;
final next = state.lightStatus == DeviceLightStatus.on ? 'off' : 'on';
final id = _msgService.nextId();
final ok = await _msgService.send(DeviceMessage.request(
messageId: id,
type: DeviceMessageType.lightControl,
data: {'status': next},
needAck: true,
));
if (ok) {
// 乐观更新;下位机的 device_info 上报会作为最终一致来源
state = state.copyWith(
lightStatus: next == 'on' ? DeviceLightStatus.on : DeviceLightStatus.off,
);
}
return ok;
}
/// 显式设定灯光
Future<bool> setLight(bool on) async {
if (!_msgService.canSend) return false;
if (on && state.lightStatus == DeviceLightStatus.on) return true;
if (!on && state.lightStatus == DeviceLightStatus.off) return true;
final id = _msgService.nextId();
final ok = await _msgService.send(DeviceMessage.request(
messageId: id,
type: DeviceMessageType.lightControl,
data: {'status': on ? 'on' : 'off'},
needAck: true,
));
if (ok) {
state = state.copyWith(
lightStatus: on ? DeviceLightStatus.on : DeviceLightStatus.off,
);
}
return ok;
}
@override
void dispose() {
_cancelSub?.call();
_cancelSub = null;
super.dispose();
}
// -- 解析 -------------------------------------------------------------
static DeviceLightStatus _parseLight(dynamic v) =>
v == 'on' ? DeviceLightStatus.on : DeviceLightStatus.off;
static DeviceDoorStatus _parseDoor(dynamic v) =>
v == 'open' ? DeviceDoorStatus.open : DeviceDoorStatus.close;
static DeviceTaskStatus _parseTask(dynamic v) {
return switch (v) {
'running' => DeviceTaskStatus.running,
'pause' => DeviceTaskStatus.pause,
_ => DeviceTaskStatus.idle,
};
}
}
/// 设备信息 Provider
final deviceInfoProvider =
StateNotifierProvider<DeviceInfoNotifier, DeviceState>((ref) {
final service = ref.watch(deviceMessageServiceProvider);
return DeviceInfoNotifier(service);
});

View File

@@ -0,0 +1,82 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/database/database_service.dart';
import '../models/serial_config.dart';
import '../services/device_message_service.dart';
import '../services/json_protocol.dart';
import '../services/runner_interface.dart';
import '../services/serial_port_service.dart';
import '../services/serial_runner.dart';
/// 串口服务单例
final serialPortServiceProvider = Provider<SerialPortService>((ref) {
final service = SerialPortService();
ref.onDispose(service.dispose);
return service;
});
/// JSON 协议编解码器(可在调试/真机协议不一致时整体替换)
final jsonProtocolProvider = Provider<JsonProtocolService>((ref) {
return JsonProtocolService();
});
/// 设备消息分发服务
///
/// 集中处理 JSON 消息的发送与订阅;下游 Provider 各自订阅感兴趣的类型。
final deviceMessageServiceProvider =
Provider<DeviceMessageService>((ref) {
final serial = ref.watch(serialPortServiceProvider);
final protocol = ref.watch(jsonProtocolProvider);
final service = DeviceMessageService(serial: serial, protocol: protocol);
ref.onDispose(service.dispose);
return service;
});
/// 当前串口配置(设置页修改后通过 notifier 写入并持久化)
class SerialConfigNotifier extends StateNotifier<SerialConfig> {
final SettingsConfigRepository _repo;
SerialConfigNotifier(this._repo) : super(SerialConfig.defaults) {
_load();
}
Future<void> _load() async {
state = await _repo.read();
}
/// 修改并持久化
Future<void> update(SerialConfig Function(SerialConfig) mutator) async {
final next = mutator(state);
state = next;
await _repo.write(next);
}
/// 重置为默认值
Future<void> reset() => update((_) => SerialConfig.defaults);
}
/// 串口配置仓库:把 [SerialConfig] 以 JSON 形式存到 settings 表
class SettingsConfigRepository {
static const _key = 'serial_config';
final DatabaseService _db = DatabaseService.instance;
Future<SerialConfig> read() async {
final raw = await _db.readSetting(_key);
return SerialConfig.fromJsonString(raw);
}
Future<void> write(SerialConfig config) async {
await _db.writeSetting(_key, config.toJsonString());
}
}
final serialConfigProvider =
StateNotifierProvider<SerialConfigNotifier, SerialConfig>((ref) {
return SerialConfigNotifier(SettingsConfigRepository());
});
/// 运行器实例(基于 JSON 协议与设备消息服务)
final runnerProvider = Provider<Runner>((ref) {
final msgService = ref.watch(deviceMessageServiceProvider);
return JsonSerialRunner(messageService: msgService);
});