feat(device): TX/RX 日志附加完整 JSON 字符串
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../models/device_state.dart';
|
import '../models/device_state.dart';
|
||||||
|
import '../services/device_log.dart';
|
||||||
import '../services/device_message.dart';
|
import '../services/device_message.dart';
|
||||||
import '../services/device_message_service.dart';
|
import '../services/device_message_service.dart';
|
||||||
import 'serial_provider.dart';
|
import 'serial_provider.dart';
|
||||||
@@ -27,18 +28,30 @@ class DeviceInfoNotifier extends StateNotifier<DeviceState> {
|
|||||||
final light = _parseLight(data['light_status']);
|
final light = _parseLight(data['light_status']);
|
||||||
final door = _parseDoor(data['door_status']);
|
final door = _parseDoor(data['door_status']);
|
||||||
final task = _parseTask(data['task_status']);
|
final task = _parseTask(data['task_status']);
|
||||||
|
final prev = state;
|
||||||
|
final changed = prev.lightStatus != light ||
|
||||||
|
prev.doorStatus != door ||
|
||||||
|
prev.taskStatus != task;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
lightStatus: light,
|
lightStatus: light,
|
||||||
doorStatus: door,
|
doorStatus: door,
|
||||||
taskStatus: task,
|
taskStatus: task,
|
||||||
lastInfoAt: DateTime.now(),
|
lastInfoAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
if (changed) {
|
||||||
|
DeviceLog.info(
|
||||||
|
'device_info updated: light=${light.name} door=${door.name} task=${task.name}',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 主动查询设备信息(发送 need_ack=true 的 create_task 之外的 device_info 请求,
|
/// 主动查询设备信息(发送 need_ack=true 的 create_task 之外的 device_info 请求,
|
||||||
/// 具体取决于下位机协议约定;目前仅作占位)。
|
/// 具体取决于下位机协议约定;目前仅作占位)。
|
||||||
Future<void> queryDeviceInfo() async {
|
Future<void> queryDeviceInfo() async {
|
||||||
if (!_msgService.canSend) return;
|
if (!_msgService.canSend) {
|
||||||
|
DeviceLog.warn('queryDeviceInfo skipped: serial disconnected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final id = _msgService.nextId();
|
final id = _msgService.nextId();
|
||||||
await _msgService.send(DeviceMessage.request(
|
await _msgService.send(DeviceMessage.request(
|
||||||
messageId: id,
|
messageId: id,
|
||||||
@@ -53,7 +66,10 @@ class DeviceInfoNotifier extends StateNotifier<DeviceState> {
|
|||||||
/// 主动切换后,下位机通常会通过 device_info 上报新的 light_status
|
/// 主动切换后,下位机通常会通过 device_info 上报新的 light_status
|
||||||
/// 覆盖本地状态。
|
/// 覆盖本地状态。
|
||||||
Future<bool> toggleLight() async {
|
Future<bool> toggleLight() async {
|
||||||
if (!_msgService.canSend) return false;
|
if (!_msgService.canSend) {
|
||||||
|
DeviceLog.warn('toggleLight skipped: serial disconnected');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final next = state.lightStatus == DeviceLightStatus.on ? 'off' : 'on';
|
final next = state.lightStatus == DeviceLightStatus.on ? 'off' : 'on';
|
||||||
final id = _msgService.nextId();
|
final id = _msgService.nextId();
|
||||||
final ok = await _msgService.send(DeviceMessage.request(
|
final ok = await _msgService.send(DeviceMessage.request(
|
||||||
@@ -67,13 +83,17 @@ class DeviceInfoNotifier extends StateNotifier<DeviceState> {
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
lightStatus: next == 'on' ? DeviceLightStatus.on : DeviceLightStatus.off,
|
lightStatus: next == 'on' ? DeviceLightStatus.on : DeviceLightStatus.off,
|
||||||
);
|
);
|
||||||
|
DeviceLog.info('toggleLight optimistic: -> $next');
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显式设定灯光
|
/// 显式设定灯光
|
||||||
Future<bool> setLight(bool on) async {
|
Future<bool> setLight(bool on) async {
|
||||||
if (!_msgService.canSend) return false;
|
if (!_msgService.canSend) {
|
||||||
|
DeviceLog.warn('setLight skipped: serial disconnected');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (on && state.lightStatus == DeviceLightStatus.on) return true;
|
if (on && state.lightStatus == DeviceLightStatus.on) return true;
|
||||||
if (!on && state.lightStatus == DeviceLightStatus.off) return true;
|
if (!on && state.lightStatus == DeviceLightStatus.off) return true;
|
||||||
final id = _msgService.nextId();
|
final id = _msgService.nextId();
|
||||||
@@ -87,6 +107,7 @@ class DeviceInfoNotifier extends StateNotifier<DeviceState> {
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
lightStatus: on ? DeviceLightStatus.on : DeviceLightStatus.off,
|
lightStatus: on ? DeviceLightStatus.on : DeviceLightStatus.off,
|
||||||
);
|
);
|
||||||
|
DeviceLog.info('setLight optimistic: -> ${on ? "on" : "off"}');
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|||||||
49
lib/features/device/services/device_log.dart
Normal file
49
lib/features/device/services/device_log.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
|
/// 串口/协议层统一日志入口
|
||||||
|
///
|
||||||
|
/// 使用 `dart:developer.log` 便于 Dart DevTools 抓取与分级。
|
||||||
|
/// 所有日志统一以 `kuaishai.device` 为 name 前缀,方便在 DevTools 中过滤。
|
||||||
|
class DeviceLog {
|
||||||
|
static const String _name = 'kuaishai.device';
|
||||||
|
|
||||||
|
/// 信息级别:正常收发与状态变化
|
||||||
|
static void info(String message, {Object? error, Map<String, Object?>? context}) {
|
||||||
|
developer.log(message, name: _name, level: 800, error: error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 警告级别:可恢复的异常(解析错误、写入失败等)
|
||||||
|
static void warn(String message, {Object? error, Map<String, Object?>? context}) {
|
||||||
|
developer.log(
|
||||||
|
message,
|
||||||
|
name: _name,
|
||||||
|
level: 900,
|
||||||
|
error: error,
|
||||||
|
// dart:developer 当前不支持直接附带 context;调用方可在 message 中拼接
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 严重级别:连接断开、协议级不可恢复错误
|
||||||
|
static void severe(String message, {Object? error, Map<String, Object?>? context}) {
|
||||||
|
developer.log(message, name: _name, level: 1000, error: error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将 Map 数据截断为可读摘要,避免在日志中泄露超长负载
|
||||||
|
static String summarizeData(Map<String, dynamic>? data, {int maxKeys = 8}) {
|
||||||
|
if (data == null || data.isEmpty) return '{}';
|
||||||
|
final keys = data.keys.take(maxKeys).toList();
|
||||||
|
final shown = keys.map((k) {
|
||||||
|
final v = data[k];
|
||||||
|
if (v is String) return '$k="${_truncate(v, 32)}"';
|
||||||
|
if (v is List) return '$k=[${v.length}]';
|
||||||
|
return '$k=$v';
|
||||||
|
}).join(', ');
|
||||||
|
final suffix = data.length > maxKeys ? ', ...' : '';
|
||||||
|
return '{$shown$suffix}';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _truncate(String s, int max) {
|
||||||
|
if (s.length <= max) return s;
|
||||||
|
return '${s.substring(0, max)}…';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'serial_port_service.dart';
|
import 'serial_port_service.dart';
|
||||||
|
import 'device_log.dart';
|
||||||
import 'device_message.dart';
|
import 'device_message.dart';
|
||||||
import 'json_protocol.dart';
|
import 'json_protocol.dart';
|
||||||
|
|
||||||
@@ -77,10 +78,18 @@ class DeviceMessageService {
|
|||||||
_emitError('写入失败:${message.type.wireName}');
|
_emitError('写入失败:${message.type.wireName}');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
DeviceLog.info(
|
||||||
|
'TX ${message.type.wireName} '
|
||||||
|
'id=${message.messageId} '
|
||||||
|
'ack=${message.ack ?? "-"} '
|
||||||
|
'needAck=${message.needAck} '
|
||||||
|
'data=${DeviceLog.summarizeData(message.data)}\n'
|
||||||
|
' json=${message.encode()}',
|
||||||
|
);
|
||||||
_messageCtrl.add(message);
|
_messageCtrl.add(message);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_emitError('编码异常: $e');
|
_emitError('编码异常: $e', error: e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +113,7 @@ class DeviceMessageService {
|
|||||||
_stateSub?.cancel();
|
_stateSub?.cancel();
|
||||||
|
|
||||||
_rxSub = _serial.onData.listen(_onData);
|
_rxSub = _serial.onData.listen(_onData);
|
||||||
_errorSub = _serial.onError.listen(_emitError);
|
_errorSub = _serial.onError.listen((msg) => _emitError(msg));
|
||||||
_stateSub = _serial.connectionStateChanges.listen((s) {
|
_stateSub = _serial.connectionStateChanges.listen((s) {
|
||||||
if (s == SerialConnectionState.disconnected) {
|
if (s == SerialConnectionState.disconnected) {
|
||||||
_protocol.reset();
|
_protocol.reset();
|
||||||
@@ -129,6 +138,13 @@ class DeviceMessageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _dispatch(DeviceMessage msg) {
|
void _dispatch(DeviceMessage msg) {
|
||||||
|
DeviceLog.info(
|
||||||
|
'RX ${msg.type.wireName} '
|
||||||
|
'id=${msg.messageId} '
|
||||||
|
'ack=${msg.ack ?? "-"} '
|
||||||
|
'data=${DeviceLog.summarizeData(msg.data)}\n'
|
||||||
|
' json=${msg.encode()}',
|
||||||
|
);
|
||||||
_messageCtrl.add(msg);
|
_messageCtrl.add(msg);
|
||||||
final list = _handlers[msg.type];
|
final list = _handlers[msg.type];
|
||||||
if (list == null) return;
|
if (list == null) return;
|
||||||
@@ -136,12 +152,18 @@ class DeviceMessageService {
|
|||||||
try {
|
try {
|
||||||
handler(msg);
|
handler(msg);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_emitError('消息处理异常 (${msg.type.wireName}): $e');
|
_emitError('消息处理异常 (${msg.type.wireName}): $e', error: e);
|
||||||
|
DeviceLog.severe('消息处理异常 (${msg.type.wireName})', error: e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _emitError(String message) {
|
void _emitError(String message, {Object? error}) {
|
||||||
|
if (error != null) {
|
||||||
|
DeviceLog.warn(message, error: error);
|
||||||
|
} else {
|
||||||
|
DeviceLog.warn(message);
|
||||||
|
}
|
||||||
if (!_errorCtrl.isClosed) _errorCtrl.add(message);
|
if (!_errorCtrl.isClosed) _errorCtrl.add(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
225
lib/features/device/services/serial_port_service.dart
Normal file
225
lib/features/device/services/serial_port_service.dart
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:usb_serial/usb_serial.dart';
|
||||||
|
|
||||||
|
import '../models/serial_config.dart';
|
||||||
|
import 'device_log.dart';
|
||||||
|
|
||||||
|
/// 串口连接状态
|
||||||
|
enum SerialConnectionState { disconnected, connecting, connected, error }
|
||||||
|
|
||||||
|
/// 串口服务
|
||||||
|
///
|
||||||
|
/// 封装 [UsbSerial] 库,对外暴露稳定的连接/读写接口,
|
||||||
|
/// 屏蔽底层 USB 设备的插拔、权限请求、参数配置等细节。
|
||||||
|
class SerialPortService {
|
||||||
|
UsbPort? _port;
|
||||||
|
UsbDevice? _device;
|
||||||
|
StreamSubscription<Uint8List>? _readSub;
|
||||||
|
final _connectionCtrl = StreamController<SerialConnectionState>.broadcast();
|
||||||
|
final _dataCtrl = StreamController<Uint8List>.broadcast();
|
||||||
|
final _errorCtrl = StreamController<String>.broadcast();
|
||||||
|
SerialConnectionState _state = SerialConnectionState.disconnected;
|
||||||
|
String? _lastError;
|
||||||
|
|
||||||
|
/// 当前连接状态
|
||||||
|
SerialConnectionState get state => _state;
|
||||||
|
|
||||||
|
/// 最后一次错误信息
|
||||||
|
String? get lastError => _lastError;
|
||||||
|
|
||||||
|
/// 是否已连接
|
||||||
|
bool get isConnected => _state == SerialConnectionState.connected;
|
||||||
|
|
||||||
|
/// 当前连接设备(仅在已连接时可用)
|
||||||
|
UsbDevice? get currentDevice => _device;
|
||||||
|
|
||||||
|
/// 连接状态变更流
|
||||||
|
Stream<SerialConnectionState> get connectionStateChanges =>
|
||||||
|
_connectionCtrl.stream;
|
||||||
|
|
||||||
|
/// 接收到的数据流
|
||||||
|
Stream<Uint8List> get onData => _dataCtrl.stream;
|
||||||
|
|
||||||
|
/// 错误信息流
|
||||||
|
Stream<String> get onError => _errorCtrl.stream;
|
||||||
|
|
||||||
|
/// 列出当前连接的 USB 串口设备
|
||||||
|
Future<List<UsbDevice>> listDevices() async {
|
||||||
|
return await UsbSerial.listDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设备名(vendor/product 拼接)
|
||||||
|
static String deviceLabel(UsbDevice d) {
|
||||||
|
final v = _hex(d.vid);
|
||||||
|
final p = _hex(d.pid);
|
||||||
|
final name = d.productName ?? d.manufacturerName ?? 'USB Serial';
|
||||||
|
return '$name [$v:$p]';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _hex(int? v) =>
|
||||||
|
(v ?? 0).toRadixString(16).padLeft(4, '0').toUpperCase();
|
||||||
|
|
||||||
|
/// 打开指定设备并按 [config] 配置参数
|
||||||
|
Future<bool> connect(UsbDevice device, SerialConfig config) async {
|
||||||
|
if (_state == SerialConnectionState.connected ||
|
||||||
|
_state == SerialConnectionState.connecting) {
|
||||||
|
await disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceLog.info('连接串口设备: ${deviceLabel(device)}, '
|
||||||
|
'baud=${config.baudRate} dataBits=${config.dataBits} '
|
||||||
|
'stopBits=${config.stopBits} parity=${config.parity.name}');
|
||||||
|
_setState(SerialConnectionState.connecting);
|
||||||
|
try {
|
||||||
|
// UsbSerial.create 内部会触发 Android USB 权限弹窗
|
||||||
|
final port = await device.create();
|
||||||
|
if (port == null) {
|
||||||
|
_fail('创建设备端口失败(可能未授予 USB 权限)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final opened = await port.open();
|
||||||
|
if (!opened) {
|
||||||
|
await port.close();
|
||||||
|
_fail('打开端口失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _applyConfig(port, config);
|
||||||
|
|
||||||
|
_port = port;
|
||||||
|
_device = device;
|
||||||
|
_subscribeStream(port);
|
||||||
|
_setState(SerialConnectionState.connected);
|
||||||
|
DeviceLog.info('串口已连接: ${deviceLabel(device)}');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
_fail('连接异常: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用串口参数
|
||||||
|
Future<void> _applyConfig(UsbPort port, SerialConfig config) async {
|
||||||
|
await port.setPortParameters(
|
||||||
|
config.baudRate,
|
||||||
|
config.dataBits,
|
||||||
|
config.stopBits,
|
||||||
|
_parityToCode(config.parity),
|
||||||
|
);
|
||||||
|
await port.setFlowControl(_flowControlToCode(config.flowControl));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 断开当前连接
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
if (_state == SerialConnectionState.disconnected) return;
|
||||||
|
await _readSub?.cancel();
|
||||||
|
_readSub = null;
|
||||||
|
try {
|
||||||
|
await _port?.close();
|
||||||
|
} catch (_) {}
|
||||||
|
_port = null;
|
||||||
|
_device = null;
|
||||||
|
_setState(SerialConnectionState.disconnected);
|
||||||
|
DeviceLog.info('串口已断开');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入数据,返回成功写入的字节数(设备库本身不返回字节数,连接失败时返回 0)
|
||||||
|
Future<int> write(Uint8List data) async {
|
||||||
|
final port = _port;
|
||||||
|
if (port == null || !isConnected) {
|
||||||
|
_emitError('串口未连接,写入失败');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await port.write(data);
|
||||||
|
DeviceLog.info('串口 TX: ${data.length} 字节');
|
||||||
|
return data.length;
|
||||||
|
} catch (e) {
|
||||||
|
_emitError('写入异常: $e', error: e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源(应用退出时调用)
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await disconnect();
|
||||||
|
await _connectionCtrl.close();
|
||||||
|
await _dataCtrl.close();
|
||||||
|
await _errorCtrl.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _subscribeStream(UsbPort port) {
|
||||||
|
_readSub?.cancel();
|
||||||
|
final stream = port.inputStream;
|
||||||
|
if (stream == null) {
|
||||||
|
_emitError('当前端口无输入流');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_readSub = stream.listen(
|
||||||
|
(data) {
|
||||||
|
if (data.isNotEmpty) {
|
||||||
|
DeviceLog.info('串口 RX: ${data.length} 字节');
|
||||||
|
_dataCtrl.add(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (Object e) {
|
||||||
|
_emitError('读取异常: $e', error: e);
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
DeviceLog.info('串口输入流关闭');
|
||||||
|
_setState(SerialConnectionState.disconnected);
|
||||||
|
},
|
||||||
|
cancelOnError: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setState(SerialConnectionState s) {
|
||||||
|
_state = s;
|
||||||
|
if (s != SerialConnectionState.error) {
|
||||||
|
_lastError = null;
|
||||||
|
}
|
||||||
|
if (!_connectionCtrl.isClosed) {
|
||||||
|
_connectionCtrl.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fail(String message) {
|
||||||
|
_lastError = message;
|
||||||
|
_setState(SerialConnectionState.error);
|
||||||
|
_emitError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitError(String message, {Object? error}) {
|
||||||
|
_lastError = message;
|
||||||
|
if (error != null) {
|
||||||
|
DeviceLog.severe(message, error: error);
|
||||||
|
} else {
|
||||||
|
DeviceLog.warn(message);
|
||||||
|
}
|
||||||
|
if (!_errorCtrl.isClosed) {
|
||||||
|
_errorCtrl.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parityToCode(SerialParity p) {
|
||||||
|
return switch (p) {
|
||||||
|
SerialParity.none => UsbPort.PARITY_NONE,
|
||||||
|
SerialParity.odd => UsbPort.PARITY_ODD,
|
||||||
|
SerialParity.even => UsbPort.PARITY_EVEN,
|
||||||
|
SerialParity.mark => UsbPort.PARITY_MARK,
|
||||||
|
SerialParity.space => UsbPort.PARITY_SPACE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _flowControlToCode(SerialFlowControl f) {
|
||||||
|
return switch (f) {
|
||||||
|
SerialFlowControl.none => UsbPort.FLOW_CONTROL_OFF,
|
||||||
|
SerialFlowControl.rtsCts => UsbPort.FLOW_CONTROL_RTS_CTS,
|
||||||
|
SerialFlowControl.xonXoff => UsbPort.FLOW_CONTROL_XON_XOFF,
|
||||||
|
SerialFlowControl.dtrDsr => UsbPort.FLOW_CONTROL_DSR_DTR,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import '../../programs/models/program.dart';
|
import '../../programs/models/program.dart';
|
||||||
import '../../programs/models/step.dart';
|
import '../../programs/models/step.dart';
|
||||||
|
import 'device_log.dart';
|
||||||
import 'device_message.dart';
|
import 'device_message.dart';
|
||||||
import 'device_message_service.dart';
|
import 'device_message_service.dart';
|
||||||
import 'runner_interface.dart';
|
import 'runner_interface.dart';
|
||||||
@@ -65,6 +66,8 @@ class JsonSerialRunner implements Runner {
|
|||||||
final messageId = _msg.nextId();
|
final messageId = _msg.nextId();
|
||||||
_pendingCreateTaskId = messageId;
|
_pendingCreateTaskId = messageId;
|
||||||
final msg = payload.toMessage(messageId, needAck: true);
|
final msg = payload.toMessage(messageId, needAck: true);
|
||||||
|
DeviceLog.info('Runner.start: program="${program.name}" steps=${steps.length} '
|
||||||
|
'temperature=${program.temperature} airflow=${program.airflowTime}');
|
||||||
_msg.send(msg).then((ok) {
|
_msg.send(msg).then((ok) {
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
_pendingCreateTaskId = null;
|
_pendingCreateTaskId = null;
|
||||||
@@ -77,6 +80,7 @@ class JsonSerialRunner implements Runner {
|
|||||||
@override
|
@override
|
||||||
void pause() {
|
void pause() {
|
||||||
if (status != RunnerStatus.running) return;
|
if (status != RunnerStatus.running) return;
|
||||||
|
DeviceLog.info('Runner.pause');
|
||||||
_sendControl('pause');
|
_sendControl('pause');
|
||||||
_stopLocalTicker();
|
_stopLocalTicker();
|
||||||
status = RunnerStatus.paused;
|
status = RunnerStatus.paused;
|
||||||
@@ -85,6 +89,7 @@ class JsonSerialRunner implements Runner {
|
|||||||
@override
|
@override
|
||||||
void resume() {
|
void resume() {
|
||||||
if (status != RunnerStatus.paused) return;
|
if (status != RunnerStatus.paused) return;
|
||||||
|
DeviceLog.info('Runner.resume');
|
||||||
_sendControl('continue');
|
_sendControl('continue');
|
||||||
status = RunnerStatus.running;
|
status = RunnerStatus.running;
|
||||||
_startLocalTicker();
|
_startLocalTicker();
|
||||||
@@ -93,6 +98,7 @@ class JsonSerialRunner implements Runner {
|
|||||||
@override
|
@override
|
||||||
void stop() {
|
void stop() {
|
||||||
if (status == RunnerStatus.idle) return;
|
if (status == RunnerStatus.idle) return;
|
||||||
|
DeviceLog.info('Runner.stop');
|
||||||
_sendControl('stop');
|
_sendControl('stop');
|
||||||
_teardown();
|
_teardown();
|
||||||
status = RunnerStatus.idle;
|
status = RunnerStatus.idle;
|
||||||
@@ -125,6 +131,7 @@ class JsonSerialRunner implements Runner {
|
|||||||
void _onCreateTaskAck(DeviceMessage ack) {
|
void _onCreateTaskAck(DeviceMessage ack) {
|
||||||
if (ack.ack != _pendingCreateTaskId) return;
|
if (ack.ack != _pendingCreateTaskId) return;
|
||||||
_pendingCreateTaskId = null;
|
_pendingCreateTaskId = null;
|
||||||
|
DeviceLog.info('Runner received create_task ack: id=${ack.messageId}');
|
||||||
// ack 即视为下位机已接受任务,进入 running 状态
|
// ack 即视为下位机已接受任务,进入 running 状态
|
||||||
if (status == RunnerStatus.idle || status == RunnerStatus.error) {
|
if (status == RunnerStatus.idle || status == RunnerStatus.error) {
|
||||||
status = RunnerStatus.running;
|
status = RunnerStatus.running;
|
||||||
@@ -135,6 +142,7 @@ class JsonSerialRunner implements Runner {
|
|||||||
void _onControlAck(DeviceMessage ack) {
|
void _onControlAck(DeviceMessage ack) {
|
||||||
if (ack.ack != _pendingControlId) return;
|
if (ack.ack != _pendingControlId) return;
|
||||||
_pendingControlId = null;
|
_pendingControlId = null;
|
||||||
|
DeviceLog.info('Runner received control ack: id=${ack.messageId}');
|
||||||
// control ack 不修改状态,状态机在调用 pause/resume/stop 时已切过
|
// control ack 不修改状态,状态机在调用 pause/resume/stop 时已切过
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +157,7 @@ class JsonSerialRunner implements Runner {
|
|||||||
if (_remainingSeconds <= 0) {
|
if (_remainingSeconds <= 0) {
|
||||||
_currentStepIndex++;
|
_currentStepIndex++;
|
||||||
if (_currentStepIndex >= _steps.length) {
|
if (_currentStepIndex >= _steps.length) {
|
||||||
|
DeviceLog.info('Runner 本地倒计时完成 (timeout fallback)');
|
||||||
_stopLocalTicker();
|
_stopLocalTicker();
|
||||||
status = RunnerStatus.completed;
|
status = RunnerStatus.completed;
|
||||||
_callbacks?.onComplete?.call();
|
_callbacks?.onComplete?.call();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:kuaishai2/features/device/models/device_state.dart';
|
import 'package:kuaishai2/features/device/models/device_state.dart';
|
||||||
|
import 'package:kuaishai2/features/device/services/device_log.dart';
|
||||||
import 'package:kuaishai2/features/device/services/device_message.dart';
|
import 'package:kuaishai2/features/device/services/device_message.dart';
|
||||||
import 'package:kuaishai2/features/device/services/json_protocol.dart';
|
import 'package:kuaishai2/features/device/services/json_protocol.dart';
|
||||||
import 'package:kuaishai2/features/device/services/task_payload.dart';
|
import 'package:kuaishai2/features/device/services/task_payload.dart';
|
||||||
@@ -218,4 +219,38 @@ void main() {
|
|||||||
expect(s.infoStale, isTrue);
|
expect(s.infoStale, isTrue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('DeviceLog', () {
|
||||||
|
test('summarizeData 截断长字符串与多余键', () {
|
||||||
|
final data = {
|
||||||
|
'a': 1,
|
||||||
|
'b': 'short',
|
||||||
|
'c': 'x' * 200,
|
||||||
|
'd': [1, 2, 3],
|
||||||
|
'e': true,
|
||||||
|
'f': null,
|
||||||
|
'g': 'extra',
|
||||||
|
'h': 'extra2',
|
||||||
|
'i': 'extra3',
|
||||||
|
};
|
||||||
|
final summary = DeviceLog.summarizeData(data, maxKeys: 4);
|
||||||
|
expect(summary.contains('a=1'), isTrue);
|
||||||
|
expect(summary.contains('b="short"'), isTrue);
|
||||||
|
expect(summary.contains('…'), isTrue); // 长字符串被截断
|
||||||
|
expect(summary.contains('…'), isTrue);
|
||||||
|
expect(summary.contains(', ...'), isTrue); // 多余键被截断
|
||||||
|
});
|
||||||
|
|
||||||
|
test('summarizeData 空 Map / null 友好处理', () {
|
||||||
|
expect(DeviceLog.summarizeData(null), '{}');
|
||||||
|
expect(DeviceLog.summarizeData({}), '{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('info / warn / severe 不抛异常', () {
|
||||||
|
// 仅验证调用不抛异常;dart:developer.log 在测试环境无可见输出
|
||||||
|
DeviceLog.info('hello');
|
||||||
|
DeviceLog.warn('warn', error: StateError('x'));
|
||||||
|
DeviceLog.severe('severe', error: 'err');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user