Files
PilotVault/Fly App/lib/flight_model.dart
T
tajniak81andClaude Opus 4.8 ec4b1bcdd3 Show drone serial number and firmware in the Fly App
Firmware was already half-wired: the bridge read
BaseProduct.getFirmwarePackageVersion() once, at connect. The SDK
returns null there until it has finished handshaking with the
aircraft, so the About panel almost always showed "-" instead. The
serial was never fetched at all.

Resolve both asynchronously after connect. fetchIdentity() reads the
serial from FlightController.getSerialNumber() and the firmware from
the product package version, falling back to the component-level
getFirmwareVersion() for aircraft that only report the latter. It
re-checks every 2s (max 6 attempts) and emits each value on a new
`identity` event as it lands, so the serial still appears when
firmware never resolves. Values are cached to answer getProductInfo
without re-fetching, and cleared on disconnect.

The SDK delivers lifecycle callbacks on arbitrary threads, so all
identity mutation hops onto the main thread, guarded by a generation
counter that strands retries queued for a product that has since
changed or dropped - otherwise a reconnect could race a stale chain
and report the previous aircraft.

Verified via flutter analyze and an APK build (which type-checks the
new MSDK calls). Runtime timing and the reported values still need a
physical aircraft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:49:57 +02:00

153 lines
6.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/foundation.dart';
import 'uploader.dart';
enum RegistrationState { idle, registering, success, failed }
/// Camera capture family, mirrors the right-rail Photo/Video/Pano switch.
enum CaptureMode { photo, video, pano }
/// Exposure program — Auto (PROGRAM) vs Pro (MANUAL) tab on Camera settings.
enum ExposureProgram { auto, pro }
/// A media file listed off the aircraft's SD card by the native MediaManager.
class MediaItem {
MediaItem({
required this.index,
required this.fileName,
required this.isVideo,
this.durationSeconds,
this.sizeBytes,
this.createdMs,
this.thumbPath,
this.localPath,
});
final int index;
final String fileName;
final bool isVideo;
final int? durationSeconds;
final int? sizeBytes;
final int? createdMs;
/// On-disk path of the fetched thumbnail (cache dir), once available.
String? thumbPath;
/// On-disk path of the fully downloaded original, once available.
String? localPath;
String? get durationLabel {
if (durationSeconds == null) return null;
final int s = durationSeconds!;
final String mm = (s ~/ 60).toString();
final String ss = (s % 60).toString().padLeft(2, '0');
return '$mm:$ss';
}
factory MediaItem.fromMap(Map<String, dynamic> m) => MediaItem(
index: (m['index'] as num).toInt(),
fileName: (m['fileName'] as String?) ?? 'file',
isVideo: (m['type'] as String?) == 'video',
durationSeconds: (m['durationSeconds'] as num?)?.toInt(),
sizeBytes: (m['sizeBytes'] as num?)?.toInt(),
createdMs: (m['createdMs'] as num?)?.toInt(),
);
}
/// Live aircraft/session state, shared across every screen. `_HomePageState`
/// owns the DJI/uploader plumbing and pushes updates here; the screens observe
/// it via [AnimatedBuilder]. Call [bump] after a batch of field writes.
class FlightModel extends ChangeNotifier {
// ── SDK / registration / connection ────────────────────────────────────────
String sdkVersion = '…';
RegistrationState registration = RegistrationState.idle;
String? registrationError;
bool connected = false;
String? model;
String? firmwareVersion;
String? serialNumber;
// ── Flight controller telemetry ────────────────────────────────────────────
int? satellites;
int? gpsSignalLevel; // 0..5
bool? isFlying;
bool? motorsOn;
String? flightMode;
double? altitude; // m, relative to home
double? latitude;
double? longitude;
double? homeLatitude;
double? homeLongitude;
double? homeDistance; // m, aircraft ↔ home
double? horizontalSpeed; // m/s
double? verticalSpeed; // m/s (up positive)
double? heading; // deg
int? flightTimeSeconds;
double? goHomeHeight; // m
// Phone's own GPS (independent of the drone's fix); streamed to the server as
// a location fallback for the Web App's automatic bounding box.
double? phoneLatitude;
double? phoneLongitude;
// ── Battery ─────────────────────────────────────────────────────────────────
int? batteryPercent;
double? batteryVoltage; // V
double? batteryTemperature; // °C
// ── Camera ──────────────────────────────────────────────────────────────────
CaptureMode captureMode = CaptureMode.video;
String? shootPhotoMode; // SINGLE / HDR / BURST / AEB / INTERVAL / PANORAMA
bool isRecording = false;
int recordSeconds = 0;
bool sdInserted = false;
int? sdRemainingMB;
int? sdPhotoCount;
int? sdVideoCount;
// ── Exposure ────────────────────────────────────────────────────────────────
ExposureProgram exposureProgram = ExposureProgram.auto;
String? iso; // "100", "AUTO", …
String? shutter; // "1/240"
String? aperture; // "f/2.8"
String? ev; // "-0.3"
String? whiteBalance; // "5200K" / "AUTO"
List<double>? histogram; // 0..1 normalized bins
// ── Gimbal ──────────────────────────────────────────────────────────────────
double? gimbalPitch; // deg (−90..30-ish)
double? gimbalRoll;
double? gimbalYaw;
// ── Flight settings (Safety / Control) ──────────────────────────────────────
int? maxHeight; // m
int? maxRadius; // m
bool? maxRadiusEnabled;
int? rthHeight; // m
String? obstacleAvoidance; // "On" / "Bypass" / "Off"
bool? noviceMode;
bool arHomePoint = true; // client-side toggle (AR overlay)
// ── Missions ────────────────────────────────────────────────────────────────
String missionState = 'idle'; // idle / ready / uploading / executing / …
bool missionRunning = false;
bool tracking = false; // ActiveTrack engaged
String? missionError;
// ── On-drone media ──────────────────────────────────────────────────────────
List<MediaItem> media = <MediaItem>[];
bool mediaLoading = false;
// ── DJI account (optional, in addition to PilotVault) ───────────────────────
String djiAccountState = 'unknown'; // notLoggedIn / tokenOutOfDate / authorized / …
String? djiAccountUser;
// ── Telemetry upload channel ────────────────────────────────────────────────
UploadStatus upload = UploadStatus.disabled;
bool get registered => registration == RegistrationState.success;
/// Notify observers after a batch of field writes.
void bump() => notifyListeners();
}