修复emit异步导致的竞态+tcp缓冲取挤压+网络堵塞+定时器恢复的造成归0被吞的情况

完成APP端释放权限的接口的集成和数据返回获取
This commit is contained in:
2026-04-16 17:39:54 +08:00
parent 407bcf026c
commit 976c5fca2f
8 changed files with 260 additions and 108 deletions

View File

@@ -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] 连接成功!');

View File

@@ -73,7 +73,7 @@ class RemoteHttpDatasource {
///app退出远程控制后释放权限
Future<bool> 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<String, dynamic>;
//debugPrint("📊 [HTTP 响应] 完整数据:$responseData");
_logger.log("releaseRemoteControlViaHttp 响应] 完整数据:$responseData");
// 🔥 关键:先获取响应的 data 字段,再获取 remoteControl
final dataField = responseData['data'] as Map<String, dynamic>?;
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');

View File

@@ -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

View File

@@ -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<Object?> get props => [
originX,
originY,
chassisLift,
mowerSpeed,
ignitionStatus,
isEmergency,
];
}

View File

@@ -28,6 +28,8 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
static const platform = MethodChannel('com.maibu.satabot/ping');
int _currentPing = 50;
final ILoggerService _logger = GetIt.I<ILoggerService>();
DateTime? _lastStopTime; // 🔥 记录最后一次停止时间
Timer? _clearStopTimeTimer; // 🔥 用于管理 _lastStopTime 的清除
@@ -226,9 +228,12 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
// 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<RemoteControlState> {
}
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<RemoteControlState> {
}
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<void> 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<void> _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<RemoteControlState> {
@override
Future<void> close() {
_timer?.cancel(); // 退出页面时务必销毁定时器
_clearStopTimeTimer?.cancel(); // 🔥 清除保护期定时器
_kickOutSub?.cancel();
_stringMessageSub?.cancel();
return super.close();

View File

@@ -50,11 +50,13 @@ class _LeftJoystickAreaState extends State<LeftJoystickArea> {
},
onPress: () {
_isTouching = true;
debugPrint('onPress');
_triggerVibration();
},
onPanEnd: () {
onPanEnd: () async {
// 🔥 松手 100% 归零
_stopJoystick();
debugPrint('onPanEnd');
await _stopJoystick();
},
),
),
@@ -73,11 +75,13 @@ class _LeftJoystickAreaState extends State<LeftJoystickArea> {
}
// 统一停车方法(万能保险)
void _stopJoystick() {
Future<void> _stopJoystick() async {
_isTouching = false;
_lastSentY = 0;
debugPrint('✅ 摇杆已停止 -> 强制 Y=0 停车');
context.read<RemoteControlCubit>().updateOriginY(0);
debugPrint('✅ 摇杆已停止 -> 强制 X=0, Y=0 停车');
// 🔥 使用安全方法,一次性清零双轴,确保只发送 (X=0, Y=0)
await context.read<RemoteControlCubit>().stopAllMovement();
}
void _triggerVibration() {

View File

@@ -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<RightJoystickArea> {
context.read<RemoteControlCubit>().updateOriginX(finalX);
},
onPress: _triggerVibration,
onPanEnd: () {
debugPrint('🕹️ [右摇杆] 松手,强制X=0');
onPanEnd: () async {
debugPrint('🕹️ [右摇杆] 松手,强制 X=0, Y=0');
_lastX = 0;
context.read<RemoteControlCubit>().updateOriginX(0);
// 🔥 使用安全方法,一次性清零双轴,确保只发送 (X=0, Y=0)
await context.read<RemoteControlCubit>().stopAllMovement();
},
),
),

View File

@@ -14,7 +14,7 @@ class CCJoystick extends StatefulWidget {
final AxisHint axisHint;
final Function(JoystickValue) onValueChanged;
final VoidCallback? onPress;
final VoidCallback? onPanEnd; // 🔥 新增:松手回调
final Future<void> Function()? onPanEnd; // 🔥 修复:改为异步回调
final double radius;
const CCJoystick({
@@ -34,6 +34,7 @@ class _CustomJoystickState extends State<CCJoystick>
with SingleTickerProviderStateMixin {
Offset _offset = Offset.zero;
late AnimationController _floatController;
bool _isPanning = false; // 🔥 跟踪是否正在拖拽
@override
void initState() {
@@ -71,47 +72,73 @@ class _CustomJoystickState extends State<CCJoystick>
@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<void> _handlePanEnd() async {
if (!_isPanning) return;
_isPanning = false;
setState(() => _offset = Offset.zero); // 归位
// 🔥 关键修复:不调用 _updateValue(),避免触发 onValueChanged 导致重复发送
// _updateValue(); // ❌ 注释掉,防止发送错误的停止指令
await widget.onPanEnd?.call(); // 🔥 等待异步回调完成
}
}
class JoystickPainter extends CustomPainter {