2026-01-18 20:21:14 +08:00
|
|
|
|
import 'dart:async';
|
2026-03-13 20:29:06 +08:00
|
|
|
|
import 'dart:convert';
|
2026-04-02 16:40:18 +08:00
|
|
|
|
import 'dart:io';
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
2026-04-02 16:40:18 +08:00
|
|
|
|
import 'package:dart_ping/dart_ping.dart';
|
2026-03-18 13:03:14 +08:00
|
|
|
|
import 'package:flutter/cupertino.dart';
|
2026-03-25 15:09:11 +08:00
|
|
|
|
import 'package:flutter/services.dart';
|
2026-01-18 20:21:14 +08:00
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
2026-04-15 08:43:39 +08:00
|
|
|
|
import 'package:get_it/get_it.dart';
|
2026-01-21 13:27:55 +08:00
|
|
|
|
import 'package:maibu_satabot_v2/features/remote_control/data/models/running_status_model.dart';
|
2026-01-18 20:21:14 +08:00
|
|
|
|
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_state.dart';
|
|
|
|
|
|
|
2026-04-15 08:43:39 +08:00
|
|
|
|
import '../../../../core/logging/i_logger_service.dart';
|
2026-03-13 20:29:06 +08:00
|
|
|
|
import '../../../../core/network/net_message_dispatcher.dart';
|
2026-03-25 15:09:11 +08:00
|
|
|
|
import '../../../devices/domain/entities/running_status_entity.dart';
|
2026-01-18 20:21:14 +08:00
|
|
|
|
import '../../domain/entities/machine_control_status_entity.dart';
|
|
|
|
|
|
import '../../domain/repositories/remote_control_repository.dart';
|
2026-03-10 10:53:58 +08:00
|
|
|
|
import '../../domain/usecase/remote_control_usecase.dart';
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
|
|
|
|
|
class RemoteControlCubit extends Cubit<RemoteControlState> {
|
|
|
|
|
|
final RemoteControlRepository _repository;
|
2026-03-10 10:53:58 +08:00
|
|
|
|
final RequestControlPermissionUseCase _requestControlPermissionUseCase;
|
2026-01-18 20:21:14 +08:00
|
|
|
|
Timer? _timer;
|
2026-03-13 20:29:06 +08:00
|
|
|
|
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
|
|
|
|
|
final NetMessageDispatcher dispatcher;
|
2026-03-25 15:09:11 +08:00
|
|
|
|
StreamSubscription? _stringMessageSub; //
|
|
|
|
|
|
static const platform = MethodChannel('com.maibu.satabot/ping');
|
|
|
|
|
|
int _currentPing = 50;
|
2026-04-15 08:43:39 +08:00
|
|
|
|
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
2026-04-17 13:36:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 🔥 参考Android版:使用成员变量存储摇杆值,避免state竞态
|
|
|
|
|
|
int _currentOriginX = 0;
|
|
|
|
|
|
int _currentOriginY = 0;
|
2026-04-15 08:43:39 +08:00
|
|
|
|
|
2026-03-25 15:09:11 +08:00
|
|
|
|
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
2026-03-13 20:29:06 +08:00
|
|
|
|
RemoteControlCubit(this._repository, this._requestControlPermissionUseCase, this.dispatcher)
|
2026-01-21 13:27:55 +08:00
|
|
|
|
: super(
|
|
|
|
|
|
RemoteControlState(
|
|
|
|
|
|
controlEntity: MachineControlStatusEntity(),
|
|
|
|
|
|
runningStatusModel: RunningStatusModel(),
|
|
|
|
|
|
),
|
|
|
|
|
|
) {
|
2026-01-18 20:21:14 +08:00
|
|
|
|
_initPacketListener();
|
2026-03-25 15:09:11 +08:00
|
|
|
|
_initStringMessageListener(); // 🔥 新增
|
2026-01-18 20:21:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-25 15:09:11 +08:00
|
|
|
|
void _initStringMessageListener() {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] begin 初始化 0x02 字符串监听器');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] begin 初始化 0x02 字符串监听器');
|
2026-03-25 15:09:11 +08:00
|
|
|
|
_stringMessageSub?.cancel();
|
|
|
|
|
|
|
|
|
|
|
|
_stringMessageSub = dispatcher.onStringMessage().listen((message) async {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// print('>>> [RemoteControl] 收到 0x02 字符串推送:$message');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 收到 0x02 字符串推送:$message');
|
2026-03-25 15:09:11 +08:00
|
|
|
|
|
|
|
|
|
|
if (message.isEmpty) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('⚠️ [RemoteControl] 消息为空,跳过解析');
|
|
|
|
|
|
_logger.logWithLevel('⚠️ [RemoteControl] 消息为为空,跳过解析');
|
2026-03-25 15:09:11 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-24 17:09:12 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-25 15:09:11 +08:00
|
|
|
|
// 🔥 关键:使用与 DeviceStatusBloc 相同的解析方法
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// print('>>> [RemoteControl] 🔍 开始解析数据(使用 fromFields)...');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 🔍 开始解析数据(使用 fromFields)...');
|
2026-03-24 17:09:12 +08:00
|
|
|
|
|
2026-03-25 15:09:11 +08:00
|
|
|
|
final fields = message.trim().split(',');
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] 字段数量:${fields.length}');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 字段数量:${fields.length}');
|
2026-03-24 17:09:12 +08:00
|
|
|
|
|
2026-03-25 15:09:11 +08:00
|
|
|
|
if (fields.length < 18) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('⚠️ [RemoteControl] 字段不足:${fields.length},期望 ≥18');
|
|
|
|
|
|
_logger.logWithLevel('⚠️ [RemoteControl] 字段不足:${fields.length},期望 ≥18');
|
2026-03-25 15:09:11 +08:00
|
|
|
|
return;
|
2026-03-24 17:09:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-25 15:09:11 +08:00
|
|
|
|
// 🔥 使用 RunningStatusEntity.fromFields() 解析
|
|
|
|
|
|
final status = RunningStatusEntity.fromFields(fields);
|
|
|
|
|
|
// 🔥 只提取电压、电量、控制模式
|
|
|
|
|
|
final voltage = status.voltage;
|
|
|
|
|
|
final battery = status.battery;
|
|
|
|
|
|
final controlMode = status.controlMode == '3' ? '远程模式' : '本地模式';
|
2026-03-24 17:09:12 +08:00
|
|
|
|
|
2026-03-25 15:09:11 +08:00
|
|
|
|
|
2026-04-02 16:40:18 +08:00
|
|
|
|
final c = getNetworkDelay(); //
|
2026-03-25 15:09:11 +08:00
|
|
|
|
emit(state.copyWith(
|
|
|
|
|
|
runningStatusModel: state.runningStatusModel.copyWith(
|
|
|
|
|
|
voltage: voltage.toString(), // 更新电压
|
|
|
|
|
|
battery: battery.toString(), // 更新电量
|
|
|
|
|
|
controlMode: controlMode, // 更新控制模式
|
|
|
|
|
|
),
|
|
|
|
|
|
battery: int.tryParse(battery) ?? 0,
|
2026-04-02 16:40:18 +08:00
|
|
|
|
ping: await c, // 这里直接使用异步返回的数值
|
2026-03-25 15:09:11 +08:00
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e, stackTrace) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// print('>>> [RemoteControl] ❌ 解析失败:$e');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] ❌ 解析失败:$e');
|
2026-03-25 15:09:11 +08:00
|
|
|
|
// print('>>> [RemoteControl] ❌ 堆栈跟踪:$stackTrace');
|
|
|
|
|
|
// print('>>> [RemoteControl] ❌ 原始消息:$message');
|
2026-03-24 17:09:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] ✅ 0x02 字符串监听器已建立完成');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] ✅ 0x02 字符串监听器已建立完成');
|
2026-03-24 17:09:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-25 15:09:11 +08:00
|
|
|
|
// 🔥 超简单方法:传入 IP,得到 ping 值
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-24 17:09:12 +08:00
|
|
|
|
// 🔥 辅助方法:更新运行状态
|
|
|
|
|
|
void _updateStatusFromDevice(RunningStatusModel newStatus) {
|
2026-03-25 15:09:11 +08:00
|
|
|
|
|
|
|
|
|
|
debugPrint('✅ [_updateStatusFromDevice] 收到运行状态更新:$newStatus');
|
2026-03-24 17:09:12 +08:00
|
|
|
|
if (!isClosed) {
|
|
|
|
|
|
emit(state.copyWith(
|
|
|
|
|
|
runningStatusModel: newStatus,
|
2026-03-25 15:09:11 +08:00
|
|
|
|
voltage: int.tryParse(newStatus.voltage) ?? 0,
|
2026-03-25 08:48:32 +08:00
|
|
|
|
battery: int.tryParse(newStatus.battery) ?? 0,
|
2026-03-24 17:09:12 +08:00
|
|
|
|
));
|
|
|
|
|
|
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// debugPrint('✅ [更新运行状态] 电压:${newStatus.voltage}V, 电量:${newStatus.battery}%');
|
|
|
|
|
|
_logger.logWithLevel('✅ [更新运行状态] 电压:${newStatus.voltage}V, 电量:${newStatus.battery}%');
|
2026-03-24 17:09:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-25 15:09:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 🔥 辅助方法:更新运行状态
|
2026-01-18 20:21:14 +08:00
|
|
|
|
// 1. 初始化回包监听 (如 0x12 权限)
|
2026-03-13 20:29:06 +08:00
|
|
|
|
// void _initPacketListener() {
|
|
|
|
|
|
// _repository.responseStream.listen((packet) {
|
|
|
|
|
|
// if (packet.command == 0x12) {
|
|
|
|
|
|
// // 根据负载判断是否有权限,更新状态
|
|
|
|
|
|
// emit(state.copyWith(hasPermission: true));
|
|
|
|
|
|
// }
|
|
|
|
|
|
// });
|
|
|
|
|
|
// }
|
2026-04-02 16:40:18 +08:00
|
|
|
|
Future<void> _initPacketListener() async {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] begin 初始化 0x12 监听器');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] begin 0x12 监听器');
|
2026-03-13 20:29:06 +08:00
|
|
|
|
_kickOutSub?.cancel(); // 防止重复监听
|
|
|
|
|
|
|
|
|
|
|
|
_kickOutSub = dispatcher.onCommand(0x12).listen((packet) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] 收到 0x12 原始包,payload 长度=${packet.payload.length}');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 收到 0x12 原始包,payload 长度=${packet.payload.length}');
|
2026-03-13 20:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 🔥 关键:手动去掉最后 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] 去除 CRC 后的 JSON: $jsonString');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 去除 CRC 后的 JSON: $jsonString');
|
2026-03-13 20:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
final jsonMap = jsonDecode(jsonString);
|
2026-03-16 15:31:14 +08:00
|
|
|
|
|
|
|
|
|
|
// 🔥 区分两种数据格式
|
|
|
|
|
|
final requestType = jsonMap['request'];
|
|
|
|
|
|
final platform = jsonMap['platform'];
|
|
|
|
|
|
final respondData = jsonMap['respond'];
|
|
|
|
|
|
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] 📋 requestType=$requestType, platform=$platform');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 📋 requestType=$requestType, platform=$platform');
|
2026-03-16 15:31:14 +08:00
|
|
|
|
|
|
|
|
|
|
// 情况 1: 响应格式 - {"respond":{"switchResult":true,"deviceId":"...","holder":"you"}}
|
|
|
|
|
|
if (respondData != null && respondData is Map) {
|
|
|
|
|
|
final switchResult = respondData['switchResult'];
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] 📊 收到切换结果响应:switchResult = $switchResult');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 📊 收到切换结果响应:switchResult = $switchResult');
|
2026-03-16 15:31:14 +08:00
|
|
|
|
|
|
|
|
|
|
if (!isClosed) {
|
|
|
|
|
|
if (switchResult == true) {
|
|
|
|
|
|
// 切换成功,当前 APP 失去控制权
|
|
|
|
|
|
emit(state.copyWith(hasPermission: true, showPermissionRequestDialog: false));
|
|
|
|
|
|
print('>>> [RemoteControl] ✅ 权限已切');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 切换失败或拒绝,保持当前状态
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: false));
|
|
|
|
|
|
print('>>> [RemoteControl] ❌ 权限切换失败/被拒绝');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 情况 2: 请求格式 - {"request":"switch_control","deviceId":"...","platform":"web",...}
|
|
|
|
|
|
else if (requestType == 'switch_control') {
|
|
|
|
|
|
// 🔥 关键判断:只有当是其他平台(web)请求时才弹窗
|
|
|
|
|
|
if (platform != null && platform.toString().toLowerCase() != 'app') {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] 🚨 $platform 端请求控制权,打开弹窗询问用户');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] 🚨 $platform 端请求控制权,打开弹窗询问用户');
|
2026-03-16 15:31:14 +08:00
|
|
|
|
if (!isClosed) {
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: true));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] ℹ️ APP 自己的请求回显,忽略不弹窗');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] ℹ️ APP 自己的请求回显,忽略不弹窗');
|
2026-03-16 15:31:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 情况 3: 异地登录通知 - {"request":"have_logged_in",...}
|
|
|
|
|
|
else if (requestType == 'have_logged_in') {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] ⚠️ 检测到异地登录,打开弹窗提示');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] ⚠️ 检测到异地登录,打开弹窗提示');
|
2026-03-16 15:31:14 +08:00
|
|
|
|
if (!isClosed) {
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: true));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// print('>>> [RemoteControl] ℹ️ 未知类型的 0x12 包,忽略');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] ℹ️ 未知类型的 0x12 包,忽略');
|
2026-03-13 20:29:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] ❌ 解析失败:$e');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] ❌ 解析失败:$e');
|
2026-01-18 20:21:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-02 16:40:18 +08:00
|
|
|
|
final c = getNetworkDelay();
|
|
|
|
|
|
emit(state.copyWith(
|
|
|
|
|
|
ping: await c, // 这里直接使用异步返回的数值
|
|
|
|
|
|
));
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('>>> [RemoteControl] ✅ 0x12 监听器已建立完成');
|
|
|
|
|
|
_logger.logWithLevel('>>> [RemoteControl] ✅ 0x12 监听器已建立完成');
|
2026-01-18 20:21:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 15:31:14 +08:00
|
|
|
|
|
2026-01-18 20:21:14 +08:00
|
|
|
|
// 2. 开启 100ms 控制循环 (在进入遥控页面或点击“开始”时调用)
|
2026-04-16 17:39:54 +08:00
|
|
|
|
/* void startControlLoop() {
|
2026-01-18 20:21:14 +08:00
|
|
|
|
_timer?.cancel();
|
|
|
|
|
|
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
2026-04-16 17:39:54 +08:00
|
|
|
|
// 🔥 关键修复:每次循环都重新读取最新的 state
|
|
|
|
|
|
final currentEntity = state.controlEntity;
|
|
|
|
|
|
|
2026-04-16 10:49:45 +08:00
|
|
|
|
// 🔥 安全检查:无权限或急停时不发送
|
|
|
|
|
|
if (!state.hasPermission || state.isEmergency) {
|
|
|
|
|
|
debugPrint('⚠️ [定时器] 跳过发送 - hasPermission=${state.hasPermission}, isEmergency=${state.isEmergency}');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-16 17:39:54 +08:00
|
|
|
|
debugPrint('⏰ [定时器] 发送控制指令 - originX=${currentEntity.originX}, originY=${currentEntity.originY}');
|
|
|
|
|
|
_logger.logWithLevel('真实的发送的实体 - originX: ${currentEntity.originX}, originY: ${currentEntity.originY}');
|
|
|
|
|
|
_repository.sendControlMachineCmd(currentEntity);
|
2026-01-18 20:21:14 +08:00
|
|
|
|
});
|
|
|
|
|
|
emit(state.copyWith(status: RemoteControlStatus.controlling));
|
2026-04-16 17:39:54 +08:00
|
|
|
|
}*/
|
|
|
|
|
|
void startControlLoop() {
|
|
|
|
|
|
_timer?.cancel();
|
|
|
|
|
|
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
|
|
|
|
|
if (isClosed) {
|
|
|
|
|
|
timer.cancel();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 🔥 参考Android版:直接读取成员变量,避免state竞态
|
|
|
|
|
|
final snapshot = MachineControlStatusEntity(
|
|
|
|
|
|
originX: _currentOriginX,
|
|
|
|
|
|
originY: _currentOriginY,
|
|
|
|
|
|
chassisLift: state.controlEntity.chassisLift,
|
|
|
|
|
|
mowerSpeed: state.controlEntity.mowerSpeed,
|
|
|
|
|
|
ignitionStatus: state.controlEntity.ignitionStatus,
|
|
|
|
|
|
isEmergency: state.isEmergency,
|
|
|
|
|
|
);
|
2026-04-16 17:39:54 +08:00
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 常规安全检查
|
2026-04-16 17:39:54 +08:00
|
|
|
|
if (!state.hasPermission) {
|
2026-04-17 13:36:54 +08:00
|
|
|
|
debugPrint('⚠️ [定时器] 无权限,跳过发送');
|
2026-04-16 17:39:54 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (state.isEmergency) {
|
|
|
|
|
|
debugPrint('🚨 [定时器] 急停状态,跳过发送');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
_logger.logWithLevel('[定时器] 发送控制指令 - originX=${snapshot.originX}, originY=${snapshot.originY}');
|
|
|
|
|
|
debugPrint('📤 [定时器] 准备发送 - originX=${snapshot.originX}, originY=${snapshot.originY}');
|
|
|
|
|
|
_repository.sendControlMachineCmd(snapshot);
|
2026-04-16 17:39:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
//通过方法拿最新 state
|
|
|
|
|
|
MachineControlStatusEntity _getCurrentControlEntity() {
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 🔥 关键修复:必须copyWith创建新对象,避免引用竞态条件
|
|
|
|
|
|
final entity = state.controlEntity;
|
|
|
|
|
|
_logger.logWithLevel('真实的发送的实体 - originX: ${entity.originX}, originY: ${entity.originY}');
|
|
|
|
|
|
return entity.copyWith(); // 返回副本,不是引用
|
2026-01-18 20:21:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 更新摇杆数据
|
|
|
|
|
|
void updateJoystick(double x, double y) {
|
|
|
|
|
|
final updatedEntity = state.controlEntity.copyWith(
|
|
|
|
|
|
x: x.toInt(),
|
|
|
|
|
|
y: y.toInt(),
|
|
|
|
|
|
);
|
|
|
|
|
|
emit(state.copyWith(controlEntity: updatedEntity));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 更新功能开关 (比如割刀速度、灯光、点火等)
|
|
|
|
|
|
void updateFunction({int? mower, int? lift, int? ignition, bool? emergency}) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// debugPrint('🔧 [updateFunction] 调用 - mower: $mower, lift: $lift, ignition: $ignition, emergency: $emergency');
|
|
|
|
|
|
_logger.logWithLevel('🔧 [updateFunction] 调用 - mower: $mower, lift: $lift, ignition: $ignition, emergency: $emergency');
|
2026-01-18 20:21:14 +08:00
|
|
|
|
final updatedEntity = state.controlEntity.copyWith(
|
|
|
|
|
|
mower: mower,
|
|
|
|
|
|
lift: lift,
|
|
|
|
|
|
ignition: ignition,
|
|
|
|
|
|
emergency: emergency,
|
|
|
|
|
|
);
|
2026-03-18 13:03:14 +08:00
|
|
|
|
//emit(state.copyWith(controlEntity: updatedEntity));
|
|
|
|
|
|
emit(state.copyWith(
|
|
|
|
|
|
controlEntity: updatedEntity,
|
|
|
|
|
|
isEmergency: emergency ?? state.isEmergency,
|
|
|
|
|
|
));
|
|
|
|
|
|
debugPrint('✅ [updateFunction] 状态已更新并 emit');
|
2026-04-15 08:43:39 +08:00
|
|
|
|
_logger.logWithLevel('✅ [updateFunction] 状态已更新并 emit');
|
2026-01-18 20:21:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 10:53:58 +08:00
|
|
|
|
void updateOriginY(int y) {
|
2026-04-17 13:36:54 +08:00
|
|
|
|
debugPrint('📥 [updateOriginY] 被调用 - y=$y');
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 参考Android版:直接更新成员变量
|
|
|
|
|
|
_currentOriginY = y;
|
2026-04-16 17:39:54 +08:00
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 同时更新state(用于UI显示)
|
2026-04-16 17:39:54 +08:00
|
|
|
|
final updatedEntity = state.controlEntity.copyWith(y: y);
|
|
|
|
|
|
emit(state.copyWith(controlEntity: updatedEntity));
|
|
|
|
|
|
debugPrint('📝 [updateOriginY] state已更新 - originY=$y');
|
2026-03-10 10:53:58 +08:00
|
|
|
|
}
|
2026-04-16 17:39:54 +08:00
|
|
|
|
|
2026-03-10 10:53:58 +08:00
|
|
|
|
void updateOriginX(int x) {
|
2026-04-17 13:36:54 +08:00
|
|
|
|
debugPrint('📥 [updateOriginX] 被调用 - x=$x');
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 参考Android版:直接更新成员变量
|
|
|
|
|
|
_currentOriginX = x;
|
2026-04-16 17:39:54 +08:00
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 同时更新state(用于UI显示)
|
2026-04-16 17:39:54 +08:00
|
|
|
|
final updatedEntity = state.controlEntity.copyWith(x: x);
|
|
|
|
|
|
emit(state.copyWith(controlEntity: updatedEntity));
|
|
|
|
|
|
debugPrint('📝 [updateOriginX] state已更新 - originX=$x');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 🔥 安全方法:同时清零双轴,确保只发送一次完全停止指令
|
|
|
|
|
|
Future<void> stopAllMovement() async {
|
2026-04-17 13:36:54 +08:00
|
|
|
|
debugPrint('🛑 [stopAllMovement] 开始执行 - 当前成员变量: originX=$_currentOriginX, originY=$_currentOriginY');
|
2026-04-16 17:39:54 +08:00
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 🔥 参考Android版:直接清零成员变量
|
|
|
|
|
|
_currentOriginX = 0;
|
|
|
|
|
|
_currentOriginY = 0;
|
2026-04-16 17:39:54 +08:00
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 🔥 关键修复:先暂停定时器,防止定时器在停止期间发送旧的运动指令
|
2026-04-16 17:39:54 +08:00
|
|
|
|
_timer?.cancel();
|
|
|
|
|
|
_timer = null; // 🔥 彻底清空,防止重复启动
|
|
|
|
|
|
|
|
|
|
|
|
final updatedEntity = state.controlEntity.copyWith(x: 0, y: 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 一次性更新双轴(即使无权限也要更新本地state)
|
2026-03-10 10:53:58 +08:00
|
|
|
|
emit(state.copyWith(controlEntity: updatedEntity));
|
2026-04-16 10:49:45 +08:00
|
|
|
|
|
2026-04-16 17:39:54 +08:00
|
|
|
|
// 🔥 安全底线:停止指令必须无视权限强制发送!
|
|
|
|
|
|
debugPrint('🛑 [stopAllMovement] 双轴归零,发送停止指令 - originX=0, originY=0');
|
|
|
|
|
|
await _sendStopCommandRepeatedly(updatedEntity); // ✅ 等待所有指令发送完成
|
|
|
|
|
|
|
2026-04-17 13:36:54 +08:00
|
|
|
|
// 🔥 恢复定时器
|
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 200));
|
2026-04-16 17:39:54 +08:00
|
|
|
|
startControlLoop();
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-04-16 10:49:45 +08:00
|
|
|
|
}
|
2026-04-16 17:39:54 +08:00
|
|
|
|
|
|
|
|
|
|
debugPrint('✅ [紧急停止] 所有30次停止指令已发出');
|
2026-03-10 10:53:58 +08:00
|
|
|
|
}
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
|
|
|
|
|
// 5. 停止控制循环
|
|
|
|
|
|
void stopControlLoop() {
|
|
|
|
|
|
_timer?.cancel();
|
|
|
|
|
|
_timer = null;
|
|
|
|
|
|
emit(state.copyWith(status: RemoteControlStatus.initial));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 17:09:12 +08:00
|
|
|
|
void toggleLock() {
|
|
|
|
|
|
emit(state.copyWith(isLocked: !state.isLocked));
|
|
|
|
|
|
}
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
|
|
|
|
|
void togglePermissionDialog(bool show) {
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: show));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void requestControlPermission() {
|
|
|
|
|
|
// 1. 关闭弹窗
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: false));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 13:27:55 +08:00
|
|
|
|
void toggleLeftPip() => emit(state.copyWith(showLeftPip: !state.showLeftPip));
|
2026-03-25 08:48:32 +08:00
|
|
|
|
void toggleRightPip() => emit(state.copyWith(showRightPip: !state.showRightPip));
|
2026-01-21 13:27:55 +08:00
|
|
|
|
|
2026-01-18 20:21:14 +08:00
|
|
|
|
@override
|
|
|
|
|
|
Future<void> close() {
|
|
|
|
|
|
_timer?.cancel(); // 退出页面时务必销毁定时器
|
2026-03-25 15:09:11 +08:00
|
|
|
|
_kickOutSub?.cancel();
|
|
|
|
|
|
_stringMessageSub?.cancel();
|
2026-01-18 20:21:14 +08:00
|
|
|
|
return super.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void updateChassisLift(int i) {}
|
|
|
|
|
|
|
2026-03-18 13:03:14 +08:00
|
|
|
|
void updateEmergency(bool bool) {
|
|
|
|
|
|
debugPrint('🚨 [急停] ${bool ? "触发急停!" : "解除急停"}');
|
|
|
|
|
|
updateFunction(emergency: bool);
|
|
|
|
|
|
}
|
2026-01-18 20:21:14 +08:00
|
|
|
|
|
2026-03-16 15:31:14 +08:00
|
|
|
|
void respondPermission(bool bool, String deviceId) {
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: false));
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 发送响应到服务端
|
|
|
|
|
|
_repository.respondPermission(bool, deviceId);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 根据用户选择更新控制状态
|
|
|
|
|
|
if (bool) {
|
|
|
|
|
|
// 用户同意 → APP 失去控制权,Web 端获得控制权
|
|
|
|
|
|
emit(state.copyWith(hasPermission: false));
|
|
|
|
|
|
//路由到首页home
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 用户拒绝 → APP 继续保持控制权
|
|
|
|
|
|
emit(state.copyWith(hasPermission: true));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2026-03-10 10:53:58 +08:00
|
|
|
|
|
|
|
|
|
|
void requestControlPermissionS(String deviceName, String deviceId) async {
|
|
|
|
|
|
// 1. 关闭弹窗
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: false));
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 调用 UseCase
|
|
|
|
|
|
final result = await _requestControlPermissionUseCase(
|
|
|
|
|
|
RequestControlPermissionParams(
|
|
|
|
|
|
deviceName: deviceName,
|
|
|
|
|
|
deviceId: deviceId,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 处理结果
|
|
|
|
|
|
result.fold(
|
|
|
|
|
|
(failure) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('❌ [RemoteControl] 请求控制权限失败:${failure.message}');
|
|
|
|
|
|
_logger.logWithLevel('❌ [RemoteControl] 请求控制权限失败:${failure.message}');
|
2026-03-10 10:53:58 +08:00
|
|
|
|
// 可以在这里显示错误提示或重新打开弹窗
|
|
|
|
|
|
emit(state.copyWith(showPermissionRequestDialog: true));
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
(success) {
|
2026-03-13 20:29:06 +08:00
|
|
|
|
// 更新状态
|
2026-04-15 08:43:39 +08:00
|
|
|
|
//print('✅ [RemoteControl] 请求控制权限成功:$success');
|
|
|
|
|
|
_logger.logWithLevel('✅ [RemoteControl] 请求控制权限成功:$success');
|
2026-03-13 20:29:06 +08:00
|
|
|
|
///处理result
|
|
|
|
|
|
if(success){
|
|
|
|
|
|
emit(state.copyWith(hasPermission: true));
|
|
|
|
|
|
}else{
|
|
|
|
|
|
emit(state.copyWith(hasPermission: false));
|
|
|
|
|
|
}
|
|
|
|
|
|
print('✅ c:$success');
|
2026-03-10 10:53:58 +08:00
|
|
|
|
// 权限申请已发送,等待 0x12 回包更新状态
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-18 09:53:04 +08:00
|
|
|
|
/// 发送底盘指令
|
|
|
|
|
|
void sendChassisCommand(int i) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// debugPrint(' [底盘指令] ${i}');
|
|
|
|
|
|
_logger.logWithLevel(' [底盘指令] ${i}');
|
2026-03-18 09:53:04 +08:00
|
|
|
|
updateFunction(lift: i);
|
|
|
|
|
|
}
|
|
|
|
|
|
/// 发送割刀指令
|
|
|
|
|
|
void sendMowerCommand(int i) {
|
2026-04-15 08:43:39 +08:00
|
|
|
|
// debugPrint(' [割刀指令] ${i}');
|
|
|
|
|
|
_logger.logWithLevel(' [割刀指令] ${i}');
|
2026-03-18 09:53:04 +08:00
|
|
|
|
updateFunction(mower: i);
|
|
|
|
|
|
}
|
|
|
|
|
|
/// 发送点火指令
|
|
|
|
|
|
void sendFireCommand(int i) {
|
|
|
|
|
|
//void updateFunction({int? mower, int? lift, int? ignition, bool? emergency})
|
|
|
|
|
|
// updateFunction(mower:0, lift: 0, ignition: i, emergency: false);
|
2026-03-18 13:03:14 +08:00
|
|
|
|
debugPrint(' [点火指令] ${i}');
|
2026-04-15 08:43:39 +08:00
|
|
|
|
_logger.logWithLevel(' [点火指令] ${i}');
|
2026-03-18 09:53:04 +08:00
|
|
|
|
updateFunction(ignition: i);
|
|
|
|
|
|
}
|
2026-03-24 17:09:12 +08:00
|
|
|
|
// 发送障碍物识别指令
|
|
|
|
|
|
void toggleObstacleRecognition() {
|
|
|
|
|
|
emit(state.copyWith(obstacleRecognitionFlag: !state.obstacleRecognitionFlag));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void toggleTopLeftExpand() {
|
|
|
|
|
|
emit(state.copyWith(topRightIsExpanded: !state.topRightIsExpanded));
|
|
|
|
|
|
}
|
2026-03-10 10:53:58 +08:00
|
|
|
|
|
2026-04-02 16:40:18 +08:00
|
|
|
|
Future<int> getNetworkDelay() async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 直接 Ping 你的服务器 IP
|
|
|
|
|
|
final ping = Ping('1.95.137.212', count: 1, timeout: 1);
|
|
|
|
|
|
|
|
|
|
|
|
// 等待一次结果
|
|
|
|
|
|
final data = await ping.stream.first;
|
2026-03-25 15:09:11 +08:00
|
|
|
|
|
2026-04-02 16:40:18 +08:00
|
|
|
|
if (data.response != null && data.response!.time != null) {
|
|
|
|
|
|
// 返回和 cmd 一样的毫秒值
|
|
|
|
|
|
return data.response!.time!.inMilliseconds;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return 9999;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return 9999;
|
|
|
|
|
|
}
|
2026-03-25 15:09:11 +08:00
|
|
|
|
}
|
2026-04-16 10:49:45 +08:00
|
|
|
|
//app退出远程遥控界面释放权限
|
|
|
|
|
|
Future<bool> releasePermission(String platform) async {
|
|
|
|
|
|
return await _repository.releasePermission(platform);
|
|
|
|
|
|
}
|
2026-03-10 10:53:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 20:29:06 +08:00
|
|
|
|
|
2026-03-16 15:31:14 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-18 20:21:14 +08:00
|
|
|
|
}
|
2026-03-13 20:29:06 +08:00
|
|
|
|
|