1.测试和修改了无人机在线实时信息的展示数据集的展示。使其能够全面展示。
2.添加了在无人机设备摄像头的为空时候,通过新增的接口获取无人机的摄像头的列表。 3.替换原先依照tcp推送消息绘制机器运动作业的轨迹过程,先用订阅mqtt特定话题推送的信息做动画过程的绘制。
This commit is contained in:
@@ -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