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
8.5 KiB
Dart
152 lines
8.5 KiB
Dart
import 'package:flutter/services.dart';
|
|
|
|
/// Thin Dart wrapper over the native DJI Mobile SDK bridge.
|
|
///
|
|
/// Mirrors the channels defined in `DjiSdkBridge.kt` (+ its per-subsystem
|
|
/// helpers):
|
|
/// * method channel `dji_msdk/methods` for imperative calls
|
|
/// * event channel `dji_msdk/events` for the SDK's async updates
|
|
///
|
|
/// Every method returns cleanly whether or not a product is connected; the
|
|
/// native side answers with a typed error the UI can surface. The event stream
|
|
/// carries maps keyed by `type`: `registration`, `connection`, `telemetry`,
|
|
/// `battery`, `camera`, `exposure`, `gimbal`, `mission`, `mediaList`,
|
|
/// `mediaDownload`, `djiAccount`, `database`, `init`.
|
|
class DjiService {
|
|
static const MethodChannel _methods = MethodChannel('dji_msdk/methods');
|
|
static const EventChannel _events = EventChannel('dji_msdk/events');
|
|
|
|
Stream<Map<String, dynamic>> events() {
|
|
return _events
|
|
.receiveBroadcastStream()
|
|
.map((dynamic e) => Map<String, dynamic>.from(e as Map));
|
|
}
|
|
|
|
// ── SDK / registration / connection ─────────────────────────────────────────
|
|
Future<String> getSdkVersion() async =>
|
|
await _methods.invokeMethod<String>('getSdkVersion') ?? 'unknown';
|
|
|
|
Future<void> registerApp() => _methods.invokeMethod<void>('registerApp');
|
|
|
|
Future<bool> startConnection() async =>
|
|
await _methods.invokeMethod<bool>('startConnection') ?? false;
|
|
|
|
Future<void> stopConnection() => _methods.invokeMethod<void>('stopConnection');
|
|
|
|
Future<Map<String, dynamic>> getProductInfo() async {
|
|
final dynamic info = await _methods.invokeMethod('getProductInfo');
|
|
return Map<String, dynamic>.from(info as Map);
|
|
}
|
|
|
|
// ── Flight controller commands ──────────────────────────────────────────────
|
|
Future<void> takeOff() => _methods.invokeMethod<void>('takeOff');
|
|
Future<void> land() => _methods.invokeMethod<void>('land');
|
|
Future<void> confirmLanding() => _methods.invokeMethod<void>('confirmLanding');
|
|
Future<void> cancelLanding() => _methods.invokeMethod<void>('cancelLanding');
|
|
Future<void> startGoHome() => _methods.invokeMethod<void>('startGoHome');
|
|
Future<void> cancelGoHome() => _methods.invokeMethod<void>('cancelGoHome');
|
|
Future<void> setHomeToCurrent() => _methods.invokeMethod<void>('setHomeToCurrent');
|
|
|
|
// ── Flight settings (Safety / Control) ──────────────────────────────────────
|
|
Future<void> setMaxFlightHeight(int m) =>
|
|
_methods.invokeMethod<void>('setMaxFlightHeight', <String, dynamic>{'value': m});
|
|
Future<void> setMaxFlightRadius(int m) =>
|
|
_methods.invokeMethod<void>('setMaxFlightRadius', <String, dynamic>{'value': m});
|
|
Future<void> setMaxRadiusEnabled(bool on) =>
|
|
_methods.invokeMethod<void>('setMaxRadiusEnabled', <String, dynamic>{'value': on});
|
|
Future<void> setGoHomeHeight(int m) =>
|
|
_methods.invokeMethod<void>('setGoHomeHeight', <String, dynamic>{'value': m});
|
|
Future<void> setNoviceMode(bool on) =>
|
|
_methods.invokeMethod<void>('setNoviceMode', <String, dynamic>{'value': on});
|
|
Future<void> setObstacleAvoidance(bool on) =>
|
|
_methods.invokeMethod<void>('setObstacleAvoidance', <String, dynamic>{'value': on});
|
|
|
|
// ── Camera ──────────────────────────────────────────────────────────────────
|
|
/// mode: `photo` | `video` | `mediaDownload` | `playback`
|
|
Future<void> setCameraMode(String mode) =>
|
|
_methods.invokeMethod<void>('setCameraMode', <String, dynamic>{'mode': mode});
|
|
Future<void> startShootPhoto() => _methods.invokeMethod<void>('startShootPhoto');
|
|
Future<void> stopShootPhoto() => _methods.invokeMethod<void>('stopShootPhoto');
|
|
Future<void> startRecordVideo() => _methods.invokeMethod<void>('startRecordVideo');
|
|
Future<void> stopRecordVideo() => _methods.invokeMethod<void>('stopRecordVideo');
|
|
|
|
/// mode: SINGLE | HDR | BURST | AEB | INTERVAL | PANORAMA
|
|
Future<void> setShootPhotoMode(String mode) =>
|
|
_methods.invokeMethod<void>('setShootPhotoMode', <String, dynamic>{'mode': mode});
|
|
|
|
/// program: `auto` (PROGRAM) | `pro` (MANUAL)
|
|
Future<void> setExposureProgram(String program) =>
|
|
_methods.invokeMethod<void>('setExposureProgram', <String, dynamic>{'program': program});
|
|
Future<void> setISO(String value) =>
|
|
_methods.invokeMethod<void>('setISO', <String, dynamic>{'value': value});
|
|
Future<void> setShutterSpeed(String value) =>
|
|
_methods.invokeMethod<void>('setShutterSpeed', <String, dynamic>{'value': value});
|
|
Future<void> setAperture(String value) =>
|
|
_methods.invokeMethod<void>('setAperture', <String, dynamic>{'value': value});
|
|
Future<void> setEV(String value) =>
|
|
_methods.invokeMethod<void>('setEV', <String, dynamic>{'value': value});
|
|
Future<void> setWhiteBalance(String value) =>
|
|
_methods.invokeMethod<void>('setWhiteBalance', <String, dynamic>{'value': value});
|
|
Future<void> setHistogramEnabled(bool on) =>
|
|
_methods.invokeMethod<void>('setHistogramEnabled', <String, dynamic>{'value': on});
|
|
|
|
// ── Gimbal ──────────────────────────────────────────────────────────────────
|
|
Future<void> rotateGimbalPitch(double deg) =>
|
|
_methods.invokeMethod<void>('rotateGimbalPitch', <String, dynamic>{'pitch': deg});
|
|
Future<void> resetGimbal() => _methods.invokeMethod<void>('resetGimbal');
|
|
|
|
// ── Missions ────────────────────────────────────────────────────────────────
|
|
/// points: [{lat, lon, altitude}], finishAction: NO_ACTION|GO_HOME|AUTO_LAND|GO_FIRST_WAYPOINT
|
|
Future<void> uploadWaypointMission(
|
|
List<Map<String, dynamic>> points, {
|
|
double speed = 8,
|
|
String finishAction = 'NO_ACTION',
|
|
}) =>
|
|
_methods.invokeMethod<void>('uploadWaypointMission', <String, dynamic>{
|
|
'points': points,
|
|
'speed': speed,
|
|
'finishAction': finishAction,
|
|
});
|
|
Future<void> startWaypointMission() => _methods.invokeMethod<void>('startWaypointMission');
|
|
Future<void> stopWaypointMission() => _methods.invokeMethod<void>('stopWaypointMission');
|
|
Future<void> pauseWaypointMission() => _methods.invokeMethod<void>('pauseWaypointMission');
|
|
Future<void> resumeWaypointMission() => _methods.invokeMethod<void>('resumeWaypointMission');
|
|
|
|
/// Rect is normalized 0..1 in the live view. mode: TRACE|PROFILE|SPOTLIGHT|QUICK_SHOT
|
|
Future<void> startActiveTrack(
|
|
double x,
|
|
double y,
|
|
double w,
|
|
double h, {
|
|
String mode = 'TRACE',
|
|
}) =>
|
|
_methods.invokeMethod<void>('startActiveTrack', <String, dynamic>{
|
|
'x': x,
|
|
'y': y,
|
|
'w': w,
|
|
'h': h,
|
|
'mode': mode,
|
|
});
|
|
Future<void> stopActiveTrack() => _methods.invokeMethod<void>('stopActiveTrack');
|
|
|
|
/// A DJI-Fly QuickShot by name (Dronie/Rocket/Circle/Helix/Boomerang/Asteroid).
|
|
/// The native side maps each to the closest real operator or returns a typed
|
|
/// "unsupported on this aircraft" error.
|
|
Future<void> startQuickShot(String name) =>
|
|
_methods.invokeMethod<void>('startQuickShot', <String, dynamic>{'name': name});
|
|
|
|
// ── Media (MediaManager) ────────────────────────────────────────────────────
|
|
Future<void> refreshMediaList() => _methods.invokeMethod<void>('refreshMediaList');
|
|
Future<String?> fetchThumbnail(int index) =>
|
|
_methods.invokeMethod<String>('fetchThumbnail', <String, dynamic>{'index': index});
|
|
Future<String?> downloadMedia(int index) =>
|
|
_methods.invokeMethod<String>('downloadMedia', <String, dynamic>{'index': index});
|
|
Future<void> deleteMedia(int index) =>
|
|
_methods.invokeMethod<void>('deleteMedia', <String, dynamic>{'index': index});
|
|
|
|
// ── DJI account (optional) ──────────────────────────────────────────────────
|
|
Future<void> djiLogin() => _methods.invokeMethod<void>('djiLogin');
|
|
Future<void> djiLogout() => _methods.invokeMethod<void>('djiLogout');
|
|
Future<void> refreshDjiAccountState() => _methods.invokeMethod<void>('getDjiAccountState');
|
|
}
|