From 2124f3bc0278e49d0f45d9229e82d270fda2c957 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Fri, 3 Jul 2026 13:39:18 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BC=98=E5=8C=96=E4=BA=86=E6=97=A0=E4=BA=BA?= =?UTF-8?q?=E7=9A=84=E8=BD=A8=E8=BF=B9=E7=9A=84=E7=BB=98=E5=88=B6=E7=9A=84?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E4=B8=AD=E7=9A=84=20=E9=A3=9E=E6=9C=BA?= =?UTF-8?q?=E4=B8=8D=E5=8A=A8=E8=B7=AF=E7=BA=BF=E8=87=AA=E5=B7=B1=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E4=B8=8D=E5=92=8C=E8=B0=90=E7=8E=B0=E8=B1=A1=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E9=A3=9E=E6=9C=BA=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=8E=A8=E9=80=81=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=BB=98=E5=88=B6=E7=8F=8D=E6=A0=BC=E6=A0=BC=E9=A3=9E=E6=9C=BA?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=9A=84=E8=BF=87=E7=A8=8B=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E4=B9=9F=E4=BC=98=E5=8C=96=E4=BA=86=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=85=A2=20=E7=99=BD=E5=BA=95=E8=89=B2?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82=202.=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E4=BA=86=E9=81=97=E7=95=99=E4=B8=80=E4=BA=9B=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81=E7=9A=84?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../float_bar/view/float_bar_widget.dart | 421 +++++++++++++----- 1 file changed, 306 insertions(+), 115 deletions(-) diff --git a/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart b/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart index 699d8dff..9da70c0a 100644 --- a/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart +++ b/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart @@ -19,6 +19,14 @@ import '../../../../../../core/managers/drone_task_state_manager.dart'; import '../../../../../../core/network/mqtt/domain/entities/drone_osd_entity.dart'; import '../../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.dart'; +/// 模拟轨迹模式枚举 +enum SimulationMode { + circle, // 圆形路径 + polygon, // 多边形路径(六边形) + bow, // 弓字形路径(Zigzag) + rectangle, // 矩形路径 +} + /// 可拖动的悬浮按钮组件 /// 点击弹出底部弹窗显示设备状态详情 class FloatBarWidget extends StatefulWidget { @@ -69,6 +77,9 @@ class FloatBarWidgetState extends State { MapController? _mapController; bool _isUserDragging = false; + + /// 用来同步刷新弹窗内部的 State + void Function(void Function())? _bottomSheetSetState; @override void initState() { @@ -89,6 +100,7 @@ class FloatBarWidgetState extends State { _droneOsdDataSource?.dispose(); _mapController?.dispose(); _destroyRtcEngine(); + _simulationTimer?.cancel(); droneTaskStateManager.currentTaskInfo.removeListener(_onTaskInfoChanged); super.dispose(); } @@ -159,7 +171,10 @@ class FloatBarWidgetState extends State { /// 初始化无人机相关组件 void _initDroneComponents() { - if (_droneTaskInfo == null) return; + if (_droneTaskInfo == null) { + debugPrint('❌ [FloatBarWidget] _droneTaskInfo 为空,无法初始化'); + return; + } debugPrint('🛸 [FloatBarWidget] 初始化无人机组件'); debugPrint(' droneSn: ${_droneTaskInfo!.droneSn}'); @@ -174,30 +189,34 @@ class FloatBarWidgetState extends State { // 初始化地图控制器 _mapController = MapController(); - - // 立即设置初始位置,确保飞机能立即显示 - setState(() { - _currentPosition = const LatLng(39.9042, 116.4074); - _currentHeading = 90; - _trajectoryPoints = [const LatLng(39.9042, 116.4074)]; - }); - - // 启动模拟轨迹数据(用于测试) - _startSimulationTrajectory(); + debugPrint('✅ [FloatBarWidget] 地图控制器已初始化'); // 只有在线时才启动 OSD 监听和视频流加载 if (_droneTaskInfo!.isOnline) { + debugPrint('🟢 [FloatBarWidget] 无人机在线,启动完整功能'); + // 初始化 OSD 数据源 _droneOsdDataSource = sl(); _startOsdListening(); // 加载视频流 _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); } } @@ -225,129 +244,311 @@ class FloatBarWidgetState extends State { /// 处理 OSD 数据更新 void _handleOsdUpdate(DroneOsdEntity osd) { + // ⚠️ 调试期间:忽略 MQTT 数据,只看模拟轨迹 + if (_simulationTimer != null && _simulationTimer!.isActive) { + debugPrint('⚠️ [FloatBarWidget] 模拟轨迹运行中,忽略 MQTT 数据'); + return; + } + final rawData = osd.rawData; - // 获取经纬度 - double? latitude = (rawData['latitude'] as num?)?.toDouble(); - double? longitude = (rawData['longitude'] as num?)?.toDouble(); - double? heading = (rawData['heading'] as num?)?.toDouble(); + debugPrint('📍 [FloatBarWidget] 收到 OSD 数据: ${rawData.keys.join(', ')}'); - if (latitude != null && - longitude != null && - latitude != 0 && - longitude != 0) { + // 1. 解析基础数据 + double? lat = (rawData['latitude'] as num?)?.toDouble() ?? + (rawData['lat'] as num?)?.toDouble(); + double? lng = (rawData['longitude'] as num?)?.toDouble() ?? + (rawData['lng'] as num?)?.toDouble() ?? + (rawData['lon'] as num?)?.toDouble(); + double? heading = (rawData['heading'] as num?)?.toDouble() ?? + (rawData['yaw'] as num?)?.toDouble(); + + if (lat != null && lng != null && lat != 0 && lng != 0) { + final newPos = LatLng(lat, lng); + setState(() { - _currentPosition = LatLng(latitude, longitude); - _currentHeading = heading; + // 计算航向:如果 MQTT 没给角度,我们根据上一个点自动算一个方向 + if (heading != null) { + _currentHeading = heading; + } else if (_trajectoryPoints.isNotEmpty) { + final last = _trajectoryPoints.last; + // 使用 atan2 计算方位角 + _currentHeading = atan2(lng - last.longitude, lat - last.latitude) * 180 / pi; + } - // 添加到轨迹点 - if (_currentPosition != null) { - _trajectoryPoints.add(_currentPosition!); - // 限制轨迹点数量,避免内存问题 - if (_trajectoryPoints.length > 500) { - _trajectoryPoints.removeAt(0); + // 更新当前位置(驱动飞机 Marker) + _currentPosition = newPos; + + // 轨迹“拉烟”逻辑:距离过滤(防止 GPS 抖动导致轨迹像乱麻) + if (_trajectoryPoints.isEmpty) { + _trajectoryPoints.add(newPos); + } else { + final distance = _calculateDistance( + _trajectoryPoints.last.latitude, + _trajectoryPoints.last.longitude, + lat, + lng, + ); + // 只有移动超过 1.5 米才画线,否则认为是原地漂移 + if (distance > 1.5) { + _trajectoryPoints.add(newPos); + // 性能优化:只保留最近 1000 个点 + if (_trajectoryPoints.length > 1000) _trajectoryPoints.removeAt(0); } } }); - // 自动移动地图中心 - if (_mapController != null && _currentPosition != null) { - _mapController?.move(_currentPosition!, 17); + // 2. 地图跟随:如果用户在手动拖地图,就不要强制跳回中心 + if (!_isUserDragging && _mapController != null) { + _mapController?.move(newPos, _mapController!.camera.zoom); } + } else { + debugPrint('⚠️ [FloatBarWidget] 位置数据无效: lat=$lat, lng=$lng'); } } - /// 模拟轨迹数据(用于测试) - /// 简化为三个固定点,飞机在这三个点之间缓慢移动 - void _startSimulationTrajectory() { - debugPrint('🎯 [FloatBarWidget] 启动模拟轨迹数据(三点模式)'); + /// 计算两点之间的距离(米)- Haversine 公式 + double _calculateDistance(double lat1, double lon1, double lat2, double lon2) { + const double earthRadius = 6371000; // 地球半径(米) + + final dLat = _toRadians(lat2 - lat1); + final dLon = _toRadians(lon2 - lon1); + + final a = sin(dLat / 2) * sin(dLat / 2) + + cos(_toRadians(lat1)) * cos(_toRadians(lat2)) * + sin(dLon / 2) * sin(dLon / 2); + + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + + return earthRadius * c; + } + + double _toRadians(double degrees) { + return degrees * pi / 180; + } + + /// 启动智能模拟轨迹(支持多种路径模式) + void _startSimulationTrajectory({ + SimulationMode mode = SimulationMode.circle, + int pointCount = 20, + }) { + debugPrint('🎯 [FloatBarWidget] ========== 开始启动模拟轨迹 =========='); + debugPrint(' 模式: $mode'); + debugPrint(' 点数: $pointCount'); + debugPrint(' mounted: $mounted'); // 如果已经有定时器在运行,先取消 if (_simulationTimer != null && _simulationTimer!.isActive) { + debugPrint('⚠️ [FloatBarWidget] 检测到已有定时器,先取消'); _simulationTimer!.cancel(); } - // 三个点间距拉大,形成明显的三角形路径 - final waypoints = [ - const LatLng(39.9042, 116.4074), // 起点 - const LatLng(39.9060, 116.4110), // 东北方向 - const LatLng(39.9020, 116.4120), // 东南方向 - ]; + // 生成路径点集 + final waypoints = _generateWaypoints(mode, pointCount); + debugPrint(' 生成的路径点数: ${waypoints.length}'); + + if (waypoints.isEmpty) { + debugPrint('❌ [FloatBarWidget] 路径点生成失败'); + return; + } + + debugPrint(' 第一个点: ${waypoints[0]}'); - int currentWaypoint = 0; - int nextWaypoint = 1; + int currentIndex = 0; double progress = 0; - const double speed = 0.02; // 每步移动2% + const double speed = 0.05; // 每步移动5%,更快看到效果 - // 初始化位置 + debugPrint('📍 [FloatBarWidget] 设置初始位置...'); setState(() { - _currentPosition = waypoints[0]; - _currentHeading = 90; - _trajectoryPoints = [waypoints[0]]; + _currentPosition = waypoints[0]; // 从起点开始 + _trajectoryPoints = [waypoints[0]]; // 轨迹从起点开始 + debugPrint(' ✅ 起始位置: $_currentPosition'); + debugPrint(' ✅ 轨迹点数: ${_trajectoryPoints.length}'); }); - // 使用定时器模拟缓慢移动(500ms间隔,肉眼可见移动) - _simulationTimer = Timer.periodic(const Duration(milliseconds: 500), ( - timer, - ) { + // 首次定位到起始区域 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_mapController != null) { + debugPrint('🗺️ [FloatBarWidget] 移动地图到起始点...'); + _mapController!.move(waypoints[0], 16); + debugPrint(' ✅ 地图已移动'); + } else { + debugPrint('⚠️ [FloatBarWidget] 地图控制器为空,无法移动'); + } + }); + + debugPrint('⏱️ [FloatBarWidget] 启动定时器(300ms间隔)...'); + debugPrint(' 📊 当前 mounted: $mounted'); + debugPrint(' 📊 当前 _currentPosition: $_currentPosition'); + + // 使用定时器模拟飞行(300ms间隔,流畅移动) + _simulationTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + debugPrint('🔥 [定时器触发] mounted=$mounted, currentIndex=$currentIndex, progress=${progress.toStringAsFixed(2)}'); + if (!mounted) { + debugPrint('⚠️ [FloatBarWidget] Widget 未挂载,取消定时器'); timer.cancel(); return; } - // 首次触发时定位地图到起始区域 - if (progress == speed && _mapController != null) { - try { - _mapController!.move(waypoints[0], 16); - } catch (_) {} - } + final start = waypoints[currentIndex]; + final nextIndex = (currentIndex + 1) % waypoints.length; + final end = waypoints[nextIndex]; - // 计算当前点到下一个点的移动 - final start = waypoints[currentWaypoint]; - final end = waypoints[nextWaypoint]; - - // 线性插值 + // 线性插值 - 计算轨迹的"前端"位置 progress += speed; if (progress >= 1.0) { progress = 0; - currentWaypoint = nextWaypoint; - nextWaypoint = (currentWaypoint + 1) % waypoints.length; + currentIndex = nextIndex; + + // 到达起点时继续循环(不要停!) + if (currentIndex == 0) { + debugPrint('✅ [FloatBarWidget] 🎉 完成一圈,继续飞行...'); + // timer.cancel(); // 不要停止,继续飞 + // return; // 不要返回 + } } - // 计算当前位置 - final lat = start.latitude + (end.latitude - start.latitude) * progress; - final lng = - start.longitude + (end.longitude - start.longitude) * progress; + // 计算轨迹前端位置(水流到哪里了) + final trailLat = start.latitude + (end.latitude - start.latitude) * progress; + final trailLng = start.longitude + (end.longitude - start.longitude) * progress; + final trailPosition = LatLng(trailLat, trailLng); - // 计算航向角 - final dx = end.longitude - start.longitude; - final dy = end.latitude - start.latitude; - final heading = atan2(dy, dx) * 180 / pi; + // 核心改进:计算飞机的航向角(让头朝向前进方向) + final angle = atan2(end.longitude - start.longitude, end.latitude - start.latitude) * 180 / pi; - setState(() { - _currentPosition = LatLng(lat, lng); - _currentHeading = heading; + // 1. 刷新悬浮条自身(如果需要) + if (mounted) { + setState(() { + _currentPosition = trailPosition; + _currentHeading = angle; // 赋予飞机灵魂 + + // 轨迹产生(拉烟) + _trajectoryPoints.add(trailPosition); + if (_trajectoryPoints.length > 800) _trajectoryPoints.removeAt(0); + + debugPrint(' 🔄 setState更新: 飞机=$_currentPosition, 航向=${angle.toStringAsFixed(1)}°, 轨迹点=${_trajectoryPoints.length}'); + }); + } - // 添加轨迹点 - if (_currentPosition != null) { - _trajectoryPoints.add(_currentPosition!); - if (_trajectoryPoints.length > 500) { - _trajectoryPoints.removeAt(0); - } - } + // 2. 🔥 核心:刷新已经打开的弹窗内部 + _bottomSheetSetState?.call(() { + // 弹窗内部组件会重新执行 build,读取到最新的 _currentPosition 和 _currentHeading + debugPrint(' 📱 弹窗已刷新'); }); - // 移动地图中心到无人机当前位置 - if (_currentPosition != null) { - _mapController?.move(_currentPosition!, 16); + // 3. 移动地图中心 + if (!_isUserDragging) { + _mapController?.move(trailPosition, 16); } - debugPrint( - '📍 模拟位置: lat=${lat.toStringAsFixed(6)}, lng=${lng.toStringAsFixed(6)}, heading=${heading.toStringAsFixed(1)}, progress=${progress.toStringAsFixed(2)}', - ); + if (progress < 0.1 || progress > 0.9) { + debugPrint( + '✈️ 飞机飞行: 进度=${(progress * 100).toStringAsFixed(0)}%, ' + '轨迹点=${_trajectoryPoints.length}, ' + '位置=(${trailLat.toStringAsFixed(4)}, ${trailLng.toStringAsFixed(4)})', + ); + } }); - debugPrint(' 模拟定时器已启动,三点循环模式(500ms间隔)'); + debugPrint(' ✅ 模拟定时器已启动'); + debugPrint(' ✈️ 飞机将沿路径飞行(像拉烟画画)'); + debugPrint(' 🛤️ 轨迹将从飞机尾部产生'); + debugPrint('🎯 [FloatBarWidget] ========== 模拟轨迹启动完成 =========='); + } + + /// 生成不同类型的路径点集 + List _generateWaypoints(SimulationMode mode, int count) { + const centerLat = 39.9042; + const centerLng = 116.4074; + + switch (mode) { + case SimulationMode.circle: + return _generateCirclePath(centerLat, centerLng, count); + case SimulationMode.polygon: + return _generatePolygonPath(centerLat, centerLng, count); + case SimulationMode.bow: + return _generateBowPath(centerLat, centerLng, count); + case SimulationMode.rectangle: + return _generateRectanglePath(centerLat, centerLng, count); + } + } + + /// 生成圆形路径 + List _generateCirclePath(double centerLat, double centerLng, int points) { + final List path = []; + const double radius = 0.002; // 约200米半径 + const double angleStep = (2 * pi) / 30; // 30个点形成圆 + + for (int i = 0; i < 30; i++) { + final angle = i * angleStep; + final lat = centerLat + radius * cos(angle); + final lng = centerLng + radius * sin(angle); + path.add(LatLng(lat, lng)); + } + path.add(path[0]); // 闭合圆形 + + debugPrint('🔵 生成圆形路径: ${path.length}个点'); + return path; + } + + /// 生成多边形路径(六边形) + List _generatePolygonPath(double centerLat, double centerLng, int points) { + final List path = []; + const double radius = 0.0025; // 约250米 + const int sides = 6; // 六边形 + const double angleStep = (2 * pi) / sides; + + for (int i = 0; i <= sides; i++) { + final angle = i * angleStep - pi / 2; // 从顶部开始 + final lat = centerLat + radius * cos(angle); + final lng = centerLng + radius * sin(angle); + path.add(LatLng(lat, lng)); + } + + debugPrint('⬡ 生成六边形路径: ${path.length}个点'); + return path; + } + + /// 生成弓字形路径(Zigzag) + List _generateBowPath(double centerLat, double centerLng, int points) { + final List path = []; + const double width = 0.004; // 宽度约400米 + const double height = 0.003; // 高度约300米 + const int rows = 5; // 5行 + + for (int row = 0; row <= rows; row++) { + final lat = centerLat - height / 2 + (height / rows) * row; + + // 偶数行从左到右,奇数行从右到左 + if (row % 2 == 0) { + path.add(LatLng(lat, centerLng - width / 2)); + path.add(LatLng(lat, centerLng + width / 2)); + } else { + path.add(LatLng(lat, centerLng + width / 2)); + path.add(LatLng(lat, centerLng - width / 2)); + } + } + + debugPrint('〰️ 生成弓字形路径: ${path.length}个点, $rows行'); + return path; + } + + /// 生成矩形路径 + List _generateRectanglePath(double centerLat, double centerLng, int points) { + final List path = []; + const double width = 0.004; // 宽约400米 + const double height = 0.003; // 高约300米 + + // 四个角点(顺时针) + path.add(LatLng(centerLat + height / 2, centerLng - width / 2)); // 左上 + path.add(LatLng(centerLat + height / 2, centerLng + width / 2)); // 右上 + path.add(LatLng(centerLat - height / 2, centerLng + width / 2)); // 右下 + path.add(LatLng(centerLat - height / 2, centerLng - width / 2)); // 左下 + path.add(path[0]); // 闭合 + + debugPrint('▭ 生成矩形路径: ${path.length}个点'); + return path; } /// 加载视频流 @@ -492,8 +693,13 @@ class FloatBarWidgetState extends State { constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.85, ), - builder: (ctx) => _buildBottomSheetContent(status), - ); + builder: (ctx) => StatefulBuilder( // 🔥 关键:使用 StatefulBuilder + builder: (context, setSheetState) { + _bottomSheetSetState = setSheetState; // 捕获弹窗内部的刷新函数 + return _buildBottomSheetContent(status); + }, + ), + ).then((_) => _bottomSheetSetState = null); // 弹窗关闭时置空,防止内存泄漏 } /// 构建底部弹窗内容 @@ -1308,33 +1514,18 @@ class FloatBarWidgetState extends State { MarkerLayer( markers: [ Marker( - key: const ValueKey('drone_current_marker'), + key: const ValueKey('drone_marker'), // 加上 Key 确保状态刷新 point: _currentPosition!, - width: 60, - height: 60, + width: 40, + height: 40, child: Transform.rotate( - angle: (_currentHeading ?? 0) * pi / 180, + // 注意:Icons.flight 默认头朝上(0度),如果你的图标偏了,在这里加个 pi/2 之类的偏移 + angle: (_currentHeading ?? 0) * pi / 180, alignment: Alignment.center, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(2, 2), - ), - ], - ), - child: const Padding( - padding: EdgeInsets.all(8), - child: Icon( - Icons.flight, - color: Color(0xFF165DFF), - size: 40, - ), - ), + child: const Icon( + Icons.flight, + color: Color(0xFF165DFF), + size: 32, ), ), ),