From 354f7d6290efcf865b2a620d59afb14d66d0d283 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Mon, 8 Jun 2026 13:35:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86tcp=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=9B=B4=E6=96=B0=E5=AF=B9UI=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E7=9A=84=E5=8E=8B=E5=8A=9B=E5=92=8C=E5=AF=B9=E7=94=A8=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E7=9A=84=E9=87=8D=E5=A4=8D=E5=BC=B9=E7=AA=97=E7=9A=84?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=E3=80=82=20=E6=9B=B4=E6=8D=A2=E4=BA=86?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=A7=84=E5=88=92=E7=9A=84=E7=9A=84=E7=BB=8F?= =?UTF-8?q?=E7=BA=AC=E5=BA=A6=E7=9A=84=E5=AD=97=E6=AE=B5=E5=90=8D=E5=92=8C?= =?UTF-8?q?=E5=8F=96=E6=93=8D=E4=BD=9C=E7=9A=84=E5=90=8D=E5=AD=97=E6=9B=B4?= =?UTF-8?q?=E6=8D=A2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile | 3 +- .../auth/presentation/bloc/auth_cubit.dart | 7 +- .../presentation/bloc/device_status_bloc.dart | 206 +++++++------- .../bloc/permission_request_bloc.dart | 147 ++++------ .../widgets/map/testmap_pages.dart | 4 +- .../bloc/remote_control_cubit.dart | 251 ++++++++++++++---- .../bloc/remote_control_state.dart | 23 +- .../pages/remote_control_page.dart | 80 ++++-- 8 files changed, 445 insertions(+), 276 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 1eefd3d3..f3bf4e0b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,6 +1,7 @@ # Uncomment this line to define a global platform for your project # platform :ios, '13.0' -source 'https://github.com/volcengine/volcengine-specs.git' +source 'https://github.com/volcengine/volcengine-specs.git'error: Classes can only extend other classes. (extends_non_class at [maibu_satabot_v2] lib\features\v2\device_list\domain\entities\video_stream_entity.dart:2)error: Classes can only extend other classes. (extends_non_class at [maibu_satabot_v2] lib\features\v2\device_list\domain\entities\video_stream_entity.dart:2)you + # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index b1bee062..4325edeb 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -62,8 +62,11 @@ class AuthCubit extends Cubit { if (user != null) { // 🔥 冷启动时重新初始化 TCP 连接 - await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); - + await tcp.initializeTcp( + host: TCPConsts.TCP_IP, + port: TCPConsts.TCP_PORT, + ); + // 2. 同步全局 App 状态 appCubit.setAuth(user); // 3. 进入已登录状态 diff --git a/lib/features/devices/presentation/bloc/device_status_bloc.dart b/lib/features/devices/presentation/bloc/device_status_bloc.dart index ff47be1c..db1fb4a9 100644 --- a/lib/features/devices/presentation/bloc/device_status_bloc.dart +++ b/lib/features/devices/presentation/bloc/device_status_bloc.dart @@ -21,9 +21,15 @@ class DeviceStatusBloc extends Bloc { // 🔥 保存订阅引用,用于管理生命周期 StreamSubscription? _tcpSubscription; - DeviceStatusBloc(this._dispatcher, {TcpClient? client}) - : tcpClient = client ?? GetIt.I(), - super(DeviceStatusInitial()) { + // 🔥 节流相关:500ms节流控制0x02数据推送频率 + Timer? _throttleTimer; + static const _throttleDuration = Duration(milliseconds: 500); + RunningStatusEntity? _cachedStatus; + GPSEntity? _cachedGps; + + DeviceStatusBloc(this._dispatcher, {TcpClient? client}) + : tcpClient = client ?? GetIt.I(), + super(DeviceStatusInitial()) { // 🔥 核心改动:直接在构造函数中建立TCP监听,类似RemoteControlCubit _initDirectTcpListener(); @@ -69,97 +75,110 @@ class DeviceStatusBloc extends Bloc { // 🔥 关键修复:直接监听 tcpClient.packetStream,不经过 dispatcher 的 filtered stream // 这样即使没有其他监听者,TCP流也不会暂停 _tcpSubscription = tcpClient.packetStream - .where((p) => p.command == 0x02) - .map((p) { - try { - final result = utf8.decode(p.payload, allowMalformed: true); - // debugPrint('✅ [DeviceStatusBloc] 收到0x02数据: $result'); - return result; - } catch (e) { - // debugPrint('❌ [DeviceStatusBloc] 解码失败: $e'); - return ''; - } - }) - .listen( - (message) { - //debugPrint('📩 [DeviceStatusBloc] 直接收到0x02数据,长度:${message.length}'); + .where((p) => p.command == 0x02) + .map((p) { + try { + final result = utf8.decode(p.payload, allowMalformed: true); + // debugPrint('✅ [DeviceStatusBloc] 收到0x02数据: $result'); + return result; + } catch (e) { + // debugPrint('❌ [DeviceStatusBloc] 解码失败: $e'); + return ''; + } + }) + .listen( + (message) { + //debugPrint('📩 [DeviceStatusBloc] 直接收到0x02数据,长度:${message.length}'); - if (message.isEmpty) { - //debugPrint('⚠️ [DeviceStatusBloc] 消息为空,跳过'); - return; - } - - try { - // 🔥 直接解析,不经过事件转换 - final fields = message.trim().split(','); - - if (fields.length < 18) { - //debugPrint('⚠️ [DeviceStatusBloc] 字段不足:${fields.length},期望≥18'); - if (!isClosed) { - emit(DeviceStatusError('字段不足,期望≥18,实际:${fields.length}')); + if (message.isEmpty) { + //debugPrint('⚠️ [DeviceStatusBloc] 消息为空,跳过'); + return; } - return; - } - final status = RunningStatusEntity.fromFields(fields); - final gps = GPSEntity(status.latitude, status.longitude); + try { + // 🔥 直接解析,不经过事件转换 + final fields = message.trim().split(','); - //debugPrint('✅ [DeviceStatusBloc] 直接解析成功,更新状态:Lat=${gps.latitude}, Lng=${gps.longitude}'); - // _logger.log('✅ [DeviceStatusBloc] 直接解析成功,更新状态'); - - // 🔥 关键修复:BLoC有Equatable去重机制,必须创建新对象才能触发UI更新 - if (!isClosed) { - // 创建全新的status和gps对象,绕过Equatable去重 - final newStatus = RunningStatusEntity( - voltage: status.voltage, - leftTargetSpeed: status.leftTargetSpeed, - rightTargetSpeed: status.rightTargetSpeed, - leftMeasureSpeed: status.leftMeasureSpeed, - rightMeasureSpeed: status.rightMeasureSpeed, - leftCurrent: status.leftCurrent, - rightCurrent: status.rightCurrent, - leftMotorTemp: status.leftMotorTemp, - rightMotorTemp: status.rightMotorTemp, - chipTemp: status.chipTemp, - yaw: status.yaw, - pitch: status.pitch, - roll: status.roll, - satelliteCnt: status.satelliteCnt, - qual: status.qual, - headingStatus: status.headingStatus, - latitude: status.latitude, - longitude: status.longitude, - timestamp: status.timestamp, - knifeCuttingSpeed: status.knifeCuttingSpeed, - controlMode: status.controlMode, - battery: status.battery, - workingArea: status.workingArea, - obstacleFlag: status.obstacleFlag, - ); - final newGps = GPSEntity(status.latitude, status.longitude); - // debugPrint('📤 [DeviceStatusBloc] emit DeviceStatusUpdated - 电压:${status.voltage}, 电量:${status.battery}, 模式:${status.controlMode}'); - emit(DeviceStatusUpdated(newStatus, newGps)); - } - } catch (e, stack) { - //debugPrint('❌ [DeviceStatusBloc] 直接解析异常:$e\n$stack'); - // _logger.log('❌ [DeviceStatusBloc] 直接解析异常:$e'); - if (!isClosed) { - emit(DeviceStatusError('解析失败:$e')); - } - } - }, - onDone: () => debugPrint('⚠️ [DeviceStatusBloc] TCP流已结束(onDone)'), - onError: (e) => debugPrint('❌ [DeviceStatusBloc] TCP流错误:$e'), - ); + if (fields.length < 18) { + //debugPrint('⚠️ [DeviceStatusBloc] 字段不足:${fields.length},期望≥18'); + // 🔥 错误不节流,立即emit以便UI显示错误 + if (!isClosed) { + emit(DeviceStatusError('字段不足,期望≥18,实际:${fields.length}')); + } + return; + } + + final status = RunningStatusEntity.fromFields(fields); + final gps = GPSEntity(status.latitude, status.longitude); + + // 🔥 缓存最新数据用于节流发射 + _cachedStatus = RunningStatusEntity( + voltage: status.voltage, + leftTargetSpeed: status.leftTargetSpeed, + rightTargetSpeed: status.rightTargetSpeed, + leftMeasureSpeed: status.leftMeasureSpeed, + rightMeasureSpeed: status.rightMeasureSpeed, + leftCurrent: status.leftCurrent, + rightCurrent: status.rightCurrent, + leftMotorTemp: status.leftMotorTemp, + rightMotorTemp: status.rightMotorTemp, + chipTemp: status.chipTemp, + yaw: status.yaw, + pitch: status.pitch, + roll: status.roll, + satelliteCnt: status.satelliteCnt, + qual: status.qual, + headingStatus: status.headingStatus, + latitude: status.latitude, + longitude: status.longitude, + timestamp: status.timestamp, + knifeCuttingSpeed: status.knifeCuttingSpeed, + controlMode: status.controlMode, + battery: status.battery, + workingArea: status.workingArea, + obstacleFlag: status.obstacleFlag, + ); + _cachedGps = GPSEntity(status.latitude, status.longitude); + + // 🔥 节流:取消之前的timer,重新计时500ms + _throttleTimer?.cancel(); + _throttleTimer = Timer(_throttleDuration, () { + _emitCachedStatus(); + }); + } catch (e, stack) { + //debugPrint('❌ [DeviceStatusBloc] 直接解析异常:$e\n$stack'); + // _logger.log('❌ [DeviceStatusBloc] 直接解析异常:$e'); + // 🔥 解析错误不节流,立即emit以便UI显示错误 + if (!isClosed) { + emit(DeviceStatusError('解析失败:$e')); + } + } + }, + onDone: () => debugPrint('⚠️ [DeviceStatusBloc] TCP流已结束(onDone)'), + onError: (e) => debugPrint('❌ [DeviceStatusBloc] TCP流错误:$e'), + ); debugPrint('✅ [DeviceStatusBloc] 直接TCP监听器已建立完成'); _logger.log('✅ [DeviceStatusBloc] 直接TCP监听器已建立完成'); } + // 🔥 节流发射:500ms到期后发射缓存的最新数据 + void _emitCachedStatus() { + if (_cachedStatus != null && _cachedGps != null && !isClosed) { + // debugPrint('📤 [DeviceStatusBloc] 🔥节流发射 - 电压:${_cachedStatus!.voltage}, 电量:${_cachedStatus!.battery}'); + emit(DeviceStatusUpdated(_cachedStatus!, _cachedGps!)); + } + } + // 🔥 重置时仅清空状态 - Future _handleReset(DeviceStatusReset event, Emitter emit) async { + Future _handleReset( + DeviceStatusReset event, + Emitter emit, + ) async { debugPrint('🔄 [DeviceStatusBloc] 收到重置事件:清空状态 - 当前状态:${state.runtimeType}'); - _logger.log('🔄 [DeviceStatusBloc] 收到重置事件:清空状态 - 当前状态:${state.runtimeType}'); + _logger.log( + '🔄 [DeviceStatusBloc] 收到重置事件:清空状态 - 当前状态:${state.runtimeType}', + ); // 只 emit 初始状态,让 UI 清除旧设备的数据 emit(DeviceStatusInitial()); @@ -167,11 +186,11 @@ class DeviceStatusBloc extends Bloc { } Future _handleDeviceStatusLoaded( - DeviceStatusLoaded event, - Emitter emit, - ) async { + DeviceStatusLoaded event, + Emitter emit, + ) async { try { - // debugPrint('🔍 开始解析数据:${event.jsonString}'); + // debugPrint('🔍 开始解析数据:${event.jsonString}'); _logger.log('🔍 开始解析数据:${event.jsonString}'); final fields = event.jsonString.trim().split(','); @@ -195,13 +214,13 @@ class DeviceStatusBloc extends Bloc { } Future _handlePushMessageReceived( - PushMessageReceived event, - Emitter emit, - ) async { + PushMessageReceived event, + Emitter emit, + ) async { try { final eventStr = event.jsonData['event'] ?? ''; final deviceId = event.jsonData['deviceId'] ?? '未知'; - // debugPrint('收到推送事件:$eventStr, 设备:$deviceId'); + // debugPrint('收到推送事件:$eventStr, 设备:$deviceId'); _logger.log('收到推送事件:$eventStr, 设备:$deviceId'); // 这里可以根据需要 emit 新状态 } catch (e) { @@ -214,6 +233,11 @@ class DeviceStatusBloc extends Bloc { debugPrint('🚫 [DeviceStatusBloc] 页面退出,仅取消TCP订阅(不关闭BLoC)'); _logger.log('🚫 [DeviceStatusBloc] 页面退出,仅取消TCP订阅(不关闭BLoC)'); //_tcpSubscription?.cancel(); + // 🔥 清理节流timer和缓存 + _throttleTimer?.cancel(); + _throttleTimer = null; + _cachedStatus = null; + _cachedGps = null; // 🔥 关键修复:不调用 super.close(),保持 BLoC 活跃 return Future.value(); } diff --git a/lib/features/home/presentation/bloc/permission_request_bloc.dart b/lib/features/home/presentation/bloc/permission_request_bloc.dart index accd44bd..b48dc154 100644 --- a/lib/features/home/presentation/bloc/permission_request_bloc.dart +++ b/lib/features/home/presentation/bloc/permission_request_bloc.dart @@ -10,7 +10,8 @@ import '../../../remote_control/domain/repositories/remote_control_repository.da import 'permission_request_event.dart'; import 'permission_request_state.dart'; -class PermissionRequestBloc extends Bloc { +class PermissionRequestBloc + extends Bloc { final NetMessageDispatcher _dispatcher; final RemoteControlRepository _repository; final ILoggerService _logger = GetIt.I(); @@ -18,9 +19,11 @@ class PermissionRequestBloc extends Bloc(_handleRequestReceived); @@ -28,100 +31,53 @@ class PermissionRequestBloc extends Bloc>> [PermissionRequestBloc] ✅✅✅ 0x12 包被监听到了!', shouldLog: true); - - try { - _logger.logWithLevel('>>> [PermissionRequestBloc] ✅ 收到 0x12 原始包', shouldLog: true); - - // 🔥 关键:手动去掉最后 2 个 CRC 字节 - String jsonString; - if (packet.payload.length > 2) { - jsonString = utf8.decode(packet.payload.sublist(0, packet.payload.length - 2)); - } else { - jsonString = utf8.decode(packet.payload); - } + // 🔥 已禁用:权限请求统一由 RemoteControlCubit 处理,避免重复弹窗 + // _permissionSubscription = _dispatcher.onCommand(0x12).listen((packet) { + // ... + // }); - debugPrint('>>> [PermissionRequestBloc] 收到 0x12: $jsonString'); - _logger.logWithLevel('>>> [PermissionRequestBloc] 收到 0x12: $jsonString', shouldLog: true); - - final jsonMap = jsonDecode(jsonString); - final requestType = jsonMap['request']; - final platform = jsonMap['platform']; - final respondData = jsonMap['respond']; - - _logger.logWithLevel('>>> [PermissionRequestBloc] 解析结果: requestType=$requestType, platform=$platform', shouldLog: true); - - // 情况 1: 响应格式(忽略) - if (respondData != null && respondData is Map) { - debugPrint('>>> [PermissionRequestBloc] 📊 收到响应格式,忽略'); - _logger.logWithLevel('>>> [PermissionRequestBloc] 📊 收到响应格式,忽略', shouldLog: true); - return; - } - - // 情况 2: 请求格式 - switch_control - if (requestType == 'switch_control') { - // 🔥 关键判断:只有当是其他平台(web)请求时才弹窗 - if (platform != null && platform.toString().toLowerCase() != 'app') { - debugPrint('>>> [PermissionRequestBloc] 🚨 $platform 端请求控制权'); - _logger.logWithLevel('>>> [PermissionRequestBloc] 🚨 $platform 端请求控制权', shouldLog: true); - - if (!isClosed) { - _logger.logWithLevel('>>> [PermissionRequestBloc] 📤 准备 add PermissionRequestReceived 事件', shouldLog: true); - add(PermissionRequestReceived( - platform: platform.toString(), - requestType: requestType, - )); - } else { - _logger.logWithLevel('>>> [PermissionRequestBloc] ⚠️ Bloc 已关闭,无法添加事件', shouldLog: true); - } - } else { - debugPrint('>>> [PermissionRequestBloc] ℹ️ APP 自己的请求回显,忽略'); - _logger.logWithLevel('>>> [PermissionRequestBloc] ℹ️ APP 自己的请求回显,忽略', shouldLog: true); - } - } - // 情况 3: 异地登录通知 - else if (requestType == 'have_logged_in') { - debugPrint('>>> [PermissionRequestBloc] ⚠️ 检测到异地登录'); - _logger.logWithLevel('>>> [PermissionRequestBloc] ⚠️ 检测到异地登录', shouldLog: true); - - if (!isClosed) { - _logger.logWithLevel('>>> [PermissionRequestBloc] 📤 准备 add PermissionRequestReceived 事件(异地登录)', shouldLog: true); - add(PermissionRequestReceived( - platform: 'unknown', - requestType: requestType, - )); - } - } - } catch (e) { - debugPrint('>>> [PermissionRequestBloc] ❌ 解析失败:$e'); - _logger.logWithLevel('>>> [PermissionRequestBloc] ❌ 解析失败:$e', shouldLog: true); - } - }); - - debugPrint('✅ [PermissionRequestBloc] 0x12 监听器已建立完成'); - _logger.logWithLevel('✅ [PermissionRequestBloc] 0x12 监听器已建立完成', shouldLog: true); + debugPrint( + '🔍 [PermissionRequestBloc] ⚠️ 权限监听已禁用,统一由 RemoteControlCubit 处理', + ); + _logger.logWithLevel( + '>>> [PermissionRequestBloc] ⚠️ 权限监听已禁用,统一由 RemoteControlCubit 处理', + shouldLog: true, + ); } Future _handleRequestReceived( PermissionRequestReceived event, Emitter emit, ) async { - debugPrint('📤 [PermissionRequestBloc] emit DialogVisible - platform=${event.platform}'); - _logger.logWithLevel('📤 [PermissionRequestBloc] emit DialogVisible - platform=${event.platform}', shouldLog: true); - - emit(PermissionRequestDialogVisible( - platform: event.platform, - requestType: event.requestType, - )); - - _logger.logWithLevel('📤 [PermissionRequestBloc] ✅ 已 emit DialogVisible 状态', shouldLog: true); + debugPrint( + '📤 [PermissionRequestBloc] emit DialogVisible - platform=${event.platform}', + ); + _logger.logWithLevel( + '📤 [PermissionRequestBloc] emit DialogVisible - platform=${event.platform}', + shouldLog: true, + ); + + emit( + PermissionRequestDialogVisible( + platform: event.platform, + requestType: event.requestType, + ), + ); + + _logger.logWithLevel( + '📤 [PermissionRequestBloc] ✅ 已 emit DialogVisible 状态', + shouldLog: true, + ); } Future _handleDialogDismissed( @@ -130,13 +86,15 @@ class PermissionRequestBloc extends Bloc { List newOuterPoints = []; for (var item in pathList) { double? lat = safeToDouble(item['lat']); - double? lon = safeToDouble(item['lon']); + double? lon = safeToDouble(item['lng'] ?? item['lon']); if (lat != null && lon != null) { newPathPoints.add(convertWGS84ToGCJ02(lat, lon)); } } for (var item in outerList) { - double? lng = safeToDouble(item['lng']); + double? lng = safeToDouble(item['lng'] ?? item['lon']); double? lat = safeToDouble(item['lat']); if (lng != null && lat != null) { newOuterPoints.add(convertWGS84ToGCJ02(lat, lng)); diff --git a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart index d6d2b708..4c40f43e 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart @@ -1,6 +1,7 @@ -import 'dart:async'; +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:dart_ping/dart_ping.dart'; import 'package:flutter/cupertino.dart'; @@ -39,13 +40,12 @@ class RemoteControlCubit extends Cubit { int _currentOriginX = 0; int _currentOriginY = 0; - // 🔥 权限请求冷却期机制 - 防止Web端持续发送请求导致弹窗不断显示 - DateTime? _lastPermissionResponseTime; // 记录上次响应权限请求的时间 - static const _coolDownDuration = Duration(seconds: 5); // 冷却期5秒 - - // 🔥 同步锁 - 防止状态更新期间接收新请求导致重复弹窗 + // 🔥 权限请求处理标志位 - 防止竞态条件,避免重复显示弹窗 bool _isProcessingPermissionRequest = false; + // 🔥 模拟数据推送定时器 + // Timer? _simulationTimer; + // 🔥 获取带时间戳的日志前缀 String _getTimePrefix() { final now = DateTime.now(); @@ -69,40 +69,138 @@ class RemoteControlCubit extends Cubit { } // 🔥 新增:订阅 DeviceStatusBloc 的状态流,而不是直接监听TCP + // 类全局变量 + DateTime? _lastUiUpdateTime; + String? _cacheVoltage; + String? _cacheBattery; + String? _cacheCtrlMode; + int? _cachePing; + void _initDeviceStatusListener() { - // // _logger.logWithLevel('>>> [RemoteControl] begin 订阅 DeviceStatusBloc 状态流'); _deviceStatusSub?.cancel(); _deviceStatusSub = deviceStatusBloc.stream.listen((deviceState) async { if (deviceState is DeviceStatusUpdated) { + // 第一步:所有数据先存入缓存,不管来多频繁都存最新值 final voltage = deviceState.status.voltage; final battery = deviceState.status.battery; final controlMode = deviceState.status.controlMode == '3' ? '远程模式' : '本地模式'; + final c = await getNetworkDelay(); - final c = getNetworkDelay(); + _cacheVoltage = voltage.toString(); + _cacheBattery = battery.toString(); + _cacheCtrlMode = controlMode; + _cachePing = c; + + // 500ms节流,不到时间不刷新UI + final now = DateTime.now(); + if (_lastUiUpdateTime != null && + now.difference(_lastUiUpdateTime!) < + const Duration(milliseconds: 500)) { + return; + } + _lastUiUpdateTime = now; + + // 间隔达标,统一一次刷新UI emit( state.copyWith( runningStatusModel: state.runningStatusModel.copyWith( - voltage: voltage.toString(), - battery: battery.toString(), - controlMode: controlMode, + voltage: _cacheVoltage, + battery: _cacheBattery, + controlMode: _cacheCtrlMode, ), - battery: int.tryParse(battery) ?? 0, - ping: await c, + battery: int.tryParse(_cacheBattery ?? '') ?? 0, + ping: _cachePing, + // 🔥 标记为设备状态更新 + updateType: 'device_status', ), ); - - // // _logger.logWithLevel('✅ [RemoteControl] 从 DeviceStatusBloc 收到更新: 电压=$voltage, 电量=$battery, 模式=$controlMode'); } }); - - // _logger.logWithLevel('>>> [RemoteControl] ✅ DeviceStatusBloc 订阅已建立完成'); } // 🔥 超简单方法:传入 IP,得到 ping 值 + // 🔥 模拟设备状态更新 - 用于测试 + // void simulateDeviceStatusUpdate({ + // String? voltage, + // String? battery, + // String? controlMode, + // int? ping, + // // 🔥 是否随机生成数据 + // bool random = false, + // }) { + // // 如果开启随机模式,生成随机数据 + // final rand = Random(); + + /* if (random) { + voltage = (22.0 + rand.nextDouble() * 4.0).toStringAsFixed( + 1, + ); // 22.0-26.0V + battery = (rand.nextInt(100) + 1).toString(); // 1-100% + controlMode = rand.nextBool() ? '远程模式' : '本地模式'; + ping = rand.nextInt(150) + 20; // 20-170ms + } */ + + /* debugPrint( + '🔧 [模拟设备状态更新] voltage=$voltage V, battery=$battery%, controlMode=$controlMode, ping=$ping ms', + ); */ + + // 更新缓存 + /* if (voltage != null) _cacheVoltage = voltage; + if (battery != null) _cacheBattery = battery; + if (controlMode != null) _cacheCtrlMode = controlMode; + if (ping != null) _cachePing = ping; + + // 直接触发状态更新(跳过节流,立即更新) + emit( + state.copyWith( + runningStatusModel: state.runningStatusModel.copyWith( + voltage: _cacheVoltage, + battery: _cacheBattery, + controlMode: _cacheCtrlMode, + ), + battery: int.tryParse(_cacheBattery ?? '') ?? 0, + ping: _cachePing, + // 🔥 标记为设备状态更新 + updateType: 'device_status', + ), + ); + } */ + + // 🔥 开始模拟设备状态推送(随机数据) + /* void startSimulation({int intervalMs = 500}) { + // 如果已经在运行,先停止 + stopSimulation(); + + // debugPrint('🔔 [模拟推送] 开始模拟设备状态推送,间隔:${intervalMs}ms'); + + // 立即发送一次初始数据 + simulateDeviceStatusUpdate(random: true); + + // 定时推送随机数据 + _simulationTimer = Timer.periodic(Duration(milliseconds: intervalMs), ( + timer, + ) { + if (!isClosed) { + simulateDeviceStatusUpdate(random: true); + } else { + stopSimulation(); + } + }); + } */ + + // 🔥 停止模拟设备状态推送 + /* void stopSimulation() { + if (_simulationTimer != null) { + _simulationTimer!.cancel(); + _simulationTimer = null; + debugPrint('🔔 [模拟推送] 已停止模拟设备状态推送'); + } + } */ + // 🔥 辅助方法:更新运行状态 void _updateStatusFromDevice(RunningStatusModel newStatus) { // // debugPrint('✅ [_updateStatusFromDevice] 收到运行状态更新:$newStatus'); @@ -141,14 +239,37 @@ class RemoteControlCubit extends Cubit { _kickOutSub = dispatcher.onCommand(0x12).listen((packet) { final timeNow = _getTimePrefix(); debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ✅ 收到 0x12 原始包'); - debugPrint( + /* debugPrint( '$timeNow 🔍 [RemoteControl] [0x12监听器] payload长度: ${packet.payload.length}', - ); + );*/ _logger.logWithLevel( '$timeNow 🔍 [RemoteControl] [0x12监听器] 收到 0x12 原始包,payload长度=${packet.payload.length}', shouldLog: true, ); + // 先解析JSON判断是否为响应格式 + String jsonStringForCheck; + try { + if (packet.payload.length > 2) { + jsonStringForCheck = utf8.decode( + packet.payload.sublist(0, packet.payload.length - 2), + ); + } else { + jsonStringForCheck = utf8.decode(packet.payload); + } + final jsonMap = jsonDecode(jsonStringForCheck); + final respondData = jsonMap['respond']; + + // 如果是响应格式,继续处理(更新权限状态) + // 如果是请求格式且弹窗已显示,忽略 + if (respondData == null && state.showPermissionRequestDialog) { + debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ⚠️ 弹窗已显示(状态),忽略请求'); + return; + } + } catch (e) { + // 解析失败,继续处理 + } + try { // 🔥 关键:手动去掉最后 2 个 CRC 字节 String jsonString; @@ -179,11 +300,13 @@ class RemoteControlCubit extends Cubit { // 情况 1: 响应格式 - {"respond":{"switchResult":true,"deviceId":"...","holder":"you"}} if (respondData != null && respondData is Map) { + // 🔥 收到响应格式,更新权限状态 + final switchResult = respondData['switchResult']; - debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] 📊 收到切换结果响应'); + /* debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] 📊 收到切换结果响应'); debugPrint( '$timeNow 🔍 [RemoteControl] [0x12监听器] switchResult: $switchResult', - ); + );*/ _logger.logWithLevel( '$timeNow 🔍 [RemoteControl] [0x12监听器] 📊 收到切换结果响应 - switchResult: $switchResult', shouldLog: true, @@ -218,6 +341,15 @@ class RemoteControlCubit extends Cubit { final webLogPrefix = '${_getTimePrefix()} 🚨 [RemoteControl] [Web端权限请求]'; + // 🔥 新增:记录收到请求的时间,便于排查是否为后端持续推送 + debugPrint( + '$webLogPrefix ⚡️⚡️⚡️ 收到Web端权限请求 - ${DateTime.now().toString()}', + ); + _logger.logWithLevel( + '$webLogPrefix ⚡️⚡️⚡️ 收到Web端权限请求 - ${DateTime.now().toString()}', + shouldLog: true, + ); + debugPrint('$webLogPrefix ========================================='); debugPrint('$webLogPrefix 收到 switch_control 请求'); debugPrint('$webLogPrefix platform: $platform'); @@ -235,28 +367,32 @@ class RemoteControlCubit extends Cubit { if (currentDeviceId != null && requestDeviceId == currentDeviceId) { debugPrint('$webLogPrefix 设备ID匹配'); - // 🔥 添加防重复检查:只有当弹窗还没显示时才弹出 - if (!state.showPermissionRequestDialog) { - debugPrint('$webLogPrefix 弹出权限请求对话框'); - _logger.logWithLevel( - '$webLogPrefix 设备ID匹配,弹出权限请求对话框', - shouldLog: true, - ); - if (!isClosed) { - emit( - state.copyWith( - showPermissionRequestDialog: true, - requestingDeviceId: requestDeviceId?.toString(), - requestingPlatform: platform.toString(), - ), - ); - } - } else { + + // 🔥 只有当弹窗还没显示时才弹出,防止重复弹窗叠加 + if (state.showPermissionRequestDialog) { debugPrint('$webLogPrefix ⚠️ 弹窗已显示,忽略重复请求'); _logger.logWithLevel( '$webLogPrefix ⚠️ 弹窗已显示,忽略重复请求', shouldLog: true, ); + return; + } + + debugPrint('$webLogPrefix 弹出权限请求对话框'); + _logger.logWithLevel( + '$webLogPrefix 设备ID匹配,弹出权限请求对话框', + shouldLog: true, + ); + if (!isClosed) { + emit( + state.copyWith( + showPermissionRequestDialog: true, + requestingDeviceId: requestDeviceId?.toString(), + requestingPlatform: platform.toString(), + // 🔥 标记为弹窗状态更新 + updateType: 'permission_dialog', + ), + ); } } else { debugPrint('$webLogPrefix ⚠️ 设备ID不匹配,忽略'); @@ -284,7 +420,8 @@ class RemoteControlCubit extends Cubit { '${_getTimePrefix()} ⚠️ [RemoteControl] 检测到异地登录,打开弹窗提示', shouldLog: true, ); - if (!isClosed) { + // 🔥 只有当弹窗还没显示时才弹出 + if (!isClosed && !state.showPermissionRequestDialog) { emit(state.copyWith(showPermissionRequestDialog: true)); } } else { @@ -331,6 +468,9 @@ class RemoteControlCubit extends Cubit { //debugPrint('$logPrefix 启动控制循环,间隔: 100ms'); ///debugPrint('$logPrefix ========================================='); + // 🔥 启动模拟设备状态推送(用于测试) + // startSimulation(); + _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) { if (isClosed) { // debugPrint('$logPrefix ❌ Cubit已关闭,取消定时器'); @@ -538,27 +678,16 @@ class RemoteControlCubit extends Cubit { } void respondPermission(bool agreed, String deviceId) { - - - - // 🔥 记录响应时间,开启冷却期 - _lastPermissionResponseTime = DateTime.now(); - //debugPrint('$logPrefix 开启权限请求冷却期,持续${_coolDownDuration.inSeconds}秒'); - // 1. 关闭弹窗(立即关闭,防止重复点击) emit(state.copyWith(showPermissionRequestDialog: false)); // 2. 发送响应到服务器 - //debugPrint('$logPrefix 📤 发送权限响应命令到服务器'); try { - // 🔥 关键调用:发送 TCP 指令 - debugPrint('{_coolDownDuration.inSeconds}秒'); _repository.respondPermission(agreed, deviceId); } catch (e) { debugPrint(' ❌ TCP权限响应指令发送失败: $e'); debugPrint(' ❌ 错误类型: ${e.runtimeType}'); _logger.logWithLevel(' TCP权限响应指令发送失败 $e', shouldLog: true); - // 即使发送失败,也要更新状态 rethrow; } @@ -577,9 +706,7 @@ class RemoteControlCubit extends Cubit { state.copyWith(hasPermission: true, showPermissionRequestDialog: false), ); } - debugPrint( - '${_getTimePrefix()} ====权限弹窗响应结束=====', - ); + debugPrint('${_getTimePrefix()} ====权限弹窗响应结束====='); } Future requestControlPermissionS( @@ -625,12 +752,13 @@ class RemoteControlCubit extends Cubit { final failLogPrefix = '${_getTimePrefix()} 🔑 [RemoteControl] [请求权限接口-$source]'; debugPrint('$failLogPrefix APP请求控制权限失败: ${failure.message}'); - debugPrint('$failLogPrefix 重新打开权限请求弹窗'); _logger.logWithLevel( '$failLogPrefix APP请求控制权限失败: ${failure.message}', shouldLog: true, ); - emit(state.copyWith(showPermissionRequestDialog: true)); + // 🔥 修复:HTTP请求失败时不要自动打开弹窗,避免形成循环 + // emit(state.copyWith(showPermissionRequestDialog: true)); + debugPrint('$failLogPrefix HTTP请求失败,不自动打开弹窗'); }, (permissionInfo) async { final bool hasPermission = @@ -689,7 +817,11 @@ class RemoteControlCubit extends Cubit { ); } - /// 🔥 权限弹窗确认后调用- 发送TCP响应 + HTTP确认最终权限状态 + /// 🔥 重置弹窗状态 - 在弹窗关闭后调用(已简化,不再需要标志位) + void resetPermissionCoolDown() { + // 标志位已移除,此方法保留以保持向后兼容性 + } + Future confirmPermissionResponse( String deviceName, String platform, @@ -702,11 +834,12 @@ class RemoteControlCubit extends Cubit { debugPrint('$logPrefix 用户操作: ${agreed ? "同意" : "拒绝"}'); debugPrint('$logPrefix deviceName: $deviceName'); debugPrint('$logPrefix platform: $platform'); + respondPermission(agreed, deviceName); // 2. 调用 HTTP 接口获取最终权限状态 debugPrint('$logPrefix 📡 调用 HTTP 接口确认最终权限状态..'); - /* final result = await _requestControlPermissionUseCase( + /* final result = await _requestControlPermissionUseCase( RequestControlPermissionParams( deviceName: deviceName, deviceId: platform, @@ -714,7 +847,7 @@ class RemoteControlCubit extends Cubit { );*/ // 3. 根据 HTTP 返回的真实权限状态更新UI - /* result.fold( + /* result.fold( (failure) { final failLogPrefix = '${_getTimePrefix()} 🔑 [RemoteControl] [权限确认]'; debugPrint('$failLogPrefix APP HTTP请求失败: ${failure.message}'); @@ -734,7 +867,7 @@ class RemoteControlCubit extends Cubit { '$successLogPrefix 🔄 正在更新UI - hasPermission: ${state.hasPermission} -> $hasPermission', ); // 🔥 直接用HTTP 返回的权限状态覆盖 - emit(state.copyWith(hasPermission: hasPermission)); + // emit(state.copyWith(hasPermission: hasPermission)); debugPrint( '$successLogPrefix 📊 UI已同步完成- 当前 hasPermission=$hasPermission', ); diff --git a/lib/features/remote_control/presentation/bloc/remote_control_state.dart b/lib/features/remote_control/presentation/bloc/remote_control_state.dart index bfaa0229..620ee70d 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_state.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_state.dart @@ -14,8 +14,8 @@ class RemoteControlState extends Equatable { final bool isEmergency; final bool isLocked; final int ping; - final int battery;//电池量 - final int voltage;//电压 + final int battery; //电池量 + final int voltage; //电压 final String permissionPlatform; final String currentPlatform; final bool showPermissionRequestDialog; @@ -28,8 +28,11 @@ class RemoteControlState extends Equatable { final bool obstacleRecognitionFlag; // 障碍物识别标志位(这是UI显示的) final String obstacleFlag; //障碍物标志位 - final DeviceEntity? targetDevice; // 🔥 待控制的设备 final RunningStatusModel runningStatusModel; + final DeviceEntity? targetDevice; + + // 🔥 状态更新类型标记 - 用于区分是设备状态更新还是弹窗状态更新 + final String? updateType; const RemoteControlState({ this.status = RemoteControlStatus.initial, @@ -53,6 +56,7 @@ class RemoteControlState extends Equatable { this.topRightIsExpanded = false, this.obstacleRecognitionFlag = true, this.targetDevice, + this.updateType, }); // 便利 UI 更新部分属性 @@ -78,7 +82,7 @@ class RemoteControlState extends Equatable { bool? topRightIsExpanded, bool? obstacleRecognitionFlag, DeviceEntity? targetDevice, - + String? updateType, }) { return RemoteControlState( status: status ?? this.status, @@ -101,9 +105,10 @@ class RemoteControlState extends Equatable { runningStatusModel: runningStatusModel ?? this.runningStatusModel, obstacleFlag: obstacleFlag ?? this.obstacleFlag, topRightIsExpanded: topRightIsExpanded ?? this.topRightIsExpanded, - obstacleRecognitionFlag: obstacleRecognitionFlag ?? this.obstacleRecognitionFlag, + obstacleRecognitionFlag: + obstacleRecognitionFlag ?? this.obstacleRecognitionFlag, targetDevice: targetDevice ?? this.targetDevice, - + updateType: updateType, // 不使用 ??,因为我们想要 null 的时候就是 null ); } @@ -130,8 +135,6 @@ class RemoteControlState extends Equatable { topRightIsExpanded, obstacleRecognitionFlag, targetDevice, + updateType, ]; - - - -} \ No newline at end of file +} diff --git a/lib/features/remote_control/presentation/pages/remote_control_page.dart b/lib/features/remote_control/presentation/pages/remote_control_page.dart index f584cef1..4231890b 100644 --- a/lib/features/remote_control/presentation/pages/remote_control_page.dart +++ b/lib/features/remote_control/presentation/pages/remote_control_page.dart @@ -33,6 +33,7 @@ class _RemoteControlPageState extends State { DevicesCubit? _devicesCubit; StreamSubscription? _permissionSubscription; // 🔥 权限监听订阅 bool _isShowingPermissionDialog = false; // 🔥 防止弹窗重复显示 + bool? _lastPermissionDialogState; // 🔥 记录上次弹窗状态,检测状态变化 bool _isLoadingPermission = true; // 🔥 标记是否正在加载权限状态 @override @@ -44,10 +45,28 @@ class _RemoteControlPageState extends State { // 🔥 监听权限弹窗状态变化 (只订阅一次) if (_permissionSubscription == null) { _permissionSubscription = _cubit.stream.listen((state) { - // 🔥 加强防重复逻辑:只在状态真正变化且不在显示弹窗时才显示 - if (mounted && + // 🔥 关键修复:只有当状态更新类型是弹窗更新时才处理弹窗逻辑 + // 设备状态更新(500ms)不会触发弹窗显示 + if (state.updateType != 'permission_dialog') { + // 如果是设备状态更新,只重置状态记录,不显示弹窗 + if (!state.showPermissionRequestDialog) { + _lastPermissionDialogState = false; + } + return; // 跳过设备状态更新 + } + + // 🔥 只有当状态从 false 变为 true 时才显示弹窗 + // 使用三个条件确保不会重复显示 + final wasFalseBefore = + _lastPermissionDialogState == null || + _lastPermissionDialogState == false; + final shouldShow = state.showPermissionRequestDialog && - !_isShowingPermissionDialog) { + !_isShowingPermissionDialog && + wasFalseBefore; + + if (mounted && shouldShow) { + _lastPermissionDialogState = true; // 记录当前状态 _isShowingPermissionDialog = true; debugPrint('🔔 [权限弹窗] 检测到 showPermissionRequestDialog=true,准备显示弹窗'); // 使用微任务确保标志位已设置 @@ -55,10 +74,17 @@ class _RemoteControlPageState extends State { _showPermissionDialog(state).then((_) { debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位'); _isShowingPermissionDialog = false; // 弹窗关闭后重置标志 + _lastPermissionDialogState = false; // 重置状态记录 }); }); + } else if (!state.showPermissionRequestDialog) { + // 状态变为 false 时,重置记录 + _lastPermissionDialogState = false; } }); + } else { + // 🔥 如果已经订阅,重置状态记录,确保下次能正确检测状态变化 + _lastPermissionDialogState = _cubit.state.showPermissionRequestDialog; } } @@ -344,7 +370,7 @@ class _RemoteControlPageState extends State { actions: [ TextButton( onPressed: () async { - // debugPrint('👆 [权限弹窗] 用户点击了拒绝按钮'); + // debugPrint('👆 [权限弹窗] 用户点击了拒绝按钮'); final deviceId = context .read() @@ -355,19 +381,19 @@ class _RemoteControlPageState extends State { final remoteCubit = context.read(); // 🔥 添加详细状态日志 - 检查为什么TCP指令发不出去 - // debugPrint('👆 [权限弹窗] ====================状态检查===================='); - // debugPrint('👆 [权限弹窗] showPermissionRequestDialog: ${remoteCubit.state.showPermissionRequestDialog}'); - // debugPrint('👆 [权限弹窗] targetDevice: ${remoteCubit.state.targetDevice}'); - // debugPrint('👆 [权限弹窗] requestingDeviceId: ${remoteCubit.state.requestingDeviceId}'); - // debugPrint('👆 [权限弹窗] hasPermission: ${remoteCubit.state.hasPermission}'); - // debugPrint('👆 [权限弹窗] =============================================='); + // debugPrint('👆 [权限弹窗] ====================状态检查===================='); + // debugPrint('👆 [权限弹窗] showPermissionRequestDialog: ${remoteCubit.state.showPermissionRequestDialog}'); + // debugPrint('👆 [权限弹窗] targetDevice: ${remoteCubit.state.targetDevice}'); + // debugPrint('👆 [权限弹窗] requestingDeviceId: ${remoteCubit.state.requestingDeviceId}'); + // debugPrint('👆 [权限弹窗] hasPermission: ${remoteCubit.state.hasPermission}'); + // debugPrint('👆 [权限弹窗] =============================================='); - /* debugPrint( + /* debugPrint( '📋 [权限弹窗] targetDevice=${remoteCubit.state.targetDevice}', );*/ // 🔥 先发送 TCP 响应,等完成后再关闭弹窗 - // debugPrint('🔑 [权限弹窗] 用户拒绝,调用 confirmPermissionResponse'); + // debugPrint('🔑 [权限弹窗] 用户拒绝,调用 confirmPermissionResponse'); final targetDevice = remoteCubit.state.targetDevice; if (targetDevice != null) { debugPrint( @@ -385,8 +411,18 @@ class _RemoteControlPageState extends State { ); } - // 🔥 关闭弹窗(在 TCP 发送完成后) + // 🔥 关键修复:先重置弹窗标志位,再关闭弹窗 + // 避免 Navigator.pop() 后上下文失效导致后续代码不执行 + context.read().resetPermissionCoolDown(); + context.read().emit( + context.read().state.copyWith( + showPermissionRequestDialog: false, + ), + ); + + // 🔥 关闭弹窗(在状态重置之后) Navigator.pop(dialogContext); + debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位'); }, child: Text( AppLocalizations.of( @@ -407,7 +443,7 @@ class _RemoteControlPageState extends State { final remoteCubit = context.read(); // 🔥 添加详细状态日志 - 检查为什么TCP指令发不出去 - /* debugPrint( + /* debugPrint( '👆 [权限弹窗] ====================状态检查====================', ); debugPrint( @@ -434,9 +470,7 @@ class _RemoteControlPageState extends State { debugPrint('🔑 [权限弹窗] 用户同意,调用 confirmPermissionResponse'); final targetDevice = remoteCubit.state.targetDevice; if (targetDevice != null) { - debugPrint( - '✅ [权限弹窗] targetDevice 不为空', - ); + debugPrint('✅ [权限弹窗] targetDevice 不为空'); await remoteCubit.confirmPermissionResponse( targetDevice.deviceName, 'app', @@ -449,8 +483,18 @@ class _RemoteControlPageState extends State { ); } - // 🔥 关闭弹窗(在 TCP 发送完成后) + // 🔥 关键修复:先重置弹窗标志位,再关闭弹窗 + // 避免 Navigator.pop() 后上下文失效导致后续代码不执行 + context.read().resetPermissionCoolDown(); + context.read().emit( + context.read().state.copyWith( + showPermissionRequestDialog: false, + ), + ); + + // 🔥 关闭弹窗(在状态重置之后) Navigator.pop(dialogContext); + debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位'); }, child: Text( AppLocalizations.of(