1.测试和修改了无人机在线实时信息的展示数据集的展示。使其能够全面展示。

2.添加了在无人机设备摄像头的为空时候,通过新增的接口获取无人机的摄像头的列表。
3.替换原先依照tcp推送消息绘制机器运动作业的轨迹过程,先用订阅mqtt特定话题推送的信息做动画过程的绘制。
This commit is contained in:
2026-06-24 14:44:16 +08:00
parent b7ea788298
commit d5a83c1221
7 changed files with 689 additions and 185 deletions

View File

@@ -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 ?? [],
),
),
);

View File

@@ -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)),
),
],
),
);
}
}

View File

@@ -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)