A Flutter app that runs next to DJI Fly without the DJI SDK. It receives the aircraft's Remote ID (Bluetooth, BT5, Wi-Fi NaN and Beacon) in a foreground service, streams the paired aircraft to the hub over /ws/device, and turns take-off to landing into a logbook entry filed through /api/flights, queued when offline. Covers phases A0-A3 of the Fly Agent plan; needs no server change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.1 KiB
Dart
99 lines
3.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../services.dart';
|
|
import '../theme.dart';
|
|
import '../uplink/uplink.dart';
|
|
import 'flight_tab.dart';
|
|
import 'logbook_tab.dart';
|
|
import 'nearby_tab.dart';
|
|
import 'settings_page.dart';
|
|
|
|
/// Signed-in home: Flight (session), Nearby (Remote ID receiver), Logbook.
|
|
class AgentShell extends StatefulWidget {
|
|
const AgentShell({super.key});
|
|
|
|
@override
|
|
State<AgentShell> createState() => _AgentShellState();
|
|
}
|
|
|
|
class _AgentShellState extends State<AgentShell> {
|
|
int _tab = 0;
|
|
bool _started = false;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_started) return;
|
|
_started = true;
|
|
// Signing in is what turns the Agent on: GPS, the receiver and the uplink
|
|
// all start here so the pilot only has to open DJI Fly.
|
|
final Services s = Services.of(context);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
await s.pilot.start();
|
|
if (!s.rid.running) {
|
|
try {
|
|
await s.rid.start();
|
|
} catch (_) {
|
|
// Shown on the Nearby tab; the session still works by hand.
|
|
}
|
|
}
|
|
s.uplink.enable(s.api.serverUrl);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
const List<String> titles = <String>['Flight', 'Nearby aircraft', 'Logbook'];
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(titles[_tab]),
|
|
actions: <Widget>[
|
|
const _UplinkChip(),
|
|
IconButton(
|
|
tooltip: 'Settings',
|
|
icon: const Icon(Icons.settings_outlined),
|
|
onPressed: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SettingsPage())),
|
|
),
|
|
],
|
|
),
|
|
body: IndexedStack(index: _tab, children: const <Widget>[FlightTab(), NearbyTab(), LogbookTab()]),
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: _tab,
|
|
onDestinationSelected: (int i) => setState(() => _tab = i),
|
|
destinations: const <NavigationDestination>[
|
|
NavigationDestination(icon: Icon(Icons.flight_takeoff), label: 'Flight'),
|
|
NavigationDestination(icon: Icon(Icons.radar), label: 'Nearby'),
|
|
NavigationDestination(icon: Icon(Icons.menu_book_outlined), label: 'Logbook'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _UplinkChip extends StatelessWidget {
|
|
const _UplinkChip();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Uplink uplink = Services.of(context).uplink;
|
|
return ListenableBuilder(
|
|
listenable: uplink,
|
|
builder: (BuildContext context, _) {
|
|
final (Color c, String t) = switch (uplink.status) {
|
|
UplinkStatus.live => (PV.ready, 'LIVE'),
|
|
UplinkStatus.connecting => (PV.inkMuted, 'IDLE'),
|
|
UplinkStatus.offline => (PV.caution, 'OFFLINE'),
|
|
UplinkStatus.off => (PV.inkMuted, 'OFF'),
|
|
};
|
|
return Tooltip(
|
|
message: 'Live uplink to PilotVault',
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
child: Row(children: <Widget>[StatusDot(c), const SizedBox(width: 6), Text(t, style: PV.label)]),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|