import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/telemetry/drone_event.dart'; import '../dmc_colors.dart'; import '../providers/drone_event_log_provider.dart'; import '../providers/telemetry_provider.dart'; enum _PanelTab { status, warnings } /// Vollbild-Statusmenue fuer den Fly-Modus (Doku: "ein neues menue anlegen /// fuer die neue drone status pille, nach dem vorbild des menues fuer /// drones and missions, aber jetzt fuer drone status and warnings") - /// dieselbe Tab-Struktur wie [MissionsDronesPanel], nur mit den beiden /// telemetriebezogenen Tabs Status/Warnings statt Missions/Drones. /// Erreichbar ueber die Drohnen-Status-Pille (Status-Tab) bzw. den /// Warnungs-Knopf (Warnings-Tab) in der Fusszeile im Fly-Modus (siehe /// bottom_stats_bar.dart/fly_screen.dart). class DroneStatusWarningsPanel extends ConsumerStatefulWidget { const DroneStatusWarningsPanel.status({super.key}) : _initialTab = _PanelTab.status; const DroneStatusWarningsPanel.warnings({super.key}) : _initialTab = _PanelTab.warnings; final _PanelTab _initialTab; @override ConsumerState createState() => _DroneStatusWarningsPanelState(); } class _DroneStatusWarningsPanelState extends ConsumerState { late _PanelTab _tab = widget._initialTab; @override Widget build(BuildContext context) { return Material( color: const Color(0xEB080808), child: SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 8, 12), child: Row( children: [ const Expanded( child: Text( 'Drone Status & Warnings', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700, ), ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: Colors.white), ), ], ), ), Row( children: [ _tabButton('Status', _PanelTab.status), _tabButton('Warnings', _PanelTab.warnings), ], ), const Divider(color: Colors.white24, height: 1), Expanded( child: switch (_tab) { _PanelTab.status => _statusTab(), _PanelTab.warnings => _warningsTab(), }, ), ], ), ), ); } Widget _tabButton(String label, _PanelTab tab) { final active = _tab == tab; return Expanded( child: InkWell( onTap: () => setState(() => _tab = tab), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: active ? const Color(0xFF4D9DFF) : Colors.transparent, width: 2, ), ), ), child: Text( label, textAlign: TextAlign.center, style: TextStyle( color: active ? Colors.white : Colors.white60, fontWeight: FontWeight.w600, fontSize: 14, ), ), ), ), ); } // --- Status --- Widget _statusTab() { final telemetry = ref.watch(telemetryProvider); return telemetry.when( data: (frame) => ListView( padding: const EdgeInsets.all(16), children: [ _statusRow('GPS satellites', '${frame.numSat}'), _statusRow( 'GPS precision (HDOP)', frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—', ), _statusRow( 'GPS coordinates', frame.hasFix ? '${frame.lat.toStringAsFixed(6)}, ${frame.lon.toStringAsFixed(6)}' : '—', ), _statusRow('Current draw', '${frame.currentA.toStringAsFixed(1)} A'), _statusRow('Battery voltage', '${frame.batteryVoltage.toStringAsFixed(2)} V'), _statusRow('Heading', '${frame.headingDeg.round()}°'), _statusRow('RX quality / SNR', '${frame.linkQuality}% / ${frame.snrDb} dB'), // Downlink-Qualitaet/-SNR und die Akku-SoC des Sendemoduls selbst // (nicht der Drohne) sind ueber keinen MSP-GET-Befehl abrufbar - // rxLinkStatistics_t (iNAV 9.1, rx.h) hat zwar ein downlinkLQ-Feld, // das wird aber nur per MSP2_COMMON_SET_MSP_RC_LINK_STATS von // aussen HINEINgeschrieben, nie per GET zurueckgelesen; eine // Downlink-SNR oder eine Sender-Akkugroesse existiert im Struct // ueberhaupt nicht (verifiziert im lokalen iNAV-Checkout, // release/9.1). _statusRow('TX battery SoC / quality / SNR', 'Not available (not exposed via MSP)'), ], ), loading: () => const Center( child: CircularProgressIndicator(color: Color(0xFF4D9DFF)), ), error: (err, st) => const Center( child: Text('No telemetry', style: TextStyle(color: Colors.white60, fontSize: 15)), ), ); } Widget _statusRow(String label, String value) { return Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Expanded(child: Text(label, style: const TextStyle(color: Colors.white70))), Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600)), ], ), ); } // --- Warnings --- Widget _warningsTab() { final events = ref.watch(droneEventLogProvider); if (events.isEmpty) { return const Center( child: Text('No events yet', style: TextStyle(color: Colors.white60, fontSize: 15)), ); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: events.length, itemBuilder: (context, index) => _eventRow(events[index]), ); } Widget _eventRow(DroneEvent event) { final color = event.severity == DroneEventSeverity.critical ? DmcColors.warnRed : DmcColors.pafAmber; return Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.04), border: Border.all(color: color.withValues(alpha: 0.4)), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Text(_formatTime(event.timestamp), style: const TextStyle(color: Colors.white60)), const SizedBox(width: 12), Expanded( child: Text( event.message, style: TextStyle(color: color, fontWeight: FontWeight.w600), ), ), ], ), ); } String _formatTime(DateTime dt) => '${dt.hour.toString().padLeft(2, '0')}:' '${dt.minute.toString().padLeft(2, '0')}:' '${dt.second.toString().padLeft(2, '0')}'; }