Merge branch 'feature/my' of http://1.95.137.212:57001/APP/FlutterApp into feature/my
This commit is contained in:
@@ -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] 连接成功!');
|
||||
|
||||
|
||||
@@ -70,4 +70,39 @@ class RemoteHttpDatasource {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
///app退出远程控制后释放权限
|
||||
Future<bool> releaseRemoteControlViaHttp( String platform) async {
|
||||
// _logger.logWithLevel("app退出远程控制后释放权限 开始");
|
||||
final user = await _userStorage.getUser();
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
final token = user.token;
|
||||
final response = await _dio.post(
|
||||
'/forward/device/releaseControl',
|
||||
options: Options(
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${token}',
|
||||
},
|
||||
),
|
||||
data: {'platform': "app"});
|
||||
try {
|
||||
final responseData = response.data as Map<String, dynamic>;
|
||||
//debugPrint("📊 [HTTP 响应] 完整数据:$responseData");
|
||||
//_logger.log("releaseRemoteControlViaHttp 响应] 完整数据:$responseData");
|
||||
if(responseData['code']==200){
|
||||
// _logger.logWithLevel("app退出远程控制后释放权限 成功");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
// debugPrint('❌ [RemoteHttp] 解析响应失败:$e');
|
||||
_logger.log('❌ [RemoteHttp] 解析响应失败:$e');
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,17 +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']!,
|
||||
@@ -54,15 +46,12 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository {
|
||||
ignition: status.ignitionStatus,
|
||||
emergency: status.isEmergency ? 1 : 0,
|
||||
);
|
||||
debugPrint('📦 [发送数据] payload: ${payload}');
|
||||
debugPrint('📦 [发送数据] emergency 字节值:${payload[7]}');
|
||||
// 3. 调用 TcpClient:发送指令。
|
||||
// TcpClient.sendRaw 会自动帮你加上 [0xAB, 0xAA] 头和 [0xAA, 0xAB] 尾
|
||||
|
||||
// 3. 调用 TcpClient 发送指令
|
||||
_tcpClient.sendRaw(
|
||||
MachineProtocolConstants.cmdRemoteControl, // 这里通常是 0x00
|
||||
MachineProtocolConstants.cmdRemoteControl,
|
||||
payload,
|
||||
);
|
||||
debugPrint('✅ [TCP] 指令已发送');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -91,4 +80,9 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository {
|
||||
_remoteTcp.sendSwitchControlResponse(false, deviceId);
|
||||
}
|
||||
}
|
||||
///app退出远程控制后释放权限
|
||||
@override
|
||||
Future<bool> releasePermission(String platform) async {
|
||||
return await _remoteHttp.releaseRemoteControlViaHttp(platform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -20,4 +20,7 @@ abstract class RemoteControlRepository {
|
||||
|
||||
/// 新增:响应控制权限 同意和拒绝(发送 0x05 指令)
|
||||
void respondPermission(bool bool, String deviceId);
|
||||
|
||||
///APP 推出远程控制页面后释放权限
|
||||
Future<bool> releasePermission(String platform);
|
||||
}
|
||||
|
||||
@@ -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,13 +228,66 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
|
||||
// 2. 开启 100ms 控制循环 (在进入遥控页面或点击“开始”时调用)
|
||||
/* 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}');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
// 核心调用:直接把 state 里的实体丢给 repository
|
||||
_repository.sendControlMachineCmd(state.controlEntity);
|
||||
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);
|
||||
});
|
||||
emit(state.copyWith(status: RemoteControlStatus.controlling));
|
||||
}
|
||||
//通过方法拿最新 state
|
||||
MachineControlStatusEntity _getCurrentControlEntity() {
|
||||
_logger.logWithLevel('真实的发送的实体 - originX: ${state.controlEntity.originX}, originY: ${state.controlEntity.originY}');
|
||||
return state.controlEntity;
|
||||
}
|
||||
|
||||
// 3. 更新摇杆数据
|
||||
@@ -264,16 +319,109 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
}
|
||||
|
||||
void updateOriginY(int y) {
|
||||
final updatedEntity = state.controlEntity.copyWith(
|
||||
y: y,
|
||||
);
|
||||
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) {
|
||||
final updatedEntity = state.controlEntity.copyWith(
|
||||
x: x,
|
||||
);
|
||||
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. 停止控制循环
|
||||
@@ -302,6 +450,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
@override
|
||||
Future<void> close() {
|
||||
_timer?.cancel(); // 退出页面时务必销毁定时器
|
||||
_clearStopTimeTimer?.cancel(); // 🔥 清除保护期定时器
|
||||
_kickOutSub?.cancel();
|
||||
_stringMessageSub?.cancel();
|
||||
return super.close();
|
||||
@@ -416,6 +565,10 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
return 9999;
|
||||
}
|
||||
}
|
||||
//app退出远程遥控界面释放权限
|
||||
Future<bool> releasePermission(String platform) async {
|
||||
return await _repository.releasePermission(platform);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,11 +28,13 @@ class RemoteControlPage extends StatefulWidget {
|
||||
class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
String _videoStreamUrl = "";
|
||||
late RemoteControlCubit _cubit;
|
||||
DevicesCubit? _devicesCubit;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_cubit = context.read<RemoteControlCubit>();
|
||||
_devicesCubit = context.read<DevicesCubit>();
|
||||
}
|
||||
@override
|
||||
void initState() {
|
||||
@@ -53,9 +55,16 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 释放远程控制权限
|
||||
// _cubit.releasePermission("app");
|
||||
//print("远程控制要推出啦");
|
||||
final deviceState = _devicesCubit?.state;
|
||||
if (deviceState?.selectedDevice != null) {
|
||||
_cubit.releasePermission("app");
|
||||
}
|
||||
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
// context.read<RemoteControlCubit>().stopControlLoop();
|
||||
_cubit.stopControlLoop();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -138,7 +147,7 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
width: sideAreaWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LeftJoystickArea(isLocked: remoteState.isLocked, width: sideAreaWidth * 0.5 * 0.7),
|
||||
child: LeftJoystickArea(isLocked: remoteState.isLocked, width: sideAreaWidth * 0.45),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -156,7 +165,7 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
width: sideAreaWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: RightJoystickArea(isLocked: remoteState.isLocked, width: sideAreaWidth * 0.5 * 0.7),
|
||||
child: RightJoystickArea(isLocked: remoteState.isLocked, width: sideAreaWidth * 0.45),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,69 +1,92 @@
|
||||
import 'dart:async';
|
||||
import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
|
||||
class LeftJoystickArea extends StatelessWidget {
|
||||
class LeftJoystickArea extends StatefulWidget {
|
||||
final bool isLocked;
|
||||
final double width;
|
||||
|
||||
const LeftJoystickArea({Key? key, required this.isLocked, required this.width}) : super(key: key);
|
||||
const LeftJoystickArea({
|
||||
Key? key,
|
||||
required this.isLocked,
|
||||
required this.width,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LeftJoystickArea> createState() => _LeftJoystickAreaState();
|
||||
}
|
||||
|
||||
class _LeftJoystickAreaState extends State<LeftJoystickArea> {
|
||||
int _lastSentY = 0;
|
||||
bool _isTouching = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min, // 紧凑布局
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 使用 IgnorePointer 处理锁定逻辑,对应 Android 的 isLocked 判断
|
||||
IgnorePointer(
|
||||
ignoring: isLocked,
|
||||
ignoring: widget.isLocked,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: isLocked ? 0.3 : 1.0, // 锁定后变透明
|
||||
opacity: widget.isLocked ? 0.3 : 1.0,
|
||||
child: CCJoystick(
|
||||
radius: width, // 对应 size(200.dp)
|
||||
radius: widget.width,
|
||||
axisHint: AxisHint.forwardBackward,
|
||||
onValueChanged: (value) {
|
||||
debugPrint('🎮 [左摇杆] value: ${value.y},${value.x}');
|
||||
// 对应 viewModel.updateOriginY(y)
|
||||
context.read<RemoteControlCubit>().updateOriginY(value.y);
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
_triggerVibration();
|
||||
});
|
||||
// 标记:正在触摸
|
||||
_isTouching = true;
|
||||
|
||||
int currentY = value.y.toInt();
|
||||
if (currentY != _lastSentY) {
|
||||
_lastSentY = currentY;
|
||||
debugPrint('左摇杆的数据- x: 0, y: $currentY');
|
||||
context.read<RemoteControlCubit>().updateOriginY(currentY);
|
||||
}
|
||||
},
|
||||
onPress: () {
|
||||
// 对应 VibrateOnce(current, 100)
|
||||
//HapticFeedback.mediumImpact();
|
||||
_isTouching = true;
|
||||
debugPrint('onPress');
|
||||
_triggerVibration();
|
||||
},
|
||||
onPanEnd: () async {
|
||||
// 🔥 松手 100% 归零
|
||||
debugPrint('onPanEnd');
|
||||
await _stopJoystick();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
"前后控制",
|
||||
style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12, fontWeight: FontWeight.w500),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _triggerVibration() {
|
||||
Vibration.hasVibrator()
|
||||
.then((hasVibrator) {
|
||||
if (hasVibrator ?? false) {
|
||||
Vibration.vibrate(duration: 12);
|
||||
debugPrint('📳 [右摇杆] 震动执行 - 10ms');
|
||||
} else {
|
||||
debugPrint('⚠️ [右摇杆] 设备不支持震动');
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
debugPrint('❌ [右摇杆] 震动失败:$e');
|
||||
});
|
||||
// 统一停车方法(万能保险)
|
||||
Future<void> _stopJoystick() async {
|
||||
_isTouching = false;
|
||||
_lastSentY = 0;
|
||||
debugPrint('✅ 摇杆已停止 -> 强制 X=0, Y=0 停车');
|
||||
|
||||
// 🔥 使用安全方法,一次性清零双轴,确保只发送 (X=0, Y=0)
|
||||
await context.read<RemoteControlCubit>().stopAllMovement();
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerVibration() {
|
||||
Vibration.hasVibrator().then((has) {
|
||||
if (has ?? false) Vibration.vibrate(duration: 12);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,77 @@
|
||||
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/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
|
||||
class RightJoystickArea extends StatelessWidget {
|
||||
class RightJoystickArea extends StatefulWidget {
|
||||
final bool isLocked;
|
||||
final double width;
|
||||
|
||||
// 修复:删除重复的 key 定义
|
||||
const RightJoystickArea({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.isLocked,
|
||||
required this.width,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<RightJoystickArea> createState() => _RightJoystickAreaState();
|
||||
}
|
||||
|
||||
class _RightJoystickAreaState extends State<RightJoystickArea> {
|
||||
int _lastX = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min, // 垂直方向紧凑布局
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1. 交互锁定逻辑:对应 Compose 的 isLocked 判断
|
||||
IgnorePointer(
|
||||
ignoring: isLocked,
|
||||
ignoring: widget.isLocked,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: isLocked ? 0.3 : 1.0, // 锁定后变透明/灰色
|
||||
opacity: widget.isLocked ? 0.3 : 1.0,
|
||||
child: CCJoystick(
|
||||
radius: width, // 对应 size(200.dp)
|
||||
axisHint: AxisHint.leftRight, // 关键:指定为左右控制
|
||||
radius: widget.width,
|
||||
axisHint: AxisHint.leftRight,
|
||||
onValueChanged: (value) {
|
||||
// 对应 viewModel.updateOriginX(x)
|
||||
/// context.read<RemoteControlCubit>().updateOriginY(value.x);
|
||||
context.read<RemoteControlCubit>().updateOriginX(value.x);
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
_triggerVibration();
|
||||
});
|
||||
int currentX = value.x.toInt();
|
||||
if (currentX == _lastX) return;
|
||||
_lastX = currentX;
|
||||
|
||||
int finalX = currentX.abs() < 1 ? 0 : currentX;
|
||||
debugPrint('右摇杆的数据- x: $finalX, y: 0');
|
||||
context.read<RemoteControlCubit>().updateOriginX(finalX);
|
||||
},
|
||||
onPress: _triggerVibration,
|
||||
onPanEnd: () async {
|
||||
debugPrint('🕹️ [右摇杆] 松手,强制 X=0, Y=0');
|
||||
_lastX = 0;
|
||||
|
||||
// 🔥 使用安全方法,一次性清零双轴,确保只发送 (X=0, Y=0)
|
||||
await context.read<RemoteControlCubit>().stopAllMovement();
|
||||
},
|
||||
onPress: () {
|
||||
_triggerVibration();
|
||||
}
|
||||
,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
// 2. 底部文字:对应 Text("左右控制", color = Color.Gray)
|
||||
Text(
|
||||
"左右控制",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _triggerVibration() {
|
||||
Vibration.hasVibrator().then((hasVibrator) {
|
||||
if (hasVibrator ?? false) {
|
||||
Vibration.vibrate(duration: 12);
|
||||
debugPrint('📳 [右摇杆] 震动执行 - 100ms');
|
||||
} else {
|
||||
debugPrint('⚠️ [右摇杆] 设备不支持震动');
|
||||
}
|
||||
}).catchError((e) {
|
||||
debugPrint('❌ [右摇杆] 震动失败:$e');
|
||||
Vibration.hasVibrator().then((has) {
|
||||
if (has ?? false) Vibration.vibrate(duration: 12);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class CCJoystick extends StatefulWidget {
|
||||
final AxisHint axisHint;
|
||||
final Function(JoystickValue) onValueChanged;
|
||||
final VoidCallback? onPress;
|
||||
final Future<void> Function()? onPanEnd; // 🔥 修复:改为异步回调
|
||||
final double radius;
|
||||
|
||||
const CCJoystick({
|
||||
@@ -21,6 +22,7 @@ class CCJoystick extends StatefulWidget {
|
||||
required this.axisHint,
|
||||
required this.onValueChanged,
|
||||
this.onPress,
|
||||
this.onPanEnd, // 🔥 新增
|
||||
this.radius = 80,
|
||||
});
|
||||
|
||||
@@ -32,6 +34,7 @@ class _CustomJoystickState extends State<CCJoystick>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Offset _offset = Offset.zero;
|
||||
late AnimationController _floatController;
|
||||
bool _isPanning = false; // 🔥 跟踪是否正在拖拽
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -69,45 +72,73 @@ class _CustomJoystickState extends State<CCJoystick>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
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();
|
||||
},
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user