Rebuild the Flutter/DJI-MSDK-V4 Fly App to the v2 UI kit and wire the full SDK surface behind it. Native (Kotlin): split DjiSdkBridge into a method/event router delegating to per-subsystem SubBridge helpers sharing a BridgeCtx — FlightController (takeoff/land/RTH + rich telemetry), Camera (mode/record/photo/exposure), Gimbal, Mission (Waypoint + ActiveTrack; QuickShots via ActiveTrack QUICK_SHOT), Media (MediaManager list/thumbnail/download), and optional DJI account login. Manifest gains scoped media permissions. Flutter: ten screens under lib/ui/ (Flight HUD, capture modes, camera settings, settings menu, map+waypoints, home, album, academy, profile, routes/flight logs), driven by an expanded FlightModel. New PVIcon renders the kit's SVG paths via flutter_svg; map uses flutter_map + latlong2. Pin transitive androidx.core/browser down to SDK-35-compatible versions so the newer plugins don't force AGP 8.9.1 onto the DJI toolchain. Verified with `flutter build apk --debug` (compiles Dart + all Kotlin); runtime behaviour is untested here — it needs a physical DJI-connected device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
6.2 KiB
Dart
152 lines
6.2 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;
|
||
|
||
// ── 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();
|
||
}
|