优化和路径规划新适配

This commit is contained in:
2026-07-11 16:46:53 +08:00
parent 2124f3bc02
commit 1a4d0aedfd
43 changed files with 2454 additions and 685 deletions

View File

@@ -34,4 +34,14 @@ abstract class DroneStationDataSource {
VideoQualityType qualityType = VideoQualityType.adaptive,
int videoExpire = 720000000,
});
/// 暂停飞行任务(通过 flightTaskCommand 接口)
Future<Map<String, dynamic>> pauseFlightTask({
required String deviceSn,
});
/// 返航(通过 flightTaskCommand 接口)
Future<Map<String, dynamic>> returnHome({
required String deviceSn,
});
}

View File

@@ -330,4 +330,88 @@ class DroneStationDataSourceImpl implements DroneStationDataSource {
return 'high';
}
}
@override
Future<Map<String, dynamic>> pauseFlightTask({
required String deviceSn,
}) async {
final response = await dio.post(
HttpApiConsts.flightTaskCommand,
data: {
'command': 'flighttask_pause',
'deviceSn': deviceSn,
},
);
if (response.statusCode != 200) {
throw Exception('网络请求失败: ${response.statusCode}');
}
// 🔥 重要:接口返回的是 text/plain,需要手动解析 JSON
dynamic responseData;
if (response.data is String) {
try {
responseData = jsonDecode(response.data as String);
} catch (e) {
throw Exception('响应数据解析失败: $e');
}
} else {
responseData = response.data;
}
// 确保 responseData 是 Map
if (responseData is! Map<String, dynamic>) {
throw Exception('响应数据格式错误');
}
// 🔥 直接返回接口返回的 message,不自己拟定
if (responseData['code'] != 200) {
final message = responseData['message'] ?? '操作失败';
throw Exception(message);
}
return responseData;
}
@override
Future<Map<String, dynamic>> returnHome({
required String deviceSn,
}) async {
final response = await dio.post(
HttpApiConsts.flightTaskCommand,
data: {
'command': 'return_home',
'deviceSn': deviceSn,
},
);
if (response.statusCode != 200) {
throw Exception('网络请求失败: ${response.statusCode}');
}
// 🔥 重要:接口返回的是 text/plain,需要手动解析 JSON
dynamic responseData;
if (response.data is String) {
try {
responseData = jsonDecode(response.data as String);
} catch (e) {
throw Exception('响应数据解析失败: $e');
}
} else {
responseData = response.data;
}
// 确保 responseData 是 Map
if (responseData is! Map<String, dynamic>) {
throw Exception('响应数据格式错误');
}
// 🔥 直接返回接口返回的 message,不自己拟定
if (responseData['code'] != 200) {
final message = responseData['message'] ?? '操作失败';
throw Exception(message);
}
return responseData;
}
}

View File

@@ -120,4 +120,28 @@ class DroneStationRepositoryImpl implements DroneStationRepository {
return Left(Failure(e.toString()));
}
}
@override
Future<Either<Failure, Map<String, dynamic>>> pauseFlightTask({
required String deviceSn,
}) async {
try {
final result = await dataSource.pauseFlightTask(deviceSn: deviceSn);
return Right(result);
} catch (e) {
return Left(Failure(e.toString()));
}
}
@override
Future<Either<Failure, Map<String, dynamic>>> returnHome({
required String deviceSn,
}) async {
try {
final result = await dataSource.returnHome(deviceSn: deviceSn);
return Right(result);
} catch (e) {
return Left(Failure(e.toString()));
}
}
}

View File

@@ -37,4 +37,14 @@ abstract class DroneStationRepository {
VideoQualityType qualityType,
int videoExpire,
});
/// 暂停飞行任务(通过 flightTaskCommand 接口)
Future<Either<Failure, Map<String, dynamic>>> pauseFlightTask({
required String deviceSn,
});
/// 返航(通过 flightTaskCommand 接口)
Future<Either<Failure, Map<String, dynamic>>> returnHome({
required String deviceSn,
});
}

View File

@@ -55,7 +55,8 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
DroneTaskInfo? _droneTaskInfo;
DroneStationBloc? _droneStationBloc;
DroneOsdDataSource? _droneOsdDataSource;
StreamSubscription<DroneOsdEntity>? _osdSubscription;
StreamSubscription<DroneOsdEntity>? _osdSubscription; // 无人机 OSD
StreamSubscription<DroneOsdEntity>? _stationOsdSubscription; // 🔥 机场 OSD
Timer? _simulationTimer;
List<LatLng> _trajectoryPoints = [];
LatLng? _currentPosition;
@@ -97,6 +98,7 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
_bloc.close();
_droneStationBloc?.close();
_osdSubscription?.cancel();
_stationOsdSubscription?.cancel(); // 🔥 取消机场 OSD 订阅
_droneOsdDataSource?.dispose();
_mapController?.dispose();
_destroyRtcEngine();
@@ -160,6 +162,8 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
_simulationTimer = null;
_osdSubscription?.cancel();
_osdSubscription = null;
_stationOsdSubscription?.cancel(); // 🔥 取消机场 OSD 订阅
_stationOsdSubscription = null;
_droneOsdDataSource?.dispose();
_droneOsdDataSource = null;
_droneStationBloc?.close();
@@ -201,22 +205,12 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
// 加载视频流
_loadVideoStream();
// 🔥 启动模拟轨迹用于测试(可以选择不同模式)
// 模式选项: circle(圆形)、polygon(六边形)、bow(弓字形)、rectangle(矩形)
// 等天气好了,注释掉下面这行即可使用真实 MQTT 数据
debugPrint('🚀 [FloatBarWidget] 准备启动模拟轨迹...');
_startSimulationTrajectory(mode: SimulationMode.circle, pointCount: 30);
} else {
debugPrint('⚠️ [FloatBarWidget] 无人机离线,不启动 OSD 监听和视频流');
debugPrint('🚀 [FloatBarWidget] 但仍然启动模拟轨迹用于测试...');
setState(() {
_videoError = '无人机离线,无视频信号';
});
// 离线时也启动模拟轨迹,方便测试
_startSimulationTrajectory(mode: SimulationMode.circle, pointCount: 30);
}
}
@@ -231,25 +225,31 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
gatewaySn: _droneTaskInfo!.gatewaySn,
);
// 🔥 监听无人机 OSD(轨迹、位置等)
_osdSubscription = _droneOsdDataSource!.droneOsdStream.listen(
(osd) {
if (!mounted) return;
_handleOsdUpdate(osd);
_handleDroneOsdUpdate(osd);
},
onError: (error) {
debugPrint('❌ [FloatBarWidget] OSD 监听错误: $error');
debugPrint('❌ [FloatBarWidget] 无人机 OSD 监听错误: $error');
},
);
// 🔥 新增:监听机场 OSD(环境温度、风速等)
_stationOsdSubscription = _droneOsdDataSource!.stationOsdStream.listen(
(osd) {
if (!mounted) return;
_handleStationOsdUpdate(osd);
},
onError: (error) {
debugPrint('❌ [FloatBarWidget] 机场 OSD 监听错误: $error');
},
);
}
/// 处理 OSD 数据更新
void _handleOsdUpdate(DroneOsdEntity osd) {
// ⚠️ 调试期间:忽略 MQTT 数据,只看模拟轨迹
if (_simulationTimer != null && _simulationTimer!.isActive) {
debugPrint('⚠️ [FloatBarWidget] 模拟轨迹运行中,忽略 MQTT 数据');
return;
}
/// 处理无人机 OSD 数据更新(轨迹、位置等)
void _handleDroneOsdUpdate(DroneOsdEntity osd) {
final rawData = osd.rawData;
debugPrint('📍 [FloatBarWidget] 收到 OSD 数据: ${rawData.keys.join(', ')}');
@@ -307,6 +307,20 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
}
}
/// 🔥 处理机场 OSD 数据更新(环境温度、风速等)
void _handleStationOsdUpdate(DroneOsdEntity osd) {
final rawData = osd.rawData;
debugPrint('\n========== 📥 [FloatBarWidget] 机场OSD 完整原始数据 ==========');
debugPrint('$rawData');
debugPrint('===========================================\n');
// TODO: 在这里解析机场的环境温度和风速数据
// 例如:
// final environmentTemp = rawData['environment_temperature'];
// final windSpeed = rawData['wind_speed'];
}
/// 计算两点之间的距离(米)- Haversine 公式
double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
const double earthRadius = 6371000; // 地球半径(米)

View File

@@ -204,22 +204,53 @@ class _CreateTaskPageState extends State<CreateTaskPage> {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFE5E6EB)),
minimumSize: const Size(80, 36),
// 取消按钮
SizedBox(
width: 100,
height: 44,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFE5E6EB)),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'取消',
style: TextStyle(
fontSize: 14,
color: Color(0xFF4E5969),
fontWeight: FontWeight.w500,
),
),
),
child: const Text('取消'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _onCreateTask,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
minimumSize: const Size(100, 36),
// 确认创建按钮
SizedBox(
width: 140,
height: 44,
child: ElevatedButton(
onPressed: _onCreateTask,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: const Text(
'确认创建',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
child: const Text('确认创建'),
),
],
),

View File

@@ -9,6 +9,8 @@ import '../../domain/entities/flight_task_entity.dart';
import '../../domain/entities/flight_task_detail_entity.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../../domain/usecases/update_flight_task_status_usecase.dart';
import '../../domain/usecases/pause_flight_task_usecase.dart';
import '../../domain/usecases/return_home_usecase.dart';
import '../bloc/drone_station_bloc.dart';
import '../float_bar/view/float_bar_widget.dart';
@@ -28,6 +30,10 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
FlightTaskDetailEntity? _detailTask; // 详情数据
bool _isLoading = false;
final Dio _dio = Dio();
// 🔥 任务状态管理
bool _isPaused = false; // 是否已暂停(用于切换暂停/恢复按钮)
bool _isReturning = false; // 是否正在返航中
@override
void initState() {
@@ -201,6 +207,147 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
}
}
// 🔥 暂停任务
Future<void> _pauseTask() async {
if (_detailTask == null || _detailTask!.sn.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('设备序列号不存在')));
return;
}
try {
print('🔍 [DroneMissionControl] 开始暂停任务, deviceSn: ${_detailTask!.sn}');
final useCase = GetIt.I<PauseFlightTaskUseCase>();
final result = await useCase.execute(
deviceSn: _detailTask!.sn,
);
result.fold(
(failure) {
print('❌ [DroneMissionControl] 暂停任务失败: ${failure.message}');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('暂停任务失败: ${failure.message}')));
},
(data) {
print('✅ [DroneMissionControl] 任务暂停成功: $data');
setState(() {
_isPaused = true; // 更新状态为已暂停
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('任务已暂停')));
},
);
} catch (e) {
print('❌ [DroneMissionControl] 暂停任务异常: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('暂停任务失败: $e')));
}
}
// 🔥 恢复任务
Future<void> _resumeTask() async {
if (_detailTask == null || _detailTask!.uuid.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('任务ID不存在')));
return;
}
try {
print('🔍 [DroneMissionControl] 开始恢复任务: ${_detailTask!.uuid}');
final useCase = GetIt.I<UpdateFlightTaskStatusUseCase>();
final result = await useCase.execute(
taskId: _detailTask!.uuid,
status: 'restored',
);
result.fold(
(failure) {
print('❌ [DroneMissionControl] 恢复任务失败: ${failure.message}');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('恢复任务失败: ${failure.message}')));
},
(data) {
print('✅ [DroneMissionControl] 任务恢复成功: $data');
setState(() {
_isPaused = false; // 更新状态为执行中
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('任务已恢复执行')));
},
);
} catch (e) {
print('❌ [DroneMissionControl] 恢复任务异常: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('恢复任务失败: $e')));
}
}
// 🔥 返航降落
Future<void> _returnHome() async {
if (_detailTask == null || _detailTask!.sn.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('设备序列号不存在')));
return;
}
// 防止重复点击
if (_isReturning) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已在返航中...')));
return;
}
try {
print('🔍 [DroneMissionControl] 开始返航, deviceSn: ${_detailTask!.sn}');
setState(() {
_isReturning = true; // 标记正在返航
});
final useCase = GetIt.I<ReturnHomeUseCase>();
final result = await useCase.execute(deviceSn: _detailTask!.sn);
result.fold(
(failure) {
print('❌ [DroneMissionControl] 返航失败: ${failure.message}');
setState(() {
_isReturning = false; // 重置状态
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('返航失败: ${failure.message}')));
},
(data) {
print('✅ [DroneMissionControl] 返航指令发送成功: $data');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已在返航')));
// 注意:不重置 _isReturning,因为返航是一个持续过程
},
);
} catch (e) {
print('❌ [DroneMissionControl] 返航异常: $e');
setState(() {
_isReturning = false; // 重置状态
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('返航失败: $e')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -588,9 +735,9 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {},
onPressed: _isPaused ? _resumeTask : _pauseTask,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF7D00),
backgroundColor: _isPaused ? const Color(0xFF165DFF) : const Color(0xFFFF7D00),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
@@ -598,27 +745,27 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
),
elevation: 0,
),
child: const Text(
'暂停任务',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
child: Text(
_isPaused ? '恢复任务' : '暂停任务',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: () {},
onPressed: _isReturning ? null : _returnHome,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF4E5969),
side: const BorderSide(color: Color(0xFFC9CDD4)),
foregroundColor: _isReturning ? const Color(0xFF86909C) : const Color(0xFF4E5969),
side: BorderSide(color: _isReturning ? const Color(0xFFE5E6EB) : const Color(0xFFC9CDD4)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'返航降落',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
child: Text(
_isReturning ? '已在返航' : '返航降落',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
),
),
),

View File

@@ -32,6 +32,9 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
// 无人机状态轮询计时器
Timer? _droneStatusPollingTimer;
// 🔥 标记是否已经初始化过(用于判断是否从其他页面返回)
bool _hasInitialized = false;
@override
void initState() {
@@ -43,12 +46,45 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
deviceSn: widget.station.deviceSn,
),
);
// 🔥 标记已初始化
_hasInitialized = true;
// 启动无人机状态轮询(每5秒刷新一次)
// 🔥 已禁用自动轮询,改为手动下拉刷新
// _startDroneStatusPolling();
}
/// 🔥 页面重新激活时调用(从其他页面返回时)
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 🔥 只有在已经初始化后才执行刷新(避免首次加载时重复刷新)
if (_hasInitialized && _bloc.state is UAVDetailLoaded) {
debugPrint('🔄 [DroneStationDetailPage] 从其他页面返回,刷新数据');
// 延迟一下再刷新,避免与 Bloc 状态更新冲突
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
_refreshData();
}
});
}
}
/// 🔥 刷新数据(无人机详情 + OSD数据会自动通过MQTT更新)
void _refreshData() {
if (!mounted) return;
debugPrint('📡 [DroneStationDetailPage] 刷新无人机详情数据');
_bloc.add(
UAVDetailLoad(
gatewaySn: widget.station.gatewaySn,
deviceSn: widget.station.deviceSn,
),
);
}
@override
void dispose() {
_bloc.close();
@@ -216,6 +252,20 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
/// 处理下拉刷新
Future<void> _handleRefresh() async {
debugPrint('🔄 [DroneStationDetailPage] 开始下拉刷新');
// 🔥 创建一个 Completer 来等待 Bloc 状态更新
final completer = Completer<void>();
// 监听 Bloc 状态变化
final subscription = _bloc.stream.listen((state) {
if (state is UAVDetailLoaded || state is UAVDetailError) {
if (!completer.isCompleted) {
completer.complete();
}
}
});
// 重新加载无人机详情
_bloc.add(
UAVDetailLoad(
@@ -223,9 +273,19 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
deviceSn: widget.station.deviceSn,
),
);
// 重置轮询计时器,使用新的状态
// 🔥 已禁用自动轮询,无需重置
// _scheduleDroneStatusPoll();
// 🔥 等待数据加载完成(最多等待5秒)
await completer.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint('⚠️ [DroneStationDetailPage] 下拉刷新超时');
},
);
// 取消订阅
subscription.cancel();
debugPrint('✅ [DroneStationDetailPage] 下拉刷新完成');
}
Widget _buildMonitorCard() {

View File

@@ -1,7 +1,13 @@
import 'dart:async';
import 'dart:math';
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:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.dart';
import '../../../../../core/network/mqtt/domain/entities/drone_osd_entity.dart';
import '../../domain/entities/uav_video_stream_entity.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../bloc/drone_station_bloc.dart';
@@ -53,11 +59,23 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
final volc.IRTCRoomEventHandler _roomEventHandler =
volc.IRTCRoomEventHandler();
// 🔥 实时轨迹相关状态
MapController? _mapController;
List<LatLng> _trajectoryPoints = [];
LatLng? _currentPosition;
double? _currentHeading;
StreamSubscription<DroneOsdEntity>? _osdSubscription;
DroneOsdDataSource? _droneOsdDataSource;
@override
void initState() {
super.initState();
_bloc = sl<DroneStationBloc>();
// 🔥 初始化 MQTT OSD 数据源
_droneOsdDataSource = sl<DroneOsdDataSource>();
_startOsdListening();
// 初始化事件处理器
_initVolcEventHandlers();
@@ -110,6 +128,9 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
@override
void dispose() {
_osdSubscription?.cancel();
_droneOsdDataSource?.dispose();
_mapController?.dispose();
_destroyRtcEngine();
_bloc.close();
super.dispose();
@@ -219,6 +240,147 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
}
}
/// 🔥 开始监听 MQTT OSD 数据
void _startOsdListening() async {
if (_droneOsdDataSource == null) return;
debugPrint('🛸 [DroneVideoControlPage] 开始监听 OSD 数据');
debugPrint(' droneSn: ${widget.droneSn}');
debugPrint(' gatewaySn: ${widget.gatewaySn}');
await _droneOsdDataSource!.startListening(
deviceSn: widget.droneSn,
gatewaySn: widget.gatewaySn,
);
_osdSubscription = _droneOsdDataSource!.droneOsdStream.listen((osdData) {
if (!mounted) return;
debugPrint('📡 [DroneVideoControlPage] 收到 droneOsdStream 事件');
_handleOsdUpdate(osdData);
}, onError: (error) {
debugPrint('❌ [DroneVideoControlPage] OSD 监听错误: $error');
});
debugPrint('✅ [DroneVideoControlPage] OSD 监听已启动');
}
/// 🔥 处理 OSD 数据更新,绘制轨迹
void _handleOsdUpdate(DroneOsdEntity osdData) {
// 从 rawData 中提取位置信息
final rawData = osdData.rawData;
// 🔥 尝试从嵌套结构中获取经纬度
double? lat;
double? lng;
double? heading;
// 路径1: rawData['data']['host']['99-0-0']['measure_target_latitude'] (无人机)
if (rawData['data'] is Map &&
(rawData['data'] as Map)['host'] is Map) {
final host = (rawData['data'] as Map)['host'] as Map;
// 尝试从 99-0-0 载荷获取(无人机)
if (host.containsKey('99-0-0') && host['99-0-0'] is Map) {
final payload = host['99-0-0'] as Map;
lat = (payload['measure_target_latitude'] as num?)?.toDouble();
lng = (payload['measure_target_longitude'] as num?)?.toDouble();
debugPrint('✅ [DroneVideoControlPage] 从 99-0-0 获取位置: lat=$lat, lng=$lng');
}
// 如果 99-0-0 中没有,尝试从 host 直接获取(机场)
if (lat == null || lng == null) {
lat = (host['latitude'] as num?)?.toDouble();
lng = (host['longitude'] as num?)?.toDouble();
if (lat != null && lng != null) {
debugPrint('✅ [DroneVideoControlPage] 从 host 获取位置: lat=$lat, lng=$lng');
}
}
// 获取航向角
heading = (host['attitude_head'] as num?)?.toDouble() ??
(host['heading'] as num?)?.toDouble();
}
// 兼容旧格式:直接从 rawData 获取
if (lat == null || lng == null) {
lat = lat ?? (rawData['latitude'] as num?)?.toDouble() ??
(rawData['lat'] as num?)?.toDouble();
lng = lng ?? (rawData['longitude'] as num?)?.toDouble() ??
(rawData['lng'] as num?)?.toDouble() ??
(rawData['lon'] as num?)?.toDouble();
heading = heading ?? (rawData['heading'] as num?)?.toDouble() ??
(rawData['attitudeHeading'] as num?)?.toDouble();
}
debugPrint('🛰️ [DroneVideoControlPage] 收到 OSD 数据');
debugPrint(' lat=$lat, lng=$lng, heading=$heading');
debugPrint(' 当前轨迹点数: ${_trajectoryPoints.length}');
debugPrint(' 当前位置: $_currentPosition');
// 验证位置有效性
if (lat != null && lng != null && lat.abs() <= 90 && lng.abs() <= 180) {
final newPos = LatLng(lat, lng);
setState(() {
// 更新当前位置(驱动飞机 Marker)
_currentPosition = newPos;
_currentHeading = heading;
// 轨迹"拉烟"逻辑:距离过滤(防止 GPS 抖动导致轨迹像乱麻)
if (_trajectoryPoints.isEmpty) {
_trajectoryPoints.add(newPos);
debugPrint('✅ [DroneVideoControlPage] 添加第一个轨迹点: $newPos');
// 🔥 重要:第一个点添加后,等待 UI 构建完成再移动地图
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_mapController != null && mounted) {
_mapController!.move(newPos, 18); // 缩放到 18 级
debugPrint('🗺️ [DroneVideoControlPage] 首次定位到: $newPos');
}
});
} else {
final distance = _calculateDistance(
_trajectoryPoints.last.latitude,
_trajectoryPoints.last.longitude,
lat!, // ✅ 空安全:已经通过 if 检查,使用 ! 断言
lng!, // ✅ 空安全:已经通过 if 检查,使用 ! 断言
);
debugPrint(' 📏 距离上一个点: ${distance.toStringAsFixed(2)} 米');
// 只有移动超过 0.5 米才画线,否则认为是原地漂移
if (distance > 0.5) {
_trajectoryPoints.add(newPos);
debugPrint('✅ [DroneVideoControlPage] 添加新轨迹点,当前总数: ${_trajectoryPoints.length}');
// 性能优化:只保留最近 1000 个点
if (_trajectoryPoints.length > 1000) {
_trajectoryPoints.removeAt(0);
}
} else {
debugPrint('⚠️ [DroneVideoControlPage] 距离不足0.5米(${distance.toStringAsFixed(2)}m),跳过此点');
}
}
});
// 地图跟随:后续点也移动地图(保持飞机在视野中)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_mapController != null && mounted) {
_mapController!.move(newPos, _mapController!.camera.zoom);
debugPrint('🗺️ [DroneVideoControlPage] 地图已移动到: $newPos');
}
});
} else {
debugPrint('⚠️ [DroneVideoControlPage] 位置数据无效: lat=$lat, lng=$lng');
}
}
/// 🔥 计算两点之间的距离(米)
double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
const p = 0.017453292519943295; // Math.PI / 180
final a = 0.5 -
cos((lat2 - lat1) * p) / 2 +
cos(lat1 * p) * cos(lat2 * p) * (1 - cos((lon2 - lon1) * p)) / 2;
return 12742 * asin(sqrt(a)) * 1000; // 2 * R * asin(sqrt(a)) * 1000 (R = 6371km)
}
// 初始化火山引擎事件处理器
void _initVolcEventHandlers() {
_engineEventHandler.onWarning = (volc.WarningCode code) {
@@ -522,7 +684,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
}
} else if (state is UavVideoStreamError) {
setState(() {
_errorMessage = state.message;
_errorMessage = '暂无视频';
_isLoading = false;
});
}
@@ -537,11 +699,15 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
children: [
_buildVideoPlayer(),
const SizedBox(height: 12),
// 🔥 实时轨迹地图(放在视频和飞行数据之间)
_buildTrajectoryMap(),
const SizedBox(height: 12),
_buildFlightData(),
const SizedBox(height: 12),
_buildAIResults(),
const SizedBox(height: 12),
_buildMapAndJoystick(),
// 🔥 摇杆控制(单独一行)
_buildJoystickControl(),
const SizedBox(height: 16),
_buildBottomToolbar(),
],
@@ -901,29 +1067,351 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
);
}
/// 🔥 实时轨迹地图(放在视频和飞行数据之间)
Widget _buildTrajectoryMap() {
debugPrint('🗺️ [DroneVideoControlPage] 构建地图轨迹组件');
debugPrint(' currentPosition: $_currentPosition');
debugPrint(' currentHeading: $_currentHeading');
debugPrint(' trajectoryPoints.length: ${_trajectoryPoints.length}');
return Container(
height: 200,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
// 🔥 地图
FlutterMap(
mapController: _mapController ??= MapController(), // ✅ 懒加载初始化
options: MapOptions(
initialCenter: _currentPosition ?? const LatLng(39.9042, 116.4074),
initialZoom: 18,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
),
children: [
// 高德地图瓦片(最底层)
TileLayer(
urlTemplate: 'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
subdomains: ['1', '2', '3', '4'],
userAgentPackageName: 'com.example.app',
),
// 🔥 轨迹线(中间层)
if (_trajectoryPoints.length >= 1)
PolylineLayer(
polylines: [
Polyline(
points: _trajectoryPoints,
color: const Color(0xFF00B42A).withOpacity(0.9),
strokeWidth: 4,
borderColor: Colors.white,
borderStrokeWidth: 2,
),
],
),
// 🔥 无人机当前位置标记(最上层)
if (_currentPosition != null)
MarkerLayer(
markers: [
Marker(
key: const ValueKey('drone_marker'),
point: _currentPosition!,
width: 40,
height: 40,
child: Transform.rotate(
angle: (_currentHeading ?? 0) * pi / 180,
alignment: Alignment.center,
child: const Icon(
Icons.flight,
color: Color(0xFF165DFF),
size: 32,
),
),
),
],
),
],
),
// 🔥 GPS定位标签
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'GPS定位',
style: TextStyle(fontSize: 10, color: Colors.white),
),
],
),
),
),
// 🔥 轨迹点数
if (_trajectoryPoints.isNotEmpty)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'轨迹点: ${_trajectoryPoints.length}',
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
// 🔥 等待定位提示
if (_currentPosition == null)
const Positioned(
bottom: 8,
right: 8,
child: Text(
'等待定位...',
style: TextStyle(fontSize: 10, color: Colors.black54),
),
),
],
),
),
);
}
/// 🔥 摇杆控制(单独一行)
Widget _buildJoystickControl() {
return Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFC9CDD4),
shape: BoxShape.circle,
),
),
Positioned(
top: 16,
child: IconButton(
icon: const Icon(
Icons.arrow_drop_up,
size: 32,
color: Color(0xFF4E5969),
),
onPressed: () {},
),
),
Positioned(
bottom: 16,
child: IconButton(
icon: const Icon(
Icons.arrow_drop_down,
size: 32,
color: Color(0xFF4E5969),
),
onPressed: () {},
),
),
Positioned(
left: 16,
child: IconButton(
icon: const Icon(
Icons.arrow_left,
size: 32,
color: Color(0xFF4E5969),
),
onPressed: () {},
),
),
Positioned(
right: 16,
child: IconButton(
icon: const Icon(
Icons.arrow_right,
size: 32,
color: Color(0xFF4E5969),
),
onPressed: () {},
),
),
],
),
);
}
Widget _buildMapAndJoystick() {
return Row(
children: [
Expanded(
child: Container(
height: 160,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
'assets/images/xunjian.png',
fit: BoxFit.cover,
width: double.infinity,
child: Stack(
children: [
// 🔥 地图
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _currentPosition ?? const LatLng(39.9042, 116.4074),
initialZoom: 18,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
),
children: [
// 高德地图瓦片(最底层)
TileLayer(
urlTemplate: 'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
subdomains: ['1', '2', '3', '4'],
userAgentPackageName: 'com.example.app',
),
// 🔥 轨迹线(中间层)
if (_trajectoryPoints.length >= 1)
PolylineLayer(
polylines: [
Polyline(
points: _trajectoryPoints,
color: const Color(0xFF00B42A).withOpacity(0.9),
strokeWidth: 4,
borderColor: Colors.white,
borderStrokeWidth: 2,
),
],
),
// 🔥 无人机当前位置标记(最上层)
if (_currentPosition != null)
MarkerLayer(
markers: [
Marker(
key: const ValueKey('drone_marker'),
point: _currentPosition!,
width: 40,
height: 40,
child: Transform.rotate(
angle: (_currentHeading ?? 0) * pi / 180,
alignment: Alignment.center,
child: const Icon(
Icons.flight,
color: Color(0xFF165DFF),
size: 32,
),
),
),
],
),
],
),
// 🔥 GPS定位标签
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'GPS定位',
style: TextStyle(fontSize: 10, color: Colors.white),
),
],
),
),
),
// 🔥 轨迹点数
if (_trajectoryPoints.isNotEmpty)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'轨迹点: ${_trajectoryPoints.length}',
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
// 🔥 等待定位提示
if (_currentPosition == null)
const Positioned(
bottom: 8,
right: 8,
child: Text(
'等待定位...',
style: TextStyle(fontSize: 10, color: Colors.black54),
),
),
],
),
),
),
@@ -931,7 +1419,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
const SizedBox(width: 12),
Expanded(
child: Container(
height: 160,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),

View File

@@ -25,13 +25,32 @@ class RobotControlPage extends StatelessWidget {
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
onPressed: () => Navigator.pop(context),
),
title: Text(
'${robot['type']}控制',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
title: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
robot['alias'] != null && (robot['alias'] as String).isNotEmpty
? robot['alias']
: '暂无别名',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
robot['name'] ?? '', // 🔥 使用 name 字段(长序列号 deviceName)
style: const TextStyle(
fontSize: 11,
color: Color(0xFF86909C),
),
textAlign: TextAlign.center,
maxLines: 2,
softWrap: true,
),
],
),
centerTitle: true,
actions: [

View File

@@ -112,7 +112,8 @@ class _RobotListViewState extends State<RobotListView> {
.where((r) => r.status == '在线')
.length;
final onlineCleaning = cleaningRobots.where((r) => r.status == '在线').length;
final onlineWeeding = weedingRobots.where((r) => r.status == '在线').length;
// 暂且所有设备都视为除草机器人,在线数使用总数
final onlineWeeding = allRobots.where((r) => r.status == '在线').length;
return Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -351,7 +352,7 @@ class _RobotListViewState extends State<RobotListView> {
),
const SizedBox(width: 8),
Text(
'${weedingRobots.length}',
'${allRobots.length}',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,

View File

@@ -70,7 +70,10 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
_subscription = _dataSource.droneOsdStream.listen((osd) {
if (!mounted) return;
debugPrint('📥 [DroneOsdCard] 收到无人机 OSD 数据');
// 🔥 打印完整的 MQTT 原始数据(不做任何解析)
debugPrint('\n========== 📥 [无人机OSD] 完整原始数据 ==========');
debugPrint('${osd.rawData}');
debugPrint('===========================================\n');
setState(() {
_currentOsd = osd;
@@ -115,7 +118,9 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
return;
}
debugPrint('📊 [DroneOsdCard] 数据键: ${droneData.keys.toList()}');
// 🔥 打印完整原始数据(用于调试)
debugPrint('📊 [DroneOsdCard] drone/host 数据键: ${droneData.keys.toList()}');
debugPrint('📊 [DroneOsdCard] 完整原始数据: $droneData');
// ========== 1. 基础飞行信息 ==========
// 无人机高度(height / altitude)
@@ -321,28 +326,28 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
? const Color(0xFF00B42A)
: const Color(0xFFFF7D00),
},
{
'icon': Icons.signal_cellular_alt,
'label': '遥控信号',
'key': 'rcSignal',
'newValue': rcSignal != null ? '$rcSignal%' : null,
'color': rcSignal != null && rcSignal > 80
? const Color(0xFF00B42A)
: rcSignal != null && rcSignal > 50
? const Color(0xFFFF7D00)
: const Color(0xFFF53F3F),
},
{
'icon': Icons.video_label,
'label': '图传信号',
'key': 'videoSignal',
'newValue': videoSignal != null ? '$videoSignal%' : null,
'color': videoSignal != null && videoSignal > 80
? const Color(0xFF00B42A)
: videoSignal != null && videoSignal > 50
? const Color(0xFFFF7D00)
: const Color(0xFFF53F3F),
},
// {
// 'icon': Icons.signal_cellular_alt,
// 'label': '遥控信号',
// 'key': 'rcSignal',
// 'newValue': rcSignal != null ? '$rcSignal%' : null,
// 'color': rcSignal != null && rcSignal > 80
// ? const Color(0xFF00B42A)
// : rcSignal != null && rcSignal > 50
// ? const Color(0xFFFF7D00)
// : const Color(0xFFF53F3F),
// },
// {
// 'icon': Icons.video_label,
// 'label': '图传信号',
// 'key': 'videoSignal',
// 'newValue': videoSignal != null ? '$videoSignal%' : null,
// 'color': videoSignal != null && videoSignal > 80
// ? const Color(0xFF00B42A)
// : videoSignal != null && videoSignal > 50
// ? const Color(0xFFFF7D00)
// : const Color(0xFFF53F3F),
// },
{
'icon': Icons.flight,
'label': '飞行模式',
@@ -350,22 +355,22 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
'newValue': flightMode,
'color': const Color(0xFF165DFF),
},
{
'icon': Icons.radio_button_checked,
'label': '任务进度',
'key': 'mission',
'newValue': missionProgress != null ? '$missionProgress%' : null,
'color': const Color(0xFF00B42A),
},
{
'icon': Icons.map,
'label': '航点',
'key': 'waypoint',
'newValue': (currentWaypoint != null && waypointCount != null)
? '$currentWaypoint/$waypointCount'
: null,
'color': const Color(0xFF722ED1),
},
// {
// 'icon': Icons.radio_button_checked,
// 'label': '任务进度',
// 'key': 'mission',
// 'newValue': missionProgress != null ? '$missionProgress%' : null,
// 'color': const Color(0xFF00B42A),
// },
// {
// 'icon': Icons.map,
// 'label': '航点',
// 'key': 'waypoint',
// 'newValue': (currentWaypoint != null && waypointCount != null)
// ? '$currentWaypoint/$waypointCount'
// : null,
// 'color': const Color(0xFF722ED1),
// },
];
// 应用缓存逻辑:有新值则更新缓存,否则使用旧值

View File

@@ -135,10 +135,9 @@ class DroneStationItemCard extends StatelessWidget {
// ],
],
),
//
// const SizedBox(height: 12),
//
// // 电量卡片
const SizedBox(height: 12),
// 🔥 电量状态 - 已隐藏
// _buildInfoCard(
// title: '电量状态',
// icon: Icons.battery_full_rounded,
@@ -146,10 +145,10 @@ class DroneStationItemCard extends StatelessWidget {
// value: station.capacityPercent != null ? '${station.capacityPercent}%' : '未知',
// progressValue: station.capacityPercent,
// ),
//
// const SizedBox(height: 12),
//
// // 环境信息卡片
// 🔥 环境监测 - 已隐藏
// _buildSectionCard(
// title: '环境监测',
// icon: Icons.thermostat_rounded,

View File

@@ -66,7 +66,10 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
_subscription = _dataSource.stationOsdStream.listen((osd) {
if (!mounted) return;
//debugPrint('📥 [DroneStationOsdCard] 收到机场 OSD 原始数据');
// 🔥 打印完整的 MQTT 原始数据(不做任何解析)
debugPrint('\n========== 📥 [机场OSD] 完整原始数据 ==========');
debugPrint('${osd.rawData}');
debugPrint('===========================================\n');
setState(() {
_currentOsd = osd;
@@ -95,8 +98,11 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
return;
}
// debugPrint('📊 [DroneStationOsdCard] 开始解析 OSD 数据...');
//debugPrint('📊 [DroneStationOsdCard] host 数据键: ${hostData.keys.toList()}');
// 🔥 打印完整的 host 数据键,查看所有可用字段
debugPrint('📊 [DroneStationOsdCard] host 数据键: ${hostData.keys.toList()}');
// 🔥 打印完整原始数据(用于调试)
debugPrint('📊 [DroneStationOsdCard] 完整原始数据: $hostData');
// ========== 1. 电池相关 ==========
// 提取无人机电量(从 drone_battery_maintenance_info.batteries[0].capacity_percent)

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
import 'package:maibu_satabot_v2/core/app/app_user_state.dart'; // 🔥 导入 AppUserState
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart';
@@ -15,7 +16,6 @@ class RobotHeaderCard extends StatefulWidget {
}
class _RobotHeaderCardState extends State<RobotHeaderCard> {
String _videoStreamUrl = '';
int _currentViewIndex = 0; // 0=前, 1=后, 2=左, 3=右, 4=上
// 视角配置
@@ -30,30 +30,7 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
@override
void initState() {
super.initState();
_initVideoUrl();
}
/// 初始化视频流 URL
void _initVideoUrl() {
final userState = context.read<AppUserCubit>().state;
final deviceId = widget.robot['id'] as String?;
debugPrint(' [RobotHeaderCard] 开始初始化视频URL');
debugPrint('🎬 [RobotHeaderCard] deviceId: $deviceId');
debugPrint('🎬 [RobotHeaderCard] user: ${userState.user}');
debugPrint('🎬 [RobotHeaderCard] token: ${userState.user?.token}');
if (deviceId != null && deviceId.isNotEmpty && userState.user != null && userState.user!.token != null) {
setState(() {
_videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
});
debugPrint('✅ [RobotHeaderCard] 视频URL初始化成功: $_videoStreamUrl');
} else {
debugPrint('❌ [RobotHeaderCard] 视频URL初始化失败');
debugPrint(' - deviceId.isEmpty: ${deviceId == null || deviceId.isEmpty}');
debugPrint(' - user == null: ${userState.user == null}');
debugPrint(' - token == null: ${userState.user?.token == null}');
}
debugPrint('🎬 [RobotHeaderCard] 初始化 - robot: ${widget.robot}');
}
@override
@@ -147,26 +124,59 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
aspectRatio: 16 / 9,
child: Container(
color: Colors.black, // 🔥 强制整个视频区域为黑色背景
child: _videoStreamUrl.isNotEmpty
? WebRTCLocalPlayer(
streamUrl: _videoStreamUrl,
showLeftPip: false, // 不显示悬浮小窗
showRightPip: false,
isFrontMain: _currentViewIndex == 0, // 根据当前视角决定主画面
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.videocam_off, size: 48, color: Colors.white54),
const SizedBox(height: 12),
Text(
'无视频信号',
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500),
child: BlocBuilder<AppUserCubit, AppUserState>(
builder: (context, userState) {
final deviceId = widget.robot['name'] as String?; // 🔥 使用 name 字段(长序列号)
String videoStreamUrl = '';
debugPrint('🎬 [RobotHeaderCard] BlocBuilder 重建');
debugPrint(' - deviceId (name): $deviceId');
debugPrint(' - user != null: ${userState.user != null}');
debugPrint(' - token != null: ${userState.user?.token != null}');
debugPrint(' - TCP_IP: ${TCPConsts.TCP_IP}');
if (deviceId != null &&
deviceId.isNotEmpty &&
userState.user != null &&
userState.user!.token != null) {
videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
debugPrint('✅ [RobotHeaderCard] 视频URL构建成功: $videoStreamUrl');
} else {
debugPrint('❌ [RobotHeaderCard] 视频URL构建失败');
if (deviceId == null || deviceId.isEmpty) {
debugPrint(' - 原因: deviceId 为空');
}
if (userState.user == null) {
debugPrint(' - 原因: user 为 null');
}
if (userState.user?.token == null) {
debugPrint(' - 原因: token 为 null');
}
}
return videoStreamUrl.isNotEmpty
? WebRTCLocalPlayer(
key: ValueKey(videoStreamUrl),
streamUrl: videoStreamUrl,
showLeftPip: false,
showRightPip: false,
mainViewAlignment: _viewConfigs[_currentViewIndex]['alignment'] as Alignment,
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.videocam_off, size: 48, color: Colors.white54),
const SizedBox(height: 12),
Text(
'暂无视频',
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
],
),
),
);
},
),
),
),
@@ -224,3 +234,5 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
);
}
}

View File

@@ -73,7 +73,7 @@ class RobotItemCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
alias != null && alias!.isNotEmpty ? alias! : '暂无别名',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@@ -84,13 +84,13 @@ class RobotItemCard extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
alias != null && alias!.isNotEmpty
? '别名: $alias'
: 'ID: $id',
name, // 🔥 显示完整的长序列号 (deviceName)
style: const TextStyle(
fontSize: 12,
fontSize: 11,
color: Color(0xFF86909C),
),
maxLines: 2,
softWrap: true,
),
],
),