1.测试和修改了无人机在线实时信息的展示数据集的展示。使其能够全面展示。
2.添加了在无人机设备摄像头的为空时候,通过新增的接口获取无人机的摄像头的列表。 3.替换原先依照tcp推送消息绘制机器运动作业的轨迹过程,先用订阅mqtt特定话题推送的信息做动画过程的绘制。
This commit is contained in:
@@ -3,25 +3,26 @@ import 'package:equatable/equatable.dart';
|
||||
class TaskArriveEntity extends Equatable {
|
||||
final String type;
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final int? taskId;
|
||||
final dynamic status;
|
||||
final ArriveLocation entity;
|
||||
final ArriveLocation? entity;
|
||||
|
||||
const TaskArriveEntity({
|
||||
required this.type,
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
this.taskId,
|
||||
this.status,
|
||||
required this.entity,
|
||||
this.entity,
|
||||
});
|
||||
|
||||
factory TaskArriveEntity.fromJson(Map<String, dynamic> json) {
|
||||
final entityJson = json['entity'] as Map<String, dynamic>?;
|
||||
return TaskArriveEntity(
|
||||
type: json['type'] as String? ?? '',
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskId: json['taskId'] as int? ?? 0,
|
||||
taskId: json['taskId'] as int?,
|
||||
status: json['status'],
|
||||
entity: ArriveLocation.fromJson(json['entity'] as Map<String, dynamic>? ?? {}),
|
||||
entity: entityJson != null ? ArriveLocation.fromJson(entityJson) : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ class TaskArriveEntity extends Equatable {
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'status': status,
|
||||
'entity': entity.toJson(),
|
||||
'entity': entity?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,10 +44,7 @@ class ArriveLocation extends Equatable {
|
||||
final double lat;
|
||||
final double lng;
|
||||
|
||||
const ArriveLocation({
|
||||
required this.lat,
|
||||
required this.lng,
|
||||
});
|
||||
const ArriveLocation({required this.lat, required this.lng});
|
||||
|
||||
factory ArriveLocation.fromJson(Map<String, dynamic> json) {
|
||||
return ArriveLocation(
|
||||
@@ -56,10 +54,7 @@ class ArriveLocation extends Equatable {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lat': lat,
|
||||
'lng': lng,
|
||||
};
|
||||
return {'lat': lat, 'lng': lng};
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,12 +6,18 @@ import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/running_status_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/gps_entity.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/mqtt/domain/entities/task_arrive_entity.dart';
|
||||
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/net_message_dispatcher.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import 'device_status_event.dart';
|
||||
import 'device_status_state.dart';
|
||||
import 'devices_cubit.dart';
|
||||
|
||||
// 定义全局的TaskMessageRepository获取方式
|
||||
TaskMessageRepository get _taskMessageRepo => GetIt.I<TaskMessageRepository>();
|
||||
|
||||
class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
final NetMessageDispatcher _dispatcher;
|
||||
@@ -20,6 +26,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
|
||||
// 🔥 保存订阅引用,用于管理生命周期
|
||||
StreamSubscription? _tcpSubscription;
|
||||
StreamSubscription? _mqttArriveSubscription;
|
||||
|
||||
// 🔥 节流相关:500ms节流控制0x02数据推送频率
|
||||
Timer? _throttleTimer;
|
||||
@@ -27,16 +34,23 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
RunningStatusEntity? _cachedStatus;
|
||||
GPSEntity? _cachedGps;
|
||||
|
||||
// 🔥 当前监听的设备ID
|
||||
String? _currentDeviceId;
|
||||
|
||||
DeviceStatusBloc(this._dispatcher, {TcpClient? client})
|
||||
: tcpClient = client ?? GetIt.I<TcpClient>(),
|
||||
super(DeviceStatusInitial()) {
|
||||
// 🔥 核心改动:直接在构造函数中建立TCP监听,类似RemoteControlCubit
|
||||
_initDirectTcpListener();
|
||||
|
||||
// 🔥 初始化MQTT到达点监听
|
||||
_initMqttArriveListener();
|
||||
|
||||
// 保留事件处理(用于手动重置等场景)
|
||||
on<DeviceStatusReset>(_handleReset);
|
||||
on<DeviceStatusLoaded>(_handleDeviceStatusLoaded);
|
||||
on<PushMessageReceived>(_handlePushMessageReceived);
|
||||
on<DeviceTaskArrivePointEvent>(_handleDeviceTaskArrivePointEvent);
|
||||
}
|
||||
|
||||
// 🔥 已废弃:重新初始化TCP监听器会导致数据流中断
|
||||
@@ -162,6 +176,62 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
_logger.log('✅ [DeviceStatusBloc] 直接TCP监听器已建立完成');
|
||||
}
|
||||
|
||||
// 🔥 初始化MQTT到达点监听
|
||||
void _initMqttArriveListener() {
|
||||
debugPrint('🔗 [DeviceStatusBloc] 初始化MQTT到达点监听器');
|
||||
_logger.log('🔗 [DeviceStatusBloc] 初始化MQTT到达点监听器');
|
||||
|
||||
_mqttArriveSubscription = _taskMessageRepo.taskArriveStream.listen(
|
||||
(TaskArriveEntity arrive) {
|
||||
debugPrint(
|
||||
'📍 [DeviceStatusBloc] 收到MQTT到达点消息: type=${arrive.type}, deviceId=${arrive.deviceId}',
|
||||
);
|
||||
_logger.log(
|
||||
'📍 [DeviceStatusBloc] 收到MQTT到达点消息: type=${arrive.type}, deviceId=${arrive.deviceId}',
|
||||
);
|
||||
|
||||
// 过滤出路径规划到达点消息
|
||||
if (arrive.type == 'device_task_arrive_point') {
|
||||
// 检查设备ID是否匹配当前监听的设备
|
||||
if (_currentDeviceId != null && arrive.deviceId != _currentDeviceId) {
|
||||
debugPrint(
|
||||
'⚠️ [DeviceStatusBloc] 设备ID不匹配,跳过 - 当前:$_currentDeviceId, 收到:${arrive.deviceId}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取坐标(支持空entity场景)
|
||||
double? lat = arrive.entity?.lat;
|
||||
double? lng = arrive.entity?.lng;
|
||||
|
||||
// 发送事件到Bloc
|
||||
add(
|
||||
DeviceTaskArrivePointEvent(
|
||||
deviceId: arrive.deviceId,
|
||||
taskId: arrive.taskId,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (e) {
|
||||
debugPrint('❌ [DeviceStatusBloc] MQTT到达点监听错误: $e');
|
||||
_logger.log('❌ [DeviceStatusBloc] MQTT到达点监听错误: $e');
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('✅ [DeviceStatusBloc] MQTT到达点监听器已建立完成');
|
||||
_logger.log('✅ [DeviceStatusBloc] MQTT到达点监听器已建立完成');
|
||||
}
|
||||
|
||||
// 🔥 设置当前监听的设备ID(用于过滤MQTT消息)
|
||||
void setListeningDeviceId(String deviceId) {
|
||||
debugPrint('📱 [DeviceStatusBloc] 设置监听设备ID: $deviceId');
|
||||
_logger.log('📱 [DeviceStatusBloc] 设置监听设备ID: $deviceId');
|
||||
_currentDeviceId = deviceId;
|
||||
}
|
||||
|
||||
// 🔥 节流发射:500ms到期后发射缓存的最新数据
|
||||
void _emitCachedStatus() {
|
||||
if (_cachedStatus != null && _cachedGps != null && !isClosed) {
|
||||
@@ -228,17 +298,59 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 处理MQTT到达点事件
|
||||
Future<void> _handleDeviceTaskArrivePointEvent(
|
||||
DeviceTaskArrivePointEvent event,
|
||||
Emitter<DeviceStatusState> emit,
|
||||
) async {
|
||||
debugPrint(
|
||||
'📍 [DeviceStatusBloc] 处理到达点事件: deviceId=${event.deviceId}, taskId=${event.taskId}, lat=${event.lat}, lng=${event.lng}',
|
||||
);
|
||||
_logger.log(
|
||||
'📍 [DeviceStatusBloc] 处理到达点事件: deviceId=${event.deviceId}, taskId=${event.taskId}, lat=${event.lat}, lng=${event.lng}',
|
||||
);
|
||||
|
||||
if (event.lat != null && event.lng != null && event.lat != 0.0) {
|
||||
// ✅ 收到有效到达点 - 更新位置状态
|
||||
final gps = GPSEntity(event.lat!, event.lng!);
|
||||
|
||||
// 发送到DevicesCubit更新到达位置(触发地图轨迹更新)
|
||||
final devicesCubit = GetIt.I<DevicesCubit>();
|
||||
devicesCubit.setArrivedLocation(event.lat!, event.lng!);
|
||||
|
||||
// 同时更新DeviceStatusBloc的状态
|
||||
emit(DeviceStatusUpdated(_cachedStatus ?? RunningStatusEntity(), gps));
|
||||
} else {
|
||||
// ❌ entity为空或坐标无效 - 表示任务停止
|
||||
debugPrint('🛑 [DeviceStatusBloc] 收到任务停止信号');
|
||||
_logger.log('🛑 [DeviceStatusBloc] 收到任务停止信号');
|
||||
|
||||
final devicesCubit = GetIt.I<DevicesCubit>();
|
||||
devicesCubit.finishWork();
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
devicesCubit.resetWorkStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
debugPrint('🚫 [DeviceStatusBloc] 页面退出,仅取消TCP订阅(不关闭BLoC)');
|
||||
_logger.log('🚫 [DeviceStatusBloc] 页面退出,仅取消TCP订阅(不关闭BLoC)');
|
||||
//_tcpSubscription?.cancel();
|
||||
debugPrint('🚫 [DeviceStatusBloc] 页面退出,清理资源');
|
||||
_logger.log('🚫 [DeviceStatusBloc] 页面退出,清理资源');
|
||||
|
||||
// 🔥 清理MQTT订阅
|
||||
_mqttArriveSubscription?.cancel();
|
||||
_mqttArriveSubscription = null;
|
||||
|
||||
// 🔥 清理节流timer和缓存
|
||||
_throttleTimer?.cancel();
|
||||
_throttleTimer = null;
|
||||
_cachedStatus = null;
|
||||
_cachedGps = null;
|
||||
// 🔥 关键修复:不调用 super.close(),保持 BLoC 活跃
|
||||
|
||||
// 🔥 重置设备ID
|
||||
_currentDeviceId = null;
|
||||
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,22 @@ class PushMessageReceived extends DeviceStatusEvent {
|
||||
|
||||
class DeviceStatusReset extends DeviceStatusEvent {
|
||||
@override
|
||||
// TODO: implement props
|
||||
List<Object?> get props => throw UnimplementedError();
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class DeviceTaskArrivePointEvent extends DeviceStatusEvent {
|
||||
final String deviceId;
|
||||
final int? taskId;
|
||||
final double? lat;
|
||||
final double? lng;
|
||||
|
||||
const DeviceTaskArrivePointEvent({
|
||||
required this.deviceId,
|
||||
this.taskId,
|
||||
this.lat,
|
||||
this.lng,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [deviceId, taskId, lat, lng];
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicen
|
||||
import '../../../../core/consts/tcp_consts.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../../core/network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import '../../data/models/device_add_path_point_model.dart';
|
||||
import '../../data/models/device_work_area_param_model.dart';
|
||||
import '../../domain/usecases/bind_device_usecase.dart';
|
||||
@@ -30,6 +31,9 @@ import 'device_status_bloc.dart';
|
||||
import 'device_status_event.dart';
|
||||
import 'devices_state.dart';
|
||||
|
||||
// 定义全局的TaskMessageRepository获取方式
|
||||
TaskMessageRepository get _taskMessageRepo => GetIt.I<TaskMessageRepository>();
|
||||
|
||||
class DevicesCubit extends Cubit<DevicesState> {
|
||||
final GetUserDeviceUseCase _getUserDeviceUseCase;
|
||||
final GetDeviceLocationUseCase _getDeviceLocationUseCase;
|
||||
@@ -672,4 +676,42 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 启动MQTT到达点监听(用于路径规划动画)
|
||||
/// [deviceId] - 目标设备ID,即targetDevice的deviceId
|
||||
Future<void> startListeningMqttArrive({required String deviceId}) async {
|
||||
debugPrint('📡 [DevicesCubit] 启动MQTT到达点监听 - deviceId: $deviceId');
|
||||
_logger.logWithLevel('📡 [DevicesCubit] 启动MQTT到达点监听 - deviceId: $deviceId');
|
||||
|
||||
try {
|
||||
// 启动MQTT订阅
|
||||
await _taskMessageRepo.startListening(deviceId: deviceId);
|
||||
|
||||
// 设置DeviceStatusBloc监听的设备ID
|
||||
_deviceStatusBloc.setListeningDeviceId(deviceId);
|
||||
|
||||
debugPrint('✅ [DevicesCubit] MQTT到达点监听已启动');
|
||||
_logger.logWithLevel('✅ [DevicesCubit] MQTT到达点监听已启动');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [DevicesCubit] 启动MQTT监听失败: $e');
|
||||
_logger.logWithLevel('❌ [DevicesCubit] 启动MQTT监听失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 停止MQTT到达点监听
|
||||
Future<void> stopListeningMqttArrive() async {
|
||||
debugPrint('🛑 [DevicesCubit] 停止MQTT到达点监听');
|
||||
_logger.logWithLevel('🛑 [DevicesCubit] 停止MQTT到达点监听');
|
||||
|
||||
try {
|
||||
await _taskMessageRepo.stopListening();
|
||||
_deviceStatusBloc.setListeningDeviceId('');
|
||||
debugPrint('✅ [DevicesCubit] MQTT到达点监听已停止');
|
||||
_logger.logWithLevel('✅ [DevicesCubit] MQTT到达点监听已停止');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [DevicesCubit] 停止MQTT监听失败: $e');
|
||||
_logger.logWithLevel('❌ [DevicesCubit] 停止MQTT监听失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,8 +409,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DroneVideoControlPage(
|
||||
droneSn: detail.deviceSn,
|
||||
gatewaySn: detail.gatewaySn,
|
||||
// 使用无人机摄像头列表,如果为null则传空列表
|
||||
cameraList: detail.droneCameraList ?? [],
|
||||
// 传入网关摄像头列表作为备选视频源
|
||||
gatewayCameraList: detail.gatewayCameraList ?? [],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -11,12 +11,16 @@ import '../bloc/drone_station_state.dart';
|
||||
/// 无人机视频回传/远程控制页面
|
||||
class DroneVideoControlPage extends StatefulWidget {
|
||||
final String droneSn; // 无人机设备序列号
|
||||
final List<CameraInfo>? cameraList; // 摄像头列表
|
||||
final String gatewaySn; // 网关(机场)设备序列号
|
||||
final List<CameraInfo>? cameraList; // 无人机摄像头列表
|
||||
final List<CameraInfo>? gatewayCameraList; // 网关摄像头列表(备选)
|
||||
|
||||
const DroneVideoControlPage({
|
||||
super.key,
|
||||
required this.droneSn,
|
||||
required this.gatewaySn,
|
||||
this.cameraList,
|
||||
this.gatewayCameraList,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -31,43 +35,73 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
String? _errorMessage;
|
||||
UavLensType? _currentLensType;
|
||||
CameraInfo? _currentCamera; // 当前选中的摄像头
|
||||
|
||||
|
||||
// 备选视频源相关
|
||||
bool _useBackupSource = false; // 是否使用备选视频源
|
||||
List<CameraInfo> _effectiveCameraList = []; // 实际使用的摄像头列表
|
||||
List<CameraInfo>? _backupCameraList; // 备选摄像头列表(网关摄像头)
|
||||
|
||||
// 火山引擎 RTC
|
||||
volc.RTCEngine? _rtcEngine;
|
||||
volc.RTCRoom? _rtcRoom;
|
||||
volc.RTCViewContext? _remoteRenderContext;
|
||||
String? _remoteUserId;
|
||||
|
||||
|
||||
// 事件处理器(注意:使用 I 前缀的接口)
|
||||
final volc.IRTCEngineEventHandler _engineEventHandler = volc.IRTCEngineEventHandler();
|
||||
final volc.IRTCRoomEventHandler _roomEventHandler = volc.IRTCRoomEventHandler();
|
||||
final volc.IRTCEngineEventHandler _engineEventHandler =
|
||||
volc.IRTCEngineEventHandler();
|
||||
final volc.IRTCRoomEventHandler _roomEventHandler =
|
||||
volc.IRTCRoomEventHandler();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
|
||||
|
||||
// 初始化事件处理器
|
||||
_initVolcEventHandlers();
|
||||
|
||||
|
||||
// 打印接收到的参数
|
||||
debugPrint('=== 视频控制页面接收参数 ===');
|
||||
debugPrint('droneSn: ${widget.droneSn}');
|
||||
debugPrint('gatewaySn: ${widget.gatewaySn}');
|
||||
debugPrint('cameraList: ${widget.cameraList}');
|
||||
debugPrint('gatewayCameraList: ${widget.gatewayCameraList}');
|
||||
if (widget.cameraList != null) {
|
||||
for (var camera in widget.cameraList!) {
|
||||
debugPrint(' - cameraIndex: ${camera.cameraIndex}');
|
||||
debugPrint(' - 无人机摄像头: ${camera.cameraIndex}');
|
||||
}
|
||||
}
|
||||
if (widget.gatewayCameraList != null) {
|
||||
for (var camera in widget.gatewayCameraList!) {
|
||||
debugPrint(' - 网关摄像头: ${camera.cameraIndex}');
|
||||
}
|
||||
}
|
||||
debugPrint('===========================\n');
|
||||
|
||||
|
||||
// 保存备选摄像头列表
|
||||
_backupCameraList = widget.gatewayCameraList;
|
||||
|
||||
// 确定实际使用的摄像头列表
|
||||
_effectiveCameraList = widget.cameraList ?? [];
|
||||
|
||||
// 检查是否需要使用备选视频源
|
||||
if (_effectiveCameraList.isEmpty &&
|
||||
widget.gatewayCameraList != null &&
|
||||
widget.gatewayCameraList!.isNotEmpty) {
|
||||
debugPrint('⚠️ 无人机摄像头列表为空,自动切换到备选视频源(网关摄像头)');
|
||||
_useBackupSource = true;
|
||||
_effectiveCameraList = widget.gatewayCameraList!;
|
||||
}
|
||||
|
||||
// 默认选择第一个摄像头
|
||||
_currentCamera = widget.cameraList?.isNotEmpty == true
|
||||
? widget.cameraList!.first
|
||||
_currentCamera = _effectiveCameraList.isNotEmpty
|
||||
? _effectiveCameraList.first
|
||||
: null;
|
||||
|
||||
|
||||
debugPrint('默认选择的摄像头: ${_currentCamera?.cameraIndex}');
|
||||
|
||||
debugPrint('当前视频源: ${_useBackupSource ? "备选(网关)" : "主视频源(无人机)"}');
|
||||
|
||||
// 默认加载广角镜头
|
||||
if (_currentCamera != null) {
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
@@ -113,6 +147,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
debugPrint('当前摄像头: ${_currentCamera!.cameraIndex}');
|
||||
debugPrint('新镜头类型: ${lensType.name} (${_getLensTypeName(lensType)})');
|
||||
debugPrint('旧镜头类型: ${_currentLensType?.name ?? "none"}');
|
||||
debugPrint('视频源: ${_useBackupSource ? "备选(网关)" : "主视频源(无人机)"}');
|
||||
debugPrint('==================\n');
|
||||
|
||||
setState(() {
|
||||
@@ -121,9 +156,14 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
_currentLensType = lensType;
|
||||
});
|
||||
|
||||
// 根据是否使用备选视频源决定使用哪个 SN
|
||||
final effectiveSn = _useBackupSource ? widget.gatewaySn : widget.droneSn;
|
||||
|
||||
debugPrint('📤 请求视频流 - SN: $effectiveSn, 摄像头: ${_currentCamera!.cameraIndex}');
|
||||
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
sn: effectiveSn,
|
||||
cameraIndex: _currentCamera!.cameraIndex,
|
||||
lensType: lensType,
|
||||
qualityType: VideoQualityType.adaptive,
|
||||
@@ -137,21 +177,50 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
if (camera.cameraIndex == _currentCamera?.cameraIndex) {
|
||||
return; // 如果选择的摄像头与当前相同,不执行操作
|
||||
}
|
||||
|
||||
|
||||
setState(() {
|
||||
_currentCamera = camera;
|
||||
});
|
||||
|
||||
|
||||
// 重新加载视频流
|
||||
_loadVideoStream(_currentLensType ?? UavLensType.wide);
|
||||
}
|
||||
|
||||
/// 切换备选视频源
|
||||
void _toggleBackupSource(bool useBackup) {
|
||||
debugPrint('=== 🔄 切换视频源 ===');
|
||||
debugPrint('切换到: ${useBackup ? "备选(网关)" : "主视频源(无人机)"}');
|
||||
|
||||
setState(() {
|
||||
_useBackupSource = useBackup;
|
||||
// 更新实际使用的摄像头列表
|
||||
_effectiveCameraList = useBackup
|
||||
? (_backupCameraList ?? [])
|
||||
: (widget.cameraList ?? []);
|
||||
// 选择新列表的第一个摄像头
|
||||
_currentCamera = _effectiveCameraList.isNotEmpty ? _effectiveCameraList.first : null;
|
||||
});
|
||||
|
||||
debugPrint('新摄像头列表长度: ${_effectiveCameraList.length}');
|
||||
debugPrint('选中的摄像头: ${_currentCamera?.cameraIndex}');
|
||||
|
||||
// 重新加载视频流
|
||||
if (_currentCamera != null) {
|
||||
_loadVideoStream(_currentLensType ?? UavLensType.wide);
|
||||
} else {
|
||||
setState(() {
|
||||
_errorMessage = useBackup ? '备选视频源没有可用摄像头' : '没有可用的摄像头';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化火山引擎事件处理器
|
||||
void _initVolcEventHandlers() {
|
||||
_engineEventHandler.onWarning = (volc.WarningCode code) {
|
||||
debugPrint('⚠️ Volc Warning: $code');
|
||||
};
|
||||
|
||||
|
||||
_engineEventHandler.onError = (volc.ErrorCode code) {
|
||||
debugPrint('❌ Volc Error: $code');
|
||||
if (mounted) {
|
||||
@@ -162,37 +231,35 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
}
|
||||
};
|
||||
|
||||
_engineEventHandler.onFirstRemoteVideoFrameDecoded = (
|
||||
String streamId,
|
||||
volc.StreamInfo streamInfo,
|
||||
volc.VideoFrameInfo frameInfo,
|
||||
) {
|
||||
debugPrint('✅✅✅ Volc 第一帧视频解码完成!');
|
||||
debugPrint(' streamId: $streamId, userId: ${streamInfo.userId}');
|
||||
|
||||
if (streamInfo.userId.isNotEmpty && mounted) {
|
||||
setState(() {
|
||||
_remoteUserId = streamInfo.userId;
|
||||
_remoteRenderContext = volc.RTCViewContext.remoteContext(
|
||||
roomId: _videoStream?.roomId ?? '',
|
||||
userId: streamInfo.userId,
|
||||
streamId: streamId,
|
||||
);
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
_engineEventHandler.onFirstRemoteVideoFrameDecoded =
|
||||
(
|
||||
String streamId,
|
||||
volc.StreamInfo streamInfo,
|
||||
volc.VideoFrameInfo frameInfo,
|
||||
) {
|
||||
debugPrint('✅✅✅ Volc 第一帧视频解码完成!');
|
||||
debugPrint(' streamId: $streamId, userId: ${streamInfo.userId}');
|
||||
|
||||
_roomEventHandler.onUserPublishStreamVideo = (
|
||||
String userId,
|
||||
volc.StreamInfo streamInfo,
|
||||
bool isPublish,
|
||||
) {
|
||||
debugPrint('📹 Volc 远端用户 $userId 视频流状态: $isPublish');
|
||||
if (isPublish && mounted && _remoteUserId == null) {
|
||||
debugPrint('⏳ 检测到 Volc 视频流推送,等待第一帧解码...');
|
||||
}
|
||||
};
|
||||
if (streamInfo.userId.isNotEmpty && mounted) {
|
||||
setState(() {
|
||||
_remoteUserId = streamInfo.userId;
|
||||
_remoteRenderContext = volc.RTCViewContext.remoteContext(
|
||||
roomId: _videoStream?.roomId ?? '',
|
||||
userId: streamInfo.userId,
|
||||
streamId: streamId,
|
||||
);
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_roomEventHandler.onUserPublishStreamVideo =
|
||||
(String userId, volc.StreamInfo streamInfo, bool isPublish) {
|
||||
debugPrint('📹 Volc 远端用户 $userId 视频流状态: $isPublish');
|
||||
if (isPublish && mounted && _remoteUserId == null) {
|
||||
debugPrint('⏳ 检测到 Volc 视频流推送,等待第一帧解码...');
|
||||
}
|
||||
};
|
||||
|
||||
_roomEventHandler.onUserLeave = (String userId, int reason) {
|
||||
debugPrint('👋 Volc 用户离开: $userId');
|
||||
@@ -305,36 +372,100 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
if (widget.cameraList != null && widget.cameraList!.isNotEmpty)
|
||||
if (_effectiveCameraList.isNotEmpty || _backupCameraList?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<CameraInfo>(
|
||||
value: _currentCamera,
|
||||
hint: const Text(
|
||||
'选择摄像头',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||
),
|
||||
items: widget.cameraList!.map((camera) {
|
||||
return DropdownMenuItem<CameraInfo>(
|
||||
value: camera,
|
||||
child: Text(
|
||||
camera.cameraIndex,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 备选视频源切换按钮
|
||||
if (_backupCameraList?.isNotEmpty == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<bool>(
|
||||
value: _useBackupSource,
|
||||
items: [
|
||||
DropdownMenuItem<bool>(
|
||||
value: false,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.flight, size: 16, color: Color(0xFF165DFF)),
|
||||
SizedBox(width: 4),
|
||||
Text('无人机', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<bool>(
|
||||
value: true,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.home_work, size: 16, color: Color(0xFFFF7D00)),
|
||||
SizedBox(width: 4),
|
||||
Text('备选', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (bool? useBackup) {
|
||||
if (useBackup != null && useBackup != _useBackupSource) {
|
||||
_toggleBackupSource(useBackup);
|
||||
}
|
||||
},
|
||||
isDense: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (CameraInfo? newCamera) {
|
||||
if (newCamera != null) {
|
||||
_switchCamera(newCamera);
|
||||
}
|
||||
},
|
||||
isDense: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
// 摄像头选择下拉框
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton<CameraInfo>(
|
||||
value: _currentCamera,
|
||||
hint: const Text(
|
||||
'选择摄像头',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
items: _effectiveCameraList.map((camera) {
|
||||
return DropdownMenuItem<CameraInfo>(
|
||||
value: camera,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_useBackupSource ? Icons.home_work : Icons.videocam,
|
||||
size: 16,
|
||||
color: _useBackupSource ? const Color(0xFFFF7D00) : const Color(0xFF165DFF),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
camera.cameraIndex,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (CameraInfo? newCamera) {
|
||||
if (newCamera != null) {
|
||||
_switchCamera(newCamera);
|
||||
}
|
||||
},
|
||||
isDense: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -355,7 +486,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
});
|
||||
debugPrint('=== 视频流加载成功 ===');
|
||||
debugPrint('URL Type: ${state.videoStream.urlType}');
|
||||
|
||||
|
||||
if (state.videoStream.urlType == 'volc') {
|
||||
_destroyRtcEngine();
|
||||
_initRtcEngine(state.videoStream);
|
||||
@@ -439,7 +570,9 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFF2F3F5),
|
||||
color: isSelected
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
@@ -473,7 +606,9 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
@@ -484,46 +619,68 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
),
|
||||
)
|
||||
: _errorMessage != null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.white, size: 48),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _loadVideoStream(_currentLensType ?? UavLensType.wide),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF165DFF)),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
)
|
||||
: _videoStream != null && _remoteRenderContext != null
|
||||
? volc.RTCSurfaceView(
|
||||
context: _remoteRenderContext!,
|
||||
renderMode: volc.VideoRenderMode.fit,
|
||||
)
|
||||
: _videoStream != null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.videocam_off, size: 48, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
const Text('等待视频流推送...', style: TextStyle(fontSize: 14, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Image.asset('assets/images/xunjian.png', fit: BoxFit.cover, width: double.infinity),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _loadVideoStream(
|
||||
_currentLensType ?? UavLensType.wide,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _videoStream != null && _remoteRenderContext != null
|
||||
? volc.RTCSurfaceView(
|
||||
context: _remoteRenderContext!,
|
||||
renderMode: volc.VideoRenderMode.fit,
|
||||
)
|
||||
: _videoStream != null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.videocam_off,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'等待视频流推送...',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -531,9 +688,23 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
left: 12,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 8, height: 8, decoration: const BoxDecoration(color: Color(0xFFF53F3F), shape: BoxShape.circle)),
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF53F3F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text('REC 00:12:36', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)),
|
||||
const Text(
|
||||
'REC 00:12:36',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -550,9 +721,16 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_getLensTypeName(_currentLensType ?? UavLensType.wide), style: const TextStyle(fontSize: 12, color: Colors.white)),
|
||||
Text(
|
||||
_getLensTypeName(_currentLensType ?? UavLensType.wide),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.white, size: 16),
|
||||
const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -565,11 +743,21 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
debugPrint('⚠️ 镜头类型相同,忽略操作');
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<UavLensType>>[
|
||||
const PopupMenuItem<UavLensType>(value: UavLensType.wide, child: Text('广角')),
|
||||
const PopupMenuItem<UavLensType>(value: UavLensType.zoom, child: Text('变焦')),
|
||||
const PopupMenuItem<UavLensType>(value: UavLensType.ir, child: Text('红外')),
|
||||
],
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<UavLensType>>[
|
||||
const PopupMenuItem<UavLensType>(
|
||||
value: UavLensType.wide,
|
||||
child: Text('广角'),
|
||||
),
|
||||
const PopupMenuItem<UavLensType>(
|
||||
value: UavLensType.zoom,
|
||||
child: Text('变焦'),
|
||||
),
|
||||
const PopupMenuItem<UavLensType>(
|
||||
value: UavLensType.ir,
|
||||
child: Text('红外'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -582,7 +770,13 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -603,9 +797,19 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF86909C))),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -617,12 +821,25 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('AI 识别结果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||
const Text(
|
||||
'AI 识别结果',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildAIResultItem('热斑疑似', '3 处'),
|
||||
const SizedBox(height: 8),
|
||||
@@ -637,10 +854,22 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, color: Color(0xFFFF7D00), size: 20),
|
||||
const Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Color(0xFFFF7D00),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)))),
|
||||
Text(count, style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969))),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
count,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.chevron_right, color: Color(0xFF86909C), size: 20),
|
||||
],
|
||||
@@ -657,11 +886,21 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset('assets/images/xunjian.png', fit: BoxFit.cover, width: double.infinity),
|
||||
child: Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -672,16 +911,69 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
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: () {})),
|
||||
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: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -696,7 +988,13 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
@@ -717,10 +1015,12 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
children: [
|
||||
Icon(icon, size: 28, color: const Color(0xFF4E5969)),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969))),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,19 +93,35 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
final data = osd.rawData;
|
||||
|
||||
// 解析嵌套的 JSON 结构
|
||||
final droneData = data['data'] is Map
|
||||
? (data['data'] as Map)['drone']
|
||||
: null;
|
||||
if (droneData == null || droneData is! Map) {
|
||||
debugPrint('⚠️ [DroneOsdCard] 无法解析 drone 数据');
|
||||
// 优先级:先尝试 drone 字段,如果不存在则使用 host 字段
|
||||
final dataMap = data['data'] is Map ? data['data'] as Map : null;
|
||||
Map? droneData;
|
||||
if (dataMap != null) {
|
||||
// 优先使用 drone 字段
|
||||
if (dataMap['drone'] is Map) {
|
||||
droneData = dataMap['drone'] as Map;
|
||||
} else if (dataMap['host'] is Map) {
|
||||
// 如果没有 drone 字段,尝试 host 字段(机场设备的数据结构)
|
||||
droneData = dataMap['host'] as Map;
|
||||
}
|
||||
}
|
||||
|
||||
if (droneData == null) {
|
||||
debugPrint('⚠️ [DroneOsdCard] 无法解析 drone 或 host 数据');
|
||||
debugPrint('📋 [DroneOsdCard] 原始数据结构: ${data.keys.toList()}');
|
||||
if (dataMap != null) {
|
||||
debugPrint('📋 [DroneOsdCard] data 结构: ${dataMap.keys.toList()}');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('📊 [DroneOsdCard] drone 数据键: ${droneData.keys.toList()}');
|
||||
debugPrint('📊 [DroneOsdCard] 数据键: ${droneData.keys.toList()}');
|
||||
|
||||
// ========== 1. 基础飞行信息 ==========
|
||||
// 无人机高度(height)
|
||||
double? height = (droneData['height'] as num?)?.toDouble();
|
||||
// 无人机高度(height / altitude)
|
||||
double? height =
|
||||
(droneData['height'] as num?)?.toDouble() ??
|
||||
(droneData['altitude'] as num?)?.toDouble();
|
||||
|
||||
// 飞行速度(ground_speed)
|
||||
double? groundSpeed = (droneData['ground_speed'] as num?)?.toDouble();
|
||||
@@ -117,18 +133,32 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
int? flightTime = droneData['flight_time'] as int?;
|
||||
|
||||
// ========== 2. 电池信息 ==========
|
||||
// 电量百分比(battery_percent)
|
||||
int? batteryPercent = droneData['battery_percent'] as int?;
|
||||
// 电量百分比 - 支持两种结构:battery_percent 或 battery.capacity_percent
|
||||
final batteryMap = droneData['battery'] as Map?;
|
||||
int? batteryPercent =
|
||||
droneData['battery_percent'] as int? ??
|
||||
batteryMap?['capacity_percent'] as int?;
|
||||
|
||||
// 电池电压(battery_voltage)
|
||||
// 电池电压(battery_voltage 或 battery.batteries[0].voltage)
|
||||
double? batteryVoltage = (droneData['battery_voltage'] as num?)?.toDouble();
|
||||
if (batteryVoltage == null && batteryMap != null) {
|
||||
final batteries = batteryMap['batteries'] as List?;
|
||||
if (batteries != null && batteries.isNotEmpty) {
|
||||
final firstBattery = batteries[0] as Map?;
|
||||
batteryVoltage = (firstBattery?['voltage'] as num?)?.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
// 电池电流(battery_current)
|
||||
double? batteryCurrent = (droneData['battery_current'] as num?)?.toDouble();
|
||||
|
||||
// 电池温度(battery_temperature)
|
||||
// 电池温度(battery_temperature 或 battery.batteries[0].temperature)
|
||||
double? batteryTemp = (droneData['battery_temperature'] as num?)
|
||||
?.toDouble();
|
||||
if (batteryTemp == null && batteryMap != null) {
|
||||
final batteries = batteryMap['batteries'] as List?;
|
||||
if (batteries != null && batteries.isNotEmpty) {
|
||||
final firstBattery = batteries[0] as Map?;
|
||||
batteryTemp = (firstBattery?['temperature'] as num?)?.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. 位置与姿态 ==========
|
||||
// 纬度(latitude)
|
||||
@@ -137,14 +167,20 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
// 经度(longitude)
|
||||
double? longitude = (droneData['longitude'] as num?)?.toDouble();
|
||||
|
||||
// 航向角(heading)
|
||||
double? heading = (droneData['heading'] as num?)?.toDouble();
|
||||
// 航向角(heading / attitude_head)
|
||||
double? heading =
|
||||
(droneData['heading'] as num?)?.toDouble() ??
|
||||
(droneData['attitude_head'] as num?)?.toDouble();
|
||||
|
||||
// 俯仰角(pitch)
|
||||
double? pitch = (droneData['pitch'] as num?)?.toDouble();
|
||||
// 俯仰角(pitch / attitude_pitch)
|
||||
double? pitch =
|
||||
(droneData['pitch'] as num?)?.toDouble() ??
|
||||
(droneData['attitude_pitch'] as num?)?.toDouble();
|
||||
|
||||
// 横滚角(roll)
|
||||
double? roll = (droneData['roll'] as num?)?.toDouble();
|
||||
// 横滚角(roll / attitude_roll)
|
||||
double? roll =
|
||||
(droneData['roll'] as num?)?.toDouble() ??
|
||||
(droneData['attitude_roll'] as num?)?.toDouble();
|
||||
|
||||
// ========== 4. GPS 状态 ==========
|
||||
// GPS卫星数(gps_satellites)
|
||||
|
||||
Reference in New Issue
Block a user