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`, `identity`, /// `telemetry` (the batched snapshot — see `TelemetryReducer`), `camera`, /// `exposure`, `histogram`, `diagnostics`, `mission`, `key`, `mediaList`, `mediaDownload`, /// `djiAccount`, `productProfile`, `database`, `init`. class DjiService { static const MethodChannel _methods = MethodChannel('dji_msdk/methods'); static const EventChannel _events = EventChannel('dji_msdk/events'); Stream> events() { return _events .receiveBroadcastStream() .map((dynamic e) => Map.from(e as Map)); } // ── SDK / registration / connection ───────────────────────────────────────── Future getSdkVersion() async => await _methods.invokeMethod('getSdkVersion') ?? 'unknown'; Future registerApp() => _methods.invokeMethod('registerApp'); Future startConnection() async => await _methods.invokeMethod('startConnection') ?? false; Future stopConnection() => _methods.invokeMethod('stopConnection'); Future> getProductInfo() async { final dynamic info = await _methods.invokeMethod('getProductInfo'); return Map.from(info as Map); } /// The raw capability map `ProductProfile.fromBridge` consumes. The same map /// arrives unasked as a `productProfile` event on connect and component change. Future> getProductProfile() async { final dynamic profile = await _methods.invokeMethod('getProductProfile'); return Map.from(profile as Map); } // ── Flight controller commands ────────────────────────────────────────────── Future takeOff() => _methods.invokeMethod('takeOff'); /// Take-off that records the spot visually, so RTH lands back on it. Future precisionTakeOff() => _methods.invokeMethod('precisionTakeOff'); Future land() => _methods.invokeMethod('land'); Future confirmLanding() => _methods.invokeMethod('confirmLanding'); Future cancelLanding() => _methods.invokeMethod('cancelLanding'); Future startGoHome() => _methods.invokeMethod('startGoHome'); Future cancelGoHome() => _methods.invokeMethod('cancelGoHome'); /// Answers the smart-RTH countdown (`goHome.smartRTHState == COUNTING_DOWN`): /// true flies home now, false cancels it. Future confirmSmartRTH(bool goHome) => _methods.invokeMethod('confirmSmartRTH', {'value': goHome}); Future setHomeToCurrent() => _methods.invokeMethod('setHomeToCurrent'); /// Moves the home point (the RTH target dialog's "Current Position" = the pilot). Future setHomeLocation(double latitude, double longitude) => _methods.invokeMethod('setHomeLocation', {'latitude': latitude, 'longitude': longitude}); /// Starts compass calibration; `telemetry.compassCalibrationState` then walks /// HORIZONTAL → VERTICAL → SUCCESSFUL / FAILED as the pilot rotates the aircraft. Future startCompassCalibration() => _methods.invokeMethod('startCompassCalibration'); Future stopCompassCalibration() => _methods.invokeMethod('stopCompassCalibration'); // ── Calibration & maintenance (plan P6b, `CalibrationBridge.kt`) ──────────── // Progress arrives in the `calibration` telemetry group (`CalibrationState`). /// Every IMU, or only [index]. There is no stop: it runs to the end. Future startIMUCalibration({int? index}) => _methods.invokeMethod('startIMUCalibration', {if (index != null) 'index': index}); Future startGravityCenterCalibration() => _methods.invokeMethod('startGravityCenterCalibration'); Future stopGravityCenterCalibration() => _methods.invokeMethod('stopGravityCenterCalibration'); Future startPropellerCalibration() => _methods.invokeMethod('startPropellerCalibration'); Future stopPropellerCalibration() => _methods.invokeMethod('stopPropellerCalibration'); /// Manual-focus lens calibration (interchangeable-lens cameras). Future startLensCalibration() => _methods.invokeMethod('startLensCalibration'); Future stopLensCalibration() => _methods.invokeMethod('stopLensCalibration'); /// Sensor dedusting: enter the mode, clean (lens off), leave it. Future initSensorCleaning() => _methods.invokeMethod('initSensorCleaning'); Future startSensorCleaning() => _methods.invokeMethod('startSensorCleaning'); Future exitSensorCleaning() => _methods.invokeMethod('exitSensorCleaning'); /// Remote-controller calibration step: `MIDDLE` → `LIMITS` → `QUIT`. Future setRcCalibrationStep(String step) => _methods.invokeMethod('setRcCalibrationStep', {'step': step}); /// Streams the sticks and dials into `calibration.rcHardware` while on. Future watchRcHardware(bool on) => _methods.invokeMethod('watchRcHardware', {'on': on}); /// Starts the flight controller's built-in simulator at [latitude]/[longitude] /// (developer only; propellers off). `telemetry.simulatorActive` reflects it. Future startSimulator({required double latitude, required double longitude, int satellites = 12}) => _methods.invokeMethod('startSimulator', { 'latitude': latitude, 'longitude': longitude, 'satelliteCount': satellites, 'updateFrequency': 10, }); Future stopSimulator() => _methods.invokeMethod('stopSimulator'); // ── Flight settings (Safety / Control) ────────────────────────────────────── Future setMaxFlightHeight(int m) => _methods.invokeMethod('setMaxFlightHeight', {'value': m}); Future setMaxFlightRadius(int m) => _methods.invokeMethod('setMaxFlightRadius', {'value': m}); Future setMaxRadiusEnabled(bool on) => _methods.invokeMethod('setMaxRadiusEnabled', {'value': on}); Future setGoHomeHeight(int m) => _methods.invokeMethod('setGoHomeHeight', {'value': m}); Future setNoviceMode(bool on) => _methods.invokeMethod('setNoviceMode', {'value': on}); Future setObstacleAvoidance(bool on) => _methods.invokeMethod('setObstacleAvoidance', {'value': on}); // ── KeyManager (generic simple values) ────────────────────────────────────── /// Reads one SDK key. Enums come back as their name, arrays as lists. Future getKey(DjiKey key) => _methods.invokeMethod('getKey', key.args); /// Writes one SDK key; [value] is coerced natively to the key's Java type /// (an enum by name, a number to the exact width). Future setKey(DjiKey key, Object? value) => _methods.invokeMethod('setKey', {...key.args, 'value': value}); Future isKeySupported(DjiKey key) async => await _methods.invokeMethod('isKeySupported', key.args) ?? false; /// Subscribes to a key: its current value and every change arrive as `key` /// events whose `id` is the string returned here. Idempotent per key. Future listenKey(DjiKey key) async => await _methods.invokeMethod('listenKey', key.args) ?? key.id; /// Runs an action key that takes no arguments (`START_PAIRING`, …). Future performKeyAction(DjiKey key) => _methods.invokeMethod('performKeyAction', key.args); Future cancelKey(String id) => _methods.invokeMethod('cancelKey', {'id': id}); // ── Camera ────────────────────────────────────────────────────────────────── /// mode: `photo` | `video` | `mediaDownload` | `playback` Future setCameraMode(String mode) => _methods.invokeMethod('setCameraMode', {'mode': mode}); Future startShootPhoto() => _methods.invokeMethod('startShootPhoto'); Future stopShootPhoto() => _methods.invokeMethod('stopShootPhoto'); Future startRecordVideo() => _methods.invokeMethod('startRecordVideo'); Future stopRecordVideo() => _methods.invokeMethod('stopRecordVideo'); /// mode: SINGLE | HDR | BURST | AEB | INTERVAL | PANORAMA Future setShootPhotoMode(String mode) => _methods.invokeMethod('setShootPhotoMode', {'mode': mode}); /// program: `auto` (PROGRAM) | `pro` (MANUAL) Future setExposureProgram(String program) => _methods.invokeMethod('setExposureProgram', {'program': program}); Future setISO(String value) => _methods.invokeMethod('setISO', {'value': value}); Future setShutterSpeed(String value) => _methods.invokeMethod('setShutterSpeed', {'value': value}); Future setAperture(String value) => _methods.invokeMethod('setAperture', {'value': value}); Future setEV(String value) => _methods.invokeMethod('setEV', {'value': value}); Future setWhiteBalance(String value) => _methods.invokeMethod('setWhiteBalance', {'value': value}); /// Erases the card. location: `SDCARD` | `INTERNAL_STORAGE` Future formatStorage({String location = 'SDCARD'}) => _methods.invokeMethod('formatStorage', {'location': location}); Future setHistogramEnabled(bool on) => _methods.invokeMethod('setHistogramEnabled', {'value': on}); /// One simple camera setting by its `cameraSettings` id (`photoAspectRatio`, /// `exposureMode`, `aeLock`, …) — see the table in `CameraBridge.kt`. [value] /// is the SDK enum name, or a bool. Future setCameraValue(String id, Object value) => _methods.invokeMethod('setCameraValue', {'id': id, 'value': value}); /// resolution / frameRate: SDK enum names (`RESOLUTION_3840x2160`, `FRAME_RATE_30_FPS`). Future setVideoResolutionAndFrameRate(String resolution, String frameRate) => _methods.invokeMethod('setVideoResolutionAndFrameRate', { 'resolution': resolution, 'frameRate': frameRate, }); /// Interval shooting: [count] photos (255 = until stopped) every [seconds]. Future setPhotoTimeInterval({required int count, required int seconds}) => _methods.invokeMethod('setPhotoTimeInterval', {'count': count, 'seconds': seconds}); /// The picture style, each −3…+3 on most cameras. Future setPictureStyle({required int sharpness, required int contrast, required int saturation}) => _methods.invokeMethod('setPictureStyle', { 'sharpness': sharpness, 'contrast': contrast, 'saturation': saturation, }); /// Tap-to-focus at x/y, each 0..1 across the live view. Future setFocusTarget(double x, double y) => _methods.invokeMethod('setFocusTarget', {'x': x, 'y': y}); /// Spot metering at x/y (0..1); switches the metering mode to SPOT first. Future setSpotMeteringTarget(double x, double y) => _methods.invokeMethod('setSpotMeteringTarget', {'x': x, 'y': y}); /// Optical zoom while a T / W button is held. direction: `in` | `out`. Future startContinuousZoom(String direction) => _methods.invokeMethod('startContinuousZoom', {'direction': direction}); Future stopContinuousZoom() => _methods.invokeMethod('stopContinuousZoom'); /// Hybrid or optical focal length, in the units `cameraSettings.zoom*` report. Future setZoomFocalLength(int value) => _methods.invokeMethod('setZoomFocalLength', {'value': value}); Future setDigitalZoomFactor(double value) => _methods.invokeMethod('setDigitalZoomFactor', {'value': value}); /// The camera's own "Reset camera settings". Future restoreCameraDefaults() => _methods.invokeMethod('restoreCameraDefaults'); // ── Gimbal ────────────────────────────────────────────────────────────────── /// Absolute pitch in degrees (the pitch slider). Future rotateGimbalPitch(double deg) => _methods.invokeMethod('rotateGimbalPitch', {'pitch': deg}); /// Speed-mode rotation in °/s (drag on the video). The gimbal only keeps /// moving while this is repeated (10 Hz); send zero to stop. A null axis is /// left alone. Future rotateGimbalSpeed({double? pitch, double? yaw}) => _methods.invokeMethod('rotateGimbalSpeed', { if (pitch != null) 'pitch': pitch, if (yaw != null) 'yaw': yaw, }); /// mode: `YAW_FOLLOW` | `FPV` | `FREE` Future setGimbalMode(String mode) => _methods.invokeMethod('setGimbalMode', {'mode': mode}); /// Pitch and yaw back to centre. Future resetGimbal() => _methods.invokeMethod('resetGimbal'); Future resetGimbalYaw() => _methods.invokeMethod('resetGimbalYaw'); /// Nudges the gimbal's [axis] (`roll` | `pitch` | `yaw`) trim by [degrees]. Future fineTuneGimbal(String axis, double degrees) => _methods.invokeMethod('fineTuneGimbal', {'axis': axis, 'degrees': degrees}); /// Auto-calibration; `telemetry.gimbal.calibrating` / `calibrationProgress` follow it. Future startGimbalCalibration() => _methods.invokeMethod('startGimbalCalibration'); /// One `gimbalSettings` value by id (`pitchExtension`, `yawFollow`, /// `pitchSmoothing`, `yawMaxSpeed`, …) — see the table in `GimbalBridge.kt`. Future setGimbalSetting(String id, Object value) => _methods.invokeMethod('setGimbalSetting', {'id': id, 'value': value}); /// The controller tuning values back to their defaults. Future resetGimbalControlParameters() => _methods.invokeMethod('resetGimbalControlParameters'); // ── Missions ──────────────────────────────────────────────────────────────── /// Loads and uploads a waypoint mission — `WaypointMissionSpec.toBridge()`'s /// map (see `MissionBridge.kt`). Resolves once every waypoint is on the /// aircraft; progress arrives as `mission` events with `operator: 'waypoint'`. Future uploadWaypointMission(Map mission) => _methods.invokeMethod('uploadWaypointMission', mission); Future cancelWaypointUpload() => _methods.invokeMethod('cancelWaypointUpload'); /// The operator's `WaypointMissionState` name; also re-published as an event. Future getWaypointMissionState() => _methods.invokeMethod('getWaypointMissionState'); Future startWaypointMission() => _methods.invokeMethod('startWaypointMission'); Future stopWaypointMission() => _methods.invokeMethod('stopWaypointMission'); Future pauseWaypointMission() => _methods.invokeMethod('pauseWaypointMission'); Future resumeWaypointMission() => _methods.invokeMethod('resumeWaypointMission'); /// Changes the cruise speed of the mission in flight (m/s, −15…15). Future setWaypointAutoFlightSpeed(double speed) => _methods.invokeMethod('setWaypointAutoFlightSpeed', {'speed': speed}); /// Rect is normalized 0..1 in the live view. mode: TRACE|PROFILE|SPOTLIGHT|QUICK_SHOT Future startActiveTrack( double x, double y, double w, double h, { String mode = 'TRACE', }) => _methods.invokeMethod('startActiveTrack', { 'x': x, 'y': y, 'w': w, 'h': h, 'mode': mode, }); Future stopActiveTrack() => _methods.invokeMethod('stopActiveTrack'); /// Answers the operator's WAITING_FOR_CONFIRMATION: yes follows the subject, no drops it. Future acceptActiveTrackConfirmation() => _methods.invokeMethod('acceptActiveTrackConfirmation'); Future rejectActiveTrackConfirmation() => _methods.invokeMethod('rejectActiveTrackConfirmation'); /// mode: TRACE | PROFILE | SPOTLIGHT | SPOTLIGHT_PRO, while tracking. Future switchActiveTrackMode(String mode) => _methods.invokeMethod('switchActiveTrackMode', {'mode': mode}); /// Lets Trace fly backwards ahead of a subject walking towards the aircraft. Future setActiveTrackRetreat(bool on) => _methods.invokeMethod('setActiveTrackRetreat', {'on': on}); Future getActiveTrackRetreat() => _methods.invokeMethod('getActiveTrackRetreat'); /// Lets the aircraft look for subjects itself; they arrive in `tracking.subjects`. Future setActiveTrackAutoSensing(bool on) => _methods.invokeMethod('setActiveTrackAutoSensing', {'on': on}); /// Follows the auto-sensed subject with operator index [index]. Future startActiveTrackSubject(int index, {String mode = 'TRACE'}) => _methods.invokeMethod('startActiveTrackSubject', {'index': index, 'mode': mode}); // ── TapFly ────────────────────────────────────────────────────────────────── /// Flies towards the point [x], [y] (0..1 across the live view). /// mode: FORWARD | BACKWARD | FREE; speed in m/s. Future startTapFly(double x, double y, {String mode = 'FORWARD', double speed = 5, bool avoidance = true}) => _methods.invokeMethod('startTapFly', { 'x': x, 'y': y, 'mode': mode, 'speed': speed, 'avoidance': avoidance, }); Future stopTapFly() => _methods.invokeMethod('stopTapFly'); Future setTapFlySpeed(double speed) => _methods.invokeMethod('setTapFlySpeed', {'speed': speed}); Future resetTapFlyHeading() => _methods.invokeMethod('resetTapFlyHeading'); // ── Virtual sticks (plan P8) ──────────────────────────────────────────────── /// Puts the flight controller in (or out of) virtual-stick mode; while on, the /// native side streams the last [sendVirtualStick] at 25 Hz, or zeros once it /// is more than 300 ms old. Future setVirtualStickEnabled(bool on) => _methods.invokeMethod('setVirtualStickEnabled', {'on': on}); /// Body-frame velocities (m/s, forward / right / up positive) and yaw rate (°/s, clockwise). Future sendVirtualStick({required double forward, required double right, required double yaw, required double up}) => _methods.invokeMethod('sendVirtualStick', {'forward': forward, 'right': right, 'yaw': yaw, 'up': up}); // ── Sensing distances (plan P8) ───────────────────────────────────────────── /// `{avoidance: {Horizontal, Upward, Downward}, perception: {…}}` in metres; /// a direction the aircraft does not support reads null. Future> getSensingDistances() async { final dynamic m = await _methods.invokeMethod('getSensingDistances'); return Map.from(m as Map); } /// Where the aircraft brakes for an obstacle in [direction] (`Horizontal` / `Upward` / `Downward`). Future setAvoidanceDistance(String direction, double metres) => _methods.invokeMethod('setAvoidanceDistance', {'direction': direction, 'value': metres}); /// How far the aircraft looks for obstacles in [direction]. Future setMaxPerceptionDistance(String direction, double metres) => _methods.invokeMethod('setMaxPerceptionDistance', {'direction': direction, 'value': metres}); // ── Fly zones (plan P8) ───────────────────────────────────────────────────── /// GEO zones around the point, or around the aircraft when omitted. Future>> getFlyZonesNearby({double? latitude, double? longitude}) async { final List? l = await _methods.invokeMethod>('getFlyZonesNearby', { if (latitude != null) 'latitude': latitude, if (longitude != null) 'longitude': longitude, }); return >[for (final Object? z in l ?? const []) if (z is Map) Map.from(z)]; } /// Ids of the authorization zones unlocked on the connected aircraft. Future> getUnlockedFlyZones() async { final List? l = await _methods.invokeMethod>('getUnlockedFlyZones'); return [for (final Object? id in l ?? const []) if (id is int) id]; } /// Self-unlocks an authorization zone; needs a logged-in DJI account. Future unlockAuthorizationFlyZone(int id) => _methods.invokeMethod('unlockAuthorizationFlyZone', {'id': id}); /// The aircraft's licences (custom unlock zones): `{supported, zones: [...]}`. Future> getFlyZoneLicences() async { final dynamic m = await _methods.invokeMethod('getFlyZoneLicences'); return Map.from(m as Map); } Future setFlyZoneLicenceEnabled(int id, bool enabled) => _methods.invokeMethod('setFlyZoneLicenceEnabled', {'id': id, 'enabled': enabled}); /// Downloads the logged-in DJI account's licences and writes them to the aircraft. Future syncFlyZoneLicences() => _methods.invokeMethod('syncFlyZoneLicences'); Future deleteAllFlyZoneLicences() => _methods.invokeMethod('deleteAllFlyZoneLicences'); /// `{basicVersion, basicState, preciseVersion, preciseState}`; unknown parts are null. Future> getFlyZoneDatabase() async { final dynamic m = await _methods.invokeMethod('getFlyZoneDatabase'); return Map.from(m as Map); } /// Downloads the basic GEO database; progress arrives as `flyZoneDbProgress` events. Future startFlyZoneDatabaseUpgrade() => _methods.invokeMethod('startFlyZoneDatabaseUpgrade'); Future stopFlyZoneDatabaseUpgrade() => _methods.invokeMethod('stopFlyZoneDatabaseUpgrade'); /// 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 startQuickShot(String name) => _methods.invokeMethod('startQuickShot', {'name': name}); // ── Media (MediaManager) ──────────────────────────────────────────────────── Future refreshMediaList() => _methods.invokeMethod('refreshMediaList'); Future fetchThumbnail(int index) => _methods.invokeMethod('fetchThumbnail', {'index': index}); Future downloadMedia(int index) => _methods.invokeMethod('downloadMedia', {'index': index}); Future deleteMedia(int index) => _methods.invokeMethod('deleteMedia', {'index': index}); // ── DJI account (optional) ────────────────────────────────────────────────── Future djiLogin() => _methods.invokeMethod('djiLogin'); Future djiLogout() => _methods.invokeMethod('djiLogout'); Future refreshDjiAccountState() => _methods.invokeMethod('getDjiAccountState'); } /// One MSDK V4 KeyManager key, as `KeyBridge.kt` addresses it. /// /// [key] is the key class's constant name (`LOW_BATTERY_WARNING_THRESHOLD`) or /// the raw parameter string; [index] picks the camera / gimbal / battery and /// [subIndex] a camera lens. class DjiKey { const DjiKey(this.component, this.key, {this.index = 0, this.subIndex}); const DjiKey.flightController(String key) : this('FlightController', key); const DjiKey.flightAssistant(String key) : this('FlightAssistant', key); const DjiKey.camera(String key, {int index = 0, int? lens}) : this('Camera', key, index: index, subIndex: lens); const DjiKey.gimbal(String key, {int index = 0}) : this('Gimbal', key, index: index); const DjiKey.battery(String key, {int index = 0}) : this('Battery', key, index: index); const DjiKey.airLink(String key) : this('AirLink', key); const DjiKey.ocuSync(String key) : this('OcuSyncLink', key); const DjiKey.lightbridge(String key) : this('LightbridgeLink', key); const DjiKey.wifi(String key) : this('WiFiLink', key); const DjiKey.remoteController(String key) : this('RemoteController', key); const DjiKey.product(String key) : this('Product', key); final String component; final String key; final int index; final int? subIndex; /// The id `listenKey` answers with and `key` events carry. String get id => '$component/$key/$index/${subIndex ?? 'null'}'; Map get args => { 'component': component, 'key': key, 'index': index, if (subIndex != null) 'subIndex': subIndex, }; @override bool operator ==(Object other) => other is DjiKey && other.id == id; @override int get hashCode => id.hashCode; @override String toString() => id; }