From 83a5dc7f32eecbc0c22b5b34a6a88702011433a4 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Thu, 5 Mar 2026 21:07:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BF=AE=E5=A4=8D=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E7=BC=93=E6=85=A2=E9=97=AE=E9=A2=98-=E4=B8=BB?= =?UTF-8?q?=E8=A6=81=E6=8E=A5=E5=8F=A3=E5=93=8D=E5=BA=94=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=86=B3=E5=AE=9A=20=E5=AE=8C=E6=88=90=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=9C=BA=E5=99=A8=E7=8A=B6=E6=80=81=E7=9A=84?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E6=8E=A5=E5=8F=97=E5=92=8CUI=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E5=88=B7=E6=96=B0=20=E5=AE=8C=E6=88=90=E4=BC=98?= =?UTF-8?q?=E5=8C=96tcp=E9=87=8D=E8=BF=9E=E7=9A=84=E4=BD=93=E6=A3=80?= =?UTF-8?q?=EF=BC=8C=E6=89=8B=E5=8A=A8=E5=88=87=E6=8D=A2=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=88=96=E8=87=AA=E5=8A=A8=E6=96=AD=E5=BC=80=E5=90=8E=E9=87=8D?= =?UTF-8?q?=E8=BF=9E=E7=9A=84=E5=8C=BA=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/di/injection.dart | 3 +- lib/core/network/tcp/tcp_client.dart | 223 +++++++++++++++++- .../auth/presentation/bloc/auth_cubit.dart | 2 +- .../impl/device_http_datasource_impl.dart | 1 + .../presentation/bloc/device_status_bloc.dart | 111 ++++++--- .../bloc/device_status_event.dart | 5 + .../presentation/bloc/devices_cubit.dart | 49 +++- .../pages/running_status_page.dart | 14 ++ lib/main.dart | 6 +- 9 files changed, 357 insertions(+), 57 deletions(-) diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index a4ea8c02..0152113f 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -164,9 +164,10 @@ Future init() async { /// 5. 状态管理 (Cubit/Bloc) sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册 sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl())); - sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl())); + sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl())); sl.registerFactory(() => RemoteControlCubit(sl())); /* // 工厂模式(留存,每次获取新实例) + sl.registerFactory(() => DeviceStatusBloc(sl())); */ //单例模式(全局共享一个实例) diff --git a/lib/core/network/tcp/tcp_client.dart b/lib/core/network/tcp/tcp_client.dart index 2b6b5f1a..39c0cf4a 100644 --- a/lib/core/network/tcp/tcp_client.dart +++ b/lib/core/network/tcp/tcp_client.dart @@ -33,11 +33,28 @@ class TcpClient { String? _lastHost; int? _lastPort; + bool isUserSwitch = false; // 用户是否切换设备 + bool _isSwitching = false; final UserStorage _userStorage; bool get isConnected => _socket != null; + void disconnects({bool forSwitch = false}) { + if (forSwitch) { + _isSwitching = true; + debugPrint('🚫 [TCP] 标记为切换断开,将禁止自动重连'); + } + // 🔥 关键:立即取消可能已经存在或即将触发的重连定时器 + _reconnectTimer?.cancel(); + _heartbeatTimer?.cancel(); + + if (_socket != null) { + debugPrint('🔌 [TCP] 物理断开 Socket...'); + _socket!.destroy(); // 或者 .close() + _socket = null; + } + } // 通过参数配置,不硬编码 Future connect({required String host, required int port}) async { debugPrint('🔌 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条 @@ -89,19 +106,29 @@ class TcpClient { }, onDone: (){ // TODO: 断线重连 - debugPrint('onDone❌ [TCP] 连接已断开!'); + debugPrint('来到断线重连!'); + if (!_isSwitching) { + debugPrint('onDone❌ [TCP] 连接已断开!'); _handleDisconnect(); + } + _isSwitching = false; // 重置标志,以免影响下次 + }, onError: (e) { - debugPrint('❌ [TCP] 发生错误:$e'); - _handleDisconnect(); // 统一走重连逻辑,保护 Controller 不被关闭 + debugPrint('来到断线重连!error'); + if (!_isSwitching) { + debugPrint('❌ [TCP] 发生错误:$e'); + _handleDisconnect(); + } // 统一走重连逻辑,保护 Controller 不被关闭 + _isSwitching = false; // 重置标志,以免影响下次 + }, ); } catch (e) { rethrow; // 向上抛出连接异常 } } - +// void _handleDisconnect() { stopHeartbeat(); @@ -113,6 +140,7 @@ class TcpClient { // ✅ 新增:调度重连 Future _scheduleReconnect() async { + if (isUserSwitch) return; if (_reconnectTimer != null) return; debugPrint('⏳ 调度重连:Host=${_lastHost}, Port=${_lastPort}'); // ✅ 检查 Host/Port 是否为空 @@ -128,6 +156,25 @@ class TcpClient { }); await _sendAuthPacket(); } + + + // ✅ 新增:调度重连 + Future _scheduleReconnectBySwitch(devname) async { + + + debugPrint('⏳ 被动调度重连:Host=${_lastHost}, Port=${_lastPort}'); // ✅ 检查 Host/Port 是否为空 + if (_lastHost == null || _lastPort == null) { + debugPrint('❌ 无法重连:Host 或 Port 为空!'); + return; + } + + _reconnectTimer = Timer(const Duration(seconds: 5), () { + _reconnectTimer = null; + debugPrint('⏰ 被动-定时器触发,开始执行重连...'); + connectBySwitch(host: _lastHost!, port: _lastPort!, deviceName: devname); + }); + await _sendAuthPacketBySwitch(devname); + } /// 发送数据 void send(Map data) { if (_socket == null) throw Exception("Socket not connected"); @@ -182,16 +229,42 @@ class TcpClient { } + // void disconnect() { + // stopHeartbeat(); // 先停心跳 + // _socket?.destroy(); + // _socket = null; + // // 2. 🔥 关键:取消重连定时器!防止断开后自动触发旧逻辑重连回第一个设备 + // _reconnectTimer?.cancel(); + // _reconnectTimer = null; + // // _controller.close(); + // debugPrint("TCP Disconnected"); + // + // } void disconnect() { - stopHeartbeat(); // 先停心跳 - _socket?.destroy(); - _socket = null; - _controller.close(); - debugPrint("TCP Disconnected"); + debugPrint('🛑 [TCP] 主动断开连接...'); + // 1. 停止心跳 + stopHeartbeat(); + + // 2. 🔥 关键:取消重连定时器!防止断开后自动触发旧逻辑重连回第一个设备 + _reconnectTimer?.cancel(); + _reconnectTimer = null; + + // 3. 销毁 Socket + if (_socket != null) { + _socket!.destroy(); + _socket = null; + debugPrint('✅ [TCP] Socket 已物理销毁'); + } + + // ❌ 绝对不要关闭 _controller!否则数据流断裂,重连后收不到数据 + // _controller.close(); + + debugPrint('✅ [TCP] 连接已断开,等待手动重连'); } - void sendPathPoint(RoutePlanSendEntity routePlanSendEntity) { + + void sendPathPoint(RoutePlanSendEntity routePlanSendEntity) { if (_socket == null) return; final payload = routePlanSendEntity.toBytes(); final packet = sendRaw(0x01, payload); // 第1种的测试 声明指令结构和类型0x01 为命令类型 @@ -270,7 +343,7 @@ class TcpClient { final DeviceEntity targetDevice = devices.first; debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}'); - // 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理) + // 切换设备 final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName); await switchResult.fold( @@ -295,4 +368,132 @@ class TcpClient { if (_socket == null) return; _socket!.add(bytes); } + + Future connectBySwitch({required String host, required int port, required String deviceName}) async { + isUserSwitch=true; + debugPrint('🔌被动 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条 + _lastHost = host; + _lastPort = port; + + if (_socket != null) { + _socket!.destroy(); + _socket = null; + } + + try { + _socket = await Socket.connect( + host, + port, + timeout: const Duration(seconds: 5), + ); + debugPrint('✅ 被动[TCP] 连接成功!'); // ✅ 必须看到这条 + + _socket!.listen((data) { + debugPrint('📥 被动[TCP] 收到原始数据:${data.length} 字节, 内容:$data'); + // var packets = _decoder.decode(data); + // debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}'); + //for (var packet in packets) { + // // 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包 + // if (packet.command == 0xFF) { + // debugPrint('收到服务端心跳,自动回复...'); + // sendHeartbeat(); // 回复 AB AA FF AA AB + // } + // } + // _controller.add(packet); + // } + try { + var packets = _decoder.decode(data); + debugPrint('📦 被动[TCP] 解码成功,包数量:${packets.length}'); + for (var packet in packets) { + if (!_controller.isClosed) { + _controller.add(packet); + debugPrint('➡️被动 [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}'); + } + if (packet.command == 0xFF) { + debugPrint('收到服务端心跳,自动回复...'); + sendHeartbeat(); // 回复 AB AA FF AA AB + } + if (packet.command == 0x03) { + debugPrint('⚠️ 收到认证响应:${packet.payload}'); + // 解析 payload 看是否有错误信息 + } + + } + } catch (e, stackTrace) { + debugPrint('❌ 被动[TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常 + } + }, + onDone: (){ + // TODO: 断线重连 + debugPrint('onDone❌ 被动[TCP] 连接已断开!'); + _handleDisconnectBySwitch(deviceName); + }, + onError: (e) { + debugPrint('❌ 被动[TCP] 发生错误:$e'); + _handleDisconnectBySwitch(deviceName); // 统一走重连逻辑,保护 Controller 不被关闭 + }, + ); + // 开始认证tcp + await _sendAuthPacketBySwitch(deviceName); + } catch (e) { + rethrow; // 向上抛出连接异常 + } + } + + Future _sendAuthPacketBySwitch(String deviceName) async { + if (_socket == null) return; + String? username= ""; + String? token= ""; + final user = await _userStorage.getUser(); + deviceName= deviceName; + debugPrint('tcp被动切换认证:$user'); + if (user == null || user.token == null) { + debugPrint('❌ 被动[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包'); + disconnect(); + return; // 直接返回,不要发送无效包 + } + username = user.username; + token = user.token; + final authString = '$username:app:$token'; + debugPrint('🔑被动[TCP] 认证字符串:$authString'); + final authBytes = utf8.encode(authString); + + // 构造包结构:Head(2) + Cmd(1) + Payload(N) + CRC(2) + Foot(2) + // 总长度 = 3 + N + 4 + final builder = BytesBuilder() + ..addByte(0xAB) + ..addByte(0xAA) + ..addByte(0x03) // 认证指令 + ..add(authBytes); + + // 添加 CRC (这里简单用 0x00 0x00 占位,如果协议严格校验需计算真实 CRC) + // Android 代码中是 packet[tailStart] = 0x00; packet[tailStart+1] = 0x00; + builder.addByte(0x00); + builder.addByte(0x00); + + builder.addByte(0xAA); + builder.addByte(0xAB); + + _socket!.add(builder.takeBytes()); + debugPrint('🔑 被动[TCP] 已发送认证包 (0x03): $authString'); + + try { + // 1. 获取 Either 结果 + await switchDeviceUseCase.deviceRepository.switchDevice("app",deviceName); + } catch (e) { + debugPrint('❌ 被动[AuthTcp] 设备订阅流程异常:$e'); + rethrow; + } + + } + + void _handleDisconnectBySwitch( String deviceName) { + stopHeartbeat(); + + _socket = null; + // 注意:这里不要关闭 _controller!否则监听者会丢失数据流 + // _controller?.close(); + _scheduleReconnectBySwitch(deviceName); + + } } diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 52a2b9f3..86ce5d54 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -52,7 +52,7 @@ class AuthCubit extends Cubit { Future loginSuccess(UserEntity user) async { await storage.saveUser(user); await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); - await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数 + // await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数 tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳 appCubit.setAuth(user); emit(AuthAuthenticated(user)); diff --git a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart index f8831468..47021418 100644 --- a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart'; +import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart'; import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart'; diff --git a/lib/features/devices/presentation/bloc/device_status_bloc.dart b/lib/features/devices/presentation/bloc/device_status_bloc.dart index 2ad34136..7f04cfa8 100644 --- a/lib/features/devices/presentation/bloc/device_status_bloc.dart +++ b/lib/features/devices/presentation/bloc/device_status_bloc.dart @@ -1,10 +1,10 @@ - +import 'dart:async'; import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/running_status_entity.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/gps_entity.dart'; -import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart'; // 确保能访问 RawPacket +import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart'; import '../../../../core/network/net_message_dispatcher.dart'; import 'device_status_event.dart'; @@ -13,41 +13,70 @@ import 'device_status_state.dart'; class DeviceStatusBloc extends Bloc { final NetMessageDispatcher _dispatcher; + // 持有订阅引用,仅在 close 时取消 + StreamSubscription? _stringSub; + StreamSubscription? _jsonSub; + + // 🔥 新增:标记是否已初始化订阅,防止重复订阅 + bool _isSubscribed = false; + DeviceStatusBloc(this._dispatcher) : super(DeviceStatusInitial()) { - // 订阅 TCP 数据流 - _dispatcher.onStringMessage().listen((jsonString) { - debugPrint('📩 Bloc 收到 0x02 数据'); - add(DeviceStatusLoaded(jsonString)); - }); + // 1. 初始建立订阅(终身有效,除非 Bloc 关闭) + _setupStreamListeners(); - _dispatcher.onJsonMessage(0x12).listen((jsonData) { - add(PushMessageReceived(jsonData)); - }); - - // + // 2. 注册事件处理 + on(_handleReset); on(_handleDeviceStatusLoaded); on(_handlePushMessageReceived); - - - // 🔥 新增:模拟测试数据(调试用) - // _testMockData(); } - void _testMockData() { - // 用户提供的真实指令数据(字节数组) - final mockBytes = [ - 171, 170, 2, 48, 46, 48, 48, 44, 48, 44, 48, 44, 48, 44, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 50, 55, 46, 52, 55, 44, 49, 56, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 52, 49, 44, 52, 44, 49, 44, 51, 50, 46, 48, 52, 49, 50, 57, 54, 56, 54, 52, 48, 48, 57, 57, 57, 44, 49, 50, 48, 46, 56, 48, 56, 57, 57, 51, 48, 56, 49, 49, 53, 56, 53, 48, 44, 50, 48, 45, 49, 45, 49, 49, 45, 51, 49, 32, 48, 56, 58, 48, 48, 58, 48, 48, 44, 48, 44, 51, 44, 54, 54, 44, 57, 53, 50, 55, 46, 48, 48, 48, 48, 44, 48, 0, 0, 0, 170, 171 - ]; - // 解码为字符串(去掉帧头 3 字节和帧尾 5 字节) - final payloadBytes = mockBytes.sublist(3, mockBytes.length - 5); - final mockData = String.fromCharCodes(payloadBytes); + // 🔥 核心修复:订阅逻辑只执行一次,不再随意 cancel/relisten + void _setupStreamListeners() { + if (_isSubscribed) { + return; // 如果已经订阅过,直接返回,避免重复操作 + } - debugPrint('🧪 模拟测试数据:$mockData'); + debugPrint('🔗 [DeviceStatusBloc] 初始化 TCP 数据流订阅(终身有效)'); - // 发送模拟事件 - add(DeviceStatusLoaded(mockData)); + // 订阅字符串流 (0x02) + _stringSub = _dispatcher.onStringMessage().listen( + (jsonString) { + debugPrint('Bloc层已经!!收到 0x02 数据事件,长度:${jsonString.length}'); + if (!isClosed) { + add(DeviceStatusLoaded(jsonString)); + } + }, + onDone: () => debugPrint('⚠️ [Bloc] 0x02 流已结束 (onDone) - 这通常意味着底层 TCP 彻底关闭'), + onError: (e) => debugPrint('❌ [Bloc] 0x02 流发生错误:$e'), + ); + + // 订阅 JSON 流 (0x12) + _jsonSub = _dispatcher.onJsonMessage(0x12).listen( + (jsonData) { + debugPrint('📩 [Bloc] 收到 JSON 数据事件:$jsonData'); + if (!isClosed) { + add(PushMessageReceived(jsonData)); + } + }, + onDone: () => debugPrint('⚠️ [Bloc] JSON 流已结束 (onDone)'), + onError: (e) => debugPrint('❌ [Bloc] JSON 流发生错误:$e'), + ); + + _isSubscribed = true; + debugPrint('✅ [DeviceStatusBloc] 订阅建立完成,将持续监听数据流'); } - // 🔥 修改:使用 Emitter 发送状态 + + // 🔥 核心修复:重置时仅清空状态,绝对不再触碰订阅关系 + Future _handleReset(DeviceStatusReset event, Emitter emit) async { + debugPrint('🔄 收到重置事件:仅清空状态,保持订阅活跃(不重连)'); + + // 只 emit 初始状态,让 UI 清除旧设备的数据(如速度归零、轨迹清除) + emit(DeviceStatusInitial()); + + // ❌ 严禁在此处调用 _setupStreamListeners() 或 cancel 订阅 + // 因为 TCP 重连期间数据流可能一直在推送,cancel 会导致关键首包丢失 + } + Future _handleDeviceStatusLoaded( DeviceStatusLoaded event, Emitter emit, @@ -55,22 +84,21 @@ class DeviceStatusBloc extends Bloc { try { debugPrint('🔍 开始解析数据:${event.jsonString}'); final fields = event.jsonString.trim().split(','); - debugPrint('🔍 字段数量:${fields.length}'); if (fields.length < 18) { + debugPrint('⚠️ 字段不足:${fields.length}'); emit(DeviceStatusError('字段不足,期望 ≥18,实际:${fields.length}')); return; } final status = RunningStatusEntity.fromFields(fields); - debugPrint('设备运行时候状态:$status'); final gps = GPSEntity(status.latitude, status.longitude); - debugPrint('GPS: ${gps.latitude}, ${gps.longitude}'); - emit(DeviceStatusUpdated(status, gps)); // 🔥 使用 emit 发送状态 - } catch (e) { - debugPrint('❌ 解析异常:$e'); - emit(DeviceStatusError('解析设备运行时候状态失败:$e')); + debugPrint('✅ 解析成功,更新状态:Lat=${gps.latitude}, Lng=${gps.longitude}'); + emit(DeviceStatusUpdated(status, gps)); + } catch (e, stack) { + debugPrint('❌ 解析异常:$e\n$stack'); + emit(DeviceStatusError('解析失败:$e')); } } @@ -82,6 +110,7 @@ class DeviceStatusBloc extends Bloc { final eventStr = event.jsonData['event'] ?? ''; final deviceId = event.jsonData['deviceId'] ?? '未知'; debugPrint('收到推送事件:$eventStr, 设备:$deviceId'); + // 这里可以根据需要 emit 新状态 } catch (e) { emit(DeviceStatusError('解析推送消息失败:$e')); } @@ -89,7 +118,15 @@ class DeviceStatusBloc extends Bloc { @override Future close() { - return super.close(); + // 只有在 Bloc 彻底销毁时才取消订阅 + //debugPrint('🚫 [DeviceStatusBloc] 正在关闭,取消所有订阅 '); + // debugPrint('🚫 [DeviceStatusBloc] 正在关闭,取消所有订阅'); + //debugPrint('🔥 [DeviceStatusBloc] 销毁堆栈跟踪:\n${StackTrace.current}'); + // _stringSub?.cancel(); + // _jsonSub?.cancel(); + //return super.close(); + return Future.value(); + + } } - diff --git a/lib/features/devices/presentation/bloc/device_status_event.dart b/lib/features/devices/presentation/bloc/device_status_event.dart index c1a12f2d..ec1780de 100644 --- a/lib/features/devices/presentation/bloc/device_status_event.dart +++ b/lib/features/devices/presentation/bloc/device_status_event.dart @@ -22,3 +22,8 @@ class PushMessageReceived extends DeviceStatusEvent { List get props => [jsonData]; } +class DeviceStatusReset extends DeviceStatusEvent { + @override + // TODO: implement props + List get props => throw UnimplementedError(); +} \ No newline at end of file diff --git a/lib/features/devices/presentation/bloc/devices_cubit.dart b/lib/features/devices/presentation/bloc/devices_cubit.dart index 98519f18..ee98def1 100644 --- a/lib/features/devices/presentation/bloc/devices_cubit.dart +++ b/lib/features/devices/presentation/bloc/devices_cubit.dart @@ -11,6 +11,8 @@ import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_re import 'package:maibu_satabot_v2/features/devices/domain/usecases/unbind_device_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicename_usecase.dart'; +import '../../../../core/consts/tcp_consts.dart'; +import '../../../../core/network/tcp/tcp_client.dart'; import '../../data/models/device_add_path_point_model.dart'; import '../../data/models/device_work_area_param_model.dart'; import '../../domain/usecases/bind_device_usecase.dart'; @@ -18,6 +20,8 @@ import '../../domain/usecases/delete_work_record_usecase.dart'; import '../../domain/usecases/get_device_location_usecase.dart'; import '../../domain/usecases/get_work_record_usecase.dart'; import '../../domain/usecases/route_planning_usecase.dart'; +import 'device_status_bloc.dart'; +import 'device_status_event.dart'; import 'devices_state.dart'; class DevicesCubit extends Cubit { @@ -33,7 +37,8 @@ class DevicesCubit extends Cubit { final SaveWorkRecordUseCase _saveWorkRecordUseCase; final GeneratePathUseCase _generatePathUseCase; final RoutePlanningUseCase _routePlanningUseCase; // - + final DeviceStatusBloc _deviceStatusBloc; // 🔥 新增字段 + final TcpClient _tcpClient; DevicesCubit( this.repository, this._getUserDeviceUseCase, @@ -47,6 +52,9 @@ class DevicesCubit extends Cubit { this._generatePathUseCase, this._routePlanningUseCase, this._bindDeviceUseCase, + this._deviceStatusBloc, this._tcpClient, + + ) : super(const DevicesState()); Future unbindDevice(String deviceId, String deviceName) async { @@ -192,10 +200,41 @@ class DevicesCubit extends Cubit { final result = await repository.switchDevice("app", device.deviceName); - result.fold((l) { - emit(state.copyWith(isLoading: false, errorMessage: l.message)); - selectDevice(device); - }, (r) => emit(state.copyWith(isLoading: false, selectedDevice: device))); + result.fold( + // 失败处理 + (l) { + emit(state.copyWith(isLoading: false, errorMessage: l.message)); + selectDevice(device); + }, + // 成功处理:改为 async 函数块 + (r) async { + // 3. 最后更新 UI 状态 + try { + // 🔥 关键修复 1:先强制断开旧连接! + // 这一步会销毁旧 Socket,清除旧 Listener,防止旧数据继续推送 + if (_tcpClient.isConnected) { + debugPrint('🛑 检测到已连接,先断开旧 TCP 连接...'); + _tcpClient.disconnects(forSwitch: true); + // 稍微等待一下,确保底层 Socket 资源释放 (可选,但推荐) + await Future.delayed(const Duration(milliseconds: 100)); + } + + // 🔥 关键修复 2:发起新连接 + // connect 方法内部会自动调用 _sendAuthPacket -> 获取设备列表 -> 自动订阅当前选中的设备 + debugPrint('🔌 开始重新新连接 TCP,将自动订阅新设备:${device.deviceName}'); + await _tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: device.deviceName); + // 🔥 关键修复 3:重置设备状态 Bloc,清除旧设备图表数据 + _deviceStatusBloc.add(DeviceStatusReset()); + + debugPrint('✅ 新设备切换流程完成'); + } catch (e) { + debugPrint('⚠️ 设备切换成功,但 TCP 重连或状态重置失败:$e'); + // 即使 TCP 失败,也更新 UI 选中状态,让用户知道切换了,只是没数据 + } + + emit(state.copyWith(isLoading: false, selectedDevice: device)); + }, + ); } /// 绑定设备 diff --git a/lib/features/home/presentation/pages/running_status_page.dart b/lib/features/home/presentation/pages/running_status_page.dart index c3145836..deb963e7 100644 --- a/lib/features/home/presentation/pages/running_status_page.dart +++ b/lib/features/home/presentation/pages/running_status_page.dart @@ -878,7 +878,19 @@ class _RunningStatusPageState extends State { SliverToBoxAdapter( child: BlocBuilder( builder: (context, state) { + debugPrint('🎨 [UI-Build] BlocBuilder 重建!当前状态类型:${state.runtimeType}'); + + + // 检测到设备切换(状态重置)时,清空图表历史数据 + if (state is DeviceStatusInitial) { + debugPrint('🧹 [UI] 检测到 Initial 状态,准备重置图表数据'); + WidgetsBinding.instance.addPostFrameCallback((_) { + _resetChartData(); + }); + } + if (state is DeviceStatusUpdated) { + debugPrint('📈 [UI] 检测到 Updated 状态,准备追加图表数据'); _appendChartData(state); } return Container(margin: const EdgeInsets.all(8), child: _isCardView ? _buildCardContentView(state) : _buildChartContentView(state)); @@ -889,6 +901,8 @@ class _RunningStatusPageState extends State { ), ); } + + void _resetChartData() {} } // ====================== 带动画的圆形仪表盘 Widget ====================== diff --git a/lib/main.dart b/lib/main.dart index 6f40b851..ed0afe8b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,7 +47,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { // 在 runApp 之前调用 appStarted,确保 GoRouter 初始化时能获取到正确的初始状态 sl().appStarted(); - + final deviceStatusBloc = sl(); return MultiBlocProvider( providers: [ // 核心修正:在这里提供 AuthCubit @@ -66,7 +66,9 @@ class MyApp extends StatelessWidget { return devicesCubit; }, ), - BlocProvider(create: (_) => sl()), + BlocProvider.value( + value: deviceStatusBloc, + ), // 其他 Cubit... ], child: MaterialApp.router(title: 'Maibu Satabot', theme: AppTheme.lightTheme, routerConfig: sl()),