完成优化登录的优化

完成切换和切回APP数据的及时响应
完成服务下发退出账号的命令
完成远程遥控的权限的前置确认和获取
完后远程遥控的前后左右的控制.
This commit is contained in:
2026-03-10 10:53:58 +08:00
parent a197327066
commit f8e1e059dd
10 changed files with 260 additions and 8 deletions

View File

@@ -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<void> init() async {
sl.registerLazySingleton<DeviceHttpDatasource>(
() => DeviceHttpDatasourceImpl(sl(), sl<UserStorage>()), //
);
sl.registerLazySingleton<RemoteHttpDatasource>(
() => RemoteHttpDatasource(sl(), sl<UserStorage>()),
);
sl.registerLazySingleton<RemoteTcpDatasource>(
() => RemoteTcpDatasource(sl<TcpClient>(), sl<UserStorage>()),
);
/// 3. 仓库 (Repository)
sl.registerLazySingleton<AuthRepository>(
@@ -144,7 +154,7 @@ Future<void> init() async {
);
sl.registerLazySingleton<DeviceRepository>(() => DeviceRepositoryImpl(sl()));
sl.registerLazySingleton<RemoteControlRepository>(() => RemoteControlRepositoryImpl(sl(), sl()));
sl.registerLazySingleton<RemoteControlRepository>(() => RemoteControlRepositoryImpl(sl(), sl() ,sl(),sl()));
/// 4. 用例 (UseCase)
sl.registerLazySingleton(() => LoginUseCase(sl()));
@@ -167,11 +177,14 @@ Future<void> init() async {
),
);
sl.registerLazySingleton<RequestControlPermissionUseCase>(() => 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<NetMessageDispatcher>()));

View File

@@ -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<Map<String, dynamic>> requestControlAuth(String robotId) async {
/* Future<Map<String, dynamic>> requestControlAuth(String robotId) async {
try {
final response = await _dio.post(
'/robot/auth/request',
@@ -16,5 +23,36 @@ class RemoteHttpDatasource {
} catch (e) {
rethrow;
}
}*/
Future<bool> 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<String, dynamic>;
final success = data['success'] as bool? ?? false;
if (success) {
return true;
} else {
return false;
}
} catch (e) {
debugPrint('❌ [RemoteHttp] 解析响应失败:$e');
return false;
}
}
}

View File

@@ -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<void> 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<RawPacket> get responseStream => _tcpClient.packetStream;
/// 检查 TCP 连接状态
bool get isConnected => _tcpClient.isConnected;
}

View File

@@ -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<RawPacket> get responseStream => _tcpClient.packetStream;
@override
Future<bool> 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;
}
}
}

View File

@@ -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<bool> requestControlPermission(String deviceName, String deviceId);
}

View File

@@ -29,4 +29,8 @@ class DiffSteerUseCase {
int _signOperator(int num, int polarity) =>
polarity != polarityHigh ? -num : num;
}

View File

@@ -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<bool, RequestControlPermissionParams> {
final RemoteControlRepository remoteControlRepository;
RequestControlPermissionUseCase(this.remoteControlRepository);
@override
Future<Either<Failure, bool>> 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,
});
}

View File

@@ -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<RemoteControlState> {
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<RemoteControlState> {
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<RemoteControlState> {
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 回包更新状态
},
);
}
}

View File

@@ -33,6 +33,10 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
]);
// 2. 隐藏状态栏和虚拟按键
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<RemoteControlCubit>().startControlLoop();
});
}
@override
@@ -49,6 +53,11 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
final deviceState = context.watch<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
context.read<RemoteControlCubit>().requestControlPermissionS(
currentDevice!.deviceName,
"app",
);
if (currentDevice == null) {
return _buildOfflineScaffold();
}

View File

@@ -31,7 +31,8 @@ class RightJoystickArea extends StatelessWidget {
axisHint: AxisHint.leftRight, // 关键:指定为左右控制
onValueChanged: (value) {
// 对应 viewModel.updateOriginX(x)
context.read<RemoteControlCubit>().updateOriginY(value.x);
/// context.read<RemoteControlCubit>().updateOriginY(value.x);
context.read<RemoteControlCubit>().updateOriginX(value.x);
},
onPress: () {
// 对应 VibrateOnce(current, 100)