对接远程遥控
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
// 使用阿里云镜像加速(优先)
|
||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/central") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
// 火山引擎 RTC 仓库已禁用
|
||||
// maven {
|
||||
// url = uri("https://artifact.bytedance.com/repository/Volcengine/")
|
||||
// allowInsecureProtocol = true
|
||||
// }
|
||||
// 火山引擎 RTC 仓库
|
||||
maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") }
|
||||
// BytePlus 公共仓库(包含火山引擎依赖)
|
||||
maven { url = uri("https://artifact.byteplus.com/repository/public/") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dhttps.protocols=TLSv1.2,TLSv1.1 -Djava.net.preferIPv4Stack=true
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dhttps.protocols=TLSv1.3,TLSv1.2,TLSv1.1 -Djava.net.preferIPv4Stack=true -Djavax.net.ssl.trustStoreType=JKS
|
||||
android.useAndroidX=true
|
||||
|
||||
# \u4F7F\u7528\u963F\u91CC\u4E91\u955C\u50CF\u52A0\u901F
|
||||
# 使用阿里云镜像加速
|
||||
android.enableJetifier=true
|
||||
|
||||
# \u7981\u7528\u4EE3\u7406\uFF08\u8986\u76D6\u5168\u5C40\u914D\u7F6E\uFF09
|
||||
# 禁用代理(覆盖全局配置)
|
||||
systemProp.http.proxyHost=
|
||||
systemProp.http.proxyPort=
|
||||
systemProp.https.proxyHost=
|
||||
systemProp.https.proxyPort=
|
||||
|
||||
# \u653E\u884CSSL\u8BC1\u4E66\u4E0E\u534F\u8BAE\uFF0C\u89E3\u51B3TLS\u63E1\u624B\u5F02\u5E38
|
||||
# 放行SSL证书与协议,解决TLS握手异常
|
||||
systemProp.http.ssl.insecure=true
|
||||
systemProp.http.ssl.allowall=true
|
||||
systemProp.http.ssl.ignore.validity.dates=true
|
||||
|
||||
# Android SDK 镜像配置(使用清华大学镜像)
|
||||
systemProp.android.sdkmanager.channel=stable
|
||||
123
lib/components/tcp_status_indicator.dart
Normal file
123
lib/components/tcp_status_indicator.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/tcp/tcp_status_cubit.dart';
|
||||
|
||||
class TcpStatusIndicator extends StatefulWidget {
|
||||
final double size;
|
||||
final bool showDisconnected; // 是否显示未连接状态
|
||||
|
||||
const TcpStatusIndicator({
|
||||
super.key,
|
||||
this.size = 12.0,
|
||||
this.showDisconnected = false, // 默认不显示未连接状态
|
||||
});
|
||||
|
||||
@override
|
||||
State<TcpStatusIndicator> createState() => _TcpStatusIndicatorState();
|
||||
}
|
||||
|
||||
class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TcpStatusCubit _tcpStatusCubit;
|
||||
bool _hasActivity = false;
|
||||
AnimationController? _controller;
|
||||
Animation<double>? _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tcpStatusCubit = GetIt.I<TcpStatusCubit>();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0.6, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller!, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_controller!.repeat(reverse: true);
|
||||
|
||||
_tcpStatusCubit.stream.listen((state) {
|
||||
if (state.status != TcpConnectionStatus.disconnected) {
|
||||
_hasActivity = true;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = _tcpStatusCubit.state;
|
||||
|
||||
if (!widget.showDisconnected &&
|
||||
!_hasActivity &&
|
||||
state.status == TcpConnectionStatus.disconnected) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Color color;
|
||||
String tooltip;
|
||||
bool shouldAnimate;
|
||||
|
||||
switch (state.status) {
|
||||
case TcpConnectionStatus.connected:
|
||||
color = Colors.green;
|
||||
tooltip = 'TCP已连接';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
case TcpConnectionStatus.connecting:
|
||||
color = Colors.yellow;
|
||||
tooltip = 'TCP连接中...';
|
||||
shouldAnimate = true;
|
||||
break;
|
||||
case TcpConnectionStatus.error:
|
||||
color = Colors.red;
|
||||
tooltip = state.errorMessage ?? 'TCP连接错误';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
case TcpConnectionStatus.disconnected:
|
||||
default:
|
||||
color = Colors.grey;
|
||||
tooltip = 'TCP未连接';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation!,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation!.value : 1.0;
|
||||
return Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ class AppUserCubit extends Cubit<AppUserState> {
|
||||
AppUserCubit() : super(const AppUserState());
|
||||
|
||||
void setAuth(UserEntity user) {
|
||||
print('✅ [AppUserCubit] setAuth 被调用,用户: ${user.username}');
|
||||
emit(state.copyWith(user));
|
||||
print('✅ [AppUserCubit] 状态已更新,当前用户: ${state.user?.username}');
|
||||
}
|
||||
|
||||
void clearAuth() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class TCPConsts {
|
||||
static const String TCP_IP = "1.95.137.212";
|
||||
static const int TCP_PORT = 9001;
|
||||
static const int TCP_PORT = 59016;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/repositories/remote_control_repository_impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/request_control_permission_usecase.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../features/auth/data/datasources/auth_http_datasource.dart';
|
||||
@@ -111,6 +112,7 @@ import '../../features/home/presentation/bloc/permission_request_bloc.dart';
|
||||
import '../network/dio_client.dart';
|
||||
import '../network/net_message_dispatcher.dart';
|
||||
import '../network/tcp/tcp_client.dart';
|
||||
import '../network/tcp/tcp_status_cubit.dart';
|
||||
import '../router/app_router.dart';
|
||||
import '../storage/impl/user_storage_impl.dart';
|
||||
import '../storage/user_storage.dart';
|
||||
@@ -258,7 +260,9 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<GetHomeDataUseCase>(() => GetHomeDataUseCase(sl()));
|
||||
|
||||
// Site (场站)
|
||||
sl.registerLazySingleton<SiteDataSource>(() => SiteDataSourceImpl(sl<Dio>()));
|
||||
sl.registerLazySingleton<SiteDataSource>(
|
||||
() => SiteDataSourceImpl(sl<Dio>(), sl<UserStorage>(), sl<AppUserCubit>()),
|
||||
);
|
||||
sl.registerLazySingleton<SiteRepository>(() => SiteRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetSiteListUseCase>(() => GetSiteListUseCase(sl()));
|
||||
|
||||
@@ -301,9 +305,7 @@ Future<void> init() async {
|
||||
);
|
||||
|
||||
/// Robot List V2
|
||||
sl.registerFactory<RobotListBloc>(
|
||||
() => RobotListBloc(sl<Dio>()),
|
||||
);
|
||||
sl.registerFactory<RobotListBloc>(() => RobotListBloc(sl<Dio>()));
|
||||
|
||||
/// Alarm Center V2
|
||||
sl.registerLazySingleton<AlarmRemoteDataSource>(
|
||||
@@ -333,9 +335,7 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<ConfirmAlarmUseCase>(
|
||||
() => ConfirmAlarmUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<AIDiagnosisUseCase>(
|
||||
() => AIDiagnosisUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<AIDiagnosisUseCase>(() => AIDiagnosisUseCase(sl()));
|
||||
sl.registerFactory<AlarmDetailCubit>(
|
||||
() => AlarmDetailCubit(
|
||||
getAlarmDetailUseCase: sl(),
|
||||
@@ -347,6 +347,9 @@ Future<void> init() async {
|
||||
/// 5. 状态管理 (Cubit/Bloc)
|
||||
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
|
||||
|
||||
// TCP状态管理 Cubit
|
||||
sl.registerLazySingleton(() => TcpStatusCubit());
|
||||
|
||||
// 🔥 SiteCubit (全局共享,持久化选中场站)
|
||||
sl.registerLazySingleton(() => SiteCubit(sl<SharedPreferences>()));
|
||||
|
||||
@@ -386,7 +389,7 @@ Future<void> init() async {
|
||||
);
|
||||
|
||||
// 🔥 RemoteControlCubit 注入 DeviceStatusBloc(工厂模式,每次新建)
|
||||
sl.registerFactory(() => RemoteControlCubit(sl(), sl(), sl(), sl()));
|
||||
sl.registerFactory(() => RemoteControlCubit(sl(), sl(), sl(), sl(), sl()));
|
||||
|
||||
// 🔥 PermissionRequestBloc 用于首页权限弹窗(单例,通过 NetMessageDispatcher 监听)
|
||||
sl.registerLazySingleton(
|
||||
|
||||
@@ -17,7 +17,7 @@ import '../../../features/devices/domain/usecases/switch_device_usecase.dart';
|
||||
import '../../logging/i_logger_service.dart';
|
||||
import '../../storage/user_storage.dart';
|
||||
import '../protocol_decoder.dart';
|
||||
|
||||
import 'tcp_status_cubit.dart';
|
||||
|
||||
class TcpClient {
|
||||
Socket? _socket;
|
||||
@@ -27,11 +27,14 @@ class TcpClient {
|
||||
Stream<RawPacket> get packetStream => _controller.stream;
|
||||
final GetUserDeviceUseCase getUserDeviceUseCase;
|
||||
final SwitchDeviceUseCase switchDeviceUseCase;
|
||||
TcpClient(this._userStorage, {required this.getUserDeviceUseCase, required this.switchDeviceUseCase});
|
||||
TcpClient(
|
||||
this._userStorage, {
|
||||
required this.getUserDeviceUseCase,
|
||||
required this.switchDeviceUseCase,
|
||||
});
|
||||
|
||||
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
||||
|
||||
|
||||
// 新增:心跳定时器
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _reconnectTimer; // 重连定时器
|
||||
@@ -60,12 +63,64 @@ class TcpClient {
|
||||
_logger.logWithLevel('🔌 [TCP] 物理断开 Socket...', shouldLog: true);
|
||||
_socket!.destroy(); // 或者 .close()
|
||||
_socket = null;
|
||||
|
||||
// 更新连接状态为未连接
|
||||
try {
|
||||
GetIt.I<TcpStatusCubit>().setDisconnected();
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 彻底断开 TCP(用于退出登录),清除所有重连能力
|
||||
void forceDisconnect() {
|
||||
_logger.logWithLevel('🛑 [TCP] 强制断开并清除重连能力', shouldLog: true);
|
||||
|
||||
// 1. 停止心跳
|
||||
stopHeartbeat();
|
||||
|
||||
// 2. 取消重连定时器
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
|
||||
// 3. 清除 Host/Port,防止重连
|
||||
_lastHost = null;
|
||||
_lastPort = null;
|
||||
|
||||
// 4. 重置标志
|
||||
isUserSwitch = false;
|
||||
_isSwitching = false;
|
||||
|
||||
// 5. 销毁 Socket
|
||||
if (_socket != null) {
|
||||
_socket!.destroy();
|
||||
_socket = null;
|
||||
_logger.logWithLevel('✅ [TCP] Socket 已物理销毁', shouldLog: true);
|
||||
}
|
||||
|
||||
// 6. 更新状态
|
||||
try {
|
||||
GetIt.I<TcpStatusCubit>().setDisconnected();
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
|
||||
}
|
||||
|
||||
_logger.logWithLevel('✅ [TCP] 已彻底断开,下次登录需重新初始化', shouldLog: true);
|
||||
}
|
||||
|
||||
// 通过参数配置,不硬编码
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
//debugPrint('🔌 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
|
||||
_logger.logWithLevel('🔌 [TCP] 开始连接:$host:$port', shouldLog: true);
|
||||
|
||||
// 更新连接状态为连接中
|
||||
try {
|
||||
GetIt.I<TcpStatusCubit>().setConnecting();
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
|
||||
}
|
||||
|
||||
_lastHost = host;
|
||||
_lastPort = port;
|
||||
try {
|
||||
@@ -76,65 +131,90 @@ class TcpClient {
|
||||
);
|
||||
// 🔥 关键修复:禁用Nagle算法,确保小包立即发送
|
||||
_socket!.setOption(SocketOption.tcpNoDelay, true);
|
||||
// debugPrint('✅ [TCP] 连接成功!'); // ✅ 必须看到这条
|
||||
// debugPrint('✅ [TCP] 连接成功!'); // ✅ 必须看到这条
|
||||
_logger.logWithLevel('✅ [TCP] 连接成功!', shouldLog: true);
|
||||
// 开始认证tcp
|
||||
await _sendAuthPacket();
|
||||
_socket!.listen((data) {
|
||||
//debugPrint('📥 [TCP] 收到原始数据:${data.length} 字节, 内容:$data');
|
||||
_logger.logWithLevel('✅ [TCP] 监听数据...');
|
||||
// var packets = _decoder.decode(data);
|
||||
// debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||
//for (var packet in packets) {
|
||||
// // 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包
|
||||
// if (packet.command == 0xFF) {
|
||||
|
||||
// 更新连接状态为已连接
|
||||
try {
|
||||
GetIt.I<TcpStatusCubit>().setConnected();
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
|
||||
}
|
||||
|
||||
// 开始认证tcp
|
||||
await _sendAuthPacket();
|
||||
_socket!.listen(
|
||||
(data) {
|
||||
//debugPrint('📥 [TCP] 收到原始数据:${data.length} 字节, 内容:$data');
|
||||
_logger.logWithLevel('✅ [TCP] 监听数据...');
|
||||
// var packets = _decoder.decode(data);
|
||||
// debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||
//for (var packet in packets) {
|
||||
// // 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包
|
||||
// if (packet.command == 0xFF) {
|
||||
// debugPrint('收到服务端心跳,自动回复...');
|
||||
// sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
// sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
// }
|
||||
// }
|
||||
// _controller.add(packet);
|
||||
// }
|
||||
try {
|
||||
var packets = _decoder.decode(data);
|
||||
//debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||
_logger.logWithLevel('✅ [TCP] 解码成功,包数量:${packets.length}',shouldLog: false);
|
||||
for (var packet in packets) {
|
||||
// 🔥 最根部日志:收到任何推送都打印
|
||||
// debugPrint('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}');
|
||||
_logger.logWithLevel('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}', shouldLog: true);
|
||||
// }
|
||||
// _controller.add(packet);
|
||||
// }
|
||||
try {
|
||||
var packets = _decoder.decode(data);
|
||||
//debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||
_logger.logWithLevel(
|
||||
'✅ [TCP] 解码成功,包数量:${packets.length}',
|
||||
shouldLog: false,
|
||||
);
|
||||
for (var packet in packets) {
|
||||
// 🔥 最根部日志:收到任何推送都打印
|
||||
// debugPrint('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}');
|
||||
// _logger.logWithLevel(
|
||||
// '📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}',
|
||||
// shouldLog: true,
|
||||
// );
|
||||
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(packet);
|
||||
//debugPrint('➡️ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||
_logger.logWithLevel('✅ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}' ,shouldLog: false);
|
||||
}
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(packet);
|
||||
//debugPrint('➡️ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||
_logger.logWithLevel(
|
||||
'✅ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}',
|
||||
shouldLog: false,
|
||||
);
|
||||
}
|
||||
if (packet.command == 0xFF) {
|
||||
//debugPrint('收到服务端心跳,自动回复...');
|
||||
_logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...',shouldLog: false);
|
||||
sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
//debugPrint('收到服务端心跳,自动回复...');
|
||||
_logger.logWithLevel(
|
||||
'✅ [TCP] 收到服务端心跳,自动回复...',
|
||||
shouldLog: false,
|
||||
);
|
||||
sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
}
|
||||
if (packet.command == 0x03) {
|
||||
// debugPrint('⚠️ 收到认证响应:${packet.payload}');
|
||||
_logger.logWithLevel(
|
||||
'⚠️ 收到认证响应:${packet.payload}',
|
||||
shouldLog: false,
|
||||
);
|
||||
// 解析 payload 看是否有错误信息
|
||||
}
|
||||
}
|
||||
if (packet.command == 0x03) {
|
||||
// debugPrint('⚠️ 收到认证响应:${packet.payload}');
|
||||
_logger.logWithLevel('⚠️ 收到认证响应:${packet.payload}',shouldLog: false);
|
||||
// 解析 payload 看是否有错误信息
|
||||
}
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
// debugPrint('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常
|
||||
_logger.logWithLevel(
|
||||
'❌ [TCP] 解码数据时发生异常:$e\n$stackTrace',
|
||||
shouldLog: false,
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// debugPrint('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常
|
||||
_logger.logWithLevel('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace', shouldLog: false);
|
||||
}
|
||||
},
|
||||
onDone: (){
|
||||
// TODO: 断线重连
|
||||
// debugPrint('来到断线重连!');
|
||||
onDone: () {
|
||||
// TODO: 断线重连
|
||||
// debugPrint('来到断线重连!');
|
||||
if (!_isSwitching) {
|
||||
//debugPrint('onDone❌ [TCP] 连接已断开!');
|
||||
_logger.logWithLevel('❌ [TCP] 连接已断开!', shouldLog: false);
|
||||
_handleDisconnect();
|
||||
}
|
||||
_handleDisconnect();
|
||||
}
|
||||
_isSwitching = false; // 重置标志,以免影响下次
|
||||
|
||||
},
|
||||
onError: (e) {
|
||||
//debugPrint('来到断线重连!error');
|
||||
@@ -145,14 +225,14 @@ class TcpClient {
|
||||
_handleDisconnect();
|
||||
} // 统一走重连逻辑,保护 Controller 不被关闭
|
||||
_isSwitching = false; // 重置标志,以免影响下次
|
||||
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow; // 向上抛出连接异常
|
||||
}
|
||||
}
|
||||
//
|
||||
|
||||
//
|
||||
void _handleDisconnect() {
|
||||
stopHeartbeat();
|
||||
|
||||
@@ -167,10 +247,10 @@ class TcpClient {
|
||||
if (isUserSwitch) return;
|
||||
if (_reconnectTimer != null) return;
|
||||
|
||||
// debugPrint('⏳ 调度重连:Host=${_lastHost}, Port=${_lastPort}'); // ✅ 检查 Host/Port 是否为空
|
||||
// debugPrint('⏳ 调度重连:Host=${_lastHost}, Port=${_lastPort}'); // ✅ 检查 Host/Port 是否为空
|
||||
_logger.logWithLevel('⏳ 调度重连:Host=${_lastHost}, Port=${_lastPort}');
|
||||
if (_lastHost == null || _lastPort == null) {
|
||||
//debugPrint('❌ 无法重连:Host 或 Port 为空!');
|
||||
//debugPrint('❌ 无法重连:Host 或 Port 为空!');
|
||||
_logger.logWithLevel('❌ 无法重连:Host 或 Port 为空!', shouldLog: true);
|
||||
return;
|
||||
}
|
||||
@@ -181,17 +261,14 @@ class TcpClient {
|
||||
_logger.logWithLevel('⏰ 定时器触发,开始执行重连...', shouldLog: true);
|
||||
connect(host: _lastHost!, port: _lastPort!);
|
||||
});
|
||||
await _sendAuthPacket();
|
||||
await _sendAuthPacket();
|
||||
}
|
||||
|
||||
|
||||
// ✅ 新增:调度重连
|
||||
Future<void> _scheduleReconnectBySwitch(devname) async {
|
||||
|
||||
|
||||
//debugPrint('⏳ 被动调度重连:Host=${_lastHost}, Port=${_lastPort}'); // ✅ 检查 Host/Port 是否为空
|
||||
if (_lastHost == null || _lastPort == null) {
|
||||
// debugPrint('❌ 无法重连:Host 或 Port 为空!');
|
||||
// debugPrint('❌ 无法重连:Host 或 Port 为空!');
|
||||
_logger.logWithLevel('❌ 无法重连:Host 或 Port 为空!', shouldLog: true);
|
||||
return;
|
||||
}
|
||||
@@ -202,19 +279,19 @@ class TcpClient {
|
||||
_logger.logWithLevel('⏰ 被动-定时器触发,开始执行重连...', shouldLog: true);
|
||||
connectBySwitch(host: _lastHost!, port: _lastPort!, deviceName: devname);
|
||||
});
|
||||
await _sendAuthPacketBySwitch(devname);
|
||||
await _sendAuthPacketBySwitch(devname);
|
||||
}
|
||||
|
||||
/// 发送数据
|
||||
void send(Map<String, dynamic> data) {
|
||||
if (_socket == null) throw Exception("Socket not connected");
|
||||
_socket!.write(jsonEncode(data));
|
||||
}
|
||||
|
||||
void sendnew(List<int> data){
|
||||
void sendnew(List<int> data) {
|
||||
_socket!.add(data);
|
||||
}
|
||||
|
||||
|
||||
void sendRaw(int cmd, List<int> payload) {
|
||||
if (_socket == null) return;
|
||||
|
||||
@@ -232,19 +309,16 @@ class TcpClient {
|
||||
_socket!.flush();
|
||||
}
|
||||
|
||||
void sendHeartbeat() {
|
||||
if (_socket == null) return;
|
||||
final builder = BytesBuilder()
|
||||
..addByte(0xAB)
|
||||
..addByte(0xAA)
|
||||
..addByte(0xFF)
|
||||
..addByte(0xAA)
|
||||
..addByte(0xAB);
|
||||
_socket!.add(builder.takeBytes());
|
||||
}
|
||||
|
||||
|
||||
|
||||
void sendHeartbeat() {
|
||||
if (_socket == null) return;
|
||||
final builder = BytesBuilder()
|
||||
..addByte(0xAB)
|
||||
..addByte(0xAA)
|
||||
..addByte(0xFF)
|
||||
..addByte(0xAA)
|
||||
..addByte(0xAB);
|
||||
_socket!.add(builder.takeBytes());
|
||||
}
|
||||
|
||||
// 新增:启动心跳(每 4 秒发送一次 0xFF 指令)
|
||||
void startHeartbeat({Duration interval = const Duration(seconds: 4)}) {
|
||||
@@ -259,13 +333,124 @@ class TcpClient {
|
||||
debugPrint("TCP Connected");
|
||||
}
|
||||
|
||||
/// 🔥 封装完整的TCP初始化方法:连接 + 认证 + 心跳
|
||||
/// 在登录成功后调用此方法即可建立并维护TCP连接
|
||||
Future<void> initializeTcp({required String host, required int port}) async {
|
||||
try {
|
||||
// 1. 如果已经连接,先断开
|
||||
if (_socket != null) {
|
||||
_logger.logWithLevel('🔄 [TCP] 已有连接,先断开重新连接...', shouldLog: true);
|
||||
disconnect();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
// 2. 更新状态为连接中
|
||||
try {
|
||||
GetIt.I<TcpStatusCubit>().setConnecting();
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
|
||||
}
|
||||
|
||||
// 3. 执行连接
|
||||
_logger.logWithLevel('🔌 [TCP] 开始初始化连接:$host:$port', shouldLog: true);
|
||||
_lastHost = host;
|
||||
_lastPort = port;
|
||||
|
||||
_socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
_socket!.setOption(SocketOption.tcpNoDelay, true);
|
||||
_logger.logWithLevel('✅ [TCP] Socket 连接成功', shouldLog: true);
|
||||
|
||||
// 4. 更新状态为已连接
|
||||
try {
|
||||
GetIt.I<TcpStatusCubit>().setConnected();
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
|
||||
}
|
||||
|
||||
// 5. 发送认证包
|
||||
await _sendAuthPacket();
|
||||
_logger.logWithLevel('✅ [TCP] 认证包已发送', shouldLog: true);
|
||||
|
||||
// 6. 启动心跳
|
||||
startHeartbeat();
|
||||
_logger.logWithLevel('✅ [TCP] 心跳已启动', shouldLog: true);
|
||||
|
||||
// 7. 设置数据监听(复用原有的监听逻辑)
|
||||
_setupDataListener();
|
||||
|
||||
_logger.logWithLevel('🎉 [TCP] TCP 初始化完成!', shouldLog: true);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ [TCP] 初始化失败:$e', shouldLog: true);
|
||||
try {
|
||||
GetIt.I<TcpStatusCubit>().setError(e.toString());
|
||||
} catch (_) {}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置数据监听器
|
||||
void _setupDataListener() {
|
||||
if (_socket == null) return;
|
||||
|
||||
_socket!.listen(
|
||||
(data) {
|
||||
try {
|
||||
var packets = _decoder.decode(data);
|
||||
for (var packet in packets) {
|
||||
// 🔥 最根部日志:收到任何推送都打印
|
||||
// debugPrint('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}');
|
||||
// _logger.logWithLevel(
|
||||
// '📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}',
|
||||
// shouldLog: true,
|
||||
// );
|
||||
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(packet);
|
||||
}
|
||||
if (packet.command == 0xFF) {
|
||||
sendHeartbeat();
|
||||
}
|
||||
if (packet.command == 0x03) {
|
||||
_logger.logWithLevel(
|
||||
'⚠️ 收到认证响应:${packet.payload}',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_logger.logWithLevel(
|
||||
'❌ [TCP] 解码数据时发生异常:$e\n$stackTrace',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (!_isSwitching) {
|
||||
_logger.logWithLevel('❌ [TCP] 连接已断开!', shouldLog: false);
|
||||
_handleDisconnect();
|
||||
}
|
||||
_isSwitching = false;
|
||||
},
|
||||
onError: (e) {
|
||||
_logger.logWithLevel('❌ [TCP] 发生错误:$e', shouldLog: true);
|
||||
if (!_isSwitching) {
|
||||
_handleDisconnect();
|
||||
}
|
||||
_isSwitching = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 新增:停止心跳
|
||||
void stopHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
}
|
||||
|
||||
|
||||
// void disconnect() {
|
||||
// stopHeartbeat(); // 先停心跳
|
||||
// _socket?.destroy();
|
||||
@@ -299,23 +484,21 @@ class TcpClient {
|
||||
// ❌ 绝对不要关闭 _controller!否则数据流断裂,重连后收不到数据
|
||||
// _controller.close();
|
||||
|
||||
// debugPrint('✅ [TCP] 连接已断开,等待手动重连');
|
||||
// debugPrint('✅ [TCP] 连接已断开,等待手动重连');
|
||||
_logger.logWithLevel('✅ [TCP] 断开连接成功');
|
||||
}
|
||||
|
||||
|
||||
void sendPathPoint(RoutePlanSendEntity routePlanSendEntity) {
|
||||
if (_socket == null) return;
|
||||
final payload = routePlanSendEntity.toBytes();
|
||||
// print("sendPathPoint-0x01开始发送路径点数据");
|
||||
// print("sendPathPoint-0x01开始发送路径点数据");
|
||||
_logger.logWithLevel("sendPathPoint-0x01开始发送路径点数据");
|
||||
//sendRaw(0x01, payload); // 第1种的测试 声明指令结构和类型0x01 为命令类型
|
||||
sendnew(payload); // 第1种的测试 声明指令结构和类型0x01 为命令类型
|
||||
// _socket!.add(payload);//第二种的测试
|
||||
//s print("底层发送指令完成");
|
||||
// _socket!.add(payload);//第二种的测试
|
||||
//s print("底层发送指令完成");
|
||||
_logger.logWithLevel("底层发送指令完成");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void sendDeviceStateChange(RoutePlanSendEntity routePlanSendEntity) {
|
||||
if (_socket == null) return;
|
||||
@@ -333,21 +516,21 @@ class TcpClient {
|
||||
// ..addByte(0xAB);
|
||||
//
|
||||
// final packet = builder.takeBytes();
|
||||
sendnew(payload);
|
||||
sendnew(payload);
|
||||
// _socket!.add(packet);
|
||||
|
||||
var counts = routePlanSendEntity.pointCounts;
|
||||
//print("sendDeviceStateChange 底层发送指令完成,数据:$counts");
|
||||
_logger.logWithLevel("sendDeviceStateChange 底层发送指令完成,数据:$counts");
|
||||
/// print("📡 发送的完整数据包:${builder.takeBytes()}");
|
||||
}
|
||||
|
||||
/// print("📡 发送的完整数据包:${builder.takeBytes()}");
|
||||
}
|
||||
|
||||
//
|
||||
Future<void> _sendAuthPacket() async {
|
||||
if (_socket == null) return;
|
||||
String? username= "";
|
||||
String? token= "";
|
||||
String? username = "";
|
||||
String? token = "";
|
||||
final user = await _userStorage.getUser();
|
||||
//debugPrint('tcp使用用户信息:$user');
|
||||
_logger.log('tcp使用用户信息:$user');
|
||||
@@ -381,57 +564,57 @@ class TcpClient {
|
||||
|
||||
_socket!.add(builder.takeBytes());
|
||||
//debugPrint('🔑 [TCP] 已发送认证包 (0x03): $authString');
|
||||
_logger.log('🔑 [TCP] 已发送认证包 (0x03): $authString');
|
||||
_logger.logWithLevel('🔑 [TCP] 已发送认证包 (0x03): $authString');
|
||||
|
||||
try {
|
||||
// 1. 获取 Either 结果
|
||||
final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
|
||||
try {
|
||||
// 1. 获取 Either 结果
|
||||
final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
|
||||
|
||||
// 2. 使用 fold 解包 Either
|
||||
// left: 处理错误情况 (DeviceFailure)
|
||||
// right: 处理成功情况 (List<DeviceEntity>)
|
||||
await eitherResult.fold(
|
||||
(failure) {
|
||||
// 处理失败:打印日志或抛出异常
|
||||
// debugPrint('❌ [AuthTcp] 获取设备列表失败:$failure');
|
||||
_logger.logWithLevel('❌ [AuthTcp] 获取设备列表失败:$failure');
|
||||
// _sendAuthPacket();
|
||||
throw Exception('获取设备列表失败:$failure');
|
||||
},
|
||||
(devices) async {
|
||||
// 处理成功:devices 现在是真正的 List<DeviceEntity>
|
||||
if (devices.isEmpty) {
|
||||
// debugPrint('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
_logger.logWithLevel('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
return;
|
||||
}
|
||||
|
||||
// 取第一个设备
|
||||
final DeviceEntity targetDevice = devices.first;
|
||||
// debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
_logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
// 切换设备
|
||||
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
|
||||
await switchResult.fold(
|
||||
// 2. 使用 fold 解包 Either
|
||||
// left: 处理错误情况 (DeviceFailure)
|
||||
// right: 处理成功情况 (List<DeviceEntity>)
|
||||
await eitherResult.fold(
|
||||
(failure) {
|
||||
// debugPrint('❌ [AuthTcp] 切换设备失败:$failure');
|
||||
_logger .logWithLevel('❌ [AuthTcp] 切换设备失败:$failure');
|
||||
// 处理失败:打印日志或抛出异常
|
||||
// debugPrint('❌ [AuthTcp] 获取设备列表失败:$failure');
|
||||
_logger.logWithLevel('❌ [AuthTcp] 获取设备列表失败:$failure');
|
||||
// _sendAuthPacket();
|
||||
throw Exception('切换设备失败:$failure');
|
||||
throw Exception('获取设备列表失败:$failure');
|
||||
},
|
||||
(success) {
|
||||
// debugPrint('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
_logger.logWithLevel('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
(devices) async {
|
||||
// 处理成功:devices 现在是真正的 List<DeviceEntity>
|
||||
if (devices.isEmpty) {
|
||||
// debugPrint('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
_logger.logWithLevel('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
return;
|
||||
}
|
||||
|
||||
// 取第一个设备
|
||||
final DeviceEntity targetDevice = devices.first;
|
||||
// debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
_logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
// 切换设备
|
||||
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
|
||||
await switchResult.fold(
|
||||
(failure) {
|
||||
// debugPrint('❌ [AuthTcp] 切换设备失败:$failure');
|
||||
_logger .logWithLevel('❌ [AuthTcp] 切换设备失败:$failure');
|
||||
// _sendAuthPacket();
|
||||
throw Exception('切换设备失败:$failure');
|
||||
},
|
||||
(success) {
|
||||
// debugPrint('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
_logger.logWithLevel('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// debugPrint('❌ [AuthTcp] 设备订阅流程异常:$e');
|
||||
_logger.logWithLevel('❌ [AuthTcp] 设备订阅流程异常:$e');
|
||||
rethrow;
|
||||
}
|
||||
} catch (e) {
|
||||
// debugPrint('❌ [AuthTcp] 设备订阅流程异常:$e');
|
||||
_logger.logWithLevel('❌ [AuthTcp] 设备订阅流程异常:$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void sendRawBytes(Uint8List bytes) {
|
||||
@@ -439,9 +622,13 @@ _logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevic
|
||||
_socket!.add(bytes);
|
||||
}
|
||||
|
||||
Future<void> connectBySwitch({required String host, required int port, required String deviceName}) async {
|
||||
isUserSwitch=true;
|
||||
// debugPrint('🔌被动 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
|
||||
Future<void> connectBySwitch({
|
||||
required String host,
|
||||
required int port,
|
||||
required String deviceName,
|
||||
}) async {
|
||||
isUserSwitch = true;
|
||||
// debugPrint('🔌被动 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
|
||||
_logger.logWithLevel('🔌被动 [TCP] 开始连接:$host:$port');
|
||||
_lastHost = host;
|
||||
_lastPort = port;
|
||||
@@ -459,54 +646,56 @@ _logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevic
|
||||
);
|
||||
// 🔥 关键修复:禁用Nagle算法,确保小包立即发送
|
||||
_socket!.setOption(SocketOption.tcpNoDelay, true);
|
||||
// debugPrint('✅ 被动[TCP] 连接成功!'); // ✅ 必须看到这条
|
||||
// debugPrint('✅ 被动[TCP] 连接成功!'); // ✅ 必须看到这条
|
||||
_logger.logWithLevel('✅ 被动[TCP] 连接成功!');
|
||||
|
||||
_socket!.listen((data) {
|
||||
// debugPrint('📥 被动[TCP] 收到原始数据:${data.length} 字节, 内容:$data');
|
||||
_logger.logWithLevel('📥 被动[TCP] 收到原始数据:${data.length} 字节, 内容:$data');
|
||||
// var packets = _decoder.decode(data);
|
||||
// debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||
//for (var packet in packets) {
|
||||
// // 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包
|
||||
// if (packet.command == 0xFF) {
|
||||
// debugPrint('收到服务端心跳,自动回复...');
|
||||
// sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
// }
|
||||
// }
|
||||
// _controller.add(packet);
|
||||
// }
|
||||
try {
|
||||
var packets = _decoder.decode(data);
|
||||
//debugPrint('📦 被动[TCP] 解码成功,包数量:${packets.length}');
|
||||
_logger.logWithLevel('📦 被动[TCP] 解码成功,包数量:${packets.length}');
|
||||
for (var packet in packets) {
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(packet);
|
||||
//debugPrint('➡️被动 [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||
_logger.logWithLevel('➡️被动 [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||
_socket!.listen(
|
||||
(data) {
|
||||
// debugPrint('📥 被动[TCP] 收到原始数据:${data.length} 字节, 内容:$data');
|
||||
_logger.logWithLevel('📥 被动[TCP] 收到原始数据:${data.length} 字节, 内容:$data');
|
||||
// var packets = _decoder.decode(data);
|
||||
// debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||
//for (var packet in packets) {
|
||||
// // 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包
|
||||
// if (packet.command == 0xFF) {
|
||||
// debugPrint('收到服务端心跳,自动回复...');
|
||||
// sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
// }
|
||||
// }
|
||||
// _controller.add(packet);
|
||||
// }
|
||||
try {
|
||||
var packets = _decoder.decode(data);
|
||||
//debugPrint('📦 被动[TCP] 解码成功,包数量:${packets.length}');
|
||||
_logger.logWithLevel('📦 被动[TCP] 解码成功,包数量:${packets.length}');
|
||||
for (var packet in packets) {
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(packet);
|
||||
//debugPrint('➡️被动 [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||
_logger.logWithLevel(
|
||||
'➡️被动 [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}',
|
||||
);
|
||||
}
|
||||
if (packet.command == 0xFF) {
|
||||
//debugPrint('收到服务端心跳,自动回复...');
|
||||
_logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...');
|
||||
//
|
||||
sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
}
|
||||
if (packet.command == 0x03) {
|
||||
//debugPrint('⚠️ 收到认证响应:${packet.payload}');
|
||||
_logger.logWithLevel('⚠️ 收到认证响应:${packet.payload}');
|
||||
// 解析 payload 看是否有错误信息
|
||||
}
|
||||
}
|
||||
if (packet.command == 0xFF) {
|
||||
//debugPrint('收到服务端心跳,自动回复...');
|
||||
_logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...');
|
||||
//
|
||||
sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
}
|
||||
if (packet.command == 0x03) {
|
||||
//debugPrint('⚠️ 收到认证响应:${packet.payload}');
|
||||
_logger.logWithLevel('⚠️ 收到认证响应:${packet.payload}');
|
||||
// 解析 payload 看是否有错误信息
|
||||
}
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
//debugPrint('❌ 被动[TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常
|
||||
_logger.logWithLevel('❌ 被动[TCP] 解码数据时发生异常:$e\n$stackTrace');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
//debugPrint('❌ 被动[TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常
|
||||
_logger.logWithLevel('❌ 被动[TCP] 解码数据时发生异常:$e\n$stackTrace');
|
||||
}
|
||||
},
|
||||
onDone: (){
|
||||
},
|
||||
onDone: () {
|
||||
// TODO: 断线重连
|
||||
// debugPrint('onDone❌ 被动[TCP] 连接已断开!');
|
||||
// debugPrint('onDone❌ 被动[TCP] 连接已断开!');
|
||||
_logger.logWithLevel('onDone❌ 被动[TCP] 连接已断开!');
|
||||
if (!_isSwitching) {
|
||||
_handleDisconnectBySwitch(deviceName);
|
||||
@@ -519,13 +708,13 @@ _logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevic
|
||||
}
|
||||
},
|
||||
onError: (e) {
|
||||
// debugPrint('❌ 被动[TCP] 发生错误:$e');
|
||||
// debugPrint('❌ 被动[TCP] 发生错误:$e');
|
||||
_logger.logWithLevel('❌ 被动[TCP] 发生错误:$e');
|
||||
_handleDisconnectBySwitch(deviceName); // 统一走重连逻辑,保护 Controller 不被关闭
|
||||
},
|
||||
);
|
||||
// 开始认证tcp
|
||||
await _sendAuthPacketBySwitch(deviceName);
|
||||
await _sendAuthPacketBySwitch(deviceName);
|
||||
} catch (e) {
|
||||
rethrow; // 向上抛出连接异常
|
||||
}
|
||||
@@ -533,14 +722,14 @@ _logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevic
|
||||
|
||||
Future<void> _sendAuthPacketBySwitch(String deviceName) async {
|
||||
if (_socket == null) return;
|
||||
String? username= "";
|
||||
String? token= "";
|
||||
String? username = "";
|
||||
String? token = "";
|
||||
final user = await _userStorage.getUser();
|
||||
deviceName= deviceName;
|
||||
// debugPrint('tcp被动切换认证:$user');
|
||||
deviceName = deviceName;
|
||||
// debugPrint('tcp被动切换认证:$user');
|
||||
_logger.logWithLevel('tcp被动切换认证:$user');
|
||||
if (user == null || user.token == null) {
|
||||
// debugPrint('❌ 被动[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
|
||||
// debugPrint('❌ 被动[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
|
||||
_logger.logWithLevel('❌ 被动[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
|
||||
disconnect();
|
||||
return; // 直接返回,不要发送无效包
|
||||
@@ -548,7 +737,7 @@ _logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevic
|
||||
username = user.username;
|
||||
token = user.token;
|
||||
final authString = '$username:app:$token';
|
||||
// debugPrint('🔑被动[TCP] 认证字符串:$authString');
|
||||
// debugPrint('🔑被动[TCP] 认证字符串:$authString');
|
||||
_logger.logWithLevel('🔑被动[TCP] 认证字符串:$authString');
|
||||
final authBytes = utf8.encode(authString);
|
||||
|
||||
@@ -574,22 +763,23 @@ _logger.logWithLevel('📱 [AuthTcp] 准备切换至默认设备:${targetDevic
|
||||
|
||||
try {
|
||||
// 1. 获取 Either 结果
|
||||
await switchDeviceUseCase.deviceRepository.switchDevice("app",deviceName);
|
||||
await switchDeviceUseCase.deviceRepository.switchDevice(
|
||||
"app",
|
||||
deviceName,
|
||||
);
|
||||
} catch (e) {
|
||||
//debugPrint('❌ 被动[AuthTcp] 设备订阅流程异常:$e');
|
||||
_logger.logWithLevel('❌ 被动[AuthTcp] 设备订阅流程异常:$e');
|
||||
rethrow;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _handleDisconnectBySwitch( String deviceName) {
|
||||
void _handleDisconnectBySwitch(String deviceName) {
|
||||
stopHeartbeat();
|
||||
|
||||
_socket = null;
|
||||
// 注意:这里不要关闭 _controller!否则监听者会丢失数据流
|
||||
// _controller?.close();
|
||||
_scheduleReconnectBySwitch(deviceName);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
52
lib/core/network/tcp/tcp_status_cubit.dart
Normal file
52
lib/core/network/tcp/tcp_status_cubit.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum TcpConnectionStatus {
|
||||
disconnected, // 未连接
|
||||
connecting, // 连接中
|
||||
connected, // 已连接
|
||||
error, // 连接错误
|
||||
}
|
||||
|
||||
class TcpStatusState {
|
||||
final TcpConnectionStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
const TcpStatusState({
|
||||
this.status = TcpConnectionStatus.disconnected,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
TcpStatusState copyWith({
|
||||
TcpConnectionStatus? status,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return TcpStatusState(
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TcpStatusCubit extends Cubit<TcpStatusState> {
|
||||
TcpStatusCubit() : super(const TcpStatusState());
|
||||
|
||||
void setConnecting() {
|
||||
emit(const TcpStatusState(status: TcpConnectionStatus.connecting));
|
||||
}
|
||||
|
||||
void setConnected() {
|
||||
emit(const TcpStatusState(status: TcpConnectionStatus.connected));
|
||||
}
|
||||
|
||||
void setDisconnected() {
|
||||
emit(const TcpStatusState(status: TcpConnectionStatus.disconnected));
|
||||
}
|
||||
|
||||
void setError(String message) {
|
||||
emit(TcpStatusState(
|
||||
status: TcpConnectionStatus.error,
|
||||
errorMessage: message,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -60,48 +60,48 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
|
||||
debugPrint('✅ sendAuthPacket[AuthTcp] 认证包已发送');
|
||||
|
||||
// 获取用户设备列表
|
||||
try {
|
||||
// 1. 获取 Either 结果
|
||||
final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
|
||||
|
||||
// 2. 使用 fold 解包 Either
|
||||
// left: 处理错误情况 (DeviceFailure)
|
||||
// right: 处理成功情况 (List<DeviceEntity>)
|
||||
await eitherResult.fold(
|
||||
(failure) {
|
||||
// 处理失败:打印日志或抛出异常
|
||||
debugPrint('❌ sendAuthPacket [AuthTcp] 获取设备列表失败:$failure');
|
||||
throw Exception('获取设备列表失败:$failure');
|
||||
},
|
||||
(devices) async {
|
||||
// 处理成功:devices 现在是真正的 List<DeviceEntity>
|
||||
if (devices.isEmpty) {
|
||||
debugPrint('⚠️ sendAuthPacket[AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
return;
|
||||
}
|
||||
|
||||
// 取第一个设备
|
||||
final DeviceEntity targetDevice = devices.first;
|
||||
debugPrint('📱 sendAuthPacket[AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
|
||||
// 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
|
||||
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
|
||||
await switchResult.fold(
|
||||
(failure) {
|
||||
debugPrint('❌ sendAuthPacket[AuthTcp] 切换设备失败:$failure');
|
||||
throw Exception('切换设备失败:$failure');
|
||||
},
|
||||
(success) {
|
||||
debugPrint('✅ sendAuthPacket[AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ sendAuthPacket[AuthTcp] 设备订阅流程异常:$e');
|
||||
rethrow;
|
||||
}
|
||||
// try {
|
||||
// // 1. 获取 Either 结果
|
||||
// final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
|
||||
//
|
||||
// // 2. 使用 fold 解包 Either
|
||||
// // left: 处理错误情况 (DeviceFailure)
|
||||
// // right: 处理成功情况 (List<DeviceEntity>)
|
||||
// await eitherResult.fold(
|
||||
// (failure) {
|
||||
// // 处理失败:打印日志或抛出异常
|
||||
// debugPrint('❌ sendAuthPacket [AuthTcp] 获取设备列表失败:$failure');
|
||||
// throw Exception('获取设备列表失败:$failure');
|
||||
// },
|
||||
// (devices) async {
|
||||
// // 处理成功:devices 现在是真正的 List<DeviceEntity>
|
||||
// if (devices.isEmpty) {
|
||||
// debugPrint('⚠️ sendAuthPacket[AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 取第一个设备
|
||||
// final DeviceEntity targetDevice = devices.first;
|
||||
// debugPrint('📱 sendAuthPacket[AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
//
|
||||
// // 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
|
||||
// final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
//
|
||||
// await switchResult.fold(
|
||||
// (failure) {
|
||||
// debugPrint('❌ sendAuthPacket[AuthTcp] 切换设备失败:$failure');
|
||||
// throw Exception('切换设备失败:$failure');
|
||||
// },
|
||||
// (success) {
|
||||
// debugPrint('✅ sendAuthPacket[AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// } catch (e) {
|
||||
// debugPrint('❌ sendAuthPacket[AuthTcp] 设备订阅流程异常:$e');
|
||||
// rethrow;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,13 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
|
||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||
|
||||
AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher, this._authTcpDatasource) : super(AuthInitial()) {
|
||||
AuthCubit(
|
||||
this.storage,
|
||||
this.tcp,
|
||||
this.appCubit,
|
||||
this.dispatcher,
|
||||
this._authTcpDatasource,
|
||||
) : super(AuthInitial()) {
|
||||
// Cubit 一启动就开始监听 TCP 的“自动逻辑”
|
||||
_listenToAuthResponse();
|
||||
}
|
||||
@@ -49,21 +55,20 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
try {
|
||||
final user = await storage.getUser();
|
||||
logger.logWithLevel(
|
||||
'启动时检查本地缓存',
|
||||
level: 'INFO',
|
||||
data: {'user': user != null ? '找到用户: ${user.username}' : '未找到用户'}
|
||||
'启动时检查本地缓存',
|
||||
level: 'INFO',
|
||||
data: {'user': user != null ? '找到用户: ${user.username}' : '未找到用户'},
|
||||
);
|
||||
|
||||
if (user != null) {
|
||||
// await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
// await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
|
||||
// tcp.startHeartbeat(interval: const Duration(seconds: 4));
|
||||
// 🔥 冷启动时重新初始化 TCP 连接
|
||||
await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
|
||||
// 2. 同步全局 App 状态
|
||||
appCubit.setAuth(user);
|
||||
// 3. 进入已登录状态
|
||||
emit(AuthAuthenticated(user));
|
||||
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态', level: 'INFO');
|
||||
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态并建立TCP连接', level: 'INFO');
|
||||
} else {
|
||||
logger.logWithLevel('⚠️ [AUTH] 应用启动 - 无本地缓存,进入未登录状态', level: 'INFO');
|
||||
emit(AuthUnauthenticated());
|
||||
@@ -77,9 +82,10 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
/// 当 HTTP 登录/注册成功后调用
|
||||
Future<void> loginSuccess(UserEntity user) async {
|
||||
await storage.saveUser(user);
|
||||
// await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
// await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数
|
||||
//tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳
|
||||
|
||||
// 🔥 使用封装的TCP初始化方法:连接 + 认证 + 心跳
|
||||
await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
|
||||
appCubit.setAuth(user);
|
||||
emit(AuthAuthenticated(user));
|
||||
}
|
||||
@@ -92,7 +98,7 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
_clearAllBusinessState();
|
||||
|
||||
await storage.deleteUser();
|
||||
tcp.disconnect();
|
||||
tcp.disconnect(); // 断开后会自动重连
|
||||
appCubit.clearAuth();
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
@@ -129,65 +135,78 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
|
||||
/// TCP 指令监听
|
||||
void _listenToAuthResponse() {
|
||||
_logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'DEBUG');
|
||||
debugPrint('>>> [AUTH] _listenToAuthResponse() 被调用,开始监听 0x12 指令');
|
||||
_kickOutSub?.cancel(); // 防止重复监听
|
||||
|
||||
// 直接监听原始数据包,自己处理 JSON 解析(去掉 CRC 字节)
|
||||
_kickOutSub = dispatcher.onCommand(0x12).listen((packet) {
|
||||
_logger.logWithLevel('[AUTH] 获取原始包:$packet', level: 'DEBUG');
|
||||
_logger.logWithLevel('>>> [AUTH] 收到 0x12 原始包,payload 长度=${packet.payload.length}, 内容=${packet.payload}', level: 'DEBUG');
|
||||
debugPrint(
|
||||
'>>> [AUTH] 收到 0x12 原始包,payload 长度=${packet.payload.length}, 内容=${packet.payload}',
|
||||
);
|
||||
try {
|
||||
// 🔥 关键:手动去掉最后 2 个 CRC 字节
|
||||
String jsonString;
|
||||
if (packet.payload.length > 2) {
|
||||
jsonString = utf8.decode(packet.payload.sublist(0, packet.payload.length - 2));
|
||||
jsonString = utf8.decode(
|
||||
packet.payload.sublist(0, packet.payload.length - 2),
|
||||
);
|
||||
} else {
|
||||
jsonString = utf8.decode(packet.payload);
|
||||
}
|
||||
_logger.logWithLevel('[AUTH] 获取 JSON: $jsonString', level: 'DEBUG');
|
||||
debugPrint('>>> [AUTH] 获取 JSON: "$jsonString"');
|
||||
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
_logger.logWithLevel('[AUTH] 获取 JSON Map: $jsonMap', level: 'DEBUG');
|
||||
debugPrint('>>> [AUTH] 获取 JSON Map: $jsonMap');
|
||||
|
||||
final respond = jsonMap['respond'] ?? '';
|
||||
debugPrint('>>> [AUTH] respond 字段值: "$respond"');
|
||||
_logger.logWithLevel(
|
||||
'>>> [AUTH] respond 字段值: "$respond"',
|
||||
shouldLog: true,
|
||||
);
|
||||
debugPrint('>>> [AUTH] respond 类型: ${respond.runtimeType}');
|
||||
debugPrint(
|
||||
'>>> [AUTH] respond == "have_logged_in": ${respond == "have_logged_in"}',
|
||||
);
|
||||
|
||||
if (respond == 'have_logged_in') {
|
||||
_logger.logWithLevel('[AUTH] ⚠️ 检测到异地登录 (respond=have_logged_in),开始退出...', level: 'WARN');
|
||||
debugPrint('>>> [AUTH] ⚠️ 检测到异地登录 (respond=have_logged_in),开始退出...');
|
||||
_logger.logWithLevel(
|
||||
'⚠️ [AUTH] 检测到异地登录 (respond=have_logged_in),开始退出...',
|
||||
level: 'INFO',
|
||||
);
|
||||
logout();
|
||||
} else {
|
||||
_logger.logWithLevel('[AUTH] ⚠️ 检测到异地登录 (respond=$respond),开始退出...', level: 'WARN');
|
||||
debugPrint('>>> [AUTH] ℹ️ 收到 0x12 消息,respond="$respond",不处理');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('[AUTH] ❌ JSON 解析失败:$e', level: 'ERROR');
|
||||
debugPrint('>>> [AUTH] ❌ JSON 解析失败:$e');
|
||||
}
|
||||
});
|
||||
_logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'DEBUG');
|
||||
_logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'INFO');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
/// 🔥 息屏/后台后恢复到前台时的重连方法
|
||||
Future<void> reconnectAfterResume() async {
|
||||
_logger.logWithLevel('[AUTH] 检测到应用恢复到前台,检查 TCP 连接状态...', level: 'DEBUG');
|
||||
_logger.logWithLevel('[AUTH] 检测到应用恢复到前台,检查 TCP 连接状态...', level: 'INFO');
|
||||
// 如果 TCP 未连接,则执行重连
|
||||
if (!tcp.isConnected) {
|
||||
_logger.logWithLevel('[AUTH] TCP 未连接,开始重连...',
|
||||
level: 'DEBUG');
|
||||
_logger.logWithLevel('[AUTH] TCP 未连接,开始重连...', level: 'INFO');
|
||||
try {
|
||||
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
tcp.startHeartbeat(interval: const Duration(seconds: 4));
|
||||
_logger.logWithLevel('[AUTH] TCP 重连成功!', level: 'DEBUG');
|
||||
_logger.logWithLevel('[AUTH] TCP 重连成功!', level: 'INFO');
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('[AUTH] TCP 重连失败:$e', level: 'ERROR');
|
||||
_logger.logWithLevel('[AUTH] TCP 重连失败:$e', level: 'ERROR');
|
||||
}
|
||||
} else {
|
||||
_logger.logWithLevel('[AUTH] TCP 已连接,无需重连', level: 'DEBUG');
|
||||
_logger.logWithLevel('[AUTH] TCP 已连接,无需重连', level: 'INFO');
|
||||
// 可选:发送一个心跳包确认连接有效
|
||||
tcp.sendHeartbeat();
|
||||
}
|
||||
|
||||
@@ -20,15 +20,23 @@ class LoginCubit extends Cubit<LoginState> {
|
||||
Future<void> login(String username, String password, sourceType) async {
|
||||
emit(LoginLoading());
|
||||
try {
|
||||
final result = await loginUseCase.call(LoginParams(username, password, sourceType));
|
||||
final result = await loginUseCase.call(
|
||||
LoginParams(username, password, sourceType),
|
||||
);
|
||||
// final user = sl<AppUserCubit>().state.user;
|
||||
// final devicesCubit = sl<DevicesCubit>();
|
||||
// if (user != null) {
|
||||
// devicesCubit.fetchAllDevices(user.username);
|
||||
// }
|
||||
result.fold((failure) => emit(LoginFailure(failure.message)), (user) {
|
||||
// 先更新全局用户状态(确保首页能获取到 token)
|
||||
authCubit.appCubit.setAuth(user);
|
||||
// 发出登录成功状态(触发页面跳转)
|
||||
emit(LoginSuccess(user));
|
||||
authCubit.loginSuccess(user);
|
||||
// 后台异步执行 TCP 连接等初始化操作(不阻塞登录流程)
|
||||
authCubit.loginSuccess(user).catchError((e) {
|
||||
print('TCP 初始化失败: $e');
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
emit(LoginFailure(e.toString()));
|
||||
|
||||
@@ -122,7 +122,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
|
||||
final responseData = response.data;
|
||||
logger.logWithLevel('API 响应成功', level: 'INFO', data: {'statusCode': response.statusCode, 'data': response.data});
|
||||
if (responseData['code'] != 200 || responseData['data'] != true) {
|
||||
if (responseData['code'] != 200) {
|
||||
logger.logWithLevel('API 响应失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data});
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
@@ -4,15 +4,21 @@ class DeviceAddPathPointModel {
|
||||
|
||||
DeviceAddPathPointModel({required this.latitude, required this.longitude});
|
||||
|
||||
Map<String, dynamic> toJson() => {'lat': latitude.toString(), 'lon': longitude.toString()};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'lat': latitude.toString(),
|
||||
'lng': longitude.toString(),
|
||||
};
|
||||
|
||||
factory DeviceAddPathPointModel.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceAddPathPointModel(
|
||||
latitude: double.parse((json['lat'] ?? json['latitude']).toString()),
|
||||
longitude: double.parse((json['lon'] ?? json['longitude']).toString()),
|
||||
longitude: double.parse(
|
||||
(json['lng'] ?? json['lon'] ?? json['longitude']).toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '{lat: ${latitude.toString()}, lon: ${longitude.toString()}}';
|
||||
String toString() =>
|
||||
'{lat: ${latitude.toString()}, lon: ${longitude.toString()}}';
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ class ReferencePoint {
|
||||
|
||||
ReferencePoint({required this.lat, required this.lon});
|
||||
|
||||
Map<String, dynamic> toJson() => {'lat': lat, 'lon': lon};
|
||||
Map<String, dynamic> toJson() => {'lat': lat, 'lng': lon};
|
||||
}
|
||||
|
||||
class OuterBoundary {
|
||||
@@ -13,7 +13,10 @@ class OuterBoundary {
|
||||
|
||||
OuterBoundary({required this.position, required this.sideWidth});
|
||||
|
||||
Map<String, dynamic> toJson() => {'position': position.map((p) => p.toJson()).toList(), 'sideWidth': sideWidth};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'position': position.map((p) => p.toJson()).toList(),
|
||||
'sideWidth': sideWidth,
|
||||
};
|
||||
}
|
||||
|
||||
class HoleBoundary {
|
||||
@@ -36,13 +39,17 @@ class HoleBoundary {
|
||||
// 一维数组,包装成二维数组 [[p1,p2,p3]]
|
||||
return [input];
|
||||
} else {
|
||||
throw ArgumentError('position 必须是 List<Position> 或 List<List<Position>> 类型');
|
||||
throw ArgumentError(
|
||||
'position 必须是 List<Position> 或 List<List<Position>> 类型',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 核心修改:toJson 输出二维数组格式
|
||||
Map<String, dynamic> toJson() => {
|
||||
'position': position.map((innerList) => innerList.map((p) => p.toJson()).toList()).toList(), // 二维数组:[[{lat,lon}], ...]
|
||||
'position': position
|
||||
.map((innerList) => innerList.map((p) => p.toJson()).toList())
|
||||
.toList(), // 二维数组:[[{lat,lon}], ...]
|
||||
'sideWidth': sideWidth,
|
||||
};
|
||||
|
||||
@@ -53,7 +60,9 @@ class HoleBoundary {
|
||||
// 2. 遍历外层数组,将每个内层数组转为 List<Position>
|
||||
final List<List<Position>> position = outerList.map((innerList) {
|
||||
// 3. 处理内层数组,转为 Position 对象列表
|
||||
return (innerList as List).map((p) => Position.fromJson(p as Map<String, dynamic>)).toList();
|
||||
return (innerList as List)
|
||||
.map((p) => Position.fromJson(p as Map<String, dynamic>))
|
||||
.toList();
|
||||
}).toList();
|
||||
|
||||
// 4. 提取sideWidth(确保数字类型)
|
||||
@@ -72,9 +81,10 @@ class Position {
|
||||
factory Position.fromJson(Map<String, dynamic> json) {
|
||||
// 安全解析经纬度,避免空值/非数字
|
||||
final double lat = (json['lat'] as num?)?.toDouble() ?? 0.0;
|
||||
final double lon = (json['lon'] as num?)?.toDouble() ?? 0.0;
|
||||
final double lon =
|
||||
((json['lng'] ?? json['lon']) as num?)?.toDouble() ?? 0.0;
|
||||
return Position(lat: lat, lon: lon);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {'lat': lat, 'lon': lon};
|
||||
Map<String, dynamic> toJson() => {'lat': lat, 'lng': lon};
|
||||
}
|
||||
|
||||
@@ -75,25 +75,25 @@ class RunningStatusEntity extends Equatable {
|
||||
logger.logWithLevel(
|
||||
'字段总数: ${fields.length}',
|
||||
level: 'DEBUG',
|
||||
shouldLog: true,
|
||||
shouldLog: false,
|
||||
);
|
||||
|
||||
if (fields.length > 14) {
|
||||
logger.logWithLevel(
|
||||
'qual原始值(fields[14]): "${fields[14]}"',
|
||||
level: 'DEBUG',
|
||||
shouldLog: true,
|
||||
shouldLog: false,
|
||||
);
|
||||
logger.logWithLevel(
|
||||
'satelliteCnt(fields[13]): "${fields[13]}"',
|
||||
level: 'DEBUG',
|
||||
shouldLog: true,
|
||||
shouldLog: false,
|
||||
);
|
||||
logger.logWithLevel(
|
||||
/* logger.logWithLevel(
|
||||
'headingStatus(fields[15]): "${fields[15]}"',
|
||||
level: 'DEBUG',
|
||||
shouldLog: true,
|
||||
);
|
||||
);*/
|
||||
}
|
||||
|
||||
// 确保至少有 24 个字段,不足的用默认值填充
|
||||
|
||||
@@ -73,10 +73,10 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
.map((p) {
|
||||
try {
|
||||
final result = utf8.decode(p.payload, allowMalformed: true);
|
||||
debugPrint('✅ [DeviceStatusBloc] 收到0x02数据: $result');
|
||||
// debugPrint('✅ [DeviceStatusBloc] 收到0x02数据: $result');
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [DeviceStatusBloc] 解码失败: $e');
|
||||
// debugPrint('❌ [DeviceStatusBloc] 解码失败: $e');
|
||||
return '';
|
||||
}
|
||||
})
|
||||
@@ -105,7 +105,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
final gps = GPSEntity(status.latitude, status.longitude);
|
||||
|
||||
//debugPrint('✅ [DeviceStatusBloc] 直接解析成功,更新状态:Lat=${gps.latitude}, Lng=${gps.longitude}');
|
||||
_logger.log('✅ [DeviceStatusBloc] 直接解析成功,更新状态');
|
||||
// _logger.log('✅ [DeviceStatusBloc] 直接解析成功,更新状态');
|
||||
|
||||
// 🔥 关键修复:BLoC有Equatable去重机制,必须创建新对象才能触发UI更新
|
||||
if (!isClosed) {
|
||||
@@ -137,12 +137,12 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
obstacleFlag: status.obstacleFlag,
|
||||
);
|
||||
final newGps = GPSEntity(status.latitude, status.longitude);
|
||||
debugPrint('📤 [DeviceStatusBloc] emit DeviceStatusUpdated - 电压:${status.voltage}, 电量:${status.battery}, 模式:${status.controlMode}');
|
||||
// debugPrint('📤 [DeviceStatusBloc] emit DeviceStatusUpdated - 电压:${status.voltage}, 电量:${status.battery}, 模式:${status.controlMode}');
|
||||
emit(DeviceStatusUpdated(newStatus, newGps));
|
||||
}
|
||||
} catch (e, stack) {
|
||||
//debugPrint('❌ [DeviceStatusBloc] 直接解析异常:$e\n$stack');
|
||||
_logger.log('❌ [DeviceStatusBloc] 直接解析异常:$e');
|
||||
// _logger.log('❌ [DeviceStatusBloc] 直接解析异常:$e');
|
||||
if (!isClosed) {
|
||||
emit(DeviceStatusError('解析失败:$e'));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:maibu_satabot_v2/components/tcp_status_indicator.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart';
|
||||
@@ -55,7 +56,14 @@ class ImmersionHeader extends StatelessWidget {
|
||||
// 机器大图
|
||||
Transform.translate(
|
||||
offset: const Offset(30, 80), // 正数向右移动(例如 20 像素),负数向左
|
||||
child: Center(child: Image.asset('assets/images/car.png', width: 330, height: 190, fit: BoxFit.contain)),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/car.png',
|
||||
width: 330,
|
||||
height: 190,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 顶部状态信息
|
||||
@@ -74,12 +82,28 @@ class ImmersionHeader extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min, // 尽可能收缩高度
|
||||
children: [
|
||||
// 设备名称和电量
|
||||
Text(_formatName(device.displayName), style: GoogleFonts.roboto(fontSize: 22, fontWeight: FontWeight.w900)),
|
||||
Text(
|
||||
_formatName(device.displayName),
|
||||
style: GoogleFonts.roboto(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text('${device.onlineStatus == 1 ? "100" : "--"} %', style: GoogleFonts.roboto(fontSize: 24, fontWeight: FontWeight.w900)),
|
||||
const Icon(Icons.chevron_right, size: 20, color: Colors.grey),
|
||||
Text(
|
||||
'${device.onlineStatus == 1 ? "100" : "--"} %',
|
||||
style: GoogleFonts.roboto(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -103,14 +127,24 @@ class ImmersionHeader extends StatelessWidget {
|
||||
_buildCircleIcon(Icons.sync, () {
|
||||
// 1. 触发 Cubit 请求最新设备列表
|
||||
// 假设你的 username 存储在 AuthCubit 或类似的全局状态中
|
||||
final username = context.read<AppUserCubit>().state.user?.username ?? "";
|
||||
final username =
|
||||
context.read<AppUserCubit>().state.user?.username ??
|
||||
"";
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
|
||||
// 2. 弹出窗口(窗口内部会根据状态显示转圈或列表)
|
||||
_showDeviceSwitcher(context);
|
||||
}),
|
||||
const SizedBox(height: 20), // 这里你改大的间距,只会让两个图标拉开
|
||||
_buildCircleIcon(Icons.bluetooth, null, isAccent: true),
|
||||
// TCP状态指示灯 - 暂时关闭
|
||||
// Container(
|
||||
// padding: const EdgeInsets.all(8),
|
||||
// decoration: BoxDecoration(
|
||||
// shape: BoxShape.circle,
|
||||
// color: Colors.blue.withOpacity(0.1),
|
||||
// ),
|
||||
// child: const TcpStatusIndicator(size: 14),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -129,15 +163,22 @@ class ImmersionHeader extends StatelessWidget {
|
||||
// 在 offWhite 背景下,标签用纯白色会显得更精致
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 4)],
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 4),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(radius: 3, backgroundColor: isOnline ? Colors.green : Colors.grey),
|
||||
CircleAvatar(
|
||||
radius: 3,
|
||||
backgroundColor: isOnline ? Colors.green : Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
isOnline ? AppLocalizations.of(context).translate('home.device_online') : AppLocalizations.of(context).translate('home.device_offline'),
|
||||
isOnline
|
||||
? AppLocalizations.of(context).translate('home.device_online')
|
||||
: AppLocalizations.of(context).translate('home.device_offline'),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -151,7 +192,10 @@ class ImmersionHeader extends StatelessWidget {
|
||||
return Container(
|
||||
width: 120,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(color: Colors.blueAccent, borderRadius: BorderRadius.circular(5)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueAccent,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,7 +220,11 @@ class ImmersionHeader extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, size: 30, color: isAccent ? Colors.blue : Colors.black54),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 30,
|
||||
color: isAccent ? Colors.blue : Colors.black54,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -202,15 +250,27 @@ class ImmersionHeader extends StatelessWidget {
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
width: 36,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(10)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(modalContext).translate('home.switch_device'),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
AppLocalizations.of(
|
||||
modalContext,
|
||||
).translate('home.switch_device'),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(height: 0.5, margin: const EdgeInsets.symmetric(horizontal: 20), color: Colors.grey.withOpacity(0.1)),
|
||||
Container(
|
||||
height: 0.5,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
),
|
||||
|
||||
// 💡 动态内容区
|
||||
Expanded(
|
||||
@@ -225,7 +285,12 @@ class ImmersionHeader extends StatelessWidget {
|
||||
children: [
|
||||
const CircularProgressIndicator(color: Colors.blue),
|
||||
const SizedBox(height: 16),
|
||||
Text(AppLocalizations.of(context).translate('home.loading'), style: TextStyle(color: Colors.grey[600])),
|
||||
Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('home.loading'),
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -237,9 +302,21 @@ class ImmersionHeader extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.description_outlined, size: 60, color: Colors.grey),
|
||||
const Icon(
|
||||
Icons.description_outlined,
|
||||
size: 60,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(AppLocalizations.of(context).translate('home.no_devices'), style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('home.no_devices'),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -247,12 +324,21 @@ class ImmersionHeader extends StatelessWidget {
|
||||
|
||||
return ListView.builder(
|
||||
// 💡 底部留出安全距离,防止最后一条滚不上来
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 20, top: 10),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 20,
|
||||
top: 10,
|
||||
),
|
||||
itemCount: state.devices.length,
|
||||
itemBuilder: (context, index) {
|
||||
// 传入当前选中的 ID 方便做 UI 区分
|
||||
final isSelected = state.selectedDevice?.deviceName == state.devices[index].deviceName;
|
||||
return _buildDeviceItem(state.devices[index], context, isSelected);
|
||||
final isSelected =
|
||||
state.selectedDevice?.deviceName ==
|
||||
state.devices[index].deviceName;
|
||||
return _buildDeviceItem(
|
||||
state.devices[index],
|
||||
context,
|
||||
isSelected,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -265,7 +351,11 @@ class ImmersionHeader extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceItem(DeviceEntity device, BuildContext context, bool isSelected) {
|
||||
Widget _buildDeviceItem(
|
||||
DeviceEntity device,
|
||||
BuildContext context,
|
||||
bool isSelected,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print("跳转到详情页: ${device.deviceAlias}");
|
||||
@@ -277,14 +367,29 @@ class ImmersionHeader extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
// 💡 如果是当前选中设备,边框颜色略深
|
||||
border: Border.all(color: isSelected ? Colors.blue.withOpacity(0.5) : Colors.grey.withOpacity(0.3), width: isSelected ? 1.5 : 1),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Colors.blue.withOpacity(0.5)
|
||||
: Colors.grey.withOpacity(0.3),
|
||||
width: isSelected ? 1.5 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [if (isSelected) BoxShadow(color: Colors.blue.withOpacity(0.05), blurRadius: 4)],
|
||||
boxShadow: [
|
||||
if (isSelected)
|
||||
BoxShadow(color: Colors.blue.withOpacity(0.05), blurRadius: 4),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 设备图片
|
||||
Transform.translate(offset: const Offset(10, 0), child: Image.asset('assets/images/car.png', width: 100, height: 80)),
|
||||
Transform.translate(
|
||||
offset: const Offset(10, 0),
|
||||
child: Image.asset(
|
||||
'assets/images/car.png',
|
||||
width: 100,
|
||||
height: 80,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 设备信息
|
||||
Expanded(
|
||||
@@ -293,11 +398,19 @@ class ImmersionHeader extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.circle, color: device.isOnline ? Colors.green : Colors.grey, size: 12),
|
||||
Icon(
|
||||
Icons.circle,
|
||||
color: device.isOnline ? Colors.green : Colors.grey,
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
(device.deviceAlias?.trim() ?? '').isEmpty ? AppLocalizations.of(context).translate('home.unknown_device') : device.deviceAlias!,
|
||||
(device.deviceAlias?.trim() ?? '').isEmpty
|
||||
? AppLocalizations.of(
|
||||
context,
|
||||
).translate('home.unknown_device')
|
||||
: device.deviceAlias!,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
// 关键属性:处理文本溢出
|
||||
maxLines: 2, // 最多显示2行
|
||||
@@ -309,7 +422,9 @@ class ImmersionHeader extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
AppLocalizations.of(context).translate('home.click_to_view'),
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('home.click_to_view'),
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -324,7 +439,9 @@ class ImmersionHeader extends StatelessWidget {
|
||||
? null
|
||||
: () async {
|
||||
// 💡 执行切换逻辑
|
||||
await context.read<DevicesCubit>().switchDevice(device);
|
||||
await context.read<DevicesCubit>().switchDevice(
|
||||
device,
|
||||
);
|
||||
await LocationUtils.clearAllPlotCache();
|
||||
|
||||
// 切换成功后,UI 会自动更新(因为 BlocBuilder 在监听),我们可以关闭弹窗
|
||||
@@ -332,7 +449,9 @@ class ImmersionHeader extends StatelessWidget {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
text: isSelected ? AppLocalizations.of(context).translate('home.in_use') : AppLocalizations.of(context).translate('home.switch'),
|
||||
text: isSelected
|
||||
? AppLocalizations.of(context).translate('home.in_use')
|
||||
: AppLocalizations.of(context).translate('home.switch'),
|
||||
width: 90,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
@@ -342,7 +461,10 @@ class ImmersionHeader extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
CCPrimaryButton(
|
||||
onPressed: () {
|
||||
context.push(RoutePaths.machineDetails, extra: device); // 把当前点击的这个设备对象传过去
|
||||
context.push(
|
||||
RoutePaths.machineDetails,
|
||||
extra: device,
|
||||
); // 把当前点击的这个设备对象传过去
|
||||
print("通过按钮进入详情,设备名称:$device");
|
||||
},
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/components/tcp_status_indicator.dart';
|
||||
import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart';
|
||||
@@ -53,9 +54,39 @@ class _CustomMainContainerState extends State<CustomMainContainer>
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
index: currentIndex,
|
||||
children: _buildPages(enabledTabs),
|
||||
body: Stack(
|
||||
children: [
|
||||
IndexedStack(
|
||||
index: currentIndex,
|
||||
children: _buildPages(enabledTabs),
|
||||
),
|
||||
// TCP状态指示灯 - 右上角,带白色背景确保可见
|
||||
Positioned(
|
||||
top: 40,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5), // 透明灰色背景
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TcpStatusIndicator(size: 12),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'TCP',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -27,19 +27,23 @@ class RemoteHttpDatasource {
|
||||
}*/
|
||||
|
||||
|
||||
Future<bool> requestRemoteControlViaHttp( String deviceId, String currentDeviceId) async {
|
||||
/// 🔥 返回完整权限信息: {hasPermission, owner}
|
||||
Future<Map<String, dynamic>> requestRemoteControlViaHttp(String deviceName, String platform) async {
|
||||
print(deviceName+"@@@");
|
||||
final user = await _userStorage.getUser();
|
||||
if (user == null) {
|
||||
debugPrint('❌ [RemoteHttp] 用户未登录,无法请求远程控制权限');
|
||||
_logger.log('❌ [RemoteHttp] 用户未登录,无法请求远程控制权限');
|
||||
return false;
|
||||
return {'hasPermission': false, 'owner': null};
|
||||
}
|
||||
final token = user.token;
|
||||
|
||||
debugPrint('🔑 [RemoteHttp] 开始请求远程控制权限 - deviceId: $deviceId, platform: app');
|
||||
_logger.log('🔑 [RemoteHttp] 开始请求远程控制权限 - deviceId: $deviceId, platform: app');
|
||||
debugPrint('🔑 [RemoteHttp] 开始请求远程控制权限 - deviceName: $deviceName, platform: $platform');
|
||||
_logger.log('🔑 [RemoteHttp] 开始请求远程控制权限 - deviceName: $deviceName, platform: $platform');
|
||||
|
||||
try {
|
||||
debugPrint('📡 [RemoteHttp] 正在发送 HTTP POST 请求到 /forward/device/remoteControl');
|
||||
debugPrint('📡 [RemoteHttp] 请求参数: deviceId=$deviceName, platform=$platform');
|
||||
final response = await _dio.post(
|
||||
'/forward/device/remoteControl',
|
||||
options: Options(
|
||||
@@ -48,7 +52,8 @@ class RemoteHttpDatasource {
|
||||
'Authorization': 'Bearer ${token}',
|
||||
},
|
||||
),
|
||||
data: {'deviceId': deviceId, 'platform': "app"});
|
||||
data: {'deviceId': deviceName, 'platform': platform});
|
||||
debugPrint('📥 [RemoteHttp] HTTP 请求已发送,等待响应...');
|
||||
|
||||
debugPrint("📊 [RemoteHttp] HTTP响应状态码: ${response.statusCode}");
|
||||
_logger.log("📊 [RemoteHttp] HTTP响应状态码: ${response.statusCode}");
|
||||
@@ -57,24 +62,25 @@ class RemoteHttpDatasource {
|
||||
debugPrint("📊 [RemoteHttp] 完整响应数据:$responseData");
|
||||
_logger.log("📊 [RemoteHttp] 完整响应数据:$responseData");
|
||||
|
||||
// 🔥 关键:先获取响应的 data 字段,再获取 remoteControl
|
||||
// 🔥 关键:先获取响应的 data 字段,再获取 remoteControl 和 owner
|
||||
final dataField = responseData['data'] as Map<String, dynamic>?;
|
||||
if (dataField != null) {
|
||||
final bool hasRemoteControl = dataField['remoteControl'] as bool? ?? false;
|
||||
final String? owner = dataField['owner'] as String?;
|
||||
|
||||
debugPrint("✅ [RemoteHttp] 解析成功 - remoteControl=$hasRemoteControl");
|
||||
_logger.log("✅ [RemoteHttp] 解析成功 - remoteControl=$hasRemoteControl");
|
||||
// 🔥 直接返回 remoteControl 的布尔值
|
||||
return hasRemoteControl;
|
||||
debugPrint("✅ [RemoteHttp] 解析成功 - remoteControl=$hasRemoteControl, owner=$owner");
|
||||
_logger.log("✅ [RemoteHttp] 解析成功 - remoteControl=$hasRemoteControl, owner=$owner");
|
||||
// 🔥 返回完整权限信息
|
||||
return {'hasPermission': hasRemoteControl, 'owner': owner};
|
||||
} else {
|
||||
debugPrint("❌ [RemoteHttp] 缺少 data 字段,响应结构异常");
|
||||
_logger.log("❌ [RemoteHttp] 缺少 data 字段,响应结构异常");
|
||||
return false;
|
||||
return {'hasPermission': false, 'owner': null};
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [RemoteHttp] 请求远程控制权限失败:$e');
|
||||
_logger.log('❌ [RemoteHttp] 请求远程控制权限失败:$e');
|
||||
return false;
|
||||
return {'hasPermission': false, 'owner': null};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/protocol_decoder.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../../core/protocol/machine_protocol_constants.dart';
|
||||
@@ -14,6 +16,7 @@ import '../../../../core/storage/user_storage.dart';
|
||||
class RemoteTcpDatasource {
|
||||
final TcpClient _tcpClient;
|
||||
final UserStorage _userStorage;
|
||||
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
||||
|
||||
RemoteTcpDatasource(this._tcpClient, this._userStorage);
|
||||
|
||||
@@ -33,7 +36,9 @@ class RemoteTcpDatasource {
|
||||
// 获取用户信息
|
||||
final user = await _userStorage.getUser();
|
||||
if (user == null || user.token == null) {
|
||||
debugPrint('❌ sendSwitchControlRequest[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
|
||||
debugPrint(
|
||||
'❌ sendSwitchControlRequest[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包',
|
||||
);
|
||||
throw Exception('用户未登录或 Token 无效');
|
||||
}
|
||||
|
||||
@@ -66,12 +71,16 @@ class RemoteTcpDatasource {
|
||||
// 构造完整的协议包
|
||||
final packet = buildSwitchControlPacket(jsonBytes);
|
||||
|
||||
debugPrint('sendSwitchControlRequest[TCP] 完整数据包:${packet.map((b) => '0x${b.toRadixString(16)}').join(' ')}');
|
||||
debugPrint(
|
||||
'sendSwitchControlRequest[TCP] 完整数据包:${packet.map((b) => '0x${b.toRadixString(16)}').join(' ')}',
|
||||
);
|
||||
|
||||
// 发送数据包
|
||||
_tcpClient.sendnew(packet);
|
||||
|
||||
debugPrint('✅ sendSwitchControlRequest[TCP] 已发送 switch_control 权限请求 (0x${MachineProtocolConstants.cmdGetAuth.toRadixString(16)})');
|
||||
debugPrint(
|
||||
'✅ sendSwitchControlRequest[TCP] 已发送 switch_control 权限请求 (0x${MachineProtocolConstants.cmdGetAuth.toRadixString(16)})',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ sendSwitchControlRequest[TCP] 发送权限请求失败:$e');
|
||||
rethrow;
|
||||
@@ -105,51 +114,108 @@ class RemoteTcpDatasource {
|
||||
|
||||
return builder.takeBytes();
|
||||
}
|
||||
// TODO: 同意授权和拒绝授权
|
||||
void sendSwitchControlResponse(bool bool, String deviceId) {
|
||||
|
||||
// TODO: 同意授权和拒绝授权
|
||||
void sendSwitchControlResponse(bool agreed, String deviceId) {
|
||||
debugPrint(
|
||||
'📤 [进入了TCP层==== 被调用 - agreed=$agreed, deviceId=$deviceId',
|
||||
);
|
||||
|
||||
// 🔥 添加详细的连接状态日志
|
||||
debugPrint(
|
||||
'📤 [进入了TCP层==== 当前 TCP 连接状态: isConnected=${_tcpClient.isConnected}',
|
||||
);
|
||||
|
||||
if (!_tcpClient.isConnected) {
|
||||
debugPrint('❌sendSwitchControlResponse [TCP] 无法发送权限响应:Socket 未连接');
|
||||
debugPrint('❌ [TCP DataSource] 无法发送权限响应: Socket 未连接');
|
||||
debugPrint('❌ [TCP DataSource] isConnected=${_tcpClient.isConnected}');
|
||||
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',
|
||||
},
|
||||
};
|
||||
debugPrint('📡 [TCP DataSource] TCP 连接正常,准备发送数据...');
|
||||
|
||||
if(!bool){
|
||||
debugPrint('sendSwitchControlResponse[TCP] 拒绝授权:$deviceId');
|
||||
}else{
|
||||
debugPrint('sendSwitchControlResponse[TCP] 同意授权:$deviceId');
|
||||
}
|
||||
//debugPrint('sendSwitchControlResponse[TCP] 同意授权:$deviceId');
|
||||
final jsonStr = jsonEncode(response);
|
||||
final jsonBytes = utf8.encode(jsonStr);
|
||||
// 构造响应 JSON(参考 Android 代码)
|
||||
final response = agreed
|
||||
? {
|
||||
'respond': {
|
||||
'switchResult': true,
|
||||
'deviceId': deviceId,
|
||||
'holder': 'you',
|
||||
},
|
||||
}
|
||||
: {
|
||||
'respond': {
|
||||
'switchResult': false,
|
||||
'deviceId': deviceId,
|
||||
'reason': 'holder_denied',
|
||||
},
|
||||
};
|
||||
|
||||
debugPrint('sendSwitchControlResponse[TCP] 响应权限请求:$jsonStr');
|
||||
debugPrint(
|
||||
'📝 [TCP DataSource] ${agreed ? "同意" : "拒绝"}授权: deviceId=$deviceId',
|
||||
);
|
||||
|
||||
// 构造完整的协议包
|
||||
final packet = buildSwitchControlPacket(jsonBytes);
|
||||
final jsonStr = jsonEncode(response);
|
||||
final jsonBytes = utf8.encode(jsonStr);
|
||||
|
||||
debugPrint('sendSwitchControlResponse[TCP] 完整数据包:${packet.map((b) => '0x${b.toRadixString(16)}').join(' ')}');
|
||||
debugPrint('📦 [TCP DataSource] JSON 数据: $jsonStr');
|
||||
debugPrint('📦 [TCP DataSource] JSON 字节长度: ${jsonBytes.length} 字节');
|
||||
|
||||
// 发送数据包
|
||||
_tcpClient.sendnew(packet);
|
||||
}
|
||||
catch (e) {
|
||||
debugPrint('❌ sendSwitchControlResponse[TCP] 响应权限请求失败:$e');
|
||||
// 构造完整的协议包
|
||||
final packet = buildSwitchControlPacket(jsonBytes);
|
||||
|
||||
_logger.logWithLevel('TCP DataSource] JSON 数据: $jsonStr' ,shouldLog: true);
|
||||
|
||||
debugPrint(
|
||||
'📦 [TCP DataSource] 完整数据包(${packet.length}字节): ${packet.map((b) => '0x${b.toRadixString(16).padLeft(2, "0")}').join(' ')}',
|
||||
);
|
||||
|
||||
// 🔥 发送前再次检查连接状态
|
||||
if (!_tcpClient.isConnected) {
|
||||
debugPrint('❌ [TCP DataSource] 发送前检查: Socket 已断开连接!');
|
||||
throw Exception('TCP 连接在发送前断开');
|
||||
}
|
||||
|
||||
// 发送数据包
|
||||
debugPrint('🚀 [TCP DataSource] 正在调用 SendNew 发送数据...');
|
||||
|
||||
// 🔥 添加详细日志:记录发送的完整数据包(十六进制)
|
||||
final hexString = packet
|
||||
.map((byte) => '0x${byte.toRadixString(16).padLeft(2, '0')}')
|
||||
.join(' ');
|
||||
debugPrint('📦 [TCP DataSource] 完整数据包(${packet.length}字节): $hexString');
|
||||
|
||||
// 🔥 使用ILoggerService记录日志
|
||||
_logger.logWithLevel(
|
||||
'📦 [TCP DataSource] 完整数据包(${packet.length}字节): $hexString',
|
||||
shouldLog: true,
|
||||
);
|
||||
|
||||
// 🔥 记录JSON内容和字节数
|
||||
_logger.logWithLevel(
|
||||
'📝 [TCP DataSource] 发送内容: $jsonStr',
|
||||
shouldLog: true,
|
||||
);
|
||||
_logger.logWithLevel(
|
||||
'📊 [TCP DataSource] JSON字节数: ${jsonBytes.length}, 完整包字节数: ${packet.length}',
|
||||
shouldLog: true,
|
||||
);
|
||||
|
||||
_tcpClient.sendnew(packet);
|
||||
|
||||
debugPrint('✅ [TCP DataSource] TCP 响应已成功发送');
|
||||
debugPrint('✅ [TCP DataSource] 发送字节数: ${packet.length}');
|
||||
|
||||
// 🔥 发送成功后的日志
|
||||
_logger.logWithLevel(
|
||||
'✅ [TCP DataSource] TCP 响应已成功发送 - agreed=$agreed, deviceId=$deviceId, 字节数=${packet.length}',
|
||||
shouldLog: true,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [TCP DataSource] 响应权限请求失败: $e');
|
||||
debugPrint('❌ [TCP DataSource] 错误类型: ${e.runtimeType}');
|
||||
debugPrint('❌ [TCP DataSource] 错误堆栈: ${StackTrace.current}');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
@@ -19,11 +18,15 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository {
|
||||
final TcpClient _tcpClient;
|
||||
final DiffSteerUseCase _diffSteer;
|
||||
final RemoteHttpDatasource _remoteHttp;
|
||||
final RemoteTcpDatasource _remoteTcp;
|
||||
final RemoteTcpDatasource _remoteTcp;
|
||||
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
||||
|
||||
|
||||
RemoteControlRepositoryImpl(this._tcpClient, this._diffSteer, this._remoteHttp, this._remoteTcp);
|
||||
RemoteControlRepositoryImpl(
|
||||
this._tcpClient,
|
||||
this._diffSteer,
|
||||
this._remoteHttp,
|
||||
this._remoteTcp,
|
||||
);
|
||||
|
||||
@override
|
||||
void changeWebViewDirection(String direction) {
|
||||
@@ -34,7 +37,9 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository {
|
||||
void sendControlMachineCmd(MachineControlStatusEntity status) {
|
||||
// 1. 调用算法:将摇杆坐标 (x, y) 转换为左右轮电机转速
|
||||
final speeds = _diffSteer.calculate(status.originX, status.originY);
|
||||
_logger.logWithLevel('⚙️ [电机速度] left: ${speeds['left']}, right: ${speeds['right']}');
|
||||
_logger.logWithLevel(
|
||||
'⚙️ [电机速度] left: ${speeds['left']}, right: ${speeds['right']}',
|
||||
);
|
||||
|
||||
// 2. 调用 Codec:生成协议要求的 8 字节 Payload
|
||||
final payload = MachineProtocolCodec.encodeRemoteControlPayload(
|
||||
@@ -47,39 +52,102 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository {
|
||||
);
|
||||
|
||||
// 3. 调用 TcpClient 发送指令
|
||||
_tcpClient.sendRaw(
|
||||
MachineProtocolConstants.cmdRemoteControl,
|
||||
payload,
|
||||
);
|
||||
_tcpClient.sendRaw(MachineProtocolConstants.cmdRemoteControl, payload);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<RawPacket> get responseStream => _tcpClient.packetStream;
|
||||
|
||||
@override
|
||||
Future<bool> requestControlPermission( String deviceName, String deviceId) async {
|
||||
void sendTcpPermissionRequest(String deviceName) {
|
||||
_remoteTcp.sendSwitchControlRequest(deviceName);
|
||||
_logger.logWithLevel('🔑 [RemoteControl] TCP已发送权限请求');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> requestControlPermission(
|
||||
String deviceName,
|
||||
String deviceId,
|
||||
) async {
|
||||
try {
|
||||
_remoteTcp.sendSwitchControlRequest(deviceName);
|
||||
// debugPrint('🔑 [RemoteControl] TCP已发送权限请求');
|
||||
_logger.logWithLevel('🔑 [RemoteControl] TCP已发送权限请求');
|
||||
return await _remoteHttp.requestRemoteControlViaHttp(deviceName, deviceId);
|
||||
// 1. 先发送 HTTP 权限请求检查权限
|
||||
_logger.logWithLevel('🔑 [RemoteControl] 检查 HTTP 权限...');
|
||||
final result = await _remoteHttp.requestRemoteControlViaHttp(
|
||||
deviceName,
|
||||
deviceId,
|
||||
);
|
||||
|
||||
// 2. 如果没有权限或权限为 null,才发送 TCP 请求
|
||||
final hasPermission = result['hasPermission'] as bool? ?? false;
|
||||
if (!hasPermission) {
|
||||
_logger.logWithLevel('🔑 [RemoteControl] 无权限,发送 TCP 权限请求');
|
||||
_remoteTcp.sendSwitchControlRequest(deviceName);
|
||||
} else {
|
||||
_logger.logWithLevel('✅ [RemoteControl] 已有权限,无需 TCP 请求');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
//sdebugPrint('❌ [RemoteControl] 权限请求失败:$e');
|
||||
_logger.logWithLevel('❌ [RemoteControl] 权限请求失败:$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void respondPermission(bool bool, String deviceId) {
|
||||
// TODO: implement respondPermission
|
||||
if (bool) {
|
||||
_remoteTcp.sendSwitchControlResponse(true, deviceId);
|
||||
} else {
|
||||
_remoteTcp.sendSwitchControlResponse(false, deviceId);
|
||||
void respondPermission(bool agreed, String deviceId) {
|
||||
final timePrefix = DateTime.now().toIso8601String();
|
||||
final logPrefix = '$timePrefix 📤 [RemoteControlRepository]';
|
||||
|
||||
debugPrint('$logPrefix =========================================');
|
||||
debugPrint('$logPrefix respondPermission 被调用');
|
||||
debugPrint('$logPrefix agreed: $agreed, deviceId: $deviceId');
|
||||
_logger.logWithLevel(
|
||||
'$logPrefix respondPermission 被调用 - agreed=$agreed, deviceId=$deviceId',
|
||||
shouldLog: true,
|
||||
);
|
||||
|
||||
try {
|
||||
// 🔥 检查 _remoteTcp 是否为空
|
||||
if (_remoteTcp == null) {
|
||||
debugPrint('$logPrefix ❌ _remoteTcp 为空,无法发送 TCP 指令');
|
||||
_logger.logWithLevel('$logPrefix ❌ _remoteTcp 为空', shouldLog: true);
|
||||
throw Exception('_remoteTcp 为空');
|
||||
}
|
||||
|
||||
// 🔥 检查 TCP 连接状态
|
||||
debugPrint('$logPrefix 📡 TCP连接状态: ${_remoteTcp.isConnected}');
|
||||
_logger.logWithLevel(
|
||||
'$logPrefix 📡 TCP连接状态: ${_remoteTcp.isConnected}',
|
||||
shouldLog: true,
|
||||
);
|
||||
|
||||
if (agreed) {
|
||||
debugPrint('$logPrefix ✅ 用户同意释放权限,发送同意响应');
|
||||
_logger.logWithLevel('$logPrefix ✅ 准备发送同意响应 TCP 指令', shouldLog: true);
|
||||
_remoteTcp.sendSwitchControlResponse(true, deviceId);
|
||||
} else {
|
||||
debugPrint('$logPrefix ❌ 用户拒绝释放权限,发送拒绝响应');
|
||||
_logger.logWithLevel('$logPrefix ❌ 准备发送拒绝响应 TCP 指令', shouldLog: true);
|
||||
_remoteTcp.sendSwitchControlResponse(false, deviceId);
|
||||
}
|
||||
|
||||
debugPrint('$logPrefix ✅ TCP 响应发送完成');
|
||||
_logger.logWithLevel('$logPrefix ✅ TCP 响应发送完成', shouldLog: true);
|
||||
} catch (e) {
|
||||
debugPrint('$logPrefix ❌ respondPermission 失败: $e');
|
||||
debugPrint('$logPrefix ❌ 错误类型: ${e.runtimeType}');
|
||||
_logger.logWithLevel(
|
||||
'$logPrefix ❌ respondPermission 失败: $e',
|
||||
shouldLog: true,
|
||||
);
|
||||
// 🔥 关键修复:重新抛出异常,让上层知道 TCP 发送失败了
|
||||
rethrow;
|
||||
}
|
||||
|
||||
debugPrint('$logPrefix =========================================');
|
||||
}
|
||||
///app退出远程控制后释放权限
|
||||
|
||||
///app退出远程控制后释放权限
|
||||
@override
|
||||
Future<bool> releasePermission(String platform) async {
|
||||
return await _remoteHttp.releaseRemoteControlViaHttp(platform);
|
||||
|
||||
@@ -13,8 +13,12 @@ abstract class RemoteControlRepository {
|
||||
/// WebView 视角控制 (逻辑层面的切换,不涉及 TCP)
|
||||
void changeWebViewDirection(String direction);
|
||||
|
||||
/// 新增:请求控制权限 (发送 0x04 指令)
|
||||
Future<bool> requestControlPermission(String deviceName, String deviceId);
|
||||
/// 新增:请求控制权限 (HTTP接口返回完整权限信息)
|
||||
/// 返回: {hasPermission: bool, owner: String?}
|
||||
Future<Map<String, dynamic>> requestControlPermission(String deviceName, String deviceId);
|
||||
|
||||
/// 🔥 新增: 发送 TCP 0x12 权限请求指令
|
||||
void sendTcpPermissionRequest(String deviceName);
|
||||
|
||||
/// 新增:响应控制权限 同意和拒绝(发送 0x05 指令)
|
||||
void respondPermission(bool bool, String deviceId);
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import'package:maibu_satabot_v2/core/domain/usecases/base_usecase.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> {
|
||||
// 注意:RequestControlPermissionUseCase 和 RequestControlPermissionParams
|
||||
// 已经在专门的文件中定义 (request_control_permission_usecase.dart)
|
||||
|
||||
class RemoteControlUseCase
|
||||
implements BaseUseCase<Map<String, dynamic>, NoParams> {
|
||||
final RemoteControlRepository remoteControlRepository;
|
||||
|
||||
RequestControlPermissionUseCase(this.remoteControlRepository);
|
||||
RemoteControlUseCase(this.remoteControlRepository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> call(RequestControlPermissionParams params) async {
|
||||
Future<Either<Failure, Map<String, dynamic>>> call(NoParams params) async {
|
||||
try {
|
||||
final result = await remoteControlRepository.requestControlPermission(
|
||||
params.deviceName,
|
||||
params.deviceId,
|
||||
);
|
||||
return Right(result);
|
||||
// 实现远程控制用例的具体逻辑
|
||||
// 这里可以添加具体的远程控制逻辑
|
||||
return Right({});
|
||||
} catch (e) {
|
||||
return Left(Failure('请求控制权限失败:$e'));
|
||||
return Left(Failure('远程控制失败:$e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RequestControlPermissionParams {
|
||||
final String deviceName;
|
||||
final String deviceId;
|
||||
|
||||
const RequestControlPermissionParams({
|
||||
required this.deviceName,
|
||||
required this.deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
import 'package:maibu_satabot_v2/core/domain/usecases/base_usecase.dart';
|
||||
|
||||
import '../repositories/remote_control_repository.dart';
|
||||
|
||||
class RequestControlPermissionUseCase extends BaseUseCase<Map<String, dynamic>, RequestControlPermissionParams> {
|
||||
final RemoteControlRepository repository;
|
||||
|
||||
RequestControlPermissionUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> call(RequestControlPermissionParams params) async {
|
||||
try {
|
||||
final result = await repository.requestControlPermission(params.deviceName, params.deviceId);
|
||||
return right(result);
|
||||
} catch (e) {
|
||||
return left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RequestControlPermissionParams {
|
||||
final String deviceName;
|
||||
final String deviceId;
|
||||
|
||||
RequestControlPermissionParams({
|
||||
required this.deviceName,
|
||||
required this.deviceId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
enum ControlMode { TCP, BLE, LOCAL, NONE, OTHER }
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -7,9 +7,12 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
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';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/models/running_status_model.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/request_control_permission_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_state.dart';
|
||||
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
@@ -22,6 +25,7 @@ import '../../domain/usecase/remote_control_usecase.dart';
|
||||
class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
final RemoteControlRepository _repository;
|
||||
final RequestControlPermissionUseCase _requestControlPermissionUseCase;
|
||||
final DeviceRepository _deviceRepository; // 🔥 注入设备仓库
|
||||
Timer? _timer;
|
||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||
final NetMessageDispatcher dispatcher;
|
||||
@@ -35,11 +39,23 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
int _currentOriginX = 0;
|
||||
int _currentOriginY = 0;
|
||||
|
||||
// 🔥 权限请求冷却期机制 - 防止Web端持续发送请求导致弹窗不断显示
|
||||
DateTime? _lastPermissionResponseTime; // 记录上次响应权限请求的时间
|
||||
static const _coolDownDuration = Duration(seconds: 5); // 冷却期5秒
|
||||
|
||||
// 🔥 同步锁 - 防止状态更新期间接收新请求导致重复弹窗
|
||||
bool _isProcessingPermissionRequest = false;
|
||||
|
||||
// 🔥 获取带时间戳的日志前缀
|
||||
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')}]';
|
||||
}
|
||||
|
||||
RemoteControlCubit(
|
||||
this._repository,
|
||||
this._requestControlPermissionUseCase,
|
||||
this._deviceRepository, // 🔥 注入
|
||||
this.dispatcher,
|
||||
this.deviceStatusBloc, // 🔥 注入
|
||||
) : super(
|
||||
@@ -52,54 +68,55 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
_initDeviceStatusListener(); // 🔥 改为订阅 DeviceStatusBloc
|
||||
}
|
||||
|
||||
// 🔥 新增:订阅 DeviceStatusBloc 的状态流,而不是直接监听 TCP
|
||||
// 🔥 新增:订阅 DeviceStatusBloc 的状态流,而不是直接监听TCP
|
||||
void _initDeviceStatusListener() {
|
||||
_logger.logWithLevel('>>> [RemoteControl] begin 订阅 DeviceStatusBloc 状态流');
|
||||
// // _logger.logWithLevel('>>> [RemoteControl] begin 订阅 DeviceStatusBloc 状态流');
|
||||
_deviceStatusSub?.cancel();
|
||||
|
||||
_deviceStatusSub = deviceStatusBloc.stream.listen((deviceState) async {
|
||||
if (deviceState is DeviceStatusUpdated) {
|
||||
final voltage = deviceState.status.voltage;
|
||||
final battery = deviceState.status.battery;
|
||||
final controlMode = deviceState.status.controlMode == '3' ? '远程模式' : '本地模式';
|
||||
final controlMode = deviceState.status.controlMode == '3'
|
||||
? '远程模式'
|
||||
: '本地模式';
|
||||
|
||||
final c = getNetworkDelay();
|
||||
emit(state.copyWith(
|
||||
runningStatusModel: state.runningStatusModel.copyWith(
|
||||
voltage: voltage.toString(),
|
||||
battery: battery.toString(),
|
||||
controlMode: controlMode,
|
||||
emit(
|
||||
state.copyWith(
|
||||
runningStatusModel: state.runningStatusModel.copyWith(
|
||||
voltage: voltage.toString(),
|
||||
battery: battery.toString(),
|
||||
controlMode: controlMode,
|
||||
),
|
||||
battery: int.tryParse(battery) ?? 0,
|
||||
ping: await c,
|
||||
),
|
||||
battery: int.tryParse(battery) ?? 0,
|
||||
ping: await c,
|
||||
));
|
||||
);
|
||||
|
||||
_logger.logWithLevel('✅ [RemoteControl] 从 DeviceStatusBloc 收到更新: 电压=$voltage, 电量=$battery, 模式=$controlMode');
|
||||
// // _logger.logWithLevel('✅ [RemoteControl] 从 DeviceStatusBloc 收到更新: 电压=$voltage, 电量=$battery, 模式=$controlMode');
|
||||
}
|
||||
});
|
||||
|
||||
_logger.logWithLevel('>>> [RemoteControl] ✅ DeviceStatusBloc 订阅已建立完成');
|
||||
// _logger.logWithLevel('>>> [RemoteControl] ✅ DeviceStatusBloc 订阅已建立完成');
|
||||
}
|
||||
|
||||
|
||||
// 🔥 超简单方法:传入 IP,得到 ping 值
|
||||
|
||||
|
||||
|
||||
|
||||
// 🔥 辅助方法:更新运行状态
|
||||
void _updateStatusFromDevice(RunningStatusModel newStatus) {
|
||||
|
||||
debugPrint('✅ [_updateStatusFromDevice] 收到运行状态更新:$newStatus');
|
||||
// // debugPrint('✅ [_updateStatusFromDevice] 收到运行状态更新:$newStatus');
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(
|
||||
runningStatusModel: newStatus,
|
||||
voltage: int.tryParse(newStatus.voltage) ?? 0,
|
||||
battery: int.tryParse(newStatus.battery) ?? 0,
|
||||
));
|
||||
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}%');
|
||||
// debugPrint('✅ [更新运行状态] 电压:${newStatus.voltage}V, 电量:${newStatus.battery}%');
|
||||
// _logger.logWithLevel('✅ [更新运行状态] 电压:${newStatus.voltage}V, 电量:${newStatus.battery}%');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,25 +131,40 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
// });
|
||||
// }
|
||||
Future<void> _initPacketListener() async {
|
||||
//print('>>> [RemoteControl] begin 初始化 0x12 监听器');
|
||||
_logger.logWithLevel('>>> [RemoteControl] begin 0x12 监听器');
|
||||
final logPrefix = '${_getTimePrefix()} 🔍 [RemoteControl] [0x12监听器]';
|
||||
debugPrint('$logPrefix =========================================');
|
||||
debugPrint('$logPrefix 开始初始化 0x12 监听器');
|
||||
_logger.logWithLevel('$logPrefix 开始初始化 0x12 监听器', shouldLog: true);
|
||||
|
||||
_kickOutSub?.cancel(); // 防止重复监听
|
||||
|
||||
_kickOutSub = dispatcher.onCommand(0x12).listen((packet) {
|
||||
//print('>>> [RemoteControl] 收到 0x12 原始包,payload 长度=${packet.payload.length}');
|
||||
_logger.logWithLevel('>>> [RemoteControl] 收到 0x12 原始包,payload 长度=${packet.payload.length}');
|
||||
final timeNow = _getTimePrefix();
|
||||
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ✅ 收到 0x12 原始包');
|
||||
debugPrint(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] payload长度: ${packet.payload.length}',
|
||||
);
|
||||
_logger.logWithLevel(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] 收到 0x12 原始包,payload长度=${packet.payload.length}',
|
||||
shouldLog: true,
|
||||
);
|
||||
|
||||
try {
|
||||
// 🔥 关键:手动去掉最后 2 个 CRC 字节
|
||||
String jsonString;
|
||||
if (packet.payload.length > 2) {
|
||||
jsonString = utf8.decode(packet.payload.sublist(0, packet.payload.length - 2));
|
||||
jsonString = utf8.decode(
|
||||
packet.payload.sublist(0, packet.payload.length - 2),
|
||||
);
|
||||
} else {
|
||||
jsonString = utf8.decode(packet.payload);
|
||||
}
|
||||
|
||||
//print('>>> [RemoteControl] 去除 CRC 后的 JSON: $jsonString');
|
||||
_logger.logWithLevel('>>> [RemoteControl] 去除 CRC 后的 JSON: $jsonString');
|
||||
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] JSON内容: $jsonString');
|
||||
_logger.logWithLevel(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] 去除CRC后的JSON: $jsonString',
|
||||
shouldLog: true,
|
||||
);
|
||||
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
|
||||
@@ -141,69 +173,139 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
final platform = jsonMap['platform'];
|
||||
final respondData = jsonMap['respond'];
|
||||
|
||||
//print('>>> [RemoteControl] 📋 requestType=$requestType, platform=$platform');
|
||||
_logger.logWithLevel('>>> [RemoteControl] 📋 requestType=$requestType, platform=$platform');
|
||||
debugPrint(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] requestType: $requestType, platform: $platform, hasRespond: ${respondData != null}',
|
||||
);
|
||||
|
||||
// 情况 1: 响应格式 - {"respond":{"switchResult":true,"deviceId":"...","holder":"you"}}
|
||||
if (respondData != null && respondData is Map) {
|
||||
final switchResult = respondData['switchResult'];
|
||||
//print('>>> [RemoteControl] 📊 收到切换结果响应:switchResult = $switchResult');
|
||||
_logger.logWithLevel('>>> [RemoteControl] 📊 收到切换结果响应:switchResult = $switchResult');
|
||||
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] 📊 收到切换结果响应');
|
||||
debugPrint(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] switchResult: $switchResult',
|
||||
);
|
||||
_logger.logWithLevel(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] 📊 收到切换结果响应 - switchResult: $switchResult',
|
||||
shouldLog: true,
|
||||
);
|
||||
|
||||
if (!isClosed) {
|
||||
if (switchResult == true) {
|
||||
// 切换成功,当前 APP 失去控制权
|
||||
emit(state.copyWith(hasPermission: true, showPermissionRequestDialog: false));
|
||||
print('>>> [RemoteControl] ✅ 权限已切');
|
||||
// 切换成功,当前 APP 获得控制权
|
||||
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ✅ APP获得控制权');
|
||||
debugPrint(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] 设置 hasPermission = true',
|
||||
);
|
||||
emit(state.copyWith(hasPermission: true));
|
||||
} else {
|
||||
// 切换失败或拒绝,保持当前状态
|
||||
emit(state.copyWith(showPermissionRequestDialog: false));
|
||||
print('>>> [RemoteControl] ❌ 权限切换失败/被拒绝');
|
||||
// 切换失败或拒绝,APP 失去控制权
|
||||
debugPrint('$timeNow 🔍 [RemoteControl] [0x12监听器] ❌ APP失去控制权');
|
||||
debugPrint(
|
||||
'$timeNow 🔍 [RemoteControl] [0x12监听器] 设置 hasPermission = false',
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
hasPermission: false,
|
||||
// 🔥 修复:不强制关闭弹窗,让弹窗由用户操作控制
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 情况 2: 请求格式 - {"request":"switch_control","deviceId":"...","platform":"web",...}
|
||||
else if (requestType == 'switch_control') {
|
||||
final requestDeviceId = jsonMap['deviceId'];
|
||||
final webLogPrefix =
|
||||
'${_getTimePrefix()} 🚨 [RemoteControl] [Web端权限请求]';
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// 🔥 关键判断:只有当是其他平台(web)请求时才弹窗
|
||||
if (platform != null && platform.toString().toLowerCase() != 'app') {
|
||||
//print('>>> [RemoteControl] 🚨 $platform 端请求控制权,打开弹窗询问用户');
|
||||
_logger.logWithLevel('>>> [RemoteControl] 🚨 $platform 端请求控制权,打开弹窗询问用户');
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(showPermissionRequestDialog: true));
|
||||
// 🔥 验证设备ID是否与当前控制的 targetDevice 一致
|
||||
final currentDeviceId = state.targetDevice?.deviceName;
|
||||
debugPrint('$webLogPrefix 当前控制设备ID: $currentDeviceId');
|
||||
|
||||
if (currentDeviceId != null && requestDeviceId == currentDeviceId) {
|
||||
debugPrint('$webLogPrefix 设备ID匹配');
|
||||
// 🔥 添加防重复检查:只有当弹窗还没显示时才弹出
|
||||
if (!state.showPermissionRequestDialog) {
|
||||
debugPrint('$webLogPrefix 弹出权限请求对话框');
|
||||
_logger.logWithLevel(
|
||||
'$webLogPrefix 设备ID匹配,弹出权限请求对话框',
|
||||
shouldLog: true,
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
showPermissionRequestDialog: true,
|
||||
requestingDeviceId: requestDeviceId?.toString(),
|
||||
requestingPlatform: platform.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint('$webLogPrefix ⚠️ 弹窗已显示,忽略重复请求');
|
||||
_logger.logWithLevel(
|
||||
'$webLogPrefix ⚠️ 弹窗已显示,忽略重复请求',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint('$webLogPrefix ⚠️ 设备ID不匹配,忽略');
|
||||
debugPrint(
|
||||
'$webLogPrefix currentDeviceId: $currentDeviceId, requestDeviceId: $requestDeviceId',
|
||||
);
|
||||
_logger.logWithLevel(
|
||||
'$webLogPrefix ⚠️ 设备ID不匹配,忽略请求',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
//print('>>> [RemoteControl] ℹ️ APP 自己的请求回显,忽略不弹窗');
|
||||
_logger.logWithLevel('>>> [RemoteControl] ℹ️ APP 自己的请求回显,忽略不弹窗');
|
||||
debugPrint('$webLogPrefix ℹ️ APP自己的请求回显或platform为空,忽略不弹窗');
|
||||
_logger.logWithLevel(
|
||||
'$webLogPrefix ℹ️ APP自己的请求回显,忽略',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
debugPrint('$webLogPrefix =========================================');
|
||||
}
|
||||
// 情况 3: 异地登录通知 - {"request":"have_logged_in",...}
|
||||
else if (requestType == 'have_logged_in') {
|
||||
//print('>>> [RemoteControl] ⚠️ 检测到异地登录,打开弹窗提示');
|
||||
_logger.logWithLevel('>>> [RemoteControl] ⚠️ 检测到异地登录,打开弹窗提示');
|
||||
debugPrint('${_getTimePrefix()} ⚠️ [RemoteControl] 检测到异地登录');
|
||||
_logger.logWithLevel(
|
||||
'${_getTimePrefix()} ⚠️ [RemoteControl] 检测到异地登录,打开弹窗提示',
|
||||
shouldLog: true,
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(showPermissionRequestDialog: true));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// print('>>> [RemoteControl] ℹ️ 未知类型的 0x12 包,忽略');
|
||||
_logger.logWithLevel('>>> [RemoteControl] ℹ️ 未知类型的 0x12 包,忽略');
|
||||
} else {
|
||||
debugPrint('${_getTimePrefix()} ℹ️ [RemoteControl] 未知类型的 0x12 包,忽略');
|
||||
_logger.logWithLevel(
|
||||
'${_getTimePrefix()} ℹ️ [RemoteControl] 未知类型的 0x12 包,忽略',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
//print('>>> [RemoteControl] ❌ 解析失败:$e');
|
||||
_logger.logWithLevel('>>> [RemoteControl] ❌ 解析失败:$e');
|
||||
debugPrint('${_getTimePrefix()} ❌ [RemoteControl] 解析失败:$e');
|
||||
_logger.logWithLevel(
|
||||
'${_getTimePrefix()} ❌ [RemoteControl] 解析失败:$e',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
});
|
||||
final c = getNetworkDelay();
|
||||
emit(state.copyWith(
|
||||
ping: await c, // 这里直接使用异步返回的数值
|
||||
));
|
||||
//print('>>> [RemoteControl] ✅ 0x12 监听器已建立完成');
|
||||
_logger.logWithLevel('>>> [RemoteControl] ✅ 0x12 监听器已建立完成');
|
||||
}
|
||||
|
||||
|
||||
// 2. 开启 100ms 控制循环 (在进入遥控页面或点击“开始”时调用)
|
||||
/* void startControlLoop() {
|
||||
/* void startControlLoop() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||
// 🔥 关键修复:每次循环都重新读取最新的 state
|
||||
@@ -211,21 +313,27 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
// 🔥 安全检查:无权限或急停时不发送
|
||||
if (!state.hasPermission || state.isEmergency) {
|
||||
debugPrint('⚠️ [定时器] 跳过发送 - hasPermission=${state.hasPermission}, isEmergency=${state.isEmergency}');
|
||||
// debugPrint('⚠️ [定时器] 跳过发送 - hasPermission=${state.hasPermission}, isEmergency=${state.isEmergency}');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
debugPrint('⏰ [定时器] 发送控制指令 - originX=${currentEntity.originX}, originY=${currentEntity.originY}');
|
||||
_logger.logWithLevel('真实的发送的实体 - originX: ${currentEntity.originX}, originY: ${currentEntity.originY}');
|
||||
// debugPrint('⏰ [定时器] 发送控制指令 - originX=${currentEntity.originX}, originY=${currentEntity.originY}');
|
||||
// _logger.logWithLevel('真实的发送的实体 - originX: ${currentEntity.originX}, originY: ${currentEntity.originY}');
|
||||
_repository.sendControlMachineCmd(currentEntity);
|
||||
});
|
||||
emit(state.copyWith(status: RemoteControlStatus.controlling));
|
||||
}*/
|
||||
void startControlLoop() {
|
||||
_timer?.cancel();
|
||||
final logPrefix = '⏰ [RemoteControl] [控制循环]';
|
||||
//debugPrint('$logPrefix =========================================');
|
||||
//debugPrint('$logPrefix 启动控制循环,间隔: 100ms');
|
||||
///debugPrint('$logPrefix =========================================');
|
||||
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||
if (isClosed) {
|
||||
// debugPrint('$logPrefix ❌ Cubit已关闭,取消定时器');
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
@@ -242,25 +350,31 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
// 常规安全检查
|
||||
if (!state.hasPermission) {
|
||||
debugPrint('⚠️ [定时器] 无权限,跳过发送');
|
||||
// debugPrint('$logPrefix ⚠️ 无权限,跳过发送 - hasPermission=false');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isEmergency) {
|
||||
debugPrint('🚨 [定时器] 急停状态,跳过发送');
|
||||
// debugPrint('$logPrefix 🚨 急停状态,跳过发送 - isEmergency=true');
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.logWithLevel('[定时器] 发送控制指令 - originX=${snapshot.originX}, originY=${snapshot.originY}');
|
||||
debugPrint('📤 [定时器] 准备发送 - originX=${snapshot.originX}, originY=${snapshot.originY}');
|
||||
// 发送控制指令
|
||||
//debugPrint(
|
||||
// '$logPrefix 📤 发送控制指令: originX=${snapshot.originX}, originY=${snapshot.originY}, mower=${snapshot.mowerSpeed}, lift=${snapshot.chassisLift}, ignition=${snapshot.ignitionStatus}, emergency=${snapshot.isEmergency}',
|
||||
// );
|
||||
_repository.sendControlMachineCmd(snapshot);
|
||||
//debugPrint('$logPrefix ✅ 控制指令已发送');
|
||||
});
|
||||
}
|
||||
//通过方法拿最新 state
|
||||
|
||||
//通过方法拿最新 state
|
||||
MachineControlStatusEntity _getCurrentControlEntity() {
|
||||
// 🔥 关键修复:必须copyWith创建新对象,避免引用竞态条件
|
||||
final entity = state.controlEntity;
|
||||
_logger.logWithLevel('真实的发送的实体 - originX: ${entity.originX}, originY: ${entity.originY}');
|
||||
// _logger.logWithLevel(
|
||||
// '真实的发送的实体 - originX: ${entity.originX}, originY: ${entity.originY}',
|
||||
// );
|
||||
return entity.copyWith(); // 返回副本,不是引用
|
||||
}
|
||||
|
||||
@@ -275,8 +389,10 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
// 4. 更新功能开关 (比如割刀速度、灯光、点火等)
|
||||
void updateFunction({int? mower, int? lift, int? ignition, bool? emergency}) {
|
||||
// debugPrint('🔧 [updateFunction] 调用 - mower: $mower, lift: $lift, ignition: $ignition, emergency: $emergency');
|
||||
_logger.logWithLevel('🔧 [updateFunction] 调用 - mower: $mower, lift: $lift, ignition: $ignition, emergency: $emergency');
|
||||
// debugPrint('🔧 [updateFunction] 调用 - mower: $mower, lift: $lift, ignition: $ignition, emergency: $emergency');
|
||||
// _logger.logWithLevel(
|
||||
// '🔧 [updateFunction] 调用 - mower: $mower, lift: $lift, ignition: $ignition, emergency: $emergency',
|
||||
// );
|
||||
final updatedEntity = state.controlEntity.copyWith(
|
||||
mower: mower,
|
||||
lift: lift,
|
||||
@@ -284,16 +400,18 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
emergency: emergency,
|
||||
);
|
||||
//emit(state.copyWith(controlEntity: updatedEntity));
|
||||
emit(state.copyWith(
|
||||
controlEntity: updatedEntity,
|
||||
isEmergency: emergency ?? state.isEmergency,
|
||||
));
|
||||
debugPrint('✅ [updateFunction] 状态已更新并 emit');
|
||||
_logger.logWithLevel('✅ [updateFunction] 状态已更新并 emit');
|
||||
emit(
|
||||
state.copyWith(
|
||||
controlEntity: updatedEntity,
|
||||
isEmergency: emergency ?? state.isEmergency,
|
||||
),
|
||||
);
|
||||
// debugPrint('>>> [updateFunction] 状态已更新到emit');
|
||||
// _logger.logWithLevel('>>> [updateFunction] 状态已更新到emit');
|
||||
}
|
||||
|
||||
void updateOriginY(int y) {
|
||||
debugPrint('📥 [updateOriginY] 被调用 - y=$y');
|
||||
// debugPrint('📥 [updateOriginY] 被调用- y=$y');
|
||||
|
||||
// 🔥 参考Android版:直接更新成员变量
|
||||
_currentOriginY = y;
|
||||
@@ -301,11 +419,11 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
// 同时更新state(用于UI显示)
|
||||
final updatedEntity = state.controlEntity.copyWith(y: y);
|
||||
emit(state.copyWith(controlEntity: updatedEntity));
|
||||
debugPrint('📝 [updateOriginY] state已更新 - originY=$y');
|
||||
// debugPrint('📝 [updateOriginY] state已更新- originY=$y');
|
||||
}
|
||||
|
||||
void updateOriginX(int x) {
|
||||
debugPrint('📥 [updateOriginX] 被调用 - x=$x');
|
||||
// debugPrint('📥 [updateOriginX] 被调用- x=$x');
|
||||
|
||||
// 🔥 参考Android版:直接更新成员变量
|
||||
_currentOriginX = x;
|
||||
@@ -313,12 +431,14 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
// 同时更新state(用于UI显示)
|
||||
final updatedEntity = state.controlEntity.copyWith(x: x);
|
||||
emit(state.copyWith(controlEntity: updatedEntity));
|
||||
debugPrint('📝 [updateOriginX] state已更新 - originX=$x');
|
||||
// debugPrint('📝 [updateOriginX] state已更新- originX=$x');
|
||||
}
|
||||
|
||||
/// 🔥 安全方法:同时清零双轴,确保只发送一次完全停止指令
|
||||
Future<void> stopAllMovement() async {
|
||||
debugPrint('🛑 [stopAllMovement] 开始执行 - 当前成员变量: originX=$_currentOriginX, originY=$_currentOriginY');
|
||||
// debugPrint(
|
||||
// '🛑 [stopAllMovement] 开始执行- 当前成员变量: originX=$_currentOriginX, originY=$_currentOriginY',
|
||||
// );
|
||||
|
||||
// 🔥 参考Android版:直接清零成员变量
|
||||
_currentOriginX = 0;
|
||||
@@ -326,7 +446,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
// 🔥 关键修复:先暂停定时器,防止定时器在停止期间发送旧的运动指令
|
||||
_timer?.cancel();
|
||||
_timer = null; // 🔥 彻底清空,防止重复启动
|
||||
_timer = null; // 🔥 彻底清空,防止重复启用
|
||||
|
||||
final updatedEntity = state.controlEntity.copyWith(x: 0, y: 0);
|
||||
|
||||
@@ -334,20 +454,24 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
emit(state.copyWith(controlEntity: updatedEntity));
|
||||
|
||||
// 🔥 安全底线:停止指令必须无视权限强制发送!
|
||||
debugPrint('🛑 [stopAllMovement] 双轴归零,发送停止指令 - originX=0, originY=0');
|
||||
await _sendStopCommandRepeatedly(updatedEntity); // ✅ 等待所有指令发送完成
|
||||
// debugPrint('🛑 [stopAllMovement] 双轴归零,发送停止指令- originX=0, originY=0');
|
||||
await _sendStopCommandRepeatedly(updatedEntity); // 等待所有指令发送完成
|
||||
|
||||
// 🔥 恢复定时器
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
startControlLoop();
|
||||
|
||||
debugPrint('✅ [stopAllMovement] 执行完成');
|
||||
// debugPrint('>>> [stopAllMovement] 执行完成');
|
||||
}
|
||||
|
||||
/// 🔥 统一方法:连续发送30次停止指令,彻底清空TCP缓冲区
|
||||
Future<void> _sendStopCommandRepeatedly(MachineControlStatusEntity stopEntity) async {
|
||||
debugPrint('🛑 [紧急停止] 开始连续发送30次停止指令 - originX=${stopEntity.originX}, originY=${stopEntity.originY}');
|
||||
// 🔥 注意:_lastStopTime 已经在 stopAllMovement() 中设置了,这里不需要再设置
|
||||
/// 🔥 统一方法:连续发送10次停止指令,彻底清空TCP缓冲区
|
||||
Future<void> _sendStopCommandRepeatedly(
|
||||
MachineControlStatusEntity stopEntity,
|
||||
) async {
|
||||
// debugPrint(
|
||||
// '🛑 [紧急停止] 开始连续发送10次停止指令- originX=${stopEntity.originX}, originY=${stopEntity.originY}',
|
||||
// );
|
||||
// 🔥 注意:_lastStopTime 已经在stopAllMovement() 中设置了,这里不需要再设置
|
||||
|
||||
int sentCount = 0;
|
||||
// 🔥 关键修复:每次发送间隔5ms,避免TCP合并/丢弃
|
||||
@@ -355,14 +479,14 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
_repository.sendControlMachineCmd(stopEntity);
|
||||
sentCount++;
|
||||
if (i % 5 == 0) {
|
||||
debugPrint('🛑 [紧急停止] 已发送第${i + 1}次');
|
||||
// debugPrint('🛑 [紧急停止] 已发送第${i + 1}次');
|
||||
}
|
||||
if (i < 19) {
|
||||
await Future.delayed(const Duration(milliseconds: 5));
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 延迟后再发送10次(双重保险,对抗网络抖动)
|
||||
// 🔥 延迟后再发10次(双重保险,对抗网络抖动)
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
for (int i = 0; i < 10; i++) {
|
||||
_repository.sendControlMachineCmd(stopEntity);
|
||||
@@ -372,7 +496,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ [紧急停止] 所有30次停止指令已发出');
|
||||
// debugPrint('>>> [紧急停止] 所有20次停止指令已发出');
|
||||
}
|
||||
|
||||
// 5. 停止控制循环
|
||||
@@ -396,7 +520,8 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
}
|
||||
|
||||
void toggleLeftPip() => emit(state.copyWith(showLeftPip: !state.showLeftPip));
|
||||
void toggleRightPip() => emit(state.copyWith(showRightPip: !state.showRightPip));
|
||||
void toggleRightPip() =>
|
||||
emit(state.copyWith(showRightPip: !state.showRightPip));
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
@@ -408,34 +533,85 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
void updateChassisLift(int i) {}
|
||||
|
||||
void updateEmergency(bool bool) {
|
||||
debugPrint('🚨 [急停] ${bool ? "触发急停!" : "解除急停"}');
|
||||
// debugPrint('🚨 [急停] ${bool ? "触发急停!" : "解除急停"}');
|
||||
updateFunction(emergency: bool);
|
||||
}
|
||||
|
||||
void respondPermission(bool bool, String deviceId) {
|
||||
void respondPermission(bool agreed, String deviceId) {
|
||||
|
||||
|
||||
|
||||
// 🔥 记录响应时间,开启冷却期
|
||||
_lastPermissionResponseTime = DateTime.now();
|
||||
//debugPrint('$logPrefix 开启权限请求冷却期,持续${_coolDownDuration.inSeconds}秒');
|
||||
|
||||
// 1. 关闭弹窗(立即关闭,防止重复点击)
|
||||
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));
|
||||
// 2. 发送响应到服务器
|
||||
//debugPrint('$logPrefix 📤 发送权限响应命令到服务器');
|
||||
try {
|
||||
// 🔥 关键调用:发送 TCP 指令
|
||||
debugPrint('{_coolDownDuration.inSeconds}秒');
|
||||
_repository.respondPermission(agreed, deviceId);
|
||||
} catch (e) {
|
||||
debugPrint(' ❌ TCP权限响应指令发送失败: $e');
|
||||
debugPrint(' ❌ 错误类型: ${e.runtimeType}');
|
||||
_logger.logWithLevel(' TCP权限响应指令发送失败 $e', shouldLog: true);
|
||||
// 即使发送失败,也要更新状态
|
||||
rethrow;
|
||||
}
|
||||
|
||||
// 3. 根据用户选择更新控制状态
|
||||
if (agreed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
hasPermission: false,
|
||||
showPermissionRequestDialog: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 用户拒绝 APP 继续保持控制权
|
||||
// 🔥 必须同时设置 showPermissionRequestDialog: false,防止状态回退
|
||||
emit(
|
||||
state.copyWith(hasPermission: true, showPermissionRequestDialog: false),
|
||||
);
|
||||
}
|
||||
debugPrint(
|
||||
'${_getTimePrefix()} ====权限弹窗响应结束=====',
|
||||
);
|
||||
}
|
||||
|
||||
void requestControlPermissionS(String deviceName, String deviceId) async {
|
||||
// 1. 关闭弹窗
|
||||
emit(state.copyWith(showPermissionRequestDialog: false));
|
||||
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,
|
||||
);
|
||||
|
||||
// 2. 调用 UseCase
|
||||
// 1. 只有当弹窗不是因Web 端请求权限而显示时,才关闭弹窗
|
||||
// 避免 Web 端请求权限的弹窗被自动关闭(一闪而过的问题)
|
||||
if (state.requestingPlatform == null) {
|
||||
debugPrint('$logPrefix 关闭权限请求弹窗 (非Web端触发)');
|
||||
emit(state.copyWith(showPermissionRequestDialog: false));
|
||||
} else {
|
||||
debugPrint('$logPrefix 保留弹窗 (Web端请求触发)');
|
||||
}
|
||||
|
||||
// 2. 调用 UseCase 获取 HTTP 返回的完整权限信息
|
||||
debugPrint('$logPrefix 调用 HTTP 接口查询权限状态..');
|
||||
final result = await _requestControlPermissionUseCase(
|
||||
RequestControlPermissionParams(
|
||||
deviceName: deviceName,
|
||||
@@ -445,51 +621,158 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
// 3. 处理结果
|
||||
result.fold(
|
||||
(failure) {
|
||||
//print('❌ [RemoteControl] 请求控制权限失败:${failure.message}');
|
||||
_logger.logWithLevel('❌ [RemoteControl] 请求控制权限失败:${failure.message}');
|
||||
// 可以在这里显示错误提示或重新打开弹窗
|
||||
(failure) {
|
||||
final failLogPrefix =
|
||||
'${_getTimePrefix()} 🔑 [RemoteControl] [请求权限接口-$source]';
|
||||
debugPrint('$failLogPrefix APP请求控制权限失败: ${failure.message}');
|
||||
debugPrint('$failLogPrefix 重新打开权限请求弹窗');
|
||||
_logger.logWithLevel(
|
||||
'$failLogPrefix APP请求控制权限失败: ${failure.message}',
|
||||
shouldLog: true,
|
||||
);
|
||||
emit(state.copyWith(showPermissionRequestDialog: true));
|
||||
|
||||
},
|
||||
(success) {
|
||||
// 更新状态
|
||||
//print('✅ [RemoteControl] 请求控制权限成功:$success');
|
||||
_logger.logWithLevel('✅ [RemoteControl] 请求控制权限成功:$success',shouldLog: true);
|
||||
///处理result
|
||||
if(success){
|
||||
emit(state.copyWith(hasPermission: true));
|
||||
}else{
|
||||
emit(state.copyWith(hasPermission: false));
|
||||
}
|
||||
print('✅ c:$success');
|
||||
// 权限申请已发送,等待 0x12 回包更新状态
|
||||
(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] =========================================',
|
||||
);
|
||||
}
|
||||
/// 发送底盘指令
|
||||
|
||||
/// 🔥 权限弹窗确认后调用- 发送TCP响应 + HTTP确认最终权限状态
|
||||
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');
|
||||
respondPermission(agreed, deviceName);
|
||||
|
||||
// 2. 调用 HTTP 接口获取最终权限状态
|
||||
debugPrint('$logPrefix 📡 调用 HTTP 接口确认最终权限状态..');
|
||||
/* final result = await _requestControlPermissionUseCase(
|
||||
RequestControlPermissionParams(
|
||||
deviceName: deviceName,
|
||||
deviceId: platform,
|
||||
),
|
||||
);*/
|
||||
|
||||
// 3. 根据 HTTP 返回的真实权限状态更新UI
|
||||
/* result.fold(
|
||||
(failure) {
|
||||
final failLogPrefix = '${_getTimePrefix()} 🔑 [RemoteControl] [权限确认]';
|
||||
debugPrint('$failLogPrefix APP HTTP请求失败: ${failure.message}');
|
||||
debugPrint('$failLogPrefix ⚠️ 保持当前状态不变');
|
||||
},
|
||||
(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));
|
||||
debugPrint(
|
||||
'$successLogPrefix 📊 UI已同步完成- 当前 hasPermission=$hasPermission',
|
||||
);
|
||||
},
|
||||
);*/
|
||||
debugPrint(
|
||||
'${_getTimePrefix()} 🔑 [RemoteControl] [权限确认] =========================================',
|
||||
);
|
||||
}
|
||||
|
||||
/// 发送底盘指令
|
||||
void sendChassisCommand(int i) {
|
||||
// debugPrint(' [底盘指令] ${i}');
|
||||
_logger.logWithLevel(' [底盘指令] ${i}');
|
||||
// // debugPrint('>>> [底盘指令] ${i}');
|
||||
// _logger.logWithLevel('>>> [底盘指令] ${i}');
|
||||
updateFunction(lift: i);
|
||||
}
|
||||
/// 发送割刀指令
|
||||
|
||||
/// 发送割刀指令
|
||||
void sendMowerCommand(int i) {
|
||||
// debugPrint(' [割刀指令] ${i}');
|
||||
_logger.logWithLevel(' [割刀指令] ${i}');
|
||||
// // debugPrint('>>> [割刀指令] ${i}');
|
||||
// _logger.logWithLevel('>>> [割刀指令] ${i}');
|
||||
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);
|
||||
debugPrint(' [点火指令] ${i}');
|
||||
_logger.logWithLevel(' [点火指令] ${i}');
|
||||
// updateFunction(mower:0, lift: 0, ignition: i, emergency: false);
|
||||
// debugPrint('>>> [点火指令] ${i}');
|
||||
// _logger.logWithLevel('>>> [点火指令] ${i}');
|
||||
updateFunction(ignition: i);
|
||||
}
|
||||
// 发送障碍物识别指令
|
||||
|
||||
// 发送障碍物识别指令
|
||||
void toggleObstacleRecognition() {
|
||||
emit(state.copyWith(obstacleRecognitionFlag: !state.obstacleRecognitionFlag));
|
||||
emit(
|
||||
state.copyWith(obstacleRecognitionFlag: !state.obstacleRecognitionFlag),
|
||||
);
|
||||
}
|
||||
|
||||
void toggleTopLeftExpand() {
|
||||
@@ -498,14 +781,14 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
Future<int> getNetworkDelay() async {
|
||||
try {
|
||||
// 直接 Ping 你的服务器 IP
|
||||
// 直接 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) {
|
||||
// 返回和 cmd 一样的毫秒值
|
||||
// 返回和cmd 一样的毫秒值
|
||||
return data.response!.time!.inMilliseconds;
|
||||
} else {
|
||||
return 9999;
|
||||
@@ -514,14 +797,38 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
return 9999;
|
||||
}
|
||||
}
|
||||
//app退出远程遥控界面释放权限
|
||||
|
||||
//app退出远程遥控界面释放权限
|
||||
Future<bool> releasePermission(String platform) async {
|
||||
return await _repository.releasePermission(platform);
|
||||
}
|
||||
|
||||
/// 🔥 设置待控制的设备(从机器人列表点击进入时调用)
|
||||
void setTargetDevice(DeviceEntity device) {
|
||||
// debugPrint('🎯 [RemoteControl] 设置待控制设备 ${device.deviceName}');
|
||||
// _logger.logWithLevel('🎯 [RemoteControl] 设置待控制设备 ${device.deviceName}');
|
||||
|
||||
// 🔥 通知后端订阅该设备
|
||||
_deviceRepository.switchDevice("app", device.deviceName).then((result) {
|
||||
result.fold(
|
||||
(failure) {
|
||||
// debugPrint('>>> [RemoteControl] 切换设备失败: ${failure.message}');
|
||||
// _logger.logWithLevel('>>> [RemoteControl] 切换设备失败: ${failure.message}');
|
||||
},
|
||||
(success) {
|
||||
// debugPrint('>>> [RemoteControl] 切换设备成功, code: $success');
|
||||
// _logger.logWithLevel('>>> [RemoteControl] 切换设备成功, code: $success');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
emit(state.copyWith(targetDevice: device));
|
||||
}
|
||||
|
||||
|
||||
/// 🔥 清除待控制设备(退出远程控制页时调用)
|
||||
void clearTargetDevice() {
|
||||
// debugPrint('🧹 [RemoteControl] 清除待控制设备');
|
||||
// _logger.logWithLevel('🧹 [RemoteControl] 清除待控制设备');
|
||||
emit(state.copyWith(targetDevice: null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/models/running_status_model.dart';
|
||||
|
||||
import '../../domain/entities/machine_control_status_entity.dart';
|
||||
|
||||
enum RemoteControlStatus { initial, controlling, error }
|
||||
|
||||
enum ControlMode { TCP, BLE, LOCAL, NONE, OTHER }
|
||||
|
||||
class RemoteControlState extends Equatable {
|
||||
final RemoteControlStatus status;
|
||||
final MachineControlStatusEntity controlEntity; // 控制业务实体
|
||||
final RunningStatusModel runningStatusModel;
|
||||
//状态业务实体
|
||||
final String? errorMessage;
|
||||
final bool hasPermission; // 是否获得了 0x12 权限
|
||||
final bool hasPermission; // 是否获得 0x12 权限
|
||||
final bool isEmergency;
|
||||
final bool isLocked;
|
||||
final int ping;
|
||||
@@ -22,6 +19,8 @@ class RemoteControlState extends Equatable {
|
||||
final String permissionPlatform;
|
||||
final String currentPlatform;
|
||||
final bool showPermissionRequestDialog;
|
||||
final String? requestingDeviceId; // 🔥 请求权限的设备ID
|
||||
final String? requestingPlatform; // 🔥 请求权限的平台
|
||||
final bool showLeftPip;
|
||||
final bool showRightPip;
|
||||
|
||||
@@ -29,6 +28,8 @@ class RemoteControlState extends Equatable {
|
||||
final bool obstacleRecognitionFlag; // 障碍物识别标志位(这是UI显示的)
|
||||
|
||||
final String obstacleFlag; //障碍物标志位
|
||||
final DeviceEntity? targetDevice; // 🔥 待控制的设备
|
||||
final RunningStatusModel runningStatusModel;
|
||||
|
||||
const RemoteControlState({
|
||||
this.status = RemoteControlStatus.initial,
|
||||
@@ -43,15 +44,18 @@ class RemoteControlState extends Equatable {
|
||||
this.permissionPlatform = '',
|
||||
this.currentPlatform = '',
|
||||
this.showPermissionRequestDialog = false,
|
||||
this.requestingDeviceId,
|
||||
this.requestingPlatform,
|
||||
this.showLeftPip = true,
|
||||
this.showRightPip = true,
|
||||
this.obstacleFlag = '',
|
||||
required this.runningStatusModel,
|
||||
this.topRightIsExpanded = false,
|
||||
this.obstacleRecognitionFlag = true,
|
||||
this.targetDevice,
|
||||
});
|
||||
|
||||
// 方便 UI 更新部分属性
|
||||
// 便利 UI 更新部分属性
|
||||
RemoteControlState copyWith({
|
||||
RemoteControlStatus? status,
|
||||
MachineControlStatusEntity? controlEntity,
|
||||
@@ -65,12 +69,15 @@ class RemoteControlState extends Equatable {
|
||||
String? permissionPlatform,
|
||||
String? currentPlatform,
|
||||
bool? showPermissionRequestDialog,
|
||||
String? requestingDeviceId,
|
||||
String? requestingPlatform,
|
||||
bool? showLeftPip,
|
||||
bool? showRightPip,
|
||||
RunningStatusModel? runningStatusModel,
|
||||
String? obstacleFlag,
|
||||
bool? topRightIsExpanded,
|
||||
bool? obstacleRecognitionFlag,
|
||||
DeviceEntity? targetDevice,
|
||||
|
||||
}) {
|
||||
return RemoteControlState(
|
||||
@@ -87,12 +94,15 @@ class RemoteControlState extends Equatable {
|
||||
currentPlatform: currentPlatform ?? this.currentPlatform,
|
||||
showPermissionRequestDialog:
|
||||
showPermissionRequestDialog ?? this.showPermissionRequestDialog,
|
||||
requestingDeviceId: requestingDeviceId ?? this.requestingDeviceId,
|
||||
requestingPlatform: requestingPlatform ?? this.requestingPlatform,
|
||||
showLeftPip: showLeftPip ?? this.showLeftPip,
|
||||
showRightPip: showRightPip ?? this.showRightPip,
|
||||
runningStatusModel: runningStatusModel ?? this.runningStatusModel,
|
||||
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
|
||||
topRightIsExpanded: topRightIsExpanded ?? this.topRightIsExpanded,
|
||||
obstacleRecognitionFlag: obstacleRecognitionFlag ?? this.obstacleRecognitionFlag,
|
||||
targetDevice: targetDevice ?? this.targetDevice,
|
||||
|
||||
);
|
||||
}
|
||||
@@ -111,12 +121,15 @@ class RemoteControlState extends Equatable {
|
||||
permissionPlatform,
|
||||
currentPlatform,
|
||||
showPermissionRequestDialog,
|
||||
requestingDeviceId,
|
||||
requestingPlatform,
|
||||
showLeftPip,
|
||||
showRightPip,
|
||||
runningStatusModel,
|
||||
obstacleFlag,
|
||||
topRightIsExpanded,
|
||||
obstacleRecognitionFlag,
|
||||
targetDevice,
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class RunningStatusModel {
|
||||
final String controlMode;
|
||||
final String voltage;
|
||||
final String deviceName;
|
||||
final String deviceId;
|
||||
|
||||
const RunningStatusModel({
|
||||
this.controlMode = '',
|
||||
this.voltage = '0',
|
||||
this.deviceName = '',
|
||||
this.deviceId = '',
|
||||
});
|
||||
|
||||
RunningStatusModel copyWith({
|
||||
String? controlMode,
|
||||
String? voltage,
|
||||
String? deviceName,
|
||||
String? deviceId,
|
||||
}) {
|
||||
return RunningStatusModel(
|
||||
controlMode: controlMode ?? this.controlMode,
|
||||
voltage: voltage ?? this.voltage,
|
||||
deviceName: deviceName ?? this.deviceName,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/router/route_paths.dart';
|
||||
import '../../../devices/domain/entities/device_entity.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
import '../bloc/remote_control_state.dart';
|
||||
@@ -30,40 +31,79 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
String _videoStreamUrl = "";
|
||||
late RemoteControlCubit _cubit;
|
||||
DevicesCubit? _devicesCubit;
|
||||
StreamSubscription? _permissionSubscription; // 🔥 权限监听订阅
|
||||
bool _isShowingPermissionDialog = false; // 🔥 防止弹窗重复显示
|
||||
bool _isLoadingPermission = true; // 🔥 标记是否正在加载权限状态
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_cubit = context.read<RemoteControlCubit>();
|
||||
_devicesCubit = context.read<DevicesCubit>();
|
||||
|
||||
// 🔥 监听权限弹窗状态变化 (只订阅一次)
|
||||
if (_permissionSubscription == null) {
|
||||
_permissionSubscription = _cubit.stream.listen((state) {
|
||||
// 🔥 加强防重复逻辑:只在状态真正变化且不在显示弹窗时才显示
|
||||
if (mounted &&
|
||||
state.showPermissionRequestDialog &&
|
||||
!_isShowingPermissionDialog) {
|
||||
_isShowingPermissionDialog = true;
|
||||
debugPrint('🔔 [权限弹窗] 检测到 showPermissionRequestDialog=true,准备显示弹窗');
|
||||
// 使用微任务确保标志位已设置
|
||||
Future.microtask(() {
|
||||
_showPermissionDialog(state).then((_) {
|
||||
debugPrint('🔔 [权限弹窗] 弹窗已关闭,重置标志位');
|
||||
_isShowingPermissionDialog = false; // 弹窗关闭后重置标志
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 1. 锁定横屏
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
// 2. 隐藏状态栏和虚拟按键
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final remoteCubit = context.read<RemoteControlCubit>();
|
||||
final devicesCubit = context.read<DevicesCubit>();
|
||||
|
||||
// debugPrint('🔍 [RemoteControl] initState - targetDevice: ${remoteCubit.state.targetDevice}');
|
||||
|
||||
// 🔥 检查 targetDevice 是否存在
|
||||
if (remoteCubit.state.targetDevice == null) {
|
||||
// debugPrint('❌ [RemoteControl] targetDevice 为空,无法进入远程控制');
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 弹窗显示当前控制的设备信息
|
||||
_showTargetDeviceDialog(context, remoteCubit.state.targetDevice!);
|
||||
|
||||
// 🔥 只在控制循环未启动时才启动
|
||||
remoteCubit.startControlLoop();
|
||||
// debugPrint('✅ [RemoteControl] 控制循环已启动');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 释放远程控制权限
|
||||
// _cubit.releasePermission("app");
|
||||
_cubit.releasePermission("app");
|
||||
//print("远程控制要推出啦");
|
||||
final deviceState = _devicesCubit?.state;
|
||||
if (deviceState?.selectedDevice != null) {
|
||||
// _cubit.releasePermission("app");
|
||||
}
|
||||
//final deviceState = _devicesCubit?.state;
|
||||
//if (deviceState?.selectedDevice != null) {
|
||||
// _cubit.releasePermission("app");
|
||||
// }
|
||||
|
||||
_permissionSubscription?.cancel(); // 🔥 取消订阅
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
_cubit.stopControlLoop();
|
||||
@@ -77,12 +117,36 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
final deviceState = context.watch<DevicesCubit>().state;
|
||||
final currentDevice = deviceState.selectedDevice;
|
||||
|
||||
final remoteCubit = context.read<RemoteControlCubit>();
|
||||
final hasPermission = remoteCubit.state.hasPermission;
|
||||
|
||||
///context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
|
||||
|
||||
if (currentDevice == null) {
|
||||
return _buildOfflineScaffold();
|
||||
}
|
||||
context.read<RemoteControlCubit>().requestControlPermissionS(currentDevice!.deviceName, "app");
|
||||
|
||||
// 🔥 只在没有权限时自动请求,避免重复调用
|
||||
if (!hasPermission && _isLoadingPermission) {
|
||||
// 🔥 只使用 targetDevice
|
||||
final deviceName = remoteCubit.state.targetDevice?.deviceName;
|
||||
|
||||
if (deviceName != null && deviceName.isNotEmpty) {
|
||||
// debugPrint('🔑 [RemoteControl] 检测到无权限,自动发送权限请求 - deviceName: $deviceName');
|
||||
setState(() => _isLoadingPermission = false); // 🔥 标记为已请求
|
||||
remoteCubit.requestControlPermissionS(deviceName, "app").then((_) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingPermission = false); // 🔥 请求完成后重置
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// debugPrint('❌ [RemoteControl] 无法发送权限请求 - targetDevice 为空');
|
||||
// debugPrint(' targetDevice: ${remoteCubit.state.targetDevice}');
|
||||
setState(() => _isLoadingPermission = false);
|
||||
}
|
||||
} else {
|
||||
// debugPrint('✅ [RemoteControl] 已有权限或已请求过,跳过');
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
@@ -92,24 +156,50 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
Positioned.fill(
|
||||
child: BlocBuilder<RemoteControlCubit, RemoteControlState>(
|
||||
// 只有当显示隐藏状态改变时才重构,内部的拖拽由组件自身 State 处理,不影响这里
|
||||
buildWhen: (p, c) => p.showLeftPip != c.showLeftPip || p.showRightPip != c.showRightPip,
|
||||
buildWhen: (p, c) =>
|
||||
p.showLeftPip != c.showLeftPip ||
|
||||
p.showRightPip != c.showRightPip,
|
||||
|
||||
builder: (context, state) {
|
||||
final deviceId = context.watch<DevicesCubit>().state.selectedDevice?.deviceName;
|
||||
if (deviceId != null && userState.user != null && userState.user!.token != null) {
|
||||
_videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
|
||||
// 🔥 只从 targetDevice 获取 deviceId
|
||||
final targetDevice = context
|
||||
.watch<RemoteControlCubit>()
|
||||
.state
|
||||
.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
|
||||
// debugPrint('🔍 [WebRTC检查] targetDevice: $targetDevice, deviceId: $deviceId');
|
||||
// debugPrint('🔍 [WebRTC检查] user: ${userState.user != null}, token: ${userState.user?.token != null}');
|
||||
|
||||
if (deviceId != null &&
|
||||
userState.user != null &&
|
||||
userState.user!.token != null) {
|
||||
_videoStreamUrl =
|
||||
"webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
|
||||
// debugPrint('🎬 [WebRTC] URL构建成功: $_videoStreamUrl');
|
||||
} else {
|
||||
_videoStreamUrl = '';
|
||||
// debugPrint('❌ [WebRTC] URL构建失败 - deviceId: $deviceId, hasUser: ${userState.user != null}, hasToken: ${userState.user?.token != null}');
|
||||
}
|
||||
final int originY = context.watch<RemoteControlCubit>().state.controlEntity.originY;
|
||||
debugPrint("${originY},originY");
|
||||
final int originY = context
|
||||
.watch<RemoteControlCubit>()
|
||||
.state
|
||||
.controlEntity
|
||||
.originY;
|
||||
// debugPrint("${originY},originY");
|
||||
return WebRTCLocalPlayer(
|
||||
// 这里的 URL 拼接根据你的后端规则
|
||||
// streamUrl: "webrtc://${TCPConsts.TCP_IP}/live/livestream/${currentDevice.deviceName}?token=${userState.user!.token}",
|
||||
streamUrl: _videoStreamUrl,
|
||||
showLeftPip: state.showLeftPip, // 从 Cubit 状态中读取
|
||||
showRightPip: state.showRightPip, // 从 Cubit 状态中读取
|
||||
isFrontMain: context.watch<RemoteControlCubit>().state.controlEntity.originY >= 0,
|
||||
isFrontMain:
|
||||
context
|
||||
.watch<RemoteControlCubit>()
|
||||
.state
|
||||
.controlEntity
|
||||
.originY >=
|
||||
0,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -118,14 +208,44 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
BlocBuilder<RemoteControlCubit, RemoteControlState>(
|
||||
buildWhen: (p, c) => p.isEmergency != c.isEmergency,
|
||||
builder: (context, state) {
|
||||
return state.isEmergency ? const Positioned.fill(child: EmergencyOverlay()) : const SizedBox.shrink();
|
||||
return state.isEmergency
|
||||
? const Positioned.fill(child: EmergencyOverlay())
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const TopStatusBar(),
|
||||
// 🔥 用 BlocConsumer 包裹,确保 hasPermission 变化时重建
|
||||
BlocConsumer<RemoteControlCubit, RemoteControlState>(
|
||||
listenWhen: (p, c) => p.hasPermission != c.hasPermission,
|
||||
listener: (context, state) {
|
||||
debugPrint(
|
||||
'🔍 [RemoteControlPage] BlocConsumer listener - hasPermission=${state.hasPermission}',
|
||||
);
|
||||
},
|
||||
buildWhen: (p, c) =>
|
||||
p.hasPermission != c.hasPermission ||
|
||||
p.isLocked != c.isLocked ||
|
||||
p.topRightIsExpanded != c.topRightIsExpanded ||
|
||||
p.obstacleRecognitionFlag != c.obstacleRecognitionFlag ||
|
||||
p.showLeftPip != c.showLeftPip ||
|
||||
p.showRightPip != c.showRightPip ||
|
||||
p.ping != c.ping ||
|
||||
p.runningStatusModel.voltage !=
|
||||
c.runningStatusModel.voltage ||
|
||||
p.runningStatusModel.controlMode !=
|
||||
c.runningStatusModel.controlMode ||
|
||||
p.battery != c.battery,
|
||||
builder: (context, state) {
|
||||
// 🔥 调试日志:确认 BlocBuilder 接收到的状态值
|
||||
/* debugPrint(
|
||||
'🔍 [RemoteControlPage] BlocConsumer builder - hasPermission=${state.hasPermission}',
|
||||
);*/
|
||||
return TopStatusBar(remoteState: state);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@@ -133,7 +253,10 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
final double centerAreaWidth = constraints.maxWidth * 0.5;
|
||||
|
||||
// 关键:使用 BlocBuilder 局部刷新摇杆,不要 watch 整个 Page
|
||||
return BlocBuilder<RemoteControlCubit, RemoteControlState>(
|
||||
return BlocBuilder<
|
||||
RemoteControlCubit,
|
||||
RemoteControlState
|
||||
>(
|
||||
// 只有 isLocked 改变时才重构摇杆区域,摇杆坐标改变由内部处理
|
||||
buildWhen: (p, c) => p.isLocked != c.isLocked,
|
||||
builder: (context, remoteState) {
|
||||
@@ -148,7 +271,10 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
width: sideAreaWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LeftJoystickArea(isLocked: remoteState.isLocked, width: sideAreaWidth * 0.45),
|
||||
child: LeftJoystickArea(
|
||||
isLocked: remoteState.isLocked,
|
||||
width: sideAreaWidth * 0.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -157,7 +283,9 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
width: centerAreaWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: CenterControlArea(totalWidth: centerAreaWidth),
|
||||
child: CenterControlArea(
|
||||
totalWidth: centerAreaWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -166,7 +294,10 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
width: sideAreaWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: RightJoystickArea(isLocked: remoteState.isLocked, width: sideAreaWidth * 0.45),
|
||||
child: RightJoystickArea(
|
||||
isLocked: remoteState.isLocked,
|
||||
width: sideAreaWidth * 0.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -180,15 +311,152 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// 权限弹窗
|
||||
BlocBuilder<RemoteControlCubit, RemoteControlState>(
|
||||
buildWhen: (p, c) => p.showPermissionRequestDialog != c.showPermissionRequestDialog,
|
||||
builder: (context, state) {
|
||||
if (state.showPermissionRequestDialog) {
|
||||
return _buildPermissionDialog(context, state);
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 显示权限请求弹窗 (使用 showDialog 替代 Overlay)
|
||||
Future<void> _showPermissionDialog(RemoteControlState state) async {
|
||||
debugPrint('🔔 [权限弹窗] _showPermissionDialog 被调用');
|
||||
|
||||
final requestingDeviceId = state.requestingDeviceId ?? '未知设备';
|
||||
final requestingPlatform = state.requestingPlatform ?? '未知平台';
|
||||
final dialogContent =
|
||||
'$requestingPlatform 端正在请求控制权\n\n'
|
||||
'设备ID: $requestingDeviceId\n\n'
|
||||
'是否同意释放控制权?';
|
||||
|
||||
debugPrint(
|
||||
'🔔 [权限弹窗] 准备显示弹窗 - requestingPlatform=$requestingPlatform, requestingDeviceId=$requestingDeviceId',
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false, // 禁止点击外部关闭
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(
|
||||
dialogContext,
|
||||
).translate('remote_control.permission_request_title'),
|
||||
),
|
||||
content: Text(dialogContent),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// debugPrint('👆 [权限弹窗] 用户点击了拒绝按钮');
|
||||
final deviceId =
|
||||
context
|
||||
.read<DevicesCubit>()
|
||||
.state
|
||||
.selectedDevice
|
||||
?.deviceName ??
|
||||
"";
|
||||
final remoteCubit = context.read<RemoteControlCubit>();
|
||||
|
||||
// 🔥 添加详细状态日志 - 检查为什么TCP指令发不出去
|
||||
// debugPrint('👆 [权限弹窗] ====================状态检查====================');
|
||||
// debugPrint('👆 [权限弹窗] showPermissionRequestDialog: ${remoteCubit.state.showPermissionRequestDialog}');
|
||||
// debugPrint('👆 [权限弹窗] targetDevice: ${remoteCubit.state.targetDevice}');
|
||||
// debugPrint('👆 [权限弹窗] requestingDeviceId: ${remoteCubit.state.requestingDeviceId}');
|
||||
// debugPrint('👆 [权限弹窗] hasPermission: ${remoteCubit.state.hasPermission}');
|
||||
// debugPrint('👆 [权限弹窗] ==============================================');
|
||||
|
||||
/* debugPrint(
|
||||
'📋 [权限弹窗] targetDevice=${remoteCubit.state.targetDevice}',
|
||||
);*/
|
||||
|
||||
// 🔥 先发送 TCP 响应,等完成后再关闭弹窗
|
||||
// debugPrint('🔑 [权限弹窗] 用户拒绝,调用 confirmPermissionResponse');
|
||||
final targetDevice = remoteCubit.state.targetDevice;
|
||||
if (targetDevice != null) {
|
||||
debugPrint(
|
||||
'✅ [权限弹窗] targetDevice 不为空,开始调用 confirmPermissionResponse',
|
||||
);
|
||||
await remoteCubit.confirmPermissionResponse(
|
||||
targetDevice.deviceName,
|
||||
'app',
|
||||
false,
|
||||
);
|
||||
// debugPrint('✅ [权限弹窗] TCP响应已发送,准备关闭弹窗');
|
||||
} else {
|
||||
debugPrint(
|
||||
'❌ [权限弹窗] targetDevice 为空,无法调用 confirmPermissionResponse',
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
|
||||
// 🔥 关闭弹窗(在 TCP 发送完成后)
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
dialogContext,
|
||||
).translate('remote_control.refuse'),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
debugPrint('👆 [权限弹窗] 用户点击了同意按钮');
|
||||
final deviceId =
|
||||
context
|
||||
.read<DevicesCubit>()
|
||||
.state
|
||||
.selectedDevice
|
||||
?.deviceName ??
|
||||
"";
|
||||
final remoteCubit = context.read<RemoteControlCubit>();
|
||||
|
||||
// 🔥 添加详细状态日志 - 检查为什么TCP指令发不出去
|
||||
/* debugPrint(
|
||||
'👆 [权限弹窗] ====================状态检查====================',
|
||||
);
|
||||
debugPrint(
|
||||
'👆 [权限弹窗] showPermissionRequestDialog: ${remoteCubit.state.showPermissionRequestDialog}',
|
||||
);
|
||||
debugPrint(
|
||||
'👆 [权限弹窗] targetDevice: ${remoteCubit.state.targetDevice}',
|
||||
);
|
||||
debugPrint(
|
||||
'👆 [权限弹窗] requestingDeviceId: ${remoteCubit.state.requestingDeviceId}',
|
||||
);
|
||||
debugPrint(
|
||||
'👆 [权限弹窗] hasPermission: ${remoteCubit.state.hasPermission}',
|
||||
);
|
||||
debugPrint(
|
||||
'👆 [权限弹窗] ==============================================',
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'📋 [权限弹窗] targetDevice=${remoteCubit.state.targetDevice}',
|
||||
);*/
|
||||
|
||||
// 🔥 先发送 TCP 响应,等完成后再关闭弹窗
|
||||
debugPrint('🔑 [权限弹窗] 用户同意,调用 confirmPermissionResponse');
|
||||
final targetDevice = remoteCubit.state.targetDevice;
|
||||
if (targetDevice != null) {
|
||||
debugPrint(
|
||||
'✅ [权限弹窗] targetDevice 不为空',
|
||||
);
|
||||
await remoteCubit.confirmPermissionResponse(
|
||||
targetDevice.deviceName,
|
||||
'app',
|
||||
true,
|
||||
);
|
||||
debugPrint('✅ [权限弹窗] TCP响应已发送,准备关闭弹窗');
|
||||
} else {
|
||||
debugPrint(
|
||||
'❌ [权限弹窗] targetDevice 为空,无法调用 confirmPermissionResponse',
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 关闭弹窗(在 TCP 发送完成后)
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
dialogContext,
|
||||
).translate('remote_control.agree'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -204,36 +472,51 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
children: [
|
||||
const Icon(Icons.signal_wifi_off, color: Colors.white, size: 60),
|
||||
const SizedBox(height: 20),
|
||||
Text(AppLocalizations.of(context).translate('remote_control.device_disconnected'), style: const TextStyle(color: Colors.white, fontSize: 18)),
|
||||
Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('remote_control.device_disconnected'),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(onPressed: () => context.pop(), child: Text(AppLocalizations.of(context).translate('remote_control.back'))),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).translate('remote_control.back'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPermissionDialog(BuildContext context, RemoteControlState state) {
|
||||
final deviceId = context.read<DevicesCubit>().state.selectedDevice?.deviceName ?? "";
|
||||
return Container(
|
||||
color: Colors.black54,
|
||||
child: AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).translate('remote_control.permission_request_title')),
|
||||
// content: Text("${state.permissionPlatform}端正请求控制权,同意释放吗?"),
|
||||
content: Text(AppLocalizations.of(context).translate('remote_control.permission_request_content')),
|
||||
/// 🔥 显示当前控制设备的弹窗
|
||||
void _showTargetDeviceDialog(BuildContext context, DeviceEntity device) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('🎮 远程控制'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'设备名称: ${device.deviceName}',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'设备ID: ${device.deviceName}',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => context.read<RemoteControlCubit>().respondPermission(false, deviceId), child: Text(AppLocalizations.of(context).translate('remote_control.refuse'))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<RemoteControlCubit>().respondPermission(true, deviceId);
|
||||
// 🔥 用户同意,延迟跳转到首页
|
||||
// Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
// if (context.mounted) {
|
||||
// context.go(RoutePaths.home);
|
||||
// }
|
||||
// });
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).translate('remote_control.agree')),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,24 +3,44 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/control_mode.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/status_chip.dart';
|
||||
import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../devices/domain/entities/device_entity.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../data/models/running_status_model.dart';
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
|
||||
class TopStatusBar extends StatelessWidget {
|
||||
const TopStatusBar({super.key});
|
||||
/// 🔥 从父组件传入状态,不依赖 context.watch
|
||||
final RemoteControlState remoteState;
|
||||
|
||||
const TopStatusBar({super.key, required this.remoteState});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 监听全局设备状态
|
||||
final deviceState = context.watch<DevicesCubit>().state;
|
||||
final device = deviceState.selectedDevice;
|
||||
// 🔥 添加调试日志:确认接收到的状态值
|
||||
//debugPrint('🔍 [TopStatusBar] build - isLocked=${remoteState.isLocked}');
|
||||
|
||||
// 监听全局设备状态 (DevicesCubit 需要 watch,因为设备选择可能变化)
|
||||
// 🔥 使用 try-catch 保护,防止 BlocProvider 缺失导致整个 widget 树崩溃(白屏)
|
||||
DeviceEntity? device;
|
||||
try {
|
||||
final deviceState = context.watch<DevicesCubit>().state;
|
||||
device = deviceState.selectedDevice;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [TopStatusBar] 读取 DevicesCubit 失败: $e');
|
||||
// DevicesCubit 不可用时使用 targetDevice 作为降级方案
|
||||
try {
|
||||
device = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
} catch (e2) {
|
||||
debugPrint('⚠️ [TopStatusBar] 读取 targetDevice 也失败: $e2');
|
||||
}
|
||||
}
|
||||
var _remoteControlCubit = context.read<RemoteControlCubit>();
|
||||
ControlMode _parseControlMode(String modeString) {
|
||||
switch (modeString.toUpperCase()) {
|
||||
@@ -39,124 +59,171 @@ class TopStatusBar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听局部遥控状态
|
||||
final remoteState = context.watch<RemoteControlCubit>().state;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(1, 12, 1, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// 1. 返回按钮 (对应 SimpleSmallFunctionButton)
|
||||
_buildIconButton("assets/svgs/remote_back.svg", () => context.pop()),
|
||||
const SizedBox(width: 8),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 1. 返回按钮 (对应 SimpleSmallFunctionButton)
|
||||
_buildIconButton("assets/svgs/remote_back.svg", () {
|
||||
// 🔥 兼容两种导航栈:GoRouter 和 Navigator
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
context.pop();
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 2. 控制状态 (对应 StatusChipLeft)
|
||||
StatusChip(
|
||||
text: remoteState.hasPermission
|
||||
? AppLocalizations.of(context).translate('remote_control.controlling')
|
||||
: AppLocalizations.of(context).translate('remote_control.not_controlling'),
|
||||
color: remoteState.hasPermission
|
||||
? const Color(0xFF1DB954)
|
||||
: Colors.red,
|
||||
icon: Icons.eighteen_mp,
|
||||
breathing: !remoteState.hasPermission,
|
||||
onTap: () {
|
||||
if (!remoteState.hasPermission) {
|
||||
// 弹出请求权限对话框逻辑
|
||||
context.read<RemoteControlCubit>().togglePermissionDialog(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 2. 控制状态 (对应 StatusChipLeft)
|
||||
StatusChip(
|
||||
text: remoteState.hasPermission
|
||||
? AppLocalizations.of(
|
||||
context,
|
||||
).translate('remote_control.controlling')
|
||||
: AppLocalizations.of(
|
||||
context,
|
||||
).translate('remote_control.not_controlling'),
|
||||
color: remoteState.hasPermission
|
||||
? const Color(0xFF1DB954)
|
||||
: Colors.red,
|
||||
icon: Icons.eighteen_mp,
|
||||
breathing: !remoteState.hasPermission,
|
||||
onTap: () {
|
||||
if (!remoteState.hasPermission) {
|
||||
// 🔥 点击后重新请求权限,和刚进入页面时的逻辑一致
|
||||
debugPrint('🔑 [TopStatusBar] 👆 用户手动点击“未在控制”,重新请求权限');
|
||||
final targetDevice =
|
||||
_remoteControlCubit.state.targetDevice;
|
||||
if (targetDevice != null) {
|
||||
_remoteControlCubit.requestControlPermissionS(
|
||||
targetDevice.deviceName,
|
||||
'app',
|
||||
source: '手动点击',
|
||||
);
|
||||
} else {
|
||||
debugPrint('❌ [TopStatusBar] targetDevice 为空,无法请求权限');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 3. 锁定状态 (对应 SmallFunctionButton)
|
||||
_buildIconButton(
|
||||
remoteState.isLocked
|
||||
? "assets/svgs/remote_lock.svg"
|
||||
: "assets/svgs/remote_lock_open.svg",
|
||||
() => context.read<RemoteControlCubit>().toggleLock(),
|
||||
isSelected: remoteState.isLocked,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 3. 锁定状态 (对应 SmallFunctionButton)
|
||||
_buildIconButton(
|
||||
remoteState.isLocked
|
||||
? "assets/svgs/remote_lock.svg"
|
||||
: "assets/svgs/remote_lock_open.svg",
|
||||
() => context.read<RemoteControlCubit>().toggleLock(),
|
||||
isSelected: remoteState.isLocked,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 4. 刷新按钮
|
||||
_buildIconButton("assets/svgs/remote_refresh.svg", () {}),
|
||||
// const SizedBox(width: 8),
|
||||
// 5. 火技能按钮 (对应 SmallFunctionButton)
|
||||
//_buildIconButton("assets/svgs/fire.svg", () {}),
|
||||
_buildSliderBox(AppLocalizations.of(context).translate('remote_control.fire_skill'), false,context),
|
||||
// 4. 刷新按钮
|
||||
_buildIconButton("assets/svgs/remote_refresh.svg", () {}),
|
||||
// const SizedBox(width: 8),
|
||||
// 5. 火技能按钮 (对应 SmallFunctionButton)
|
||||
//_buildIconButton("assets/svgs/fire.svg", () {}),
|
||||
_buildSliderBox(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('remote_control.fire_skill'),
|
||||
false,
|
||||
context,
|
||||
),
|
||||
// const Spacer(), // 移除 Spacer,改用 MainAxisAlignment.spaceBetween
|
||||
_buildExpandIconButton(
|
||||
iconPath: "assets/svgs/remote_expand.svg",
|
||||
selectedIconPath: "assets/svgs/remote_unexpand.svg",
|
||||
isSelected: remoteState.topRightIsExpanded,
|
||||
onTap: () {
|
||||
_remoteControlCubit.toggleTopLeftExpand();
|
||||
},
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
_buildExpandIconButton(
|
||||
iconPath: "assets/svgs/remote_expand.svg",
|
||||
selectedIconPath: "assets/svgs/remote_unexpand.svg",
|
||||
isSelected: remoteState.topRightIsExpanded,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleTopLeftExpand();
|
||||
},
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeInOut,
|
||||
child: Row(
|
||||
children: !remoteState.topRightIsExpanded
|
||||
? [
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath:
|
||||
"assets/svgs/remote_recognition_off.svg",
|
||||
selectedIconPath:
|
||||
"assets/svgs/remote_recognition_on.svg",
|
||||
isSelected: remoteState.obstacleRecognitionFlag,
|
||||
onTap: () {
|
||||
_remoteControlCubit
|
||||
.toggleObstacleRecognition();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath:
|
||||
"assets/svgs/remote_video_left_off.svg",
|
||||
selectedIconPath:
|
||||
"assets/svgs/remote_video_left.svg",
|
||||
isSelected: remoteState.showLeftPip,
|
||||
onTap: () {
|
||||
_remoteControlCubit.toggleLeftPip();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath:
|
||||
"assets/svgs/remote_video_right_off.svg",
|
||||
selectedIconPath:
|
||||
"assets/svgs/remote_video_right.svg",
|
||||
isSelected: remoteState.showRightPip,
|
||||
onTap: () {
|
||||
_remoteControlCubit.toggleRightPip();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildControlModeChip(
|
||||
remoteState.runningStatusModel.controlMode,
|
||||
context,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildVoltageChip(
|
||||
remoteState.runningStatusModel.voltage,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildPingChip(remoteState.ping),
|
||||
const SizedBox(width: 8),
|
||||
]
|
||||
: [], // 折叠时数组为空
|
||||
),
|
||||
),
|
||||
// TODO
|
||||
// _buildControlModeChip( _parseControlMode(remoteState.runningStatusModel.controlMode)),
|
||||
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeInOut,
|
||||
child: Row(
|
||||
children: !remoteState.topRightIsExpanded
|
||||
? [
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath: "assets/svgs/remote_recognition_off.svg",
|
||||
selectedIconPath: "assets/svgs/remote_recognition_on.svg",
|
||||
isSelected: remoteState.obstacleRecognitionFlag,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleObstacleRecognition();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath: "assets/svgs/remote_video_left_off.svg",
|
||||
selectedIconPath: "assets/svgs/remote_video_left.svg",
|
||||
isSelected: remoteState.showLeftPip,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleLeftPip();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath: "assets/svgs/remote_video_right_off.svg",
|
||||
selectedIconPath: "assets/svgs/remote_video_right.svg",
|
||||
isSelected: remoteState.showRightPip,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleRightPip();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildControlModeChip(remoteState.runningStatusModel.controlMode, context),
|
||||
const SizedBox(width: 8),
|
||||
_buildVoltageChip(remoteState.runningStatusModel.voltage),
|
||||
const SizedBox(width: 8),
|
||||
_buildPingChip(remoteState.ping),
|
||||
const SizedBox(width: 8),
|
||||
]
|
||||
: [], // 折叠时数组为空
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
//_buildVoltageChip(remoteState.voltage),
|
||||
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
// 5. 信号延迟 (对应 pingStatusChip)
|
||||
//_buildPingChip(remoteState.ping),
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
// 6. 电量 (对应 StatusChipRight)
|
||||
_buildBatteryChip(remoteState.battery),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// TODO
|
||||
// _buildControlModeChip( _parseControlMode(remoteState.runningStatusModel.controlMode)),
|
||||
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
//_buildVoltageChip(remoteState.voltage),
|
||||
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
// 5. 信号延迟 (对应 pingStatusChip)
|
||||
//_buildPingChip(remoteState.ping),
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
// 6. 电量 (对应 StatusChipRight)
|
||||
_buildBatteryChip(remoteState.battery),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -218,12 +285,12 @@ class TopStatusBar extends StatelessWidget {
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF0078D4).withOpacity(0.4),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
BoxShadow(
|
||||
color: const Color(0xFF0078D4).withOpacity(0.4),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
padding: const EdgeInsets.all(7),
|
||||
@@ -250,9 +317,11 @@ class TopStatusBar extends StatelessWidget {
|
||||
|
||||
Widget _buildControlModeChip(String mode, BuildContext context) {
|
||||
String displayText;
|
||||
if(mode == ''){
|
||||
displayText = AppLocalizations.of(context).translate('remote_control.mode_none');
|
||||
}else{
|
||||
if (mode == '') {
|
||||
displayText = AppLocalizations.of(
|
||||
context,
|
||||
).translate('remote_control.mode_none');
|
||||
} else {
|
||||
displayText = mode;
|
||||
}
|
||||
// switch (mode) {
|
||||
@@ -302,10 +371,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
// 选中时为深蓝色,未选中时为半透明灰色
|
||||
color: Colors.grey.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 0.5,
|
||||
),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.3), width: 0.5),
|
||||
boxShadow: null,
|
||||
),
|
||||
padding: const EdgeInsets.all(7),
|
||||
@@ -318,7 +384,6 @@ class TopStatusBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildVoltageChip(String voltage) {
|
||||
return StatusChip(
|
||||
text: "$voltage V",
|
||||
@@ -363,9 +428,9 @@ class TopStatusBar extends StatelessWidget {
|
||||
|
||||
Widget _buildSliderBox(String label, bool isLeft, BuildContext context) {
|
||||
// 1. 核心尺寸调整:足够宽的容器解决拥挤,高度匹配状态栏
|
||||
const double boxWidth = 66; // 水平宽度放大,容纳左右图标
|
||||
const double boxHeight = 32; // 高度和其他按钮保持一致
|
||||
const double iconSize = 20; // 图标尺寸放大,避免过小拥挤
|
||||
const double boxWidth = 66; // 水平宽度放大,容纳左右图标
|
||||
const double boxHeight = 32; // 高度和其他按钮保持一致
|
||||
const double iconSize = 20; // 图标尺寸放大,避免过小拥挤
|
||||
|
||||
return SizedBox(
|
||||
width: boxWidth,
|
||||
@@ -376,26 +441,37 @@ class TopStatusBar extends StatelessWidget {
|
||||
quarterTurns: 1, // 旋转90度(竖向→横向),若方向反了可改为3
|
||||
child: SizedBox(
|
||||
// 旋转后宽高互换,这里给CCExpandSlider足够的显示空间
|
||||
width: boxHeight, // 旋转后对应原高度
|
||||
height: boxWidth, // 旋转后对应原宽度(足够长,不拥挤)
|
||||
width: boxHeight, // 旋转后对应原高度
|
||||
height: boxWidth, // 旋转后对应原宽度(足够长,不拥挤)
|
||||
child: CCExpandSlider(
|
||||
label: " ",
|
||||
// 3. 图标适配旋转后的方向(仍用上下箭头,旋转后变为左右)因为外边旋转了 90度 参数传递也要转换
|
||||
svgStart: "assets/svgs/remote_flame_plus.svg", // 旋转后→左箭头assets/svgs/remote_flame_minus.svg
|
||||
svgStart:
|
||||
"assets/svgs/remote_flame_plus.svg", // 旋转后→左箭头assets/svgs/remote_flame_minus.svg
|
||||
svgCenter: "assets/svgs/remote_flame.svg",
|
||||
svgEnd: "assets/svgs/remote_flame_minus.svg", // 旋转后→右箭头assets/svgs/remote_flame_plus.svg
|
||||
svgEnd:
|
||||
"assets/svgs/remote_flame_minus.svg", // 旋转后→右箭头assets/svgs/remote_flame_plus.svg
|
||||
// 4. 放大图标尺寸,解决拥挤
|
||||
iconSize: iconSize,
|
||||
// 保留原有业务逻辑
|
||||
onStart: () =>_handleSliderAction(label, "end", context),//_handleSliderAction(label, "start", context)
|
||||
onStart: () => _handleSliderAction(
|
||||
label,
|
||||
"end",
|
||||
context,
|
||||
), //_handleSliderAction(label, "start", context)
|
||||
onCenter: () => _handleSliderAction(label, "center", context),
|
||||
onEnd: () => _handleSliderAction(label, "start", context),//_handleSliderAction(label, "end", context)
|
||||
onEnd: () => _handleSliderAction(
|
||||
label,
|
||||
"start",
|
||||
context,
|
||||
), //_handleSliderAction(label, "end", context)
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 新增:统一的业务逻辑分发方法
|
||||
void _handleSliderAction(String label, String action, BuildContext context) {
|
||||
debugPrint('🎯 [Slider 业务] label: $label, action: $action');
|
||||
@@ -412,17 +488,17 @@ class TopStatusBar extends StatelessWidget {
|
||||
case "start":
|
||||
debugPrint('2熄火');
|
||||
// TODO: 发送 🔥点火指令
|
||||
cubit.sendFireCommand(2);
|
||||
cubit.sendFireCommand(2);
|
||||
break;
|
||||
case "center":
|
||||
debugPrint('️0熄火');
|
||||
// TODO: 发送点火停止指令
|
||||
cubit.sendFireCommand(0);
|
||||
cubit.sendFireCommand(0);
|
||||
break;
|
||||
case "end":
|
||||
debugPrint('1点火)');
|
||||
// TODO: 发送熄火后退指令
|
||||
cubit.sendFireCommand(1);
|
||||
cubit.sendFireCommand(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,13 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
||||
}
|
||||
|
||||
Future<void> _initWebRTCConnection() async {
|
||||
// 🔥 安全检查: URL 为空时不初始化
|
||||
if (widget.streamUrl.isEmpty) {
|
||||
debugPrint('⚠️ [WebRTC] streamUrl 为空,跳过初始化');
|
||||
setState(() => _isInitialized = true); // 🔥 标记为已初始化,避免卡loading
|
||||
return;
|
||||
}
|
||||
|
||||
_peerConnection = await createPeerConnection({
|
||||
"sdpSemantics": "unified-plan",
|
||||
"iceServers": [
|
||||
@@ -86,7 +93,15 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
await _peerConnection!.setRemoteDescription(RTCSessionDescription(data['sdp'], 'answer'));
|
||||
final sdp = data['sdp'];
|
||||
|
||||
// 🔥 安全检查: SDP 不能为 null
|
||||
if (sdp != null && sdp is String && sdp.isNotEmpty) {
|
||||
await _peerConnection!.setRemoteDescription(RTCSessionDescription(sdp, 'answer'));
|
||||
debugPrint('✅ [WebRTC] setRemoteDescription 成功');
|
||||
} else {
|
||||
debugPrint('❌ [WebRTC] SDP 为空或无效,跳过 setRemoteDescription');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("信令错误: $e");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
import '../../domain/entities/video_stream_entity.dart';
|
||||
|
||||
abstract class DroneStationDataSource {
|
||||
Future<List<DroneStationEntity>> getDroneStationList(int siteId);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||
import '../datasources/drone_station_datasource.dart';
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
import '../../domain/entities/video_stream_entity.dart';
|
||||
|
||||
class DroneStationDataSourceImpl implements DroneStationDataSource {
|
||||
final Dio dio;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../datasources/drone_station_datasource.dart';
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
import '../../domain/entities/video_stream_entity.dart';
|
||||
import '../../domain/repositories/drone_station_repository.dart';
|
||||
|
||||
class DroneStationRepositoryImpl implements DroneStationRepository {
|
||||
|
||||
@@ -388,33 +388,3 @@ class DroneStationEntity extends Equatable {
|
||||
userId,
|
||||
];
|
||||
}
|
||||
|
||||
/// 视频流实体
|
||||
class VideoStreamEntity extends Equatable {
|
||||
final String sn;
|
||||
final String cameraIndex;
|
||||
final String url;
|
||||
final int expireTs;
|
||||
final String urlType;
|
||||
|
||||
const VideoStreamEntity({
|
||||
required this.sn,
|
||||
required this.cameraIndex,
|
||||
required this.url,
|
||||
required this.expireTs,
|
||||
required this.urlType,
|
||||
});
|
||||
|
||||
factory VideoStreamEntity.fromJson(Map<String, dynamic> json) {
|
||||
return VideoStreamEntity(
|
||||
sn: json['sn'] ?? '',
|
||||
cameraIndex: json['camera_index'] ?? '',
|
||||
url: json['url'] ?? '',
|
||||
expireTs: json['expire_ts'] ?? 0,
|
||||
urlType: json['url_type'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [sn, cameraIndex, url, expireTs, urlType];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// 视频流实体
|
||||
class VideoStreamEntity extends Equatable {
|
||||
final String sn;
|
||||
final String cameraIndex;
|
||||
final String
|
||||
url; // 火山引擎 RTC 鉴权参数(包含 app_id、room_id、token、user_id、expire_time)
|
||||
final String url;
|
||||
final int expireTs;
|
||||
final String urlType; // 标识是 "rtc" 还是其他类型
|
||||
final String urlType;
|
||||
|
||||
const VideoStreamEntity({
|
||||
required this.sn,
|
||||
@@ -21,38 +19,43 @@ class VideoStreamEntity extends Equatable {
|
||||
return VideoStreamEntity(
|
||||
sn: json['sn'] ?? '',
|
||||
cameraIndex: json['camera_index'] ?? '',
|
||||
url: json['url'] ?? '', // 火山引擎 RTC 鉴权参数
|
||||
url: json['url'] ?? '',
|
||||
expireTs: json['expire_ts'] ?? 0,
|
||||
urlType: json['url_type'] ?? 'rtc', // 默认为 rtc 类型
|
||||
urlType: json['url_type'] ?? 'rtc',
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析火山引擎 RTC 参数
|
||||
/// url 格式示例: "app_id=xxx&room_id=xxx&token=xxx&user_id=xxx&expire_time=xxx"
|
||||
Map<String, String> parseRtcParams() {
|
||||
final params = <String, String>{};
|
||||
final pairs = url.split('&');
|
||||
for (final pair in pairs) {
|
||||
final kv = pair.split('=');
|
||||
if (kv.length == 2) {
|
||||
// 对 URL 编码的字符进行解码(如 %2F -> /, %2B -> +)
|
||||
params[kv[0]] = Uri.decodeComponent(kv[1]);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/// 获取火山引擎 App ID(从 URL 中解析)
|
||||
String get appId => parseRtcParams()['app_id'] ?? '';
|
||||
String get appId {
|
||||
final params = parseRtcParams();
|
||||
return params['app_id'] ?? params['appid'] ?? '';
|
||||
}
|
||||
|
||||
/// 获取房间 ID(从 URL 中解析)
|
||||
String get roomId => parseRtcParams()['room_id'] ?? '';
|
||||
String get roomId {
|
||||
final params = parseRtcParams();
|
||||
return params['room_id'] ?? params['roomid'] ?? params['channel'] ?? '';
|
||||
}
|
||||
|
||||
/// 获取 Token(从 URL 中解析)
|
||||
String get token => parseRtcParams()['token'] ?? '';
|
||||
String get token {
|
||||
final params = parseRtcParams();
|
||||
return params['token'] ?? '';
|
||||
}
|
||||
|
||||
/// 获取用户 ID(从 URL 中解析)
|
||||
String get userId => parseRtcParams()['user_id'] ?? '';
|
||||
String get userId {
|
||||
final params = parseRtcParams();
|
||||
return params['user_id'] ?? params['uid'] ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [sn, cameraIndex, url, expireTs, urlType];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../entities/drone_station_entity.dart';
|
||||
import '../entities/video_stream_entity.dart';
|
||||
|
||||
abstract class DroneStationRepository {
|
||||
Future<Either<Failure, List<DroneStationEntity>>> getDroneStationList(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../entities/drone_station_entity.dart';
|
||||
import '../entities/video_stream_entity.dart';
|
||||
import '../repositories/drone_station_repository.dart';
|
||||
|
||||
class GetVideoStreamUseCase {
|
||||
|
||||
@@ -76,7 +76,7 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(VideoStreamError(failure.message)),
|
||||
(videoStream) => emit(VideoStreamLoaded(videoStream)),
|
||||
(videoStream) => emit(VideoStreamLoaded(videoStream, event.cameraPosition)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
import '../../domain/entities/video_stream_entity.dart';
|
||||
|
||||
abstract class DroneStationState extends Equatable {
|
||||
const DroneStationState();
|
||||
@@ -62,11 +63,12 @@ class VideoStreamLoading extends DroneStationState {
|
||||
|
||||
class VideoStreamLoaded extends DroneStationState {
|
||||
final VideoStreamEntity videoStream;
|
||||
final String cameraPosition;
|
||||
|
||||
const VideoStreamLoaded(this.videoStream);
|
||||
const VideoStreamLoaded(this.videoStream, this.cameraPosition);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [videoStream];
|
||||
List<Object?> get props => [videoStream, cameraPosition];
|
||||
}
|
||||
|
||||
class VideoStreamError extends DroneStationState {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../../components/tcp_status_indicator.dart';
|
||||
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../bloc/device_status_bloc.dart';
|
||||
import '../bloc/device_status_event.dart';
|
||||
@@ -14,6 +15,7 @@ import '../widgets/device_item_widget.dart';
|
||||
import '../widgets/drone_station_item_card.dart';
|
||||
import 'robot_list_page.dart';
|
||||
import 'drone_station_detail_page.dart';
|
||||
|
||||
/// 设备状态页面 - 使用 BLoC 模式
|
||||
class DeviceStatusPage extends StatelessWidget {
|
||||
const DeviceStatusPage({super.key});
|
||||
@@ -24,7 +26,8 @@ class DeviceStatusPage extends StatelessWidget {
|
||||
final siteId = selectedSite?.id;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => sl<DeviceStatusBloc>()..add(DeviceStatusLoadData(siteId: siteId)),
|
||||
create: (_) =>
|
||||
sl<DeviceStatusBloc>()..add(DeviceStatusLoadData(siteId: siteId)),
|
||||
child: const DeviceStatusView(),
|
||||
);
|
||||
}
|
||||
@@ -51,9 +54,7 @@ class DeviceStatusView extends StatelessWidget {
|
||||
_buildAppBar(),
|
||||
_buildSearchBar(context),
|
||||
_buildTypeFilterBar(context),
|
||||
Expanded(
|
||||
child: _buildContent(context, state),
|
||||
),
|
||||
Expanded(child: _buildContent(context, state)),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -67,9 +68,9 @@ class DeviceStatusView extends StatelessWidget {
|
||||
return Container(
|
||||
height: 44.0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
const Text(
|
||||
'设备状态',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
@@ -77,11 +78,23 @@ class DeviceStatusView extends StatelessWidget {
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Icon(
|
||||
Icons.more_vert,
|
||||
size: 24,
|
||||
color: Color(0xFF1D2129),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'TCP',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
),
|
||||
child: const TcpStatusIndicator(size: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -94,10 +107,7 @@ class DeviceStatusView extends StatelessWidget {
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索设备名称/编号',
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF86909C),
|
||||
fontSize: 14,
|
||||
),
|
||||
hintStyle: const TextStyle(color: Color(0xFF86909C), fontSize: 14),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF86909C),
|
||||
@@ -120,34 +130,22 @@ class DeviceStatusView extends StatelessWidget {
|
||||
fillColor: const Color(0xFFF2F3F5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
borderSide: const BorderSide(color: Color(0xFFE5E6EB), width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
borderSide: const BorderSide(color: Color(0xFFE5E6EB), width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF165DFF),
|
||||
width: 1,
|
||||
),
|
||||
borderSide: const BorderSide(color: Color(0xFF165DFF), width: 1),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
|
||||
onSubmitted: (value) {
|
||||
// 🔥 点击键盘确定键时触发搜索
|
||||
context.read<DeviceStatusBloc>().add(DeviceStatusSearch(value));
|
||||
@@ -180,7 +178,9 @@ class DeviceStatusView extends StatelessWidget {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<DeviceStatusBloc>().add(DeviceStatusChangeType(type));
|
||||
context.read<DeviceStatusBloc>().add(
|
||||
DeviceStatusChangeType(type),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -190,8 +190,12 @@ class DeviceStatusView extends StatelessWidget {
|
||||
type,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: isSelected ? const Color(0xFF135BFF) : const Color(0xFF666666),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected
|
||||
? const Color(0xFF135BFF)
|
||||
: const Color(0xFF666666),
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -199,12 +203,13 @@ class DeviceStatusView extends StatelessWidget {
|
||||
width: 20,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF0D57FF) : Colors.transparent,
|
||||
color: isSelected
|
||||
? const Color(0xFF0D57FF)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -232,9 +237,7 @@ class DeviceStatusView extends StatelessWidget {
|
||||
Widget _buildDeviceList(BuildContext context, DeviceStatusState state) {
|
||||
if (state is DeviceStatusLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
child: CircularProgressIndicator(color: Color(0xFF165DFF)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,23 +246,18 @@ class DeviceStatusView extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<DeviceStatusBloc>().add(const DeviceStatusLoadData());
|
||||
context.read<DeviceStatusBloc>().add(
|
||||
const DeviceStatusLoadData(),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
@@ -284,8 +282,12 @@ class DeviceStatusView extends StatelessWidget {
|
||||
// 🔥 再根据搜索关键词过滤
|
||||
final filteredDevices = filteredByType.where((device) {
|
||||
if (state.searchKeyword.isEmpty) return true;
|
||||
return device.name.toLowerCase().contains(state.searchKeyword.toLowerCase()) ||
|
||||
device.deviceId.toLowerCase().contains(state.searchKeyword.toLowerCase());
|
||||
return device.name.toLowerCase().contains(
|
||||
state.searchKeyword.toLowerCase(),
|
||||
) ||
|
||||
device.deviceId.toLowerCase().contains(
|
||||
state.searchKeyword.toLowerCase(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return RefreshIndicator(
|
||||
@@ -334,18 +336,11 @@ class DeviceStatusView extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_off,
|
||||
size: 48,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
const Icon(Icons.location_off, size: 48, color: Color(0xFF86909C)),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'请先选择场站',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -353,14 +348,13 @@ class DeviceStatusView extends StatelessWidget {
|
||||
}
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => sl<DroneStationBloc>()..add(DroneStationLoadData(selectedSite.id)),
|
||||
create: (_) =>
|
||||
sl<DroneStationBloc>()..add(DroneStationLoadData(selectedSite.id)),
|
||||
child: BlocBuilder<DroneStationBloc, DroneStationState>(
|
||||
builder: (context, state) {
|
||||
if (state is DroneStationLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
child: CircularProgressIndicator(color: Color(0xFF165DFF)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -385,7 +379,9 @@ class DeviceStatusView extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<DroneStationBloc>().add(DroneStationLoadData(selectedSite.id));
|
||||
context.read<DroneStationBloc>().add(
|
||||
DroneStationLoadData(selectedSite.id),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
@@ -405,17 +401,16 @@ class DeviceStatusView extends StatelessWidget {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'该场站暂无无人机机场',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<DroneStationBloc>().add(DroneStationRefresh(selectedSite.id));
|
||||
context.read<DroneStationBloc>().add(
|
||||
DroneStationRefresh(selectedSite.id),
|
||||
);
|
||||
},
|
||||
color: const Color(0xFF165DFF),
|
||||
child: ListView.builder(
|
||||
@@ -429,7 +424,8 @@ class DeviceStatusView extends StatelessWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DroneStationDetailPage(station: station),
|
||||
builder: (context) =>
|
||||
DroneStationDetailPage(station: station),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -468,33 +464,21 @@ class DeviceStatusView extends StatelessWidget {
|
||||
dotColor: const Color(0xFF86909C),
|
||||
textColor: const Color(0xFF86909C), // 新增字体颜色
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFE5E6EB),
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatItem(
|
||||
value: state.deviceStatus.online.toString(),
|
||||
label: '在线',
|
||||
dotColor: const Color(0xFF00B42A),
|
||||
textColor: const Color(0xFF00B42A),
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFE5E6EB),
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatItem(
|
||||
value: state.deviceStatus.exception.toString(),
|
||||
label: '告警',
|
||||
dotColor: const Color(0xFFFF7D00),
|
||||
textColor: const Color(0xFFFF7D00),
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFE5E6EB),
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatItem(
|
||||
value: state.deviceStatus.offline.toString(),
|
||||
label: '离线',
|
||||
@@ -549,5 +533,4 @@ class DeviceStatusView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:volc_engine_rtc/volc_engine_rtc.dart' as volc;
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart' as agora;
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../domain/entities/drone_station_entity.dart';
|
||||
import '../../domain/entities/video_stream_entity.dart';
|
||||
import '../bloc/drone_station_bloc.dart';
|
||||
import '../bloc/drone_station_event.dart';
|
||||
import '../bloc/drone_station_state.dart';
|
||||
@@ -9,6 +12,9 @@ import 'drone_video_control_page.dart';
|
||||
import 'drone_mission_control_page.dart';
|
||||
import 'drone_monitor_page.dart';
|
||||
|
||||
// SDK 类型枚举
|
||||
enum RtcSdkType { volcengine, agora }
|
||||
|
||||
class DroneStationDetailPage extends StatefulWidget {
|
||||
final DroneStationEntity station;
|
||||
|
||||
@@ -26,6 +32,21 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
bool isFloatingIndoor = true;
|
||||
Offset floatingPosition = const Offset(20, 200);
|
||||
|
||||
// 视频流状态
|
||||
VideoStreamEntity? _floatingVideoStream;
|
||||
bool _isFloatingLoading = false;
|
||||
String? _floatingErrorMessage;
|
||||
String? _floatingRemoteUserId;
|
||||
bool _isFloatingAgora = false;
|
||||
|
||||
// 火山引擎 RTC
|
||||
volc.RTCEngine? _floatingRtcEngine;
|
||||
volc.RTCRoom? _floatingRtcRoom;
|
||||
volc.RTCViewContext? _floatingRemoteRenderContext;
|
||||
|
||||
// Agora RTC
|
||||
agora.RtcEngine? _floatingAgoraEngine;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -41,9 +62,42 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
_destroyFloatingRtcEngine();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 销毁悬浮窗的 RTC 引擎
|
||||
void _destroyFloatingRtcEngine() async {
|
||||
// 销毁火山引擎 RTC
|
||||
if (_floatingRtcRoom != null) {
|
||||
try {
|
||||
await _floatingRtcRoom?.leaveRoom();
|
||||
} catch (_) {}
|
||||
_floatingRtcRoom = null;
|
||||
}
|
||||
if (_floatingRtcEngine != null) {
|
||||
try {
|
||||
_floatingRtcEngine?.destroy();
|
||||
} catch (_) {}
|
||||
_floatingRtcEngine = null;
|
||||
}
|
||||
|
||||
// 销毁 Agora RTC
|
||||
if (_floatingAgoraEngine != null) {
|
||||
try {
|
||||
await _floatingAgoraEngine?.leaveChannel();
|
||||
} catch (_) {}
|
||||
try {
|
||||
_floatingAgoraEngine?.release();
|
||||
} catch (_) {}
|
||||
_floatingAgoraEngine = null;
|
||||
}
|
||||
|
||||
_floatingRemoteRenderContext = null;
|
||||
_floatingRemoteUserId = null;
|
||||
_isFloatingAgora = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
@@ -67,7 +121,21 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: BlocBuilder<DroneStationBloc, DroneStationState>(
|
||||
body: BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
// 监听视频流加载状态
|
||||
if (state is VideoStreamLoaded) {
|
||||
setState(() {
|
||||
_floatingVideoStream = state.videoStream;
|
||||
});
|
||||
_initFloatingRtcEngine();
|
||||
} else if (state is VideoStreamError) {
|
||||
setState(() {
|
||||
_floatingErrorMessage = state.message;
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is UAVDetailLoading) {
|
||||
return const Center(
|
||||
@@ -449,7 +517,14 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
icon: Icons.task,
|
||||
label: '任务下发',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DroneMissionControlPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -517,7 +592,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
setState(() => showFloatingMonitor = true);
|
||||
_loadFloatingVideoStream();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -639,7 +714,10 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => isFloatingIndoor = false),
|
||||
onTap: () {
|
||||
setState(() => isFloatingIndoor = false);
|
||||
_loadFloatingVideoStream();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
@@ -665,7 +743,10 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
),
|
||||
// 关闭按钮
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => showFloatingMonitor = false),
|
||||
onTap: () {
|
||||
_destroyFloatingRtcEngine();
|
||||
setState(() => showFloatingMonitor = false);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
@@ -775,6 +856,321 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 加载悬浮视频流
|
||||
void _loadFloatingVideoStream() {
|
||||
setState(() {
|
||||
_isFloatingLoading = true;
|
||||
_floatingErrorMessage = null;
|
||||
showFloatingMonitor = true;
|
||||
});
|
||||
|
||||
_destroyFloatingRtcEngine();
|
||||
|
||||
_bloc.add(
|
||||
VideoStreamLoad(
|
||||
sn: widget.station.gatewaySn,
|
||||
cameraIndex: '165-0-7',
|
||||
cameraPosition: isFloatingIndoor ? 'indoor' : 'outdoor',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化悬浮窗的 RTC 引擎
|
||||
Future<void> _initFloatingRtcEngine() async {
|
||||
if (_floatingVideoStream == null) return;
|
||||
|
||||
final appId = _floatingVideoStream!.appId;
|
||||
final roomId = _floatingVideoStream!.roomId;
|
||||
final token = _floatingVideoStream!.token;
|
||||
final userId = _floatingVideoStream!.userId.isNotEmpty
|
||||
? _floatingVideoStream!.userId
|
||||
: 'user_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
if (appId.isEmpty || roomId.isEmpty || token.isEmpty) {
|
||||
setState(() {
|
||||
_floatingErrorMessage = 'RTC 参数缺失';
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('=== 悬浮窗 RTC 初始化 ===');
|
||||
debugPrint('AppId: $appId');
|
||||
debugPrint('RoomId: $roomId');
|
||||
debugPrint('UserId: $userId');
|
||||
debugPrint('URL Type: ${_floatingVideoStream!.urlType}');
|
||||
|
||||
final sdkType = _floatingVideoStream!.urlType.toLowerCase() == 'agora'
|
||||
? RtcSdkType.agora
|
||||
: RtcSdkType.volcengine;
|
||||
|
||||
if (sdkType == RtcSdkType.agora) {
|
||||
await _initFloatingAgoraEngine(appId, roomId, token, userId);
|
||||
} else {
|
||||
await _initFloatingVolcEngine(appId, roomId, token, userId);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化悬浮窗的 Agora 引擎
|
||||
Future<void> _initFloatingAgoraEngine(
|
||||
String appId,
|
||||
String channelId,
|
||||
String token,
|
||||
String userId,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('=== Agora 悬浮窗初始化 ===');
|
||||
_floatingAgoraEngine = agora.createAgoraRtcEngine();
|
||||
await _floatingAgoraEngine!.initialize(
|
||||
agora.RtcEngineContext(appId: appId),
|
||||
);
|
||||
debugPrint('Agora 引擎初始化成功');
|
||||
|
||||
// 启用视频模块
|
||||
_floatingAgoraEngine!.enableVideo();
|
||||
debugPrint('Agora 视频模块已启用');
|
||||
|
||||
_floatingAgoraEngine!.registerEventHandler(
|
||||
agora.RtcEngineEventHandler(
|
||||
onJoinChannelSuccess: (agora.RtcConnection connection, int elapsed) {
|
||||
debugPrint('✅ 悬浮窗 Agora 加入频道成功: ${connection.channelId}');
|
||||
},
|
||||
onUserJoined: (agora.RtcConnection connection, int uid, int elapsed) {
|
||||
debugPrint('✅ 悬浮窗 Agora 用户加入: uid=$uid');
|
||||
setState(() {
|
||||
_floatingRemoteUserId = uid.toString();
|
||||
_isFloatingAgora = true;
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
},
|
||||
onUserOffline:
|
||||
(
|
||||
agora.RtcConnection connection,
|
||||
int uid,
|
||||
agora.UserOfflineReasonType reason,
|
||||
) {
|
||||
debugPrint('悬浮窗 Agora 用户离开: $uid');
|
||||
if (uid.toString() == _floatingRemoteUserId) {
|
||||
setState(() {
|
||||
_floatingRemoteUserId = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (agora.ErrorCodeType err, String msg) {
|
||||
debugPrint('❌ 悬浮窗 Agora 错误: $err - $msg');
|
||||
setState(() {
|
||||
_floatingErrorMessage = 'Agora RTC 错误:$err';
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await _floatingAgoraEngine!.joinChannel(
|
||||
token: token,
|
||||
channelId: channelId,
|
||||
uid: int.tryParse(userId) ?? 0,
|
||||
options: agora.ChannelMediaOptions(
|
||||
channelProfile:
|
||||
agora.ChannelProfileType.channelProfileLiveBroadcasting,
|
||||
clientRoleType: agora.ClientRoleType.clientRoleAudience,
|
||||
autoSubscribeVideo: true,
|
||||
autoSubscribeAudio: false,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ 悬浮窗 Agora RTC 初始化失败: $e');
|
||||
setState(() {
|
||||
_floatingErrorMessage = 'Agora RTC 初始化失败:$e';
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化悬浮窗的火山引擎
|
||||
Future<void> _initFloatingVolcEngine(
|
||||
String appId,
|
||||
String roomId,
|
||||
String token,
|
||||
String userId,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('=== 悬浮窗 VolcEngine 初始化 ===');
|
||||
final engineEventHandler = volc.IRTCEngineEventHandler(
|
||||
onWarning: (volc.WarningCode code) {
|
||||
debugPrint('Volc 悬浮窗 Warning: $code');
|
||||
},
|
||||
onError: (volc.ErrorCode code) {
|
||||
debugPrint('Volc 悬浮窗 Error: $code');
|
||||
setState(() {
|
||||
_floatingErrorMessage = 'Volc RTC 错误:$code';
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
_floatingRtcEngine = await volc.RTCEngine.createRTCEngine(
|
||||
volc.RTCVideoContext(appId: appId, eventHandler: engineEventHandler),
|
||||
);
|
||||
|
||||
if (_floatingRtcEngine == null) {
|
||||
debugPrint('Volc 悬浮窗引擎创建失败');
|
||||
setState(() {
|
||||
_floatingErrorMessage = 'Volc RTC 引擎创建失败';
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_floatingRtcRoom = await _floatingRtcEngine!.createRTCRoom(roomId);
|
||||
|
||||
if (_floatingRtcRoom == null) {
|
||||
debugPrint('Volc 悬浮窗房间创建失败');
|
||||
setState(() {
|
||||
_floatingErrorMessage = 'Volc RTC 房间创建失败';
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final roomEventHandler = volc.IRTCRoomEventHandler(
|
||||
onUserPublishStreamVideo:
|
||||
(String userId, volc.StreamInfo streamInfo, bool isPublish) {
|
||||
debugPrint('Volc 悬浮窗 远端用户 $userId 视频流状态: $isPublish');
|
||||
setState(() {
|
||||
if (isPublish) {
|
||||
_floatingRemoteUserId = userId;
|
||||
_isFloatingAgora = false;
|
||||
_floatingRemoteRenderContext =
|
||||
volc.RTCViewContext.remoteContext(
|
||||
roomId: roomId,
|
||||
userId: userId,
|
||||
);
|
||||
_isFloatingLoading = false;
|
||||
} else {
|
||||
if (userId == _floatingRemoteUserId) {
|
||||
_floatingRemoteRenderContext = null;
|
||||
_floatingRemoteUserId = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
onUserLeave: (String userId, int reason) {
|
||||
debugPrint('Volc 悬浮窗 用户离开: $userId');
|
||||
if (userId == _floatingRemoteUserId) {
|
||||
setState(() {
|
||||
_floatingRemoteRenderContext = null;
|
||||
_floatingRemoteUserId = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await _floatingRtcRoom?.setRTCRoomEventHandler(roomEventHandler);
|
||||
|
||||
await _floatingRtcRoom?.joinRoom(
|
||||
token: token,
|
||||
userInfo: volc.UserInfo(userId: userId, extraInfo: ''),
|
||||
userVisibility: true,
|
||||
roomConfig: volc.RoomConfig(
|
||||
isPublishAudio: false,
|
||||
isPublishVideo: false,
|
||||
isAutoSubscribeAudio: false,
|
||||
isAutoSubscribeVideo: true,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ 悬浮窗 Volc RTC 初始化失败: $e');
|
||||
setState(() {
|
||||
_floatingErrorMessage = 'Volc RTC 初始化失败:$e';
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 构建悬浮窗视频内容
|
||||
Widget _buildFloatingVideoContent() {
|
||||
if (_isFloatingLoading) {
|
||||
return Container(
|
||||
height: 140,
|
||||
color: const Color(0xFF0D1117),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_floatingErrorMessage != null) {
|
||||
return Container(
|
||||
height: 140,
|
||||
color: const Color(0xFF0D1117),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_floatingErrorMessage!,
|
||||
style: const TextStyle(color: Colors.red, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_floatingRemoteUserId == null) {
|
||||
return Container(
|
||||
height: 140,
|
||||
color: const Color(0xFF0D1117),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.video_camera_front, size: 32, color: Colors.grey),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'等待视频流...',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_isFloatingAgora && _floatingAgoraEngine != null) {
|
||||
return SizedBox(
|
||||
height: 140,
|
||||
child: agora.AgoraVideoView(
|
||||
controller: agora.VideoViewController.remote(
|
||||
rtcEngine: _floatingAgoraEngine!,
|
||||
canvas: agora.VideoCanvas(uid: int.parse(_floatingRemoteUserId!)),
|
||||
connection: agora.RtcConnection(
|
||||
channelId: _floatingVideoStream?.roomId ?? '',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_isFloatingAgora && _floatingRemoteRenderContext != null) {
|
||||
return SizedBox(
|
||||
height: 140,
|
||||
child: volc.RTCSurfaceView(
|
||||
context: _floatingRemoteRenderContext!,
|
||||
renderMode: volc.VideoRenderMode.hidden,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 140,
|
||||
color: const Color(0xFF0D1117),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'视频初始化中...',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraListRow(String label, List<CameraInfo> cameras) {
|
||||
String value = cameras
|
||||
.map((c) => '${c.cameraIndex}:${c.cameraPosition}')
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../../core/router/route_paths.dart';
|
||||
import '../widgets/robot_header_card.dart';
|
||||
import '../widgets/robot_status_bar.dart';
|
||||
import '../widgets/robot_control_panel.dart';
|
||||
@@ -81,8 +83,7 @@ class RobotControlPage extends StatelessWidget {
|
||||
Widget _buildPathPlanningCard(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// TODO: 跳转到路径规划页面
|
||||
debugPrint('点击路径规划');
|
||||
context.push(RoutePaths.routePlan);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../../devices/domain/entities/device_entity.dart';
|
||||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../data/models/robot_data_model.dart';
|
||||
import '../bloc/robot_list_bloc.dart';
|
||||
@@ -478,9 +481,12 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
battery: robot.battery,
|
||||
task: robot.task,
|
||||
onTap: () {
|
||||
// 1. 将当前机器人设置为全局选中设备
|
||||
// final logger = sl<ILoggerService>();
|
||||
debugPrint('📱 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}, status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}');
|
||||
|
||||
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
|
||||
final device = DeviceEntity(
|
||||
deviceName: robot.id,
|
||||
deviceName: robot.name, // ✅ 修正: 用 name 而不是 id
|
||||
productId: -1,
|
||||
productName: robot.type,
|
||||
tenantId: 0,
|
||||
@@ -489,7 +495,9 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
onlineStatus: robot.status == '在线' ? 1 : 0,
|
||||
);
|
||||
|
||||
context.read<DevicesCubit>().selectDevice(device);
|
||||
// 🔥 使用 GetIt 直接获取 RemoteControlCubit 单例
|
||||
final remoteCubit = GetIt.I<RemoteControlCubit>();
|
||||
remoteCubit.setTargetDevice(device);
|
||||
|
||||
// 2. 跳转到机器人控制页面
|
||||
final robotMap = {
|
||||
|
||||
@@ -33,142 +33,135 @@ class RobotControlPanel extends StatelessWidget {
|
||||
debugPrint('✅ 设置选中设备: ${device.deviceName}');
|
||||
context.read<DevicesCubit>().selectDevice(device);
|
||||
|
||||
// 打印当前选中的设备信息
|
||||
final selectedDevice = context.read<DevicesCubit>().state.selectedDevice;
|
||||
debugPrint('📱 当前选中设备信息:');
|
||||
debugPrint(' - deviceName: ${selectedDevice?.deviceName}');
|
||||
debugPrint(' - productName: ${selectedDevice?.productName}');
|
||||
debugPrint(' - status: ${selectedDevice?.status}');
|
||||
debugPrint(' - onlineStatus: ${selectedDevice?.onlineStatus}');
|
||||
// 🔥 同时设置为全局待控制设备
|
||||
final remoteCubit = sl<RemoteControlCubit>();
|
||||
remoteCubit.setTargetDevice(device);
|
||||
debugPrint('🎯 已设置 targetDevice: ${device.deviceName}');
|
||||
|
||||
// 🔥 在跳转前获取 DevicesCubit,避免在 builder 内部 context 找不到
|
||||
final devicesCubit = context.read<DevicesCubit>();
|
||||
|
||||
// 2. 跳转到远程遥控页面
|
||||
debugPrint('🚀 准备跳转...');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (_) => sl<RemoteControlCubit>()..startControlLoop(),
|
||||
builder: (newContext) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: remoteCubit,
|
||||
), // 🔥 复用已设置 targetDevice 的单例
|
||||
BlocProvider.value(value: devicesCubit), // 🔥 同时提供 DevicesCubit
|
||||
],
|
||||
child: const RemoteControlPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'控制面板',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 方向控制盘
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 背景圆
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F6F8),
|
||||
shape: BoxShape.circle,
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'控制面板',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 方向控制盘
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 背景圆
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F6F8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 上
|
||||
Positioned(
|
||||
top: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_upward, () {}),
|
||||
),
|
||||
// 下
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_downward, () {}),
|
||||
),
|
||||
// 左
|
||||
Positioned(
|
||||
left: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_back, () {}),
|
||||
),
|
||||
// 右
|
||||
Positioned(
|
||||
right: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_forward, () {}),
|
||||
),
|
||||
// 中心按钮
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF4080FF),
|
||||
Color(0xFF165DFF),
|
||||
// 上
|
||||
Positioned(
|
||||
top: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_upward, () {}),
|
||||
),
|
||||
// 下
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_downward, () {}),
|
||||
),
|
||||
// 左
|
||||
Positioned(
|
||||
left: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_back, () {}),
|
||||
),
|
||||
// 右
|
||||
Positioned(
|
||||
right: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_forward, () {}),
|
||||
),
|
||||
// 中心按钮
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF4080FF), Color(0xFF165DFF)],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 速度档位
|
||||
const Text(
|
||||
'速度档位',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
const SizedBox(height: 12),
|
||||
// 速度档位
|
||||
const Text(
|
||||
'速度档位',
|
||||
style: TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSpeedButton('低速', false, () {}),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildSpeedButton('中速', true, () {}),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildSpeedButton('高速', false, () {}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildSpeedButton('低速', false, () {})),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildSpeedButton('中速', true, () {})),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildSpeedButton('高速', false, () {})),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/data/datasources/site_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/domain/entities/site_entity.dart';
|
||||
|
||||
class SiteDataSourceImpl implements SiteDataSource {
|
||||
final Dio dio;
|
||||
final UserStorage _userStorage;
|
||||
final AppUserCubit _appUserCubit;
|
||||
|
||||
SiteDataSourceImpl(this.dio);
|
||||
SiteDataSourceImpl(this.dio, this._userStorage, this._appUserCubit);
|
||||
|
||||
@override
|
||||
Future<List<SiteEntity>> getSiteList(int orgId) async {
|
||||
print('🔍 [SiteDataSource] 开始获取 Token...');
|
||||
print('🔍 [SiteDataSource] AppUserCubit 当前用户: ${_appUserCubit.state.user?.username}');
|
||||
|
||||
// 优先从全局状态获取 Token(更快更可靠)
|
||||
var token = _appUserCubit.state.user?.token;
|
||||
|
||||
print('🔍 [SiteDataSource] 从 AppUserCubit 获取的 Token: ${token != null ? "${token.substring(0, 20)}..." : "null"}');
|
||||
|
||||
// 如果全局状态没有,再从本地存储获取
|
||||
if (token == null) {
|
||||
print('⚠️ [SiteDataSource] AppUserCubit 没有 Token,尝试从本地存储获取...');
|
||||
final user = await _userStorage.getUser();
|
||||
token = user?.token;
|
||||
print('🔍 [SiteDataSource] 从本地存储获取的 Token: ${token != null ? "${token.substring(0, 20)}..." : "null"}');
|
||||
}
|
||||
|
||||
print('🔑 [SiteDataSource] 获取到的 Token: ${token != null ? "${token.substring(0, 20)}..." : "null"}');
|
||||
|
||||
// 构建查询参数:orgId 为 0 时不传递
|
||||
final queryParams = <String, dynamic>{
|
||||
'pageNum': 1,
|
||||
@@ -23,6 +46,11 @@ class SiteDataSourceImpl implements SiteDataSource {
|
||||
final response = await dio.get(
|
||||
HttpApiConsts.getSiteList,
|
||||
queryParameters: queryParams,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': token != null ? 'Bearer $token' : '',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/work_orde
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/quick_entry_card.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_trend_chart.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/plant_overview_card.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/tcp_status_indicator.dart';
|
||||
|
||||
class HomeV2Page extends StatefulWidget {
|
||||
const HomeV2Page({super.key});
|
||||
@@ -100,10 +101,7 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner, size: 24, color: Color(0xFF1D2129)),
|
||||
onPressed: () => print('点击扫码'),
|
||||
),
|
||||
const TcpStatusIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -206,7 +204,12 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
super.didChangeDependencies();
|
||||
// 首次进入时自动加载数据,但不显示转圈
|
||||
if (_bloc.state is HomeV2Initial) {
|
||||
_bloc.add(const HomeV2LoadData());
|
||||
// 延迟 100ms,确保 AppUserCubit 状态已更新
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
_bloc.add(const HomeV2LoadData());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/tcp/tcp_status_cubit.dart';
|
||||
|
||||
/// TCP 连接状态指示灯组件
|
||||
class TcpStatusIndicator extends StatefulWidget {
|
||||
const TcpStatusIndicator({super.key});
|
||||
|
||||
@override
|
||||
State<TcpStatusIndicator> createState() => _TcpStatusIndicatorState();
|
||||
}
|
||||
|
||||
class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TcpStatusCubit _tcpStatusCubit;
|
||||
bool _hasActivity = false;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tcpStatusCubit = GetIt.I<TcpStatusCubit>();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
)..repeat(reverse: true);
|
||||
|
||||
_animation = Tween<double>(begin: 0.6, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_tcpStatusCubit.stream.listen((state) {
|
||||
if (state.status != TcpConnectionStatus.disconnected) {
|
||||
_hasActivity = true;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = _tcpStatusCubit.state;
|
||||
|
||||
Color color;
|
||||
String tooltip;
|
||||
bool shouldAnimate;
|
||||
|
||||
switch (state.status) {
|
||||
case TcpConnectionStatus.connected:
|
||||
color = Colors.green;
|
||||
tooltip = 'TCP已连接';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
case TcpConnectionStatus.connecting:
|
||||
color = Colors.yellow;
|
||||
tooltip = 'TCP连接中...';
|
||||
shouldAnimate = true;
|
||||
break;
|
||||
case TcpConnectionStatus.error:
|
||||
color = Colors.red;
|
||||
tooltip = state.errorMessage ?? 'TCP连接错误';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
case TcpConnectionStatus.disconnected:
|
||||
default:
|
||||
color = Colors.grey;
|
||||
tooltip = 'TCP未连接';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation.value : 1.0;
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -487,35 +487,35 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// 调试面板
|
||||
Positioned(
|
||||
top: 40,
|
||||
right: 10,
|
||||
child: Material(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'状态: ${state.runtimeType.toString().replaceAll('Update', '')}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
// 🔥 暂时关闭刷新按钮和点击阴影
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
// onPressed: () => context.read<UpdateCubit>().checkUpdate(),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 调试面板 - 暂时关闭
|
||||
// Positioned(
|
||||
// top: 40,
|
||||
// right: 10,
|
||||
// child: Material(
|
||||
// color: Colors.black54,
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// Text(
|
||||
// '状态: ${state.runtimeType.toString().replaceAll('Update', '')}',
|
||||
// style: const TextStyle(
|
||||
// color: Colors.white,
|
||||
// fontSize: 12,
|
||||
// ),
|
||||
// ),
|
||||
// // 🔥 暂时关闭刷新按钮和点击阴影
|
||||
// // IconButton(
|
||||
// // icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
// // onPressed: () => context.read<UpdateCubit>().checkUpdate(),
|
||||
// // ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
32
pubspec.lock
32
pubspec.lock
@@ -9,6 +9,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "93.0.0"
|
||||
agora_rtc_engine:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: agora_rtc_engine
|
||||
sha256: "6559294d18ce4445420e19dbdba10fb58cac955cd8f22dbceae26716e194d70e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.5.3"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -628,6 +636,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
hybrid_runtime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hybrid_runtime
|
||||
sha256: "8c25ccf0f84edc0c0a9bece9266e57ce8839ebaa9d7c2f6ee6d5321857c6f67f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -716,6 +732,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
iris_method_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: iris_method_channel
|
||||
sha256: "114bbe541369add8dd0727858e7df5764f375e3fb88374ad487301733fddb57f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.5"
|
||||
isar_community:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1593,6 +1617,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
volc_engine_rtc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: volc_engine_rtc
|
||||
sha256: b14448d7dd19a53abf2da9b91606df4bef91bac8491cff61b43aaa0de67f0006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.60.4"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -94,7 +94,10 @@ dependencies:
|
||||
video_player: ^2.8.2
|
||||
|
||||
# ===== 火山引擎 RTC 实时音视频 =====
|
||||
#volc_engine_rtc: ^3.60.4
|
||||
volc_engine_rtc: ^3.60.4
|
||||
|
||||
# ===== Agora 声网 RTC =====
|
||||
agora_rtc_engine: ^6.5.0
|
||||
|
||||
|
||||
# ===== 屏幕适配,高刷等 =====
|
||||
|
||||
Reference in New Issue
Block a user