Merge branch 'feature/my' of http://1.95.137.212:57001/APP/FlutterApp into feature/my
This commit is contained in:
@@ -49,9 +49,9 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
data: {'user': user, 'data': user}
|
||||
);
|
||||
if (user != null) {
|
||||
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
// await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
|
||||
tcp.startHeartbeat(interval: const Duration(seconds: 4));
|
||||
tcp.startHeartbeat(interval: const Duration(seconds: 4));
|
||||
// 2. 同步全局 App 状态
|
||||
appCubit.setAuth(user);
|
||||
// 3. 进入已登录状态
|
||||
@@ -142,4 +142,27 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
_kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
/// 🔥 息屏/后台后恢复到前台时的重连方法
|
||||
Future<void> reconnectAfterResume() async {
|
||||
print('📱 [AUTH] 检测到应用恢复到前台,检查 TCP 连接状态...');
|
||||
|
||||
// 如果 TCP 未连接,则执行重连
|
||||
if (!tcp.isConnected) {
|
||||
print('⚠️ [AUTH] TCP 未连接,开始重连...');
|
||||
try {
|
||||
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
tcp.startHeartbeat(interval: const Duration(seconds: 4));
|
||||
print('✅ [AUTH] TCP 重连成功!');
|
||||
} catch (e) {
|
||||
print('❌ [AUTH] TCP 重连失败:$e');
|
||||
// 可以选择重试或者通知用户
|
||||
}
|
||||
} else {
|
||||
print('✅ [AUTH] TCP 连接正常,无需重连');
|
||||
// 可选:发送一个心跳包确认连接有效
|
||||
tcp.sendHeartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -24,15 +25,15 @@ class RemoteTcpDatasource {
|
||||
/// 3. 再调用 HTTP API 确认控制权
|
||||
Future<void> sendSwitchControlRequest(String deviceName) async {
|
||||
if (!_tcpClient.isConnected) {
|
||||
debugPrint('❌ [TCP] 无法发送权限请求:Socket 未连接');
|
||||
throw Exception('TCP 未连接');
|
||||
debugPrint('❌sendSwitchControlRequest [TCP] 无法发送权限请求:Socket 未连接');
|
||||
throw Exception('sendSwitchControlRequest TCP 未连接');
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户信息
|
||||
final user = await _userStorage.getUser();
|
||||
if (user == null || user.token == null) {
|
||||
debugPrint('❌ [TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
|
||||
debugPrint('❌ sendSwitchControlRequest[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
|
||||
throw Exception('用户未登录或 Token 无效');
|
||||
}
|
||||
|
||||
@@ -52,19 +53,27 @@ class RemoteTcpDatasource {
|
||||
final jsonStr = jsonEncode(request);
|
||||
final jsonBytes = utf8.encode(jsonStr);
|
||||
|
||||
debugPrint('🔑 [TCP] 发送权限请求:$jsonStr');
|
||||
debugPrint('sendSwitchControlRequest[TCP] 发送权限请求指令:$jsonStr');
|
||||
debugPrint('sendSwitchControlRequest[TCP] 发送权限请求指令:$jsonBytes');
|
||||
|
||||
// 使用 0x12 指令发送(cmdGetAuth)
|
||||
// 注意:根据你的协议文档,这里可能是 0x04 或 0x12
|
||||
// Android 代码中使用的是 0x12
|
||||
_tcpClient.sendRaw(
|
||||
MachineProtocolConstants.cmdGetAuth, // 0x04 或 0x12,根据实际协议调整
|
||||
jsonBytes,
|
||||
);
|
||||
// _tcpClient.sendnew(
|
||||
// // MachineProtocolConstants.cmdGetAuth, // 0x04 或 0x12,根据实际协议调整
|
||||
// jsonBytes,
|
||||
// );
|
||||
// 构造完整的协议包
|
||||
final packet = buildSwitchControlPacket(jsonBytes);
|
||||
|
||||
debugPrint('✅ [TCP] 已发送 switch_control 权限请求 (0x${MachineProtocolConstants.cmdGetAuth.toRadixString(16)})');
|
||||
debugPrint('sendSwitchControlRequest[TCP] 完整数据包:${packet.map((b) => '0x${b.toRadixString(16)}').join(' ')}');
|
||||
|
||||
// 发送数据包
|
||||
_tcpClient.sendnew(packet);
|
||||
|
||||
debugPrint('✅ sendSwitchControlRequest[TCP] 已发送 switch_control 权限请求 (0x${MachineProtocolConstants.cmdGetAuth.toRadixString(16)})');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [TCP] 发送权限请求失败:$e');
|
||||
debugPrint('❌ sendSwitchControlRequest[TCP] 发送权限请求失败:$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -75,4 +84,73 @@ class RemoteTcpDatasource {
|
||||
|
||||
/// 检查 TCP 连接状态
|
||||
bool get isConnected => _tcpClient.isConnected;
|
||||
|
||||
/// 构建 switch_control 协议包
|
||||
///
|
||||
/// 包结构:
|
||||
/// - 帧头:0xAB, 0xAA (2 字节)
|
||||
/// - 命令字:0x12 (1 字节)
|
||||
/// - 数据载荷:JSON UTF-8 字节 (N 字节)
|
||||
/// - 帧尾:0x00, 0x00, 0xAA, 0xAB (4 字节)
|
||||
List<int> buildSwitchControlPacket(List<int> jsonBytes) {
|
||||
final builder = BytesBuilder()
|
||||
..addByte(MachineProtocolConstants.header1) // 0xAB
|
||||
..addByte(MachineProtocolConstants.header2) // 0xAA
|
||||
..addByte(0x12) // 命令字:switch_control
|
||||
..add(jsonBytes) // JSON 数据
|
||||
..addByte(0x00) // CRC 占位 1
|
||||
..addByte(0x00) // CRC 占位 2
|
||||
..addByte(MachineProtocolConstants.endFlag1) // 0xAA
|
||||
..addByte(MachineProtocolConstants.endFlag2); // 0xAB
|
||||
|
||||
return builder.takeBytes();
|
||||
}
|
||||
// TODO: 同意授权和拒绝授权
|
||||
void sendSwitchControlResponse(bool bool, String deviceId) {
|
||||
if (!_tcpClient.isConnected) {
|
||||
debugPrint('❌sendSwitchControlResponse [TCP] 无法发送权限响应:Socket 未连接');
|
||||
throw Exception('sendSwitchControlResponse TCP 未连接');
|
||||
}
|
||||
try {
|
||||
// 构造响应 JSON(参考 Android 代码)
|
||||
final response = bool
|
||||
? {
|
||||
'respond': {
|
||||
'switchResult': true,
|
||||
'deviceId': deviceId,
|
||||
'holder': 'you',
|
||||
},
|
||||
}
|
||||
: {
|
||||
'respond': {
|
||||
'switchResult': false,
|
||||
'deviceId': deviceId,
|
||||
'reason': 'holder_denied',
|
||||
},
|
||||
};
|
||||
|
||||
if(!bool){
|
||||
debugPrint('sendSwitchControlResponse[TCP] 拒绝授权:$deviceId');
|
||||
}else{
|
||||
debugPrint('sendSwitchControlResponse[TCP] 同意授权:$deviceId');
|
||||
}
|
||||
//debugPrint('sendSwitchControlResponse[TCP] 同意授权:$deviceId');
|
||||
final jsonStr = jsonEncode(response);
|
||||
final jsonBytes = utf8.encode(jsonStr);
|
||||
|
||||
debugPrint('sendSwitchControlResponse[TCP] 响应权限请求:$jsonStr');
|
||||
|
||||
// 构造完整的协议包
|
||||
final packet = buildSwitchControlPacket(jsonBytes);
|
||||
|
||||
debugPrint('sendSwitchControlResponse[TCP] 完整数据包:${packet.map((b) => '0x${b.toRadixString(16)}').join(' ')}');
|
||||
|
||||
// 发送数据包
|
||||
_tcpClient.sendnew(packet);
|
||||
}
|
||||
catch (e) {
|
||||
debugPrint('❌ sendSwitchControlResponse[TCP] 响应权限请求失败:$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +63,22 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository {
|
||||
@override
|
||||
Future<bool> requestControlPermission( String deviceName, String deviceId) async {
|
||||
try {
|
||||
await _remoteTcp.sendSwitchControlRequest(deviceName);
|
||||
debugPrint('🔑 [RemoteControl] TCP已发送权限请求 (0x04)');
|
||||
_remoteTcp.sendSwitchControlRequest(deviceName);
|
||||
debugPrint('🔑 [RemoteControl] TCP已发送权限请求');
|
||||
return await _remoteHttp.requestRemoteControlViaHttp(deviceName, deviceId);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [RemoteControl] 权限请求失败:$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void respondPermission(bool bool, String deviceId) {
|
||||
// TODO: implement respondPermission
|
||||
if (bool) {
|
||||
_remoteTcp.sendSwitchControlResponse(true, deviceId);
|
||||
} else {
|
||||
_remoteTcp.sendSwitchControlResponse(false, deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,6 @@ abstract class RemoteControlRepository {
|
||||
|
||||
/// 新增:请求控制权限 (发送 0x04 指令)
|
||||
Future<bool> requestControlPermission(String deviceName, String deviceId);
|
||||
/// 新增:响应控制权限 同意和拒绝(发送 0x05 指令)
|
||||
void respondPermission(bool bool, String deviceId);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
),
|
||||
) {
|
||||
_initPacketListener();
|
||||
|
||||
}
|
||||
|
||||
// 1. 初始化回包监听 (如 0x12 权限)
|
||||
@@ -56,16 +57,52 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
print('>>> [RemoteControl] 去除 CRC 后的 JSON: $jsonString');
|
||||
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
final respond = jsonMap['request'] ?? '';
|
||||
print('>>> [RemoteControl] ✅ 响应结果:$respond');
|
||||
// 🔥 当 Web 端推送 switch_control 权限时,更新状态触发弹窗
|
||||
if (respond == 'switch_control') {
|
||||
print('>>> [RemoteControl] ✅ 获得控制权 (respond=switch_control),准备打开弹窗');
|
||||
emit(state.copyWith(hasPermission: true, showPermissionRequestDialog: true));
|
||||
} else if (respond == 'have_logged_in') {
|
||||
// 如果是异地登录,也显示弹窗提示
|
||||
print('>>> [RemoteControl] ⚠️ 检测到异地登录');
|
||||
emit(state.copyWith(showPermissionRequestDialog: true));
|
||||
|
||||
// 🔥 区分两种数据格式
|
||||
final requestType = jsonMap['request'];
|
||||
final platform = jsonMap['platform'];
|
||||
final respondData = jsonMap['respond'];
|
||||
|
||||
print('>>> [RemoteControl] 📋 requestType=$requestType, platform=$platform');
|
||||
|
||||
// 情况 1: 响应格式 - {"respond":{"switchResult":true,"deviceId":"...","holder":"you"}}
|
||||
if (respondData != null && respondData is Map) {
|
||||
final switchResult = respondData['switchResult'];
|
||||
print('>>> [RemoteControl] 📊 收到切换结果响应:switchResult = $switchResult');
|
||||
|
||||
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') {
|
||||
print('>>> [RemoteControl] 🚨 $platform 端请求控制权,打开弹窗询问用户');
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(showPermissionRequestDialog: true));
|
||||
}
|
||||
} else {
|
||||
print('>>> [RemoteControl] ℹ️ APP 自己的请求回显,忽略不弹窗');
|
||||
}
|
||||
}
|
||||
// 情况 3: 异地登录通知 - {"request":"have_logged_in",...}
|
||||
else if (requestType == 'have_logged_in') {
|
||||
print('>>> [RemoteControl] ⚠️ 检测到异地登录,打开弹窗提示');
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(showPermissionRequestDialog: true));
|
||||
}
|
||||
}
|
||||
else {
|
||||
print('>>> [RemoteControl] ℹ️ 未知类型的 0x12 包,忽略');
|
||||
}
|
||||
} catch (e) {
|
||||
print('>>> [RemoteControl] ❌ 解析失败:$e');
|
||||
@@ -75,6 +112,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
print('>>> [RemoteControl] ✅ 0x12 监听器已建立完成');
|
||||
}
|
||||
|
||||
|
||||
// 2. 开启 100ms 控制循环 (在进入遥控页面或点击“开始”时调用)
|
||||
void startControlLoop() {
|
||||
_timer?.cancel();
|
||||
@@ -153,7 +191,24 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
void updateEmergency(bool bool) {}
|
||||
|
||||
void respondPermission(bool bool) {}
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void requestControlPermissionS(String deviceName, String deviceId) async {
|
||||
// 1. 关闭弹窗
|
||||
@@ -193,5 +248,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/router/route_paths.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
import '../bloc/remote_control_state.dart';
|
||||
@@ -51,11 +52,12 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
final deviceState = context.watch<DevicesCubit>().state;
|
||||
final currentDevice = deviceState.selectedDevice;
|
||||
|
||||
context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
|
||||
///context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
|
||||
|
||||
if (currentDevice == null) {
|
||||
return _buildOfflineScaffold();
|
||||
}
|
||||
context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
@@ -185,14 +187,27 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
}
|
||||
|
||||
Widget _buildPermissionDialog(BuildContext context, RemoteControlState state) {
|
||||
final deviceId = context.read<DevicesCubit>().state.selectedDevice?.deviceName ?? "";
|
||||
return Container(
|
||||
color: Colors.black54,
|
||||
child: AlertDialog(
|
||||
title: const Text("权限变更"),
|
||||
content: Text("${state.permissionPlatform}端正请求控制权,同意释放吗?"),
|
||||
// content: Text("${state.permissionPlatform}端正请求控制权,同意释放吗?"),
|
||||
content: Text("web端正请求控制权,同意释放吗?"),
|
||||
actions: [
|
||||
TextButton(onPressed: () => context.read<RemoteControlCubit>().respondPermission(false), child: const Text("拒绝")),
|
||||
TextButton(onPressed: () => context.read<RemoteControlCubit>().respondPermission(true), child: const Text("同意")),
|
||||
TextButton(onPressed: () => context.read<RemoteControlCubit>().respondPermission(false,deviceId), child: const Text("拒绝")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<RemoteControlCubit>().respondPermission(true, deviceId);
|
||||
// 🔥 用户同意,延迟跳转到首页
|
||||
// Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
// if (context.mounted) {
|
||||
// context.go(RoutePaths.home);
|
||||
// }
|
||||
// });
|
||||
},
|
||||
child: const Text("同意"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
|
||||
import 'core/di/injection.dart';
|
||||
import 'features/auth/presentation/bloc/auth_state.dart';
|
||||
import 'features/auth/presentation/bloc/login_cubit.dart';
|
||||
import 'features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
|
||||
@@ -71,7 +72,101 @@ class MyApp extends StatelessWidget {
|
||||
),
|
||||
// 其他 Cubit...
|
||||
],
|
||||
child: MaterialApp.router(title: 'Maibu Satabot', theme: AppTheme.lightTheme, routerConfig: sl<GoRouter>()),
|
||||
child: MaterialApp.router(title: 'Maibu Satabot', theme: AppTheme.lightTheme, routerConfig: sl<GoRouter>(),
|
||||
builder: (context, child) {
|
||||
return _LifecycleListener(child: child);
|
||||
},),
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
// 🔥 新增:应用生命周期监听器
|
||||
class _LifecycleListener extends StatefulWidget {
|
||||
final Widget? child;
|
||||
|
||||
const _LifecycleListener({required this.child});
|
||||
|
||||
@override
|
||||
State<_LifecycleListener> createState() => _LifecycleListenerState();
|
||||
}
|
||||
|
||||
class _LifecycleListenerState extends State<_LifecycleListener> with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
// 🔥 从息屏或后台恢复到前台
|
||||
debugPrint('📱 [生命周期] 应用恢复到前台 (resumed),检查 TCP 连接...');
|
||||
_handleResume();
|
||||
break;
|
||||
|
||||
case AppLifecycleState.inactive:
|
||||
// 应用处于非活动状态(iOS 来电、Android 分屏等)
|
||||
debugPrint('💤 [生命周期] 应用进入非活动状态 (inactive)');
|
||||
break;
|
||||
|
||||
case AppLifecycleState.paused:
|
||||
// 应用进入后台(息屏、按 Home 键等)
|
||||
debugPrint('🌙 [生命周期] 应用进入后台 (paused)');
|
||||
break;
|
||||
|
||||
case AppLifecycleState.detached:
|
||||
// 应用仍然托管但不会显示给用户(例如销毁中的 Activity)
|
||||
debugPrint('🚫 [生命周期] 应用已分离 (detached)');
|
||||
break;
|
||||
|
||||
case AppLifecycleState.hidden:
|
||||
// 应用不可见(Android 14+ 或未来版本)
|
||||
debugPrint('👻 [生命周期] 应用已隐藏 (hidden)');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 处理恢复到前台的逻辑
|
||||
Future<void> _handleResume() async {
|
||||
try {
|
||||
final authCubit = context.read<AuthCubit>();
|
||||
|
||||
// 检查当前是否已登录
|
||||
if (authCubit.state is! AuthAuthenticated) {
|
||||
debugPrint('⚠️ [生命周期] 用户未登录,跳过 TCP 重连');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 TCP 连接状态
|
||||
// 注意:这里需要通过 authCubit 访问 tcp 实例
|
||||
// 由于 tcp 是 private 字段,我们需要在 AuthCubit 中暴露一个方法
|
||||
debugPrint('✅ [生命周期] 准备执行 TCP 重连检查...');
|
||||
|
||||
// 延迟一点执行,确保 UI 已经加载完成
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// 调用 AuthCubit 的重连方法
|
||||
await authCubit.reconnectAfterResume();
|
||||
|
||||
debugPrint('✅ [生命周期] TCP 重连检查完成');
|
||||
} catch (e, stack) {
|
||||
debugPrint('❌ [生命周期] 重连过程发生错误:$e\n$stack');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user