diff --git a/lib/core/network/tcp/tcp_client.dart b/lib/core/network/tcp/tcp_client.dart index 42afce31..58682def 100644 --- a/lib/core/network/tcp/tcp_client.dart +++ b/lib/core/network/tcp/tcp_client.dart @@ -74,6 +74,8 @@ class TcpClient { port, timeout: const Duration(seconds: 5), ); + // 🔥 关键修复:禁用Nagle算法,确保小包立即发送 + _socket!.setOption(SocketOption.tcpNoDelay, true); debugPrint('✅ [TCP] 连接成功!'); // ✅ 必须看到这条 _logger.logWithLevel('✅ [TCP] 连接成功!'); // 开始认证tcp @@ -222,6 +224,8 @@ class TcpClient { ..addByte(0xAB); _socket!.add(builder.takeBytes()); + // 🔥 关键修复:强制flush,确保数据立即发送 + _socket!.flush(); } void sendHeartbeat() { @@ -449,6 +453,8 @@ _logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevic port, timeout: const Duration(seconds: 5), ); + // 🔥 关键修复:禁用Nagle算法,确保小包立即发送 + _socket!.setOption(SocketOption.tcpNoDelay, true); // debugPrint('✅ 被动[TCP] 连接成功!'); // ✅ 必须看到这条 _logger.logWithLevel('✅ 被动[TCP] 连接成功!'); diff --git a/lib/features/remote_control/data/datasources/remote_http_datasource.dart b/lib/features/remote_control/data/datasources/remote_http_datasource.dart index 7a8560ec..fac2103f 100644 --- a/lib/features/remote_control/data/datasources/remote_http_datasource.dart +++ b/lib/features/remote_control/data/datasources/remote_http_datasource.dart @@ -73,7 +73,7 @@ class RemoteHttpDatasource { ///app退出远程控制后释放权限 Future releaseRemoteControlViaHttp( String platform) async { - _logger.logWithLevel("app退出远程控制后释放权限 开始"); + // _logger.logWithLevel("app退出远程控制后释放权限 开始"); final user = await _userStorage.getUser(); if (user == null) { return false; @@ -91,20 +91,12 @@ class RemoteHttpDatasource { try { final responseData = response.data as Map; //debugPrint("📊 [HTTP 响应] 完整数据:$responseData"); - _logger.log("releaseRemoteControlViaHttp 响应] 完整数据:$responseData"); - // 🔥 关键:先获取响应的 data 字段,再获取 remoteControl - final dataField = responseData['data'] as Map?; - if (dataField != null) { - final bool hasRemoteControl = dataField['remoteControl'] as bool? ?? false; - //debugPrint("✅ [HTTP 响应] remoteControl=$hasRemoteControl"); - _logger.log("releaseRemoteControlViaHttp响应] remoteControl=$hasRemoteControl"); - // 🔥 直接返回 remoteControl 的布尔值 - return hasRemoteControl; - } else { - //debugPrint("❌ [HTTP 响应] 缺少 data 字段"); - _logger.log("❌ [HTTP 响应] 缺少 data 字段"); - return false; - } + //_logger.log("releaseRemoteControlViaHttp 响应] 完整数据:$responseData"); + if(responseData['code']==200){ + // _logger.logWithLevel("app退出远程控制后释放权限 成功"); + return true; + } + return false; } catch (e) { // debugPrint('❌ [RemoteHttp] 解析响应失败:$e'); _logger.log('❌ [RemoteHttp] 解析响应失败:$e'); diff --git a/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart b/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart index 1081c548..8b5b32a2 100644 --- a/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart +++ b/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart @@ -35,15 +35,9 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { void sendControlMachineCmd(MachineControlStatusEntity status) { // 1. 调用算法:将摇杆坐标 (x, y) 转换为左右轮电机转速 final speeds = _diffSteer.calculate(status.originX, status.originY); - //debugPrint('🎮 [摇杆数据] originX: ${status.originX}, originY: ${status.originY}'); _logger.logWithLevel('⚙️ [电机速度] left: ${speeds['left']}, right: ${speeds['right']}'); - //debugPrint('⚙️ [电机速度] left: ${speeds['left']}, right: ${speeds['right']}'); - _logger.logWithLevel('⚙️ [电机速度] left: ${speeds['left']}, right: ${speeds['right']}'); - //debugPrint('🚨 [急停状态] emergency: ${status.isEmergency ? 1 : 0}'); - _logger.logWithLevel('🚨 [急停状态] emergency: ${status.isEmergency ? 1 : 0}'); - - // 2. 调用 Codec:仅生成协议要求的 8 字节 Payload 负载数据 + // 2. 调用 Codec:生成协议要求的 8 字节 Payload final payload = MachineProtocolCodec.encodeRemoteControlPayload( left: speeds['left']!, right: speeds['right']!, @@ -52,18 +46,12 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { ignition: status.ignitionStatus, emergency: status.isEmergency ? 1 : 0, ); - //debugPrint('📦 [发送数据] payload: ${payload}'); - _logger.logWithLevel('📦 [发送数据] payload: ${payload}'); - //debugPrint('📦 [发送数据] emergency 字节值:${payload[7]}'); - _logger.logWithLevel('📦 [发送数据] emergency 字节值:${payload[7]}'); - // 3. 调用 TcpClient:发送指令。 - // TcpClient.sendRaw 会自动帮你加上 [0xAB, 0xAA] 头和 [0xAA, 0xAB] 尾 + + // 3. 调用 TcpClient 发送指令 _tcpClient.sendRaw( - MachineProtocolConstants.cmdRemoteControl, // 这里通常是 0x00 + MachineProtocolConstants.cmdRemoteControl, payload, ); - //debugPrint('✅ [TCP] 指令已发送'); - _logger.logWithLevel('✅ [TCP] 指令已发送'); } @override diff --git a/lib/features/remote_control/domain/entities/machine_control_status_entity.dart b/lib/features/remote_control/domain/entities/machine_control_status_entity.dart index 2dc061ba..5389b9b0 100644 --- a/lib/features/remote_control/domain/entities/machine_control_status_entity.dart +++ b/lib/features/remote_control/domain/entities/machine_control_status_entity.dart @@ -1,4 +1,6 @@ -class MachineControlStatusEntity { +import 'package:equatable/equatable.dart'; + +class MachineControlStatusEntity extends Equatable { final int originX; final int originY; final int chassisLift; // 0:停, 1:上, 2:下 @@ -32,4 +34,14 @@ class MachineControlStatusEntity { isEmergency: emergency ?? isEmergency, ); } + + @override + List get props => [ + originX, + originY, + chassisLift, + mowerSpeed, + ignitionStatus, + isEmergency, + ]; } 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 c7810dba..b5c82948 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart @@ -28,6 +28,8 @@ class RemoteControlCubit extends Cubit { static const platform = MethodChannel('com.maibu.satabot/ping'); int _currentPing = 50; final ILoggerService _logger = GetIt.I(); + DateTime? _lastStopTime; // 🔥 记录最后一次停止时间 + Timer? _clearStopTimeTimer; // 🔥 用于管理 _lastStopTime 的清除 @@ -226,9 +228,12 @@ class RemoteControlCubit extends Cubit { // 2. 开启 100ms 控制循环 (在进入遥控页面或点击“开始”时调用) - void startControlLoop() { +/* void startControlLoop() { _timer?.cancel(); _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) { + // 🔥 关键修复:每次循环都重新读取最新的 state + final currentEntity = state.controlEntity; + // 🔥 安全检查:无权限或急停时不发送 if (!state.hasPermission || state.isEmergency) { debugPrint('⚠️ [定时器] 跳过发送 - hasPermission=${state.hasPermission}, isEmergency=${state.isEmergency}'); @@ -236,11 +241,53 @@ class RemoteControlCubit extends Cubit { } - debugPrint('⏰ [定时器] 发送控制指令 - originX=${state.controlEntity.originX}, originY=${state.controlEntity.originY}'); - _logger.logWithLevel('真实的发送的实体 - originX: ${state.controlEntity.originX}, originY: ${state.controlEntity.originY}'); - _repository.sendControlMachineCmd(state.controlEntity); + debugPrint('⏰ [定时器] 发送控制指令 - originX=${currentEntity.originX}, originY=${currentEntity.originY}'); + _logger.logWithLevel('真实的发送的实体 - originX: ${currentEntity.originX}, originY: ${currentEntity.originY}'); + _repository.sendControlMachineCmd(currentEntity); }); emit(state.copyWith(status: RemoteControlStatus.controlling)); + }*/ + void startControlLoop() { + _timer?.cancel(); + _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) { + if (isClosed) { + timer.cancel(); + return; + } + + // ✅ 必须通过方法拿最新 state,不能直接读! + final entity = _getCurrentControlEntity(); + + // 🔥 关键日志:检查权限状态 + if (!state.hasPermission) { + debugPrint('⚠️ [定时器] 无权限,跳过发送 - hasPermission=${state.hasPermission}, isEmergency=${state.isEmergency}'); + return; + } + + if (state.isEmergency) { + debugPrint('🚨 [定时器] 急停状态,跳过发送'); + return; + } + + // 🔥 新增:如果刚停止5000ms内,暂停所有发送(包括零值)- 与_lastStopTime清除时间一致 + if (_lastStopTime != null) { + final elapsed = DateTime.now().difference(_lastStopTime!).inMilliseconds; + if (elapsed < 5000) { + debugPrint('⚠️ [定时器] 停止保护期内,暂停所有发送 - 已过${elapsed}ms'); + return; // 保护期内不发送任何指令,让_sendStopCommandRepeatedly独占通道 + } + // 🔥 关键修复:不在这里清空 _lastStopTime,让它继续生效以拦截 updateOriginX/Y + } + + _logger.logWithLevel('[定时器] 发送控制指令 - originX=${entity.originX}, originY=${entity.originY}'); + debugPrint('📤 [定时器] 准备发送 - originX=${entity.originX}, originY=${entity.originY}, hasPermission=${state.hasPermission}, _lastStopTime=$_lastStopTime'); + _repository.sendControlMachineCmd(entity); + }); + } +//通过方法拿最新 state + MachineControlStatusEntity _getCurrentControlEntity() { + _logger.logWithLevel('真实的发送的实体 - originX: ${state.controlEntity.originX}, originY: ${state.controlEntity.originY}'); + return state.controlEntity; } // 3. 更新摇杆数据 @@ -272,36 +319,109 @@ class RemoteControlCubit extends Cubit { } void updateOriginY(int y) { - debugPrint('📥 [updateOriginY] 收到参数 y=$y'); - final updatedEntity = state.controlEntity.copyWith( - y: y, - ); - debugPrint('📤 [updateOriginY] 创建 updatedEntity.y=${updatedEntity.originY}'); - emit(state.copyWith(controlEntity: updatedEntity)); - debugPrint('✅ [updateOriginY] emit 完成,当前 state.controlEntity.originY=${state.controlEntity.originY}'); - - // 🔥 关键修复:如果归零,立即发送停止指令,使用更新后的实体 - if (y == 0) { - _logger.logWithLevel('🛑 [updateOriginY] 检测到归零,立即发送停止指令'); - debugPrint('🛑 [updateOriginY] 发送 updatedEntity: originX=${updatedEntity.originX}, originY=${updatedEntity.originY}'); - _repository.sendControlMachineCmd(updatedEntity); + debugPrint('📥 [updateOriginY] 被调用 - y=$y, _lastStopTime=$_lastStopTime'); + // 🔥 关键修复:如果刚停止5000ms内,拒绝非零值的更新 - 与定时器保护期一致 + if (_lastStopTime != null) { + final elapsed = DateTime.now().difference(_lastStopTime!).inMilliseconds; + debugPrint('📥 [updateOriginY] 检查保护期 - elapsed=${elapsed}ms, y=$y'); + if (elapsed < 5000 && y != 0) { + debugPrint('⚠️ [updateOriginY] 保护期内拒绝非零值 - y=$y, 已过${elapsed}ms'); + return; // 拒绝更新 + } + } else { + debugPrint('📥 [updateOriginY] _lastStopTime为null,跳过保护期检查'); } + + final updatedEntity = state.controlEntity.copyWith(y: y); + emit(state.copyWith(controlEntity: updatedEntity)); + debugPrint('📝 [updateOriginY] state已更新 - originY=$y'); } + void updateOriginX(int x) { - debugPrint('📥 [updateOriginX] 收到参数 x=$x'); - final updatedEntity = state.controlEntity.copyWith( - x: x, - ); - debugPrint('📤 [updateOriginX] 创建 updatedEntity.x=${updatedEntity.originX}'); - emit(state.copyWith(controlEntity: updatedEntity)); - debugPrint('✅ [updateOriginX] emit 完成,当前 state.controlEntity.originX=${state.controlEntity.originX}'); - - // 🔥 关键修复:如果归零,立即发送停止指令,使用更新后的实体 - if (x == 0) { - _logger.logWithLevel('🛑 [updateOriginX] 检测到归零,立即发送停止指令'); - debugPrint('🛑 [updateOriginX] 发送 updatedEntity: originX=${updatedEntity.originX}, originY=${updatedEntity.originY}'); - _repository.sendControlMachineCmd(updatedEntity); + debugPrint('📥 [updateOriginX] 被调用 - x=$x, _lastStopTime=$_lastStopTime'); + // 🔥 关键修复:如果刚停止5000ms内,拒绝非零值的更新 - 与定时器保护期一致 + if (_lastStopTime != null) { + final elapsed = DateTime.now().difference(_lastStopTime!).inMilliseconds; + debugPrint('📥 [updateOriginX] 检查保护期 - elapsed=${elapsed}ms, x=$x'); + if (elapsed < 5000 && x != 0) { + debugPrint('⚠️ [updateOriginX] 保护期内拒绝非零值 - x=$x, 已过${elapsed}ms'); + return; // 拒绝更新 + } + } else { + debugPrint('📥 [updateOriginX] _lastStopTime为null,跳过保护期检查'); } + + final updatedEntity = state.controlEntity.copyWith(x: x); + emit(state.copyWith(controlEntity: updatedEntity)); + debugPrint('📝 [updateOriginX] state已更新 - originX=$x'); + } + + /// 🔥 安全方法:同时清零双轴,确保只发送一次完全停止指令 + Future stopAllMovement() async { + debugPrint('🛑 [stopAllMovement] 开始执行 - 当前state: originX=${state.controlEntity.originX}, originY=${state.controlEntity.originY}'); + + // 🔥 关键修复1:取消旧的清除定时器,防止多个定时器竞争 + _clearStopTimeTimer?.cancel(); + + // 🔥 关键修复2:立即设置 _lastStopTime,防止在 stopAllMovement 执行期间 state 被更新 + _lastStopTime = DateTime.now(); + debugPrint('🛑 [stopAllMovement] _lastStopTime 已设置为 $_lastStopTime'); + + // 🔥 关键修复3:先暂停定时器,防止定时器在停止期间发送旧的运动指令 + _timer?.cancel(); + _timer = null; // 🔥 彻底清空,防止重复启动 + + final updatedEntity = state.controlEntity.copyWith(x: 0, y: 0); + + // 一次性更新双轴(即使无权限也要更新本地state) + emit(state.copyWith(controlEntity: updatedEntity)); + + // 🔥 安全底线:停止指令必须无视权限强制发送! + debugPrint('🛑 [stopAllMovement] 双轴归零,发送停止指令 - originX=0, originY=0'); + await _sendStopCommandRepeatedly(updatedEntity); // ✅ 等待所有指令发送完成 + + // 🔥 恢复定时器(但保护期内不会发送任何指令) + await Future.delayed(const Duration(milliseconds: 500)); // 🔥 等待500ms让下位机执行停止 + startControlLoop(); + + // 🔥 关键修复4:使用 Timer 而不是 Future.delayed,确保只有一个定时器在运行 + _clearStopTimeTimer = Timer(const Duration(seconds: 5), () { + _lastStopTime = null; + debugPrint('✅ [保护期] 5秒后自动清除标记'); + }); + + debugPrint('✅ [stopAllMovement] 执行完成'); + } + + /// 🔥 统一方法:连续发送30次停止指令,彻底清空TCP缓冲区 + Future _sendStopCommandRepeatedly(MachineControlStatusEntity stopEntity) async { + debugPrint('🛑 [紧急停止] 开始连续发送30次停止指令 - originX=${stopEntity.originX}, originY=${stopEntity.originY}'); + // 🔥 注意:_lastStopTime 已经在 stopAllMovement() 中设置了,这里不需要再设置 + + int sentCount = 0; + // 🔥 关键修复:每次发送间隔5ms,避免TCP合并/丢弃 + for (int i = 0; i < 20; i++) { + _repository.sendControlMachineCmd(stopEntity); + sentCount++; + if (i % 5 == 0) { + debugPrint('🛑 [紧急停止] 已发送第${i + 1}次'); + } + if (i < 19) { + await Future.delayed(const Duration(milliseconds: 5)); + } + } + + // 🔥 延迟后再发送10次(双重保险,对抗网络抖动) + await Future.delayed(const Duration(milliseconds: 100)); + for (int i = 0; i < 10; i++) { + _repository.sendControlMachineCmd(stopEntity); + sentCount++; + if (i < 9) { + await Future.delayed(const Duration(milliseconds: 5)); + } + } + + debugPrint('✅ [紧急停止] 所有30次停止指令已发出'); } // 5. 停止控制循环 @@ -330,6 +450,7 @@ class RemoteControlCubit extends Cubit { @override Future close() { _timer?.cancel(); // 退出页面时务必销毁定时器 + _clearStopTimeTimer?.cancel(); // 🔥 清除保护期定时器 _kickOutSub?.cancel(); _stringMessageSub?.cancel(); return super.close(); diff --git a/lib/features/remote_control/presentation/widgets/left_joystick_area.dart b/lib/features/remote_control/presentation/widgets/left_joystick_area.dart index a0d43d9d..d2ba5288 100644 --- a/lib/features/remote_control/presentation/widgets/left_joystick_area.dart +++ b/lib/features/remote_control/presentation/widgets/left_joystick_area.dart @@ -50,11 +50,13 @@ class _LeftJoystickAreaState extends State { }, onPress: () { _isTouching = true; + debugPrint('onPress'); _triggerVibration(); }, - onPanEnd: () { + onPanEnd: () async { // 🔥 松手 100% 归零 - _stopJoystick(); + debugPrint('onPanEnd'); + await _stopJoystick(); }, ), ), @@ -73,11 +75,13 @@ class _LeftJoystickAreaState extends State { } // 统一停车方法(万能保险) - void _stopJoystick() { + Future _stopJoystick() async { _isTouching = false; _lastSentY = 0; - debugPrint('✅ 摇杆已停止 -> 强制 Y=0 停车'); - context.read().updateOriginY(0); + debugPrint('✅ 摇杆已停止 -> 强制 X=0, Y=0 停车'); + + // 🔥 使用安全方法,一次性清零双轴,确保只发送 (X=0, Y=0) + await context.read().stopAllMovement(); } void _triggerVibration() { diff --git a/lib/features/remote_control/presentation/widgets/right_joystick_area.dart b/lib/features/remote_control/presentation/widgets/right_joystick_area.dart index 5d6db415..c1cb77ef 100644 --- a/lib/features/remote_control/presentation/widgets/right_joystick_area.dart +++ b/lib/features/remote_control/presentation/widgets/right_joystick_area.dart @@ -1,4 +1,4 @@ -import 'package:cc_ui_kit/cc_ui_kit.dart'; + import 'package:cc_ui_kit/cc_ui_kit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:vibration/vibration.dart'; @@ -46,10 +46,12 @@ class _RightJoystickAreaState extends State { context.read().updateOriginX(finalX); }, onPress: _triggerVibration, - onPanEnd: () { - debugPrint('🕹️ [右摇杆] 松手,强制X=0'); + onPanEnd: () async { + debugPrint('🕹️ [右摇杆] 松手,强制 X=0, Y=0'); _lastX = 0; - context.read().updateOriginX(0); + + // 🔥 使用安全方法,一次性清零双轴,确保只发送 (X=0, Y=0) + await context.read().stopAllMovement(); }, ), ), diff --git a/packages/cc_ui_kit/lib/src/cc_joystick.dart b/packages/cc_ui_kit/lib/src/cc_joystick.dart index fa5ebf3a..5d4c5cdf 100644 --- a/packages/cc_ui_kit/lib/src/cc_joystick.dart +++ b/packages/cc_ui_kit/lib/src/cc_joystick.dart @@ -14,7 +14,7 @@ class CCJoystick extends StatefulWidget { final AxisHint axisHint; final Function(JoystickValue) onValueChanged; final VoidCallback? onPress; - final VoidCallback? onPanEnd; // 🔥 新增:松手回调 + final Future Function()? onPanEnd; // 🔥 修复:改为异步回调 final double radius; const CCJoystick({ @@ -34,6 +34,7 @@ class _CustomJoystickState extends State with SingleTickerProviderStateMixin { Offset _offset = Offset.zero; late AnimationController _floatController; + bool _isPanning = false; // 🔥 跟踪是否正在拖拽 @override void initState() { @@ -71,47 +72,73 @@ class _CustomJoystickState extends State @override Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, // 🔥 改为 opaque,确保所有触摸事件都被捕获 - onPanStart: (_) => widget.onPress?.call(), - onPanUpdate: (details) { - setState(() { - Offset newOffset = _offset + details.delta; - double distance = newOffset.distance; - // 限制在圆圈内 - if (distance <= widget.radius) { - _offset = newOffset; - } else { - double angle = atan2(newOffset.dy, newOffset.dx); - _offset = Offset( - cos(angle) * widget.radius, - sin(angle) * widget.radius, - ); - } - }); - _updateValue(); + return Listener( + // 🔥 兜底:监听全局指针抬起事件,防止 onPanEnd 丢失 + onPointerUp: (_) async { + if (_isPanning) { + debugPrint('🛡️ [兜底] 全局指针抬起,强制归零'); + await _handlePanEnd(); + } }, - onPanEnd: (_) { - setState(() => _offset = Offset.zero); // 归位 - _updateValue(); - widget.onPanEnd?.call(); // 🔥 调用松手回调 - }, - child: AnimatedBuilder( - animation: _floatController, - builder: (context, child) { - return CustomPaint( - size: Size(widget.radius * 2, widget.radius * 2), - painter: JoystickPainter( - offset: _offset, - radius: widget.radius, - axisHint: widget.axisHint, - floatValue: _floatController.value * 10 - 5, // -5f 到 5f - ), - ); + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanStart: (_) { + _isPanning = true; + widget.onPress?.call(); }, + onPanUpdate: (details) { + setState(() { + Offset newOffset = _offset + details.delta; + double distance = newOffset.distance; + // 限制在圆圈内 + if (distance <= widget.radius) { + _offset = newOffset; + } else { + double angle = atan2(newOffset.dy, newOffset.dx); + _offset = Offset( + cos(angle) * widget.radius, + sin(angle) * widget.radius, + ); + } + }); + _updateValue(); + }, + onPanEnd: (_) async { + debugPrint('✋ [正常] onPanEnd 触发'); + await _handlePanEnd(); + }, + onPanCancel: () async { + // 🔥 新增:处理手势取消(如来电中断) + debugPrint('⚠️ [异常] onPanCancel 触发'); + await _handlePanEnd(); + }, + child: AnimatedBuilder( + animation: _floatController, + builder: (context, child) { + return CustomPaint( + size: Size(widget.radius * 2, widget.radius * 2), + painter: JoystickPainter( + offset: _offset, + radius: widget.radius, + axisHint: widget.axisHint, + floatValue: _floatController.value * 10 - 5, // -5f 到 5f + ), + ); + }, + ), ), ); } + + // 🔥 统一的松手处理逻辑 + Future _handlePanEnd() async { + if (!_isPanning) return; + _isPanning = false; + setState(() => _offset = Offset.zero); // 归位 + // 🔥 关键修复:不调用 _updateValue(),避免触发 onValueChanged 导致重复发送 + // _updateValue(); // ❌ 注释掉,防止发送错误的停止指令 + await widget.onPanEnd?.call(); // 🔥 等待异步回调完成 + } } class JoystickPainter extends CustomPainter {