Files
PilotVault/Fly App/lib/fly/preflight_checklist.dart
T
tajniak81andClaude Opus 5.5 91ca150a35 Give the Fly App GO 4-style playback, flight records, Find My Drone and live streaming
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>
2026-10-20 22:07:10 +02:00

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 (_) {}
}
}