优化和路径规划新适配
This commit is contained in:
@@ -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('确认创建'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user