diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index 94ff6024..8b6e7446 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -51,6 +51,9 @@ import '../../features/devices/domain/usecases/save_work_record_usecase.dart'; import '../../features/devices/domain/usecases/select_work_record_usecase.dart'; import '../../features/devices/domain/usecases/switch_device_usecase.dart'; import '../../features/devices/presentation/bloc/device_status_bloc.dart'; +import '../../features/remote_control/data/datasources/remote_http_datasource.dart'; +import '../../features/remote_control/data/datasources/remote_tcp_datasource.dart'; +import '../../features/remote_control/domain/usecase/remote_control_usecase.dart'; import '../../features/remote_control/presentation/bloc/remote_control_cubit.dart'; import '../app/app_user_cubit.dart'; import '../network/dio_client.dart'; @@ -100,6 +103,13 @@ Future init() async { sl.registerLazySingleton( () => DeviceHttpDatasourceImpl(sl(), sl()), // ); + sl.registerLazySingleton( + () => RemoteHttpDatasource(sl(), sl()), + ); + + sl.registerLazySingleton( + () => RemoteTcpDatasource(sl(), sl()), + ); /// 3. 仓库 (Repository) sl.registerLazySingleton( @@ -144,7 +154,7 @@ Future init() async { ); sl.registerLazySingleton(() => DeviceRepositoryImpl(sl())); - sl.registerLazySingleton(() => RemoteControlRepositoryImpl(sl(), sl())); + sl.registerLazySingleton(() => RemoteControlRepositoryImpl(sl(), sl() ,sl(),sl())); /// 4. 用例 (UseCase) sl.registerLazySingleton(() => LoginUseCase(sl())); @@ -167,11 +177,14 @@ Future init() async { ), ); + sl.registerLazySingleton(() => RequestControlPermissionUseCase(sl())); + + /// 5. 状态管理 (Cubit/Bloc) sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册 sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl())); sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl(),sl())); - sl.registerFactory(() => RemoteControlCubit(sl())); + sl.registerFactory(() => RemoteControlCubit(sl(),sl())); /* // 工厂模式(留存,每次获取新实例) sl.registerFactory(() => DeviceStatusBloc(sl())); diff --git a/lib/features/remote_control/data/datasources/remote_http_datasource.dart b/lib/features/remote_control/data/datasources/remote_http_datasource.dart index 4e18af21..c48200a7 100644 --- a/lib/features/remote_control/data/datasources/remote_http_datasource.dart +++ b/lib/features/remote_control/data/datasources/remote_http_datasource.dart @@ -1,12 +1,19 @@ +import 'dart:ffi'; + import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; + +import '../../../../core/storage/user_storage.dart'; class RemoteHttpDatasource { final Dio _dio; + final UserStorage _userStorage; // 🔥 新增字段 - RemoteHttpDatasource(this._dio); + + RemoteHttpDatasource(this._dio, this._userStorage); /// 示例:获取控制授权 (对应之前逻辑中的权限申请) - Future> requestControlAuth(String robotId) async { +/* Future> requestControlAuth(String robotId) async { try { final response = await _dio.post( '/robot/auth/request', @@ -16,5 +23,36 @@ class RemoteHttpDatasource { } catch (e) { rethrow; } + }*/ + + + Future requestRemoteControlViaHttp( String deviceId, String currentDeviceId) async { + final user = await _userStorage.getUser(); + if (user == null) { + return false; + } + final token = user.token; + final response = await _dio.post( + '/forward/device/remoteControl', + options: Options( + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${token}', + }, + ), + data: {'deviceId': deviceId, 'platform': "app"}); + + try { + final data = response.data as Map; + final success = data['success'] as bool? ?? false; + if (success) { + return true; + } else { + return false; + } + } catch (e) { + debugPrint('❌ [RemoteHttp] 解析响应失败:$e'); + return false; + } } } diff --git a/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart b/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart index e69de29b..c5be989f 100644 --- a/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart +++ b/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../../../../core/network/protocol_decoder.dart'; +import '../../../../core/network/tcp/tcp_client.dart'; +import '../../../../core/protocol/machine_protocol_constants.dart'; +import '../../../../core/storage/user_storage.dart'; + +/// 远程遥控 TCP 数据源 +/// 负责通过 TCP 协议发送控制指令和权限请求 +class RemoteTcpDatasource { + final TcpClient _tcpClient; + final UserStorage _userStorage; + + RemoteTcpDatasource(this._tcpClient, this._userStorage); + + /// 发送 switch_control 权限请求(对应 Android 的 requestSwitchControl) + /// + /// 这是获取设备控制权的第一步: + /// 1. 先发送 TCP 0x12 指令携带 switch_control 请求 + /// 2. 等待服务端响应 + /// 3. 再调用 HTTP API 确认控制权 + Future sendSwitchControlRequest(String deviceName) async { + if (!_tcpClient.isConnected) { + debugPrint('❌ [TCP] 无法发送权限请求:Socket 未连接'); + throw Exception('TCP 未连接'); + } + + try { + // 获取用户信息 + final user = await _userStorage.getUser(); + if (user == null || user.token == null) { + debugPrint('❌ [TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包'); + throw Exception('用户未登录或 Token 无效'); + } + + final username = user.username; + final token = user.token; + final currentDeviceId = deviceName; + + // 构造 JSON 请求体(参考 Android 代码) + final request = { + 'request': 'switch_control', + 'deviceId': currentDeviceId, + 'userId': username, + 'platform': 'app', + 'token': token, + }; + + final jsonStr = jsonEncode(request); + final jsonBytes = utf8.encode(jsonStr); + + debugPrint('🔑 [TCP] 发送权限请求:$jsonStr'); + + // 使用 0x12 指令发送(cmdGetAuth) + // 注意:根据你的协议文档,这里可能是 0x04 或 0x12 + // Android 代码中使用的是 0x12 + _tcpClient.sendRaw( + MachineProtocolConstants.cmdGetAuth, // 0x04 或 0x12,根据实际协议调整 + jsonBytes, + ); + + debugPrint('✅ [TCP] 已发送 switch_control 权限请求 (0x${MachineProtocolConstants.cmdGetAuth.toRadixString(16)})'); + } catch (e) { + debugPrint('❌ [TCP] 发送权限请求失败:$e'); + rethrow; + } + } + + /// 监听 TCP 回包流 + /// 用于接收权限响应、状态更新等 + Stream get responseStream => _tcpClient.packetStream; + + /// 检查 TCP 连接状态 + bool get isConnected => _tcpClient.isConnected; +} diff --git a/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart b/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart index ec9626b5..c91d8583 100644 --- a/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart +++ b/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart @@ -1,4 +1,8 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; + +import 'package:flutter/cupertino.dart'; import '../../../../core/network/protocol_decoder.dart'; import '../../../../core/network/tcp/tcp_client.dart'; @@ -7,12 +11,17 @@ import '../../../../core/protocol/machine_protocol_constants.dart'; import '../../domain/entities/machine_control_status_entity.dart'; import '../../domain/repositories/remote_control_repository.dart'; import '../../domain/usecase/diff_steer_usecase.dart'; +import '../datasources/remote_http_datasource.dart'; +import '../datasources/remote_tcp_datasource.dart'; class RemoteControlRepositoryImpl implements RemoteControlRepository { final TcpClient _tcpClient; final DiffSteerUseCase _diffSteer; + final RemoteHttpDatasource _remoteHttp; + final RemoteTcpDatasource _remoteTcp; - RemoteControlRepositoryImpl(this._tcpClient, this._diffSteer); + + RemoteControlRepositoryImpl(this._tcpClient, this._diffSteer, this._remoteHttp, this._remoteTcp); @override void changeWebViewDirection(String direction) { @@ -24,6 +33,11 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { // 1. 调用算法:将摇杆坐标 (x, y) 转换为左右轮电机转速 final speeds = _diffSteer.calculate(status.originX, status.originY); + + debugPrint('🎮 [摇杆数据] originX: ${status.originX}, originY: ${status.originY}'); + debugPrint('⚙️ [电机速度] left: ${speeds['left']}, right: ${speeds['right']}'); + + // 2. 调用 Codec:仅生成协议要求的 8 字节 Payload 负载数据 final payload = MachineProtocolCodec.encodeRemoteControlPayload( left: speeds['left']!, @@ -33,6 +47,7 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { ignition: status.ignitionStatus, emergency: status.isEmergency ? 1 : 0, ); + debugPrint('📦 [发送数据] payload: ${payload}'); // 3. 调用 TcpClient:发送指令。 // TcpClient.sendRaw 会自动帮你加上 [0xAB, 0xAA] 头和 [0xAA, 0xAB] 尾 @@ -44,4 +59,16 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { @override Stream get responseStream => _tcpClient.packetStream; + + @override + Future requestControlPermission( String deviceName, String deviceId) async { + try { + await _remoteTcp.sendSwitchControlRequest(deviceName); + debugPrint('🔑 [RemoteControl] TCP已发送权限请求 (0x04)'); + return await _remoteHttp.requestRemoteControlViaHttp(deviceName, deviceId); + } catch (e) { + debugPrint('❌ [RemoteControl] 权限请求失败:$e'); + rethrow; + } + } } diff --git a/lib/features/remote_control/domain/repositories/remote_control_repository.dart b/lib/features/remote_control/domain/repositories/remote_control_repository.dart index 4bc0ab55..5717f6bd 100644 --- a/lib/features/remote_control/domain/repositories/remote_control_repository.dart +++ b/lib/features/remote_control/domain/repositories/remote_control_repository.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + import '../../../../core/network/protocol_decoder.dart'; import '../../domain/entities/machine_control_status_entity.dart'; @@ -12,4 +14,7 @@ abstract class RemoteControlRepository { /// WebView 视角控制 (逻辑层面的切换,不涉及 TCP) void changeWebViewDirection(String direction); + + /// 新增:请求控制权限 (发送 0x04 指令) + Future requestControlPermission(String deviceName, String deviceId); } diff --git a/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart b/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart index e9d92a4d..edaf3cce 100644 --- a/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart +++ b/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart @@ -29,4 +29,8 @@ class DiffSteerUseCase { int _signOperator(int num, int polarity) => polarity != polarityHigh ? -num : num; + + + + } diff --git a/lib/features/remote_control/domain/usecase/remote_control_usecase.dart b/lib/features/remote_control/domain/usecase/remote_control_usecase.dart new file mode 100644 index 00000000..c239c124 --- /dev/null +++ b/lib/features/remote_control/domain/usecase/remote_control_usecase.dart @@ -0,0 +1,33 @@ +import 'package:fpdart/fpdart.dart'; +import'package:maibu_satabot_v2/core/domain/usecases/base_usecase.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart'; + +class RequestControlPermissionUseCase implements BaseUseCase { + final RemoteControlRepository remoteControlRepository; + + RequestControlPermissionUseCase(this.remoteControlRepository); + + @override + Future> call(RequestControlPermissionParams params) async { + try { + final result = await remoteControlRepository.requestControlPermission( + params.deviceName, + params.deviceId, + ); + return Right(result); + } catch (e) { + return Left(Failure('请求控制权限失败:$e')); + } + } +} + +class RequestControlPermissionParams { + final String deviceName; + final String deviceId; + + const RequestControlPermissionParams({ + required this.deviceName, + required this.deviceId, + }); +} diff --git a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart index c66176fb..efa09680 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart @@ -6,12 +6,14 @@ import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remot import '../../domain/entities/machine_control_status_entity.dart'; import '../../domain/repositories/remote_control_repository.dart'; +import '../../domain/usecase/remote_control_usecase.dart'; class RemoteControlCubit extends Cubit { final RemoteControlRepository _repository; + final RequestControlPermissionUseCase _requestControlPermissionUseCase; Timer? _timer; - RemoteControlCubit(this._repository) + RemoteControlCubit(this._repository, this._requestControlPermissionUseCase) : super( RemoteControlState( controlEntity: MachineControlStatusEntity(), @@ -61,7 +63,18 @@ class RemoteControlCubit extends Cubit { emit(state.copyWith(controlEntity: updatedEntity)); } - void updateOriginY(int y) {} + void updateOriginY(int y) { + final updatedEntity = state.controlEntity.copyWith( + y: y, + ); + emit(state.copyWith(controlEntity: updatedEntity)); + } + void updateOriginX(int x) { + final updatedEntity = state.controlEntity.copyWith( + x: x, + ); + emit(state.copyWith(controlEntity: updatedEntity)); + } // 5. 停止控制循环 void stopControlLoop() { @@ -99,4 +112,35 @@ class RemoteControlCubit extends Cubit { void updateEmergency(bool bool) {} void respondPermission(bool bool) {} + + 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) { + print('❌ [RemoteControl] 请求控制权限失败:${failure.message}'); + // 可以在这里显示错误提示或重新打开弹窗 + emit(state.copyWith(showPermissionRequestDialog: true)); + + }, + (success) { + emit(state.copyWith(hasPermission: true)); + print('✅ c:$success'); + // 权限申请已发送,等待 0x12 回包更新状态 + }, + ); + } + + + } diff --git a/lib/features/remote_control/presentation/pages/remote_control_page.dart b/lib/features/remote_control/presentation/pages/remote_control_page.dart index 00422643..d7528722 100644 --- a/lib/features/remote_control/presentation/pages/remote_control_page.dart +++ b/lib/features/remote_control/presentation/pages/remote_control_page.dart @@ -33,6 +33,10 @@ class _RemoteControlPageState extends State { ]); // 2. 隐藏状态栏和虚拟按键 SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().startControlLoop(); + }); } @override @@ -49,6 +53,11 @@ class _RemoteControlPageState extends State { final deviceState = context.watch().state; final currentDevice = deviceState.selectedDevice; + context.read().requestControlPermissionS( + currentDevice!.deviceName, + "app", + ); + if (currentDevice == null) { return _buildOfflineScaffold(); } diff --git a/lib/features/remote_control/presentation/widgets/right_joystick_area.dart b/lib/features/remote_control/presentation/widgets/right_joystick_area.dart index 011a04a6..04fb2921 100644 --- a/lib/features/remote_control/presentation/widgets/right_joystick_area.dart +++ b/lib/features/remote_control/presentation/widgets/right_joystick_area.dart @@ -31,7 +31,8 @@ class RightJoystickArea extends StatelessWidget { axisHint: AxisHint.leftRight, // 关键:指定为左右控制 onValueChanged: (value) { // 对应 viewModel.updateOriginX(x) - context.read().updateOriginY(value.x); + /// context.read().updateOriginY(value.x); + context.read().updateOriginX(value.x); }, onPress: () { // 对应 VibrateOnce(current, 100)