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>
153 lines
6.3 KiB
Dart
153 lines
6.3 KiB
Dart
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();
|
||
}
|