Files
flutterApp/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart

1074 lines
41 KiB
Dart
Raw Normal View History

import 'dart:async';
2026-03-13 20:29:06 +08:00
import 'dart:convert';
import 'dart:io';
import 'dart:math';
2026-01-18 20:21:14 +08:00
import 'package:dart_ping/dart_ping.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
2026-01-18 20:21:14 +08:00
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart';
2026-06-05 19:06:21 +08:00
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
2026-01-21 13:27:55 +08:00
import 'package:maibu_satabot_v2/features/remote_control/data/models/running_status_model.dart';
2026-06-05 19:06:21 +08:00
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/request_control_permission_usecase.dart';
2026-01-18 20:21:14 +08:00
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_state.dart';
import '../../../../core/logging/i_logger_service.dart';
2026-03-13 20:29:06 +08:00
import '../../../../core/network/net_message_dispatcher.dart';
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';
import '../../domain/usecase/remote_control_usecase.dart';
2026-01-18 20:21:14 +08:00
class RemoteControlCubit extends Cubit<RemoteControlState> {
final RemoteControlRepository _repository;
final RequestControlPermissionUseCase _requestControlPermissionUseCase;
2026-06-05 19:06:21 +08:00
final DeviceRepository _deviceRepository; // 🔥 注入设备仓库
final TcpClient tcpClient; // 🔥 注入TCP客户端
2026-01-18 20:21:14 +08:00
Timer? _timer;
2026-03-13 20:29:06 +08:00
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
final NetMessageDispatcher dispatcher;
final DeviceStatusBloc deviceStatusBloc; // 🔥 注入 DeviceStatusBloc
StreamSubscription? _deviceStatusSub; // 🔥 订阅 DeviceStatusBloc 的状态流
static const platform = MethodChannel('com.maibu.satabot/ping');
int _currentPing = 50;
final ILoggerService _logger = GetIt.I<ILoggerService>();
2026-06-05 19:06:21 +08:00
// 🔥 参考Android版:使用成员变量存储摇杆值,避免state竞态
int _currentOriginX = 0;
int _currentOriginY = 0;
// 🔥 权限请求处理标志位 - 防止竞态条件,避免重复显示弹窗
2026-06-05 19:06:21 +08:00
bool _isProcessingPermissionRequest = false;
// 🔥 模拟数据推送定时器
// Timer? _simulationTimer;
2026-06-05 19:06:21 +08:00
// 🔥 获取带时间戳的日志前缀
String _getTimePrefix() {
final now = DateTime.now();
return '[${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}.${now.millisecond.toString().padLeft(3, '0')}]';
}
2026-01-18 20:21:14 +08:00
RemoteControlCubit(
this._repository,
this._requestControlPermissionUseCase,
2026-06-05 19:06:21 +08:00
this._deviceRepository, // 🔥 注入
this.tcpClient, // 🔥 注入TCP客户端
this.dispatcher,
this.deviceStatusBloc, // 🔥 注入
) : super(
2026-01-21 13:27:55 +08:00
RemoteControlState(
controlEntity: MachineControlStatusEntity(),
runningStatusModel: RunningStatusModel(),
),
) {
2026-01-18 20:21:14 +08:00
_initPacketListener();
_initDeviceStatusListener(); // 🔥 改为订阅 DeviceStatusBloc
2026-01-18 20:21:14 +08:00
}
2026-06-05 19:06:21 +08:00
// 🔥 新增:订阅 DeviceStatusBloc 的状态流,而不是直接监听TCP
// 类全局变量
DateTime? _lastUiUpdateTime;
String? _cacheVoltage;
String? _cacheBattery;
String? _cacheCtrlMode;
int? _cachePing;
2026-07-11 16:46:53 +08:00
DateTime? _lastStatusPushTime; // 最后一次收到设备状态推送的时间
void _initDeviceStatusListener() {
_deviceStatusSub?.cancel();
_deviceStatusSub = deviceStatusBloc.stream.listen((deviceState) async {
if (deviceState is DeviceStatusUpdated) {
// 第一步:所有数据先存入缓存,不管来多频繁都存最新值
final voltage = deviceState.status.voltage;
final battery = deviceState.status.battery;
2026-06-05 19:06:21 +08:00
final controlMode = deviceState.status.controlMode == '3'
? '远程模式'
: '本地模式';
final c = await getNetworkDelay();
_cacheVoltage = voltage.toString();
_cacheBattery = battery.toString();
_cacheCtrlMode = controlMode;
_cachePing = c;
2026-07-11 16:46:53 +08:00
_lastStatusPushTime = DateTime.now(); // 记录最后一次推送时间
// 500ms节流,不到时间不刷新UI
final now = DateTime.now();
if (_lastUiUpdateTime != null &&
now.difference(_lastUiUpdateTime!) <
const Duration(milliseconds: 500)) {
return;
}
_lastUiUpdateTime = now;
// 间隔达标,统一一次刷新UI
2026-06-05 19:06:21 +08:00
emit(
state.copyWith(
runningStatusModel: state.runningStatusModel.copyWith(
voltage: _cacheVoltage,
battery: _cacheBattery,
controlMode: _cacheCtrlMode,
2026-06-05 19:06:21 +08:00
),
battery: int.tryParse(_cacheBattery ?? '') ?? 0,
ping: _cachePing,
2026-07-11 16:46:53 +08:00
hasReceivedStatusPush: true, // 标记已收到设备状态推送
// 🔥 标记为设备状态更新
updateType: 'device_status',
),
2026-06-05 19:06:21 +08:00
);
}
});
}
// 🔥 超简单方法:传入 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) {
2026-06-05 19:06:21 +08:00
// // debugPrint('✅ [_updateStatusFromDevice] 收到运行状态更新:$newStatus');
if (!isClosed) {
2026-06-05 19:06:21 +08:00
emit(
state.copyWith(
runningStatusModel: newStatus,
voltage: int.tryParse(newStatus.voltage) ?? 0,
battery: int.tryParse(newStatus.battery) ?? 0,
),
);
// debugPrint('✅ [更新运行状态] 电压:${newStatus.voltage}V, 电量:${newStatus.battery}%');
// _logger.logWithLevel('✅ [更新运行状态] 电压:${newStatus.voltage}V, 电量:${newStatus.battery}%');
}
}
// 🔥 辅助方法:更新运行状态
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));
// }
// });
// }
Future<void> _initPacketListener() async {
2026-06-05 19:06:21 +08:00
final logPrefix = '${_getTimePrefix()} 🔍 [RemoteControl] [0x12监听器]';
debugPrint('$logPrefix =========================================');
debugPrint('$logPrefix 开始初始化 0x12 监听器');
_logger.logWithLevel('$logPrefix 开始初始化 0x12 监听器', shouldLog: true);
2026-03-13 20:29:06 +08:00
_kickOutSub?.cancel(); // 防止重复监听
_kickOutSub = dispatcher.onCommand(0x12).listen((packet) {
2026-06-05 19:06:21 +08:00
final timeNow = _getTimePrefix();
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ✅ 收到 0x12 原始包');
/* debugPrint(
2026-06-05 19:06:21 +08:00
'$timeNow 🔍 [RemoteControl] [0x12监听器] payload长度: ${packet.payload.length}',
);*/
2026-06-05 19:06:21 +08:00
_logger.logWithLevel(
'$timeNow 🔍 [RemoteControl] [0x12监听器] 收到 0x12 原始包,payload长度=${packet.payload.length}',
shouldLog: true,
);
2026-03-13 20:29:06 +08:00
// 先解析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) {
// 解析失败,继续处理
}
2026-03-13 20:29:06 +08:00
try {
// 🔥 关键:手动去掉最后 2 个 CRC 字节
String jsonString;
if (packet.payload.length > 2) {
2026-06-05 19:06:21 +08:00
jsonString = utf8.decode(
packet.payload.sublist(0, packet.payload.length - 2),
);
2026-03-13 20:29:06 +08:00
} else {
jsonString = utf8.decode(packet.payload);
}
2026-06-05 19:06:21 +08:00
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] JSON内容: $jsonString');
_logger.logWithLevel(
'$timeNow 🔍 [RemoteControl] [0x12监听器] 去除CRC后的JSON: $jsonString',
shouldLog: true,
);
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-06-05 19:06:21 +08:00
debugPrint(
'$timeNow 🔍 [RemoteControl] [0x12监听器] requestType: $requestType, platform: $platform, hasRespond: ${respondData != null}',
);
2026-03-16 15:31:14 +08:00
// 情况 1: 响应格式 - {"respond":{"switchResult":true,"deviceId":"...","holder":"you"}}
if (respondData != null && respondData is Map) {
// 🔥 收到响应格式,更新权限状态
2026-03-16 15:31:14 +08:00
final switchResult = respondData['switchResult'];
/* debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] 📊 收到切换结果响应');
2026-06-05 19:06:21 +08:00
debugPrint(
'$timeNow 🔍 [RemoteControl] [0x12监听器] switchResult: $switchResult',
);*/
2026-06-05 19:06:21 +08:00
_logger.logWithLevel(
'$timeNow 🔍 [RemoteControl] [0x12监听器] 📊 收到切换结果响应 - switchResult: $switchResult',
shouldLog: true,
);
2026-03-16 15:31:14 +08:00
if (!isClosed) {
if (switchResult == true) {
2026-06-05 19:06:21 +08:00
// 切换成功,当前 APP 获得控制权
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ✅ APP获得控制权');
debugPrint(
'$timeNow 🔍 [RemoteControl] [0x12监听器] 设置 hasPermission = true',
);
emit(state.copyWith(hasPermission: true));
2026-03-16 15:31:14 +08:00
} else {
2026-06-05 19:06:21 +08:00
// 切换失败或拒绝,APP 失去控制权
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ❌ APP失去控制权');
debugPrint(
'$timeNow 🔍 [RemoteControl] [0x12监听器] 设置 hasPermission = false',
);
emit(
state.copyWith(
hasPermission: false,
// 🔥 修复:不强制关闭弹窗,让弹窗由用户操作控制
),
);
2026-03-16 15:31:14 +08:00
}
}
}
// 情况 2: 请求格式 - {"request":"switch_control","deviceId":"...","platform":"web",...}
else if (requestType == 'switch_control') {
2026-06-05 19:06:21 +08:00
final requestDeviceId = jsonMap['deviceId'];
final webLogPrefix =
'${_getTimePrefix()} 🚨 [RemoteControl] [Web端权限请求]';
// 🔥 新增:记录收到请求的时间,便于排查是否为后端持续推送
debugPrint(
'$webLogPrefix ⚡️⚡️⚡️ 收到Web端权限请求 - ${DateTime.now().toString()}',
);
_logger.logWithLevel(
'$webLogPrefix ⚡️⚡️⚡️ 收到Web端权限请求 - ${DateTime.now().toString()}',
shouldLog: true,
);
2026-06-05 19:06:21 +08:00
debugPrint('$webLogPrefix =========================================');
debugPrint('$webLogPrefix 收到 switch_control 请求');
debugPrint('$webLogPrefix platform: $platform');
debugPrint('$webLogPrefix requestDeviceId: $requestDeviceId');
_logger.logWithLevel(
'$webLogPrefix 收到 switch_control 请求 - platform: $platform, deviceId: $requestDeviceId',
shouldLog: true,
);
2026-03-16 15:31:14 +08:00
// 🔥 关键判断:只有当是其他平台(web)请求时才弹窗
if (platform != null && platform.toString().toLowerCase() != 'app') {
2026-06-05 19:06:21 +08:00
// 🔥 验证设备ID是否与当前控制的 targetDevice 一致
final currentDeviceId = state.targetDevice?.deviceName;
debugPrint('$webLogPrefix 当前控制设备ID: $currentDeviceId');
if (currentDeviceId != null && requestDeviceId == currentDeviceId) {
debugPrint('$webLogPrefix 设备ID匹配');
// 🔥 只有当弹窗还没显示时才弹出,防止重复弹窗叠加
if (state.showPermissionRequestDialog) {
2026-06-05 19:06:21 +08:00
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',
),
);
2026-06-05 19:06:21 +08:00
}
} else {
debugPrint('$webLogPrefix ⚠️ 设备ID不匹配,忽略');
debugPrint(
'$webLogPrefix currentDeviceId: $currentDeviceId, requestDeviceId: $requestDeviceId',
);
_logger.logWithLevel(
'$webLogPrefix ⚠️ 设备ID不匹配,忽略请求',
shouldLog: true,
);
2026-03-16 15:31:14 +08:00
}
} else {
2026-06-05 19:06:21 +08:00
debugPrint('$webLogPrefix ℹ️ APP自己的请求回显或platform为空,忽略不弹窗');
_logger.logWithLevel(
'$webLogPrefix ℹ️ APP自己的请求回显,忽略',
shouldLog: true,
);
2026-03-16 15:31:14 +08:00
}
2026-06-05 19:06:21 +08:00
debugPrint('$webLogPrefix =========================================');
2026-03-16 15:31:14 +08:00
}
// 情况 3: 异地登录通知 - {"request":"have_logged_in",...}
else if (requestType == 'have_logged_in') {
// 🔥 关键修复:检查是否是自身 0x03 认证触发的
if (tcpClient.isOwnAuthTriggeredKick()) {
debugPrint('${_getTimePrefix()} 🛡️ [RemoteControl] 自身认证触发的 have_logged_in,忽略');
_logger.logWithLevel(
'${_getTimePrefix()} 🛡️ [RemoteControl] 自身认证触发的 have_logged_in,忽略',
shouldLog: true,
);
return;
}
debugPrint('${_getTimePrefix()} ⚠️ [RemoteControl] 检测到真正的异地登录');
2026-06-05 19:06:21 +08:00
_logger.logWithLevel(
'${_getTimePrefix()} ⚠️ [RemoteControl] 检测到异地登录,打开弹窗提示',
shouldLog: true,
);
// 🔥 只有当弹窗还没显示时才弹出
if (!isClosed && !state.showPermissionRequestDialog) {
2026-03-16 15:31:14 +08:00
emit(state.copyWith(showPermissionRequestDialog: true));
}
2026-06-05 19:06:21 +08:00
} else {
debugPrint('${_getTimePrefix()} ℹ️ [RemoteControl] 未知类型的 0x12 包,忽略');
_logger.logWithLevel(
'${_getTimePrefix()} ℹ️ [RemoteControl] 未知类型的 0x12 包,忽略',
shouldLog: true,
);
2026-03-13 20:29:06 +08:00
}
} catch (e) {
2026-06-05 19:06:21 +08:00
debugPrint('${_getTimePrefix()} ❌ [RemoteControl] 解析失败:$e');
_logger.logWithLevel(
'${_getTimePrefix()} ❌ [RemoteControl] 解析失败:$e',
shouldLog: true,
);
2026-01-18 20:21:14 +08:00
}
});
}
// 2. 开启 100ms 控制循环 (在进入遥控页面或点击“开始”时调用)
2026-06-05 19:06:21 +08:00
/* void startControlLoop() {
2026-01-18 20:21:14 +08:00
_timer?.cancel();
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
// 🔥 关键修复:每次循环都重新读取最新的 state
final currentEntity = state.controlEntity;
// 🔥 安全检查:无权限或急停时不发送
if (!state.hasPermission || state.isEmergency) {
2026-06-05 19:06:21 +08:00
// debugPrint('⚠️ [定时器] 跳过发送 - hasPermission=${state.hasPermission}, isEmergency=${state.isEmergency}');
return;
}
2026-06-05 19:06:21 +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));
}*/
void startControlLoop() {
_timer?.cancel();
2026-06-05 19:06:21 +08:00
final logPrefix = '⏰ [RemoteControl] [控制循环]';
//debugPrint('$logPrefix =========================================');
//debugPrint('$logPrefix 启动控制循环,间隔: 100ms');
///debugPrint('$logPrefix =========================================');
// 🔥 启动模拟设备状态推送(用于测试)
// startSimulation();
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
if (isClosed) {
2026-06-05 19:06:21 +08:00
// debugPrint('$logPrefix ❌ Cubit已关闭,取消定时器');
timer.cancel();
return;
}
// 🔥 参考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-06-05 19:06:21 +08:00
// 常规安全检查
if (!state.hasPermission) {
2026-06-05 19:06:21 +08:00
// debugPrint('$logPrefix ⚠️ 无权限,跳过发送 - hasPermission=false');
return;
}
2026-06-05 19:06:21 +08:00
if (state.isEmergency) {
2026-06-05 19:06:21 +08:00
// debugPrint('$logPrefix 🚨 急停状态,跳过发送 - isEmergency=true');
return;
}
2026-06-05 19:06:21 +08:00
// 发送控制指令
//debugPrint(
// '$logPrefix 📤 发送控制指令: originX=${snapshot.originX}, originY=${snapshot.originY}, mower=${snapshot.mowerSpeed}, lift=${snapshot.chassisLift}, ignition=${snapshot.ignitionStatus}, emergency=${snapshot.isEmergency}',
// );
_repository.sendControlMachineCmd(snapshot);
2026-06-05 19:06:21 +08:00
//debugPrint('$logPrefix ✅ 控制指令已发送');
});
}
2026-06-05 19:06:21 +08:00
//通过方法拿最新 state
MachineControlStatusEntity _getCurrentControlEntity() {
// 🔥 关键修复:必须copyWith创建新对象,避免引用竞态条件
final entity = state.controlEntity;
2026-06-05 19:06:21 +08:00
// _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-06-05 19:06:21 +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,
);
//emit(state.copyWith(controlEntity: updatedEntity));
2026-06-05 19:06:21 +08:00
emit(
state.copyWith(
controlEntity: updatedEntity,
isEmergency: emergency ?? state.isEmergency,
),
);
// debugPrint('>>> [updateFunction] 状态已更新到emit');
// _logger.logWithLevel('>>> [updateFunction] 状态已更新到emit');
2026-01-18 20:21:14 +08:00
}
void updateOriginY(int y) {
2026-06-05 19:06:21 +08:00
// debugPrint('📥 [updateOriginY] 被调用- y=$y');
// 🔥 参考Android版:直接更新成员变量
_currentOriginY = y;
2026-06-05 19:06:21 +08:00
// 同时更新state(用于UI显示)
final updatedEntity = state.controlEntity.copyWith(y: y);
emit(state.copyWith(controlEntity: updatedEntity));
2026-06-05 19:06:21 +08:00
// debugPrint('📝 [updateOriginY] state已更新- originY=$y');
}
2026-06-05 19:06:21 +08:00
void updateOriginX(int x) {
2026-06-05 19:06:21 +08:00
// debugPrint('📥 [updateOriginX] 被调用- x=$x');
// 🔥 参考Android版:直接更新成员变量
_currentOriginX = x;
2026-06-05 19:06:21 +08:00
// 同时更新state(用于UI显示)
final updatedEntity = state.controlEntity.copyWith(x: x);
emit(state.copyWith(controlEntity: updatedEntity));
2026-06-05 19:06:21 +08:00
// debugPrint('📝 [updateOriginX] state已更新- originX=$x');
}
2026-06-05 19:06:21 +08:00
/// 🔥 安全方法:同时清零双轴,确保只发送一次完全停止指令
Future<void> stopAllMovement() async {
2026-06-05 19:06:21 +08:00
// debugPrint(
// '🛑 [stopAllMovement] 开始执行- 当前成员变量: originX=$_currentOriginX, originY=$_currentOriginY',
// );
// 🔥 参考Android版:直接清零成员变量
_currentOriginX = 0;
_currentOriginY = 0;
2026-06-05 19:06:21 +08:00
// 🔥 关键修复:先暂停定时器,防止定时器在停止期间发送旧的运动指令
_timer?.cancel();
2026-06-05 19:06:21 +08:00
_timer = null; // 🔥 彻底清空,防止重复启用
final updatedEntity = state.controlEntity.copyWith(x: 0, y: 0);
2026-06-05 19:06:21 +08:00
// 一次性更新双轴(即使无权限也要更新本地state)
emit(state.copyWith(controlEntity: updatedEntity));
2026-06-05 19:06:21 +08:00
// 🔥 安全底线:停止指令必须无视权限强制发送!
2026-06-05 19:06:21 +08:00
// debugPrint('🛑 [stopAllMovement] 双轴归零,发送停止指令- originX=0, originY=0');
await _sendStopCommandRepeatedly(updatedEntity); // 等待所有指令发送完成
// 🔥 恢复定时器
await Future.delayed(const Duration(milliseconds: 200));
startControlLoop();
2026-06-05 19:06:21 +08:00
// debugPrint('>>> [stopAllMovement] 执行完成');
}
2026-06-05 19:06:21 +08:00
/// 🔥 统一方法:连续发送10次停止指令,彻底清空TCP缓冲区
Future<void> _sendStopCommandRepeatedly(
MachineControlStatusEntity stopEntity,
) async {
// debugPrint(
// '🛑 [紧急停止] 开始连续发送10次停止指令- 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) {
2026-06-05 19:06:21 +08:00
// debugPrint('🛑 [紧急停止] 已发送第${i + 1}次');
}
if (i < 19) {
await Future.delayed(const Duration(milliseconds: 5));
}
}
2026-06-05 19:06:21 +08:00
// 🔥 延迟后再发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-06-05 19:06:21 +08:00
// debugPrint('>>> [紧急停止] 所有20次停止指令已发出');
}
2026-01-18 20:21:14 +08:00
// 5. 停止控制循环
void stopControlLoop() {
_timer?.cancel();
_timer = null;
emit(state.copyWith(status: RemoteControlStatus.initial));
}
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-06-05 19:06:21 +08:00
void toggleRightPip() =>
emit(state.copyWith(showRightPip: !state.showRightPip));
2026-01-21 13:27:55 +08:00
2026-07-11 16:46:53 +08:00
// 🔥 切换前后视角(双击屏幕触发)
void toggleCameraView() {
final currentOriginY = state.controlEntity.originY;
// 🔥 originY >= 0 表示前视角,< 0 表示后视角
// 切换逻辑:如果当前是前视角(>=0),切换到后视角(-1);反之亦然
final newOriginY = currentOriginY >= 0 ? -1 : 1;
debugPrint('📷 [RemoteControlCubit] 切换前后视角: $currentOriginY -> $newOriginY');
final updatedEntity = state.controlEntity.copyWith(y: newOriginY);
emit(state.copyWith(controlEntity: updatedEntity));
// 🔥 发送 TCP 指令到设备
_repository.sendControlMachineCmd(updatedEntity);
}
2026-01-18 20:21:14 +08:00
@override
Future<void> close() {
_timer?.cancel();
_deviceStatusSub?.cancel(); // 🔥 取消订阅 DeviceStatusBloc
2026-01-18 20:21:14 +08:00
return super.close();
}
void updateChassisLift(int i) {}
void updateEmergency(bool bool) {
2026-06-05 19:06:21 +08:00
// debugPrint('🚨 [急停] ${bool ? "触发急停!" : "解除急停"}');
updateFunction(emergency: bool);
}
2026-01-18 20:21:14 +08:00
2026-06-05 19:06:21 +08:00
void respondPermission(bool agreed, String deviceId) {
// 1. 关闭弹窗(立即关闭,防止重复点击)
2026-03-16 15:31:14 +08:00
emit(state.copyWith(showPermissionRequestDialog: false));
2026-06-05 19:06:21 +08:00
// 2. 发送响应到服务器
try {
_repository.respondPermission(agreed, deviceId);
} catch (e) {
debugPrint(' ❌ TCP权限响应指令发送失败: $e');
debugPrint(' ❌ 错误类型: ${e.runtimeType}');
_logger.logWithLevel(' TCP权限响应指令发送失败 $e', shouldLog: true);
rethrow;
}
2026-03-16 15:31:14 +08:00
// 3. 根据用户选择更新控制状态
2026-06-05 19:06:21 +08:00
if (agreed) {
emit(
state.copyWith(
hasPermission: false,
showPermissionRequestDialog: false,
),
);
2026-03-16 15:31:14 +08:00
} else {
2026-06-05 19:06:21 +08:00
// 用户拒绝 APP 继续保持控制权
// 🔥 必须同时设置 showPermissionRequestDialog: false,防止状态回退
emit(
state.copyWith(hasPermission: true, showPermissionRequestDialog: false),
);
2026-03-16 15:31:14 +08:00
}
debugPrint('${_getTimePrefix()} ====权限弹窗响应结束=====');
2026-03-16 15:31:14 +08:00
}
2026-06-05 19:06:21 +08:00
Future<void> requestControlPermissionS(
String deviceName,
String deviceId, {
String source = '自动',
}) async {
final timePrefix = _getTimePrefix();
final logPrefix = '$timePrefix 🔑 [RemoteControl] [请求权限接口-$source]';
debugPrint('$logPrefix =========================================');
debugPrint('$logPrefix 开始请求控制权');
debugPrint('$logPrefix deviceName: $deviceName');
debugPrint('$logPrefix platform: $deviceId');
debugPrint(
'$logPrefix 当前状态 hasPermission=${state.hasPermission}, showDialog=${state.showPermissionRequestDialog}',
);
_logger.logWithLevel(
'$logPrefix 开始请求控制权- deviceName: $deviceName, platform: $deviceId',
shouldLog: true,
);
// 1. 只有当弹窗不是因Web 端请求权限而显示时,才关闭弹窗
// 避免 Web 端请求权限的弹窗被自动关闭(一闪而过的问题)
if (state.requestingPlatform == null) {
debugPrint('$logPrefix 关闭权限请求弹窗 (非Web端触发)');
emit(state.copyWith(showPermissionRequestDialog: false));
} else {
debugPrint('$logPrefix 保留弹窗 (Web端请求触发)');
}
2026-06-05 19:06:21 +08:00
// 2. 调用 UseCase 获取 HTTP 返回的完整权限信息
debugPrint('$logPrefix 调用 HTTP 接口查询权限状态..');
final result = await _requestControlPermissionUseCase(
RequestControlPermissionParams(
deviceName: deviceName,
deviceId: deviceId,
),
);
// 3. 处理结果
result.fold(
2026-06-05 19:06:21 +08:00
(failure) {
final failLogPrefix =
'${_getTimePrefix()} 🔑 [RemoteControl] [请求权限接口-$source]';
debugPrint('$failLogPrefix APP请求控制权限失败: ${failure.message}');
_logger.logWithLevel(
'$failLogPrefix APP请求控制权限失败: ${failure.message}',
shouldLog: true,
);
// 🔥 修复:HTTP请求失败时不要自动打开弹窗,避免形成循环
// emit(state.copyWith(showPermissionRequestDialog: true));
debugPrint('$failLogPrefix HTTP请求失败,不自动打开弹窗');
2026-06-05 19:06:21 +08:00
},
(permissionInfo) async {
final bool hasPermission =
permissionInfo['hasPermission'] as bool? ?? false;
final String? owner = permissionInfo['owner'] as String?;
final successLogPrefix =
'${_getTimePrefix()} 🔑 [RemoteControl] [请求权限接口-$source]';
debugPrint(
'$successLogPrefix APP HTTP返回 - hasPermission=$hasPermission, owner=$owner',
);
_logger.logWithLevel(
'$successLogPrefix APP HTTP返回 - hasPermission=$hasPermission, owner=$owner',
shouldLog: true,
);
// 🔥 关键逻辑: 如果没有权限 或owner为null,则发送TCP 请求
if (!hasPermission || owner == null) {
debugPrint('$successLogPrefix ⚠️ APP无权限或owner为null,发送TCP请求...');
_logger.logWithLevel(
'$successLogPrefix ⚠️ APP无权限或owner为null,发送TCP请求',
shouldLog: true,
);
// 🔥 发送TCP 0x12 权限请求指令
_repository.sendTcpPermissionRequest(deviceName);
// 等待 TCP 回包(通过监听器更新状态
debugPrint('$successLogPrefix 📡 TCP请求已发送,等待回包确认');
} else {
debugPrint('$successLogPrefix APP已有权限,直接更新UI');
_logger.logWithLevel(
'$successLogPrefix APP已有权限,直接更新UI',
shouldLog: true,
);
debugPrint(
'$successLogPrefix 当前 hasPermission 状态: ${state.hasPermission}',
);
// 🔥 正确的状态更新:使用最新状态
if (state.hasPermission != true) {
debugPrint('$successLogPrefix 🔄 更新 hasPermission = true');
emit(state.copyWith(hasPermission: true));
debugPrint('$successLogPrefix ✅ hasPermission 状态已更新为 true');
} else {
debugPrint('$successLogPrefix ⚠️ hasPermission 已是 true,无需更新');
// 强制触发UI刷新:通过临时改变其他属性
emit(state.copyWith(ping: state.ping + 1));
emit(state.copyWith(ping: state.ping));
}
}
},
);
debugPrint(
'${_getTimePrefix()} 🔑 [RemoteControl] [请求权限接口-$source] =========================================',
);
}
/// 🔥 重置弹窗状态 - 在弹窗关闭后调用(已简化,不再需要标志位)
void resetPermissionCoolDown() {
// 标志位已移除,此方法保留以保持向后兼容性
}
2026-06-05 19:06:21 +08:00
Future<void> confirmPermissionResponse(
String deviceName,
String platform,
bool agreed,
) async {
final timePrefix = _getTimePrefix();
final logPrefix = '$timePrefix 🔑 [RemoteControl] [权限确认]';
debugPrint('$logPrefix ========进入TCP发送=================================');
debugPrint('$logPrefix ⚡️ confirmPermissionResponse 方法被调用');
debugPrint('$logPrefix 用户操作: ${agreed ? "同意" : "拒绝"}');
debugPrint('$logPrefix deviceName: $deviceName');
debugPrint('$logPrefix platform: $platform');
2026-06-05 19:06:21 +08:00
respondPermission(agreed, deviceName);
// 2. 调用 HTTP 接口获取最终权限状态
debugPrint('$logPrefix 📡 调用 HTTP 接口确认最终权限状态..');
/* final result = await _requestControlPermissionUseCase(
2026-06-05 19:06:21 +08:00
RequestControlPermissionParams(
deviceName: deviceName,
deviceId: platform,
),
);*/
// 3. 根据 HTTP 返回的真实权限状态更新UI
/* result.fold(
2026-06-05 19:06:21 +08:00
(failure) {
final failLogPrefix = '${_getTimePrefix()} 🔑 [RemoteControl] [权限确认]';
debugPrint('$failLogPrefix APP HTTP请求失败: ${failure.message}');
debugPrint('$failLogPrefix ⚠️ 保持当前状态不变');
},
2026-06-05 19:06:21 +08:00
(permissionInfo) {
final bool hasPermission =
permissionInfo['hasPermission'] as bool? ?? false;
final String? owner = permissionInfo['owner'] as String?;
final successLogPrefix =
'${_getTimePrefix()} 🔑 [RemoteControl] [权限确认]';
debugPrint(
'$successLogPrefix APP HTTP返回真实权限状态 hasPermission=$hasPermission, owner=$owner',
);
debugPrint(
'$successLogPrefix 🔄 正在更新UI - hasPermission: ${state.hasPermission} -> $hasPermission',
);
// 🔥 直接用HTTP 返回的权限状态覆盖
// emit(state.copyWith(hasPermission: hasPermission));
2026-06-05 19:06:21 +08:00
debugPrint(
'$successLogPrefix 📊 UI已同步完成- 当前 hasPermission=$hasPermission',
);
},
2026-06-05 19:06:21 +08:00
);*/
debugPrint(
'${_getTimePrefix()} 🔑 [RemoteControl] [权限确认] =========================================',
);
}
2026-06-05 19:06:21 +08:00
/// 发送底盘指令
void sendChassisCommand(int i) {
2026-06-05 19:06:21 +08:00
// // debugPrint('>>> [底盘指令] ${i}');
// _logger.logWithLevel('>>> [底盘指令] ${i}');
updateFunction(lift: i);
}
2026-06-05 19:06:21 +08:00
/// 发送割刀指令
void sendMowerCommand(int i) {
2026-06-05 19:06:21 +08:00
// // debugPrint('>>> [割刀指令] ${i}');
// _logger.logWithLevel('>>> [割刀指令] ${i}');
updateFunction(mower: i);
}
2026-06-05 19:06:21 +08:00
/// 发送点火指令
void sendFireCommand(int i) {
//void updateFunction({int? mower, int? lift, int? ignition, bool? emergency})
2026-06-05 19:06:21 +08:00
// updateFunction(mower:0, lift: 0, ignition: i, emergency: false);
// debugPrint('>>> [点火指令] ${i}');
// _logger.logWithLevel('>>> [点火指令] ${i}');
updateFunction(ignition: i);
}
2026-06-05 19:06:21 +08:00
// 发送障碍物识别指令
void toggleObstacleRecognition() {
2026-06-05 19:06:21 +08:00
emit(
state.copyWith(obstacleRecognitionFlag: !state.obstacleRecognitionFlag),
);
}
void toggleTopLeftExpand() {
emit(state.copyWith(topRightIsExpanded: !state.topRightIsExpanded));
}
2026-07-11 16:46:53 +08:00
/// 检查设备状态推送是否活跃(5秒内有推送视为活跃)
bool isStatusPushActive() {
if (!state.hasReceivedStatusPush) return false;
if (_lastStatusPushTime == null) return false;
// 超过5秒未收到新推送,视为推送已中断
return DateTime.now().difference(_lastStatusPushTime!) < const Duration(seconds: 5);
}
Future<int> getNetworkDelay() async {
try {
2026-06-05 19:06:21 +08:00
// 直接 Ping 你的服务器IP
final ping = Ping('1.95.137.212', count: 1, timeout: 1);
// 等待一次结果
final data = await ping.stream.first;
if (data.response != null && data.response!.time != null) {
2026-06-05 19:06:21 +08:00
// 返回和cmd 一样的毫秒值
return data.response!.time!.inMilliseconds;
} else {
return 9999;
}
} catch (e) {
return 9999;
}
}
2026-06-05 19:06:21 +08:00
//app退出远程遥控界面释放权限
Future<bool> releasePermission(String platform) async {
return await _repository.releasePermission(platform);
}
2026-06-05 19:06:21 +08:00
/// 🔥 设置待控制的设备(从机器人列表点击进入时调用)
void setTargetDevice(DeviceEntity device) async {
debugPrint('🎯 [RemoteControl] ========== 开始设置目标设备 ==========');
debugPrint('🎯 [RemoteControl] 设备名称: ${device.deviceName}');
debugPrint('🎯 [RemoteControl] 当前TCP状态: ${tcpClient.isConnected ? "已连接" : "未连接"}');
2026-07-11 16:46:53 +08:00
// 🔥 关键修复:切换设备时清空所有缓存,防止显示上一个设备的数据
_clearAllCache();
debugPrint('✅ [RemoteControl] 已清空所有缓存数据');
debugPrint('📦 [RemoteControl] 更新targetDevice状态');
emit(state.copyWith(targetDevice: device));
debugPrint('✅ [RemoteControl] targetDevice状态已更新');
// 🔥 关键修复:TCP 已在登录时建立,设备切换只用 HTTP
// 不再创建新 TCP 连接,避免重复发送 0x03 触发服务端推送 have_logged_in
if (tcpClient.isConnected) {
debugPrint('✅ [RemoteControl] TCP已连接,使用HTTP切换设备: ${device.deviceName}');
_deviceRepository.switchDevice("app", device.deviceName).then((result) {
result.fold(
(failure) {
debugPrint('❌ [RemoteControl] HTTP切换设备失败: ${failure.message}');
},
(success) {
debugPrint('✅ [RemoteControl] HTTP切换设备成功, code: $success');
},
);
});
} else {
// TCP未连接(登录时的初始化可能失败或还在进行中)
// 兜底:使用 connectBySwitch 建立连接
debugPrint('🔌 [RemoteControl] TCP未连接,兜底调用 connectBySwitch');
try {
await tcpClient.connectBySwitch(
host: TCPConsts.TCP_IP,
port: TCPConsts.TCP_PORT,
deviceName: device.deviceName,
);
debugPrint('✅ [RemoteControl] TCP兜底连接成功!');
} catch (e) {
debugPrint('❌ [RemoteControl] TCP兜底连接失败: $e');
}
}
2026-06-05 19:06:21 +08:00
debugPrint('🎯 [RemoteControl] ========== 目标设备设置完成 ==========');
2026-06-05 19:06:21 +08:00
}
2026-03-13 20:29:06 +08:00
2026-06-05 19:06:21 +08:00
/// 🔥 清除待控制设备(退出远程控制页时调用)
void clearTargetDevice() {
// debugPrint('🧹 [RemoteControl] 清除待控制设备');
// _logger.logWithLevel('🧹 [RemoteControl] 清除待控制设备');
2026-07-11 16:46:53 +08:00
// 🔥 关键修复:退出页面时清空所有缓存,防止数据滞留
_clearAllCache();
debugPrint('✅ [RemoteControl] 已清空所有缓存数据');
2026-06-05 19:06:21 +08:00
emit(state.copyWith(targetDevice: null));
}
/// 🔥 获取设备
DeviceEntity? getSelectedDevice() {
return state.targetDevice;
}
2026-07-11 16:46:53 +08:00
/// 🔥 清空所有缓存数据(解决数据滞留和跨设备数据显示问题)
void _clearAllCache() {
_cacheVoltage = null;
_cacheBattery = null;
_cacheCtrlMode = null;
_cachePing = null;
_lastUiUpdateTime = null;
_lastStatusPushTime = null; // 重置推送时间
// 🔥 同时重置 UI 状态为初始值
if (!isClosed) {
emit(
state.copyWith(
runningStatusModel: state.runningStatusModel.copyWith(
voltage: '--',
battery: '--',
controlMode: '--',
),
battery: 0,
ping: null,
hasReceivedStatusPush: false, // 重置推送接收标志
),
);
}
debugPrint('🗑️ [RemoteControl] 缓存已清空 - voltage, battery, controlMode, ping');
}
2026-01-18 20:21:14 +08:00
}