Playback: the flight screen's playback button opens the album over the live view, so a video tapped there plays on the aircraft into the feed with a transport bar. Long press selects files to download or delete (delete asks first); one file opens in a viewer with the camera's preview. Leaving the album puts the camera back in its capture mode. Flight records: every flight is recorded from take-off to landing (1 Hz track, max height / distance / speed, distance flown, battery used) and kept on the phone. When it lands while signed in, it is uploaded to the PilotVault logbook through the existing /api/drones/auto and /api/flights endpoints as an automatic-path entry with the track as GeoJSON. Simulator flights are never uploaded; flights recorded while signed out retry on sign-in. Flight logs lists them with their upload state and a track map, and the profile stats come from real flights. Find My Drone: the aircraft's last fix is kept across lost links and restarts, and shown against the phone's own GPS with distance, direction and a hand-off to the maps app. Live streaming: a new LiveStreamBridge streams the main camera to an RTMP address set in Settings > General, with a LIVE pill on the flight screen that turns amber when the uplink falls behind and stops on tap. Pre-flight checklist (manual checks plus the aircraft's live status) and an in-app Academy with six short guides that work offline. None of this has run on hardware yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.4 KiB
Dart
109 lines
4.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// One thing the pilot checks by hand before a flight (plan P9 pre-flight
|
|
/// checklist). The aircraft's own checks are `AircraftChecklist`'s live rows.
|
|
@immutable
|
|
class PreflightItem {
|
|
const PreflightItem(this.id, this.group, this.title, this.detail);
|
|
|
|
final String id;
|
|
final String group;
|
|
final String title;
|
|
final String detail;
|
|
}
|
|
|
|
/// The manual checks, in walk-around order.
|
|
const List<PreflightItem> preflightItems = <PreflightItem>[
|
|
PreflightItem('site', 'Site', 'Airspace and site checked',
|
|
'Fly zones on the map, local restrictions, permission for the take-off spot, and people who are not part of the flight kept clear.'),
|
|
PreflightItem('weather', 'Site', 'Weather within limits',
|
|
'Wind below the aircraft\'s limit at height, no rain or fog, and enough daylight for the whole flight.'),
|
|
PreflightItem('takeoffArea', 'Site', 'Take-off area clear', 'Flat, open, away from metal and magnetic interference, with room to land.'),
|
|
PreflightItem('props', 'Aircraft', 'Propellers secure and undamaged', 'No nicks or cracks; each one locked on the right motor.'),
|
|
PreflightItem('frame', 'Aircraft', 'Arms unfolded, gimbal clamp off', 'Frame undamaged, lens and vision sensors clean.'),
|
|
PreflightItem('batteries', 'Aircraft', 'Batteries charged and seated',
|
|
'Aircraft battery clicked in and warm enough; remote controller and phone charged.'),
|
|
PreflightItem('sdcard', 'Aircraft', 'SD card in, with space', 'Format it in the camera if it is full of old flights you have already saved.'),
|
|
PreflightItem('operatorId', 'Paperwork', 'Operator ID on the aircraft', 'Your registration number is displayed and your certificate and insurance are valid.'),
|
|
PreflightItem('rth', 'Plan', 'Return-to-home height set', 'Above the tallest obstacle between the flight area and home.'),
|
|
PreflightItem('crew', 'Plan', 'Observer briefed, phone on flight mode', 'Everyone involved knows the plan, and calls cannot cover the live view.'),
|
|
];
|
|
|
|
/// The pilot's ticks for the current session, kept on the phone so a restart
|
|
/// mid-preparation keeps them. A session goes stale after [ttl]: the next
|
|
/// flight day starts unticked.
|
|
class PreflightChecklist extends ChangeNotifier {
|
|
PreflightChecklist({
|
|
DateTime Function()? clock,
|
|
this.ttl = const Duration(hours: 6),
|
|
Future<SharedPreferences> Function()? prefs,
|
|
this.items = preflightItems,
|
|
}) : _clock = clock ?? DateTime.now,
|
|
_prefs = prefs ?? SharedPreferences.getInstance;
|
|
|
|
static const String _key = 'fly.preflightChecklist';
|
|
|
|
final DateTime Function() _clock;
|
|
final Duration ttl;
|
|
final Future<SharedPreferences> Function() _prefs;
|
|
final List<PreflightItem> items;
|
|
|
|
Set<String> _ticked = <String>{};
|
|
DateTime? _startedAt;
|
|
|
|
bool isTicked(PreflightItem i) => _ticked.contains(i.id);
|
|
int get done => items.where(isTicked).length;
|
|
bool get complete => done == items.length;
|
|
|
|
/// The groups in order of first appearance.
|
|
List<String> get groups => <String>{for (final PreflightItem i in items) i.group}.toList();
|
|
|
|
Future<void> load() async {
|
|
try {
|
|
final String? raw = (await _prefs()).getString(_key);
|
|
final Object? j = raw == null ? null : jsonDecode(raw);
|
|
if (j is! Map) return;
|
|
final Object? at = j['at'];
|
|
final DateTime? started = at is num ? DateTime.fromMillisecondsSinceEpoch(at.toInt()) : null;
|
|
if (started == null || _clock().difference(started) > ttl) return;
|
|
_startedAt = started;
|
|
_ticked = <String>{for (final Object? id in (j['ticked'] as List<Object?>? ?? const <Object?>[])) if (id is String) id};
|
|
notifyListeners();
|
|
} catch (_) {
|
|
// Nothing saved: start unticked.
|
|
}
|
|
}
|
|
|
|
void toggle(PreflightItem i) {
|
|
final DateTime now = _clock();
|
|
if (_startedAt == null || now.difference(_startedAt!) > ttl) {
|
|
_ticked = <String>{};
|
|
_startedAt = now;
|
|
}
|
|
if (!_ticked.remove(i.id)) _ticked.add(i.id);
|
|
notifyListeners();
|
|
_save();
|
|
}
|
|
|
|
void reset() {
|
|
_ticked = <String>{};
|
|
_startedAt = null;
|
|
notifyListeners();
|
|
_save();
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
try {
|
|
final SharedPreferences p = await _prefs();
|
|
if (_startedAt == null) {
|
|
await p.remove(_key);
|
|
} else {
|
|
await p.setString(_key, jsonEncode(<String, Object?>{'at': _startedAt!.millisecondsSinceEpoch, 'ticked': _ticked.toList()..sort()}));
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
}
|