Intelligent modes: Point of Interest (orbit on the map, live radius and speed), Follow Me (follows the phone), QuickShots on a boxed subject, Course Lock / Home Lock, Tripod and Cinematic, a Free hyperlapse, and Waypoints 2.0 for V2-only aircraft. ActiveTrack gains Spotlight Pro and Circle. Every mode has a first-use tutorial and stops from the rail. Waypoints can resume an interrupted mission, change cruise speed in flight and check terrain clearance (Open-Meteo, on tap). Settings: units, C1/C2 actions, dual-operator master/slave, HDMI/SDI output, a link spectrum chart, Wi-Fi name and password, aircraft name, video cache, battery warning history and pairing, non-smart cell voltages. Calibration: compass interference meter, per-IMU calibration, lens infinity focus and a first-manual-focus prompt. Also an RTK chip, gimbal movement presets and motor tuning, Precision Takeoff gated on the flight controller, and the precise GEO database's server version. Built and unit/widget-tested only; nothing has flown yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
613 lines
35 KiB
Dart
613 lines
35 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`, `identity`,
|
||
/// `telemetry` (the batched snapshot — see `TelemetryReducer`), `camera`,
|
||
/// `exposure`, `histogram`, `diagnostics`, `mission`, `key`, `mediaList`, `mediaDownload`,
|
||
/// `djiAccount`, `productProfile`, `database`, `init`, `rcButton`, `rcMasters`,
|
||
/// `spectrum`, `flyZoneDbRemote`.
|
||
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);
|
||
}
|
||
|
||
/// The raw capability map `ProductProfile.fromBridge` consumes. The same map
|
||
/// arrives unasked as a `productProfile` event on connect and component change.
|
||
Future<Map<String, dynamic>> getProductProfile() async {
|
||
final dynamic profile = await _methods.invokeMethod('getProductProfile');
|
||
return Map<String, dynamic>.from(profile as Map);
|
||
}
|
||
|
||
// ── Flight controller commands ──────────────────────────────────────────────
|
||
Future<void> takeOff() => _methods.invokeMethod<void>('takeOff');
|
||
|
||
/// Take-off that records the spot visually, so RTH lands back on it.
|
||
Future<void> precisionTakeOff() => _methods.invokeMethod<void>('precisionTakeOff');
|
||
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');
|
||
|
||
/// Answers the smart-RTH countdown (`goHome.smartRTHState == COUNTING_DOWN`):
|
||
/// true flies home now, false cancels it.
|
||
Future<void> confirmSmartRTH(bool goHome) =>
|
||
_methods.invokeMethod<void>('confirmSmartRTH', <String, dynamic>{'value': goHome});
|
||
Future<void> setHomeToCurrent() => _methods.invokeMethod<void>('setHomeToCurrent');
|
||
|
||
/// Moves the home point (the RTH target dialog's "Current Position" = the pilot).
|
||
Future<void> setHomeLocation(double latitude, double longitude) =>
|
||
_methods.invokeMethod<void>('setHomeLocation', <String, dynamic>{'latitude': latitude, 'longitude': longitude});
|
||
|
||
/// Starts compass calibration; `telemetry.compassCalibrationState` then walks
|
||
/// HORIZONTAL → VERTICAL → SUCCESSFUL / FAILED as the pilot rotates the aircraft.
|
||
Future<void> startCompassCalibration() => _methods.invokeMethod<void>('startCompassCalibration');
|
||
Future<void> stopCompassCalibration() => _methods.invokeMethod<void>('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<void> startIMUCalibration({int? index}) =>
|
||
_methods.invokeMethod<void>('startIMUCalibration', <String, dynamic>{if (index != null) 'index': index});
|
||
Future<void> startGravityCenterCalibration() => _methods.invokeMethod<void>('startGravityCenterCalibration');
|
||
Future<void> stopGravityCenterCalibration() => _methods.invokeMethod<void>('stopGravityCenterCalibration');
|
||
Future<void> startPropellerCalibration() => _methods.invokeMethod<void>('startPropellerCalibration');
|
||
Future<void> stopPropellerCalibration() => _methods.invokeMethod<void>('stopPropellerCalibration');
|
||
|
||
/// Manual-focus lens calibration (interchangeable-lens cameras).
|
||
Future<void> startLensCalibration() => _methods.invokeMethod<void>('startLensCalibration');
|
||
Future<void> stopLensCalibration() => _methods.invokeMethod<void>('stopLensCalibration');
|
||
|
||
/// Sensor dedusting: enter the mode, clean (lens off), leave it.
|
||
Future<void> initSensorCleaning() => _methods.invokeMethod<void>('initSensorCleaning');
|
||
Future<void> startSensorCleaning() => _methods.invokeMethod<void>('startSensorCleaning');
|
||
Future<void> exitSensorCleaning() => _methods.invokeMethod<void>('exitSensorCleaning');
|
||
|
||
/// Remote-controller calibration step: `MIDDLE` → `LIMITS` → `QUIT`.
|
||
Future<void> setRcCalibrationStep(String step) =>
|
||
_methods.invokeMethod<void>('setRcCalibrationStep', <String, dynamic>{'step': step});
|
||
|
||
/// Streams the sticks and dials into `calibration.rcHardware` while on.
|
||
Future<void> watchRcHardware(bool on) => _methods.invokeMethod<void>('watchRcHardware', <String, dynamic>{'on': on});
|
||
|
||
/// Starts the flight controller's built-in simulator at [latitude]/[longitude]
|
||
/// (developer only; propellers off). `telemetry.simulatorActive` reflects it.
|
||
Future<void> startSimulator({required double latitude, required double longitude, int satellites = 12}) =>
|
||
_methods.invokeMethod<void>('startSimulator', <String, dynamic>{
|
||
'latitude': latitude,
|
||
'longitude': longitude,
|
||
'satelliteCount': satellites,
|
||
'updateFrequency': 10,
|
||
});
|
||
Future<void> stopSimulator() => _methods.invokeMethod<void>('stopSimulator');
|
||
|
||
// ── 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});
|
||
|
||
// ── KeyManager (generic simple values) ──────────────────────────────────────
|
||
/// Reads one SDK key. Enums come back as their name, arrays as lists.
|
||
Future<Object?> getKey(DjiKey key) => _methods.invokeMethod<Object?>('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<void> setKey(DjiKey key, Object? value) =>
|
||
_methods.invokeMethod<void>('setKey', <String, dynamic>{...key.args, 'value': value});
|
||
|
||
Future<bool> isKeySupported(DjiKey key) async =>
|
||
await _methods.invokeMethod<bool>('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<String> listenKey(DjiKey key) async =>
|
||
await _methods.invokeMethod<String>('listenKey', key.args) ?? key.id;
|
||
|
||
/// Runs an action key that takes no arguments (`START_PAIRING`, …).
|
||
Future<void> performKeyAction(DjiKey key) => _methods.invokeMethod<void>('performKeyAction', key.args);
|
||
|
||
Future<void> cancelKey(String id) => _methods.invokeMethod<void>('cancelKey', <String, dynamic>{'id': id});
|
||
|
||
// ── 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});
|
||
/// Erases the card. location: `SDCARD` | `INTERNAL_STORAGE`
|
||
Future<void> formatStorage({String location = 'SDCARD'}) =>
|
||
_methods.invokeMethod<void>('formatStorage', <String, dynamic>{'location': location});
|
||
Future<void> setHistogramEnabled(bool on) =>
|
||
_methods.invokeMethod<void>('setHistogramEnabled', <String, dynamic>{'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<void> setCameraValue(String id, Object value) =>
|
||
_methods.invokeMethod<void>('setCameraValue', <String, dynamic>{'id': id, 'value': value});
|
||
|
||
/// resolution / frameRate: SDK enum names (`RESOLUTION_3840x2160`, `FRAME_RATE_30_FPS`).
|
||
Future<void> setVideoResolutionAndFrameRate(String resolution, String frameRate) =>
|
||
_methods.invokeMethod<void>('setVideoResolutionAndFrameRate', <String, dynamic>{
|
||
'resolution': resolution,
|
||
'frameRate': frameRate,
|
||
});
|
||
|
||
/// Interval shooting: [count] photos (255 = until stopped) every [seconds].
|
||
Future<void> setPhotoTimeInterval({required int count, required int seconds}) =>
|
||
_methods.invokeMethod<void>('setPhotoTimeInterval', <String, dynamic>{'count': count, 'seconds': seconds});
|
||
|
||
/// The picture style, each −3…+3 on most cameras.
|
||
Future<void> setPictureStyle({required int sharpness, required int contrast, required int saturation}) =>
|
||
_methods.invokeMethod<void>('setPictureStyle', <String, dynamic>{
|
||
'sharpness': sharpness,
|
||
'contrast': contrast,
|
||
'saturation': saturation,
|
||
});
|
||
|
||
/// Tap-to-focus at x/y, each 0..1 across the live view.
|
||
Future<void> setFocusTarget(double x, double y) =>
|
||
_methods.invokeMethod<void>('setFocusTarget', <String, dynamic>{'x': x, 'y': y});
|
||
|
||
/// Spot metering at x/y (0..1); switches the metering mode to SPOT first.
|
||
Future<void> setSpotMeteringTarget(double x, double y) =>
|
||
_methods.invokeMethod<void>('setSpotMeteringTarget', <String, dynamic>{'x': x, 'y': y});
|
||
|
||
/// Optical zoom while a T / W button is held. direction: `in` | `out`.
|
||
Future<void> startContinuousZoom(String direction) =>
|
||
_methods.invokeMethod<void>('startContinuousZoom', <String, dynamic>{'direction': direction});
|
||
Future<void> stopContinuousZoom() => _methods.invokeMethod<void>('stopContinuousZoom');
|
||
|
||
/// Hybrid or optical focal length, in the units `cameraSettings.zoom*` report.
|
||
Future<void> setZoomFocalLength(int value) =>
|
||
_methods.invokeMethod<void>('setZoomFocalLength', <String, dynamic>{'value': value});
|
||
Future<void> setDigitalZoomFactor(double value) =>
|
||
_methods.invokeMethod<void>('setDigitalZoomFactor', <String, dynamic>{'value': value});
|
||
|
||
/// The camera's own "Reset camera settings".
|
||
Future<void> restoreCameraDefaults() => _methods.invokeMethod<void>('restoreCameraDefaults');
|
||
|
||
// ── Gimbal ──────────────────────────────────────────────────────────────────
|
||
/// Absolute pitch in degrees (the pitch slider).
|
||
Future<void> rotateGimbalPitch(double deg) =>
|
||
_methods.invokeMethod<void>('rotateGimbalPitch', <String, dynamic>{'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<void> rotateGimbalSpeed({double? pitch, double? yaw}) =>
|
||
_methods.invokeMethod<void>('rotateGimbalSpeed', <String, dynamic>{
|
||
if (pitch != null) 'pitch': pitch,
|
||
if (yaw != null) 'yaw': yaw,
|
||
});
|
||
|
||
/// mode: `YAW_FOLLOW` | `FPV` | `FREE`
|
||
Future<void> setGimbalMode(String mode) =>
|
||
_methods.invokeMethod<void>('setGimbalMode', <String, dynamic>{'mode': mode});
|
||
|
||
/// Pitch and yaw back to centre.
|
||
Future<void> resetGimbal() => _methods.invokeMethod<void>('resetGimbal');
|
||
Future<void> resetGimbalYaw() => _methods.invokeMethod<void>('resetGimbalYaw');
|
||
|
||
/// Nudges the gimbal's [axis] (`roll` | `pitch` | `yaw`) trim by [degrees].
|
||
Future<void> fineTuneGimbal(String axis, double degrees) =>
|
||
_methods.invokeMethod<void>('fineTuneGimbal', <String, dynamic>{'axis': axis, 'degrees': degrees});
|
||
|
||
/// Auto-calibration; `telemetry.gimbal.calibrating` / `calibrationProgress` follow it.
|
||
Future<void> startGimbalCalibration() => _methods.invokeMethod<void>('startGimbalCalibration');
|
||
|
||
/// One `gimbalSettings` value by id (`pitchExtension`, `yawFollow`,
|
||
/// `pitchSmoothing`, `yawMaxSpeed`, …) — see the table in `GimbalBridge.kt`.
|
||
Future<void> setGimbalSetting(String id, Object value) =>
|
||
_methods.invokeMethod<void>('setGimbalSetting', <String, dynamic>{'id': id, 'value': value});
|
||
|
||
/// The controller tuning values back to their defaults.
|
||
Future<void> resetGimbalControlParameters() => _methods.invokeMethod<void>('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'`.
|
||
///
|
||
/// [v2] sends it to the Waypoints 2.0 operator (Matrice 300) instead; the
|
||
/// start / pause / resume / stop below then address that one.
|
||
Future<void> uploadWaypointMission(Map<String, Object?> mission, {bool v2 = false}) =>
|
||
_methods.invokeMethod<void>('uploadWaypointMission', <String, Object?>{...mission, if (v2) 'v2': true});
|
||
Future<void> cancelWaypointUpload() => _methods.invokeMethod<void>('cancelWaypointUpload');
|
||
|
||
/// The operator's `WaypointMissionState` name; also re-published as an event.
|
||
Future<String?> getWaypointMissionState() => _methods.invokeMethod<String>('getWaypointMissionState');
|
||
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');
|
||
|
||
/// Changes the cruise speed of the mission in flight (m/s, −15…15).
|
||
Future<void> setWaypointAutoFlightSpeed(double speed) =>
|
||
_methods.invokeMethod<void>('setWaypointAutoFlightSpeed', <String, dynamic>{'speed': speed});
|
||
|
||
/// 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');
|
||
|
||
/// Answers the operator's WAITING_FOR_CONFIRMATION: yes follows the subject, no drops it.
|
||
Future<void> acceptActiveTrackConfirmation() => _methods.invokeMethod<void>('acceptActiveTrackConfirmation');
|
||
Future<void> rejectActiveTrackConfirmation() => _methods.invokeMethod<void>('rejectActiveTrackConfirmation');
|
||
|
||
/// mode: TRACE | PROFILE | SPOTLIGHT | SPOTLIGHT_PRO, while tracking.
|
||
Future<void> switchActiveTrackMode(String mode) =>
|
||
_methods.invokeMethod<void>('switchActiveTrackMode', <String, dynamic>{'mode': mode});
|
||
|
||
/// Lets Trace fly backwards ahead of a subject walking towards the aircraft.
|
||
Future<void> setActiveTrackRetreat(bool on) =>
|
||
_methods.invokeMethod<void>('setActiveTrackRetreat', <String, dynamic>{'on': on});
|
||
Future<bool?> getActiveTrackRetreat() => _methods.invokeMethod<bool>('getActiveTrackRetreat');
|
||
|
||
/// Lets the aircraft look for subjects itself; they arrive in `tracking.subjects`.
|
||
Future<void> setActiveTrackAutoSensing(bool on) =>
|
||
_methods.invokeMethod<void>('setActiveTrackAutoSensing', <String, dynamic>{'on': on});
|
||
|
||
/// Follows the auto-sensed subject with operator index [index].
|
||
Future<void> startActiveTrackSubject(int index, {String mode = 'TRACE'}) =>
|
||
_methods.invokeMethod<void>('startActiveTrackSubject', <String, dynamic>{'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<void> startTapFly(double x, double y, {String mode = 'FORWARD', double speed = 5, bool avoidance = true}) =>
|
||
_methods.invokeMethod<void>('startTapFly', <String, dynamic>{
|
||
'x': x,
|
||
'y': y,
|
||
'mode': mode,
|
||
'speed': speed,
|
||
'avoidance': avoidance,
|
||
});
|
||
Future<void> stopTapFly() => _methods.invokeMethod<void>('stopTapFly');
|
||
Future<void> setTapFlySpeed(double speed) => _methods.invokeMethod<void>('setTapFlySpeed', <String, dynamic>{'speed': speed});
|
||
Future<void> resetTapFlyHeading() => _methods.invokeMethod<void>('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<void> setVirtualStickEnabled(bool on) =>
|
||
_methods.invokeMethod<void>('setVirtualStickEnabled', <String, dynamic>{'on': on});
|
||
|
||
/// Body-frame velocities (m/s, forward / right / up positive) and yaw rate (°/s, clockwise).
|
||
Future<void> sendVirtualStick({required double forward, required double right, required double yaw, required double up}) =>
|
||
_methods.invokeMethod<void>('sendVirtualStick', <String, dynamic>{'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<Map<String, dynamic>> getSensingDistances() async {
|
||
final dynamic m = await _methods.invokeMethod('getSensingDistances');
|
||
return Map<String, dynamic>.from(m as Map);
|
||
}
|
||
|
||
/// Where the aircraft brakes for an obstacle in [direction] (`Horizontal` / `Upward` / `Downward`).
|
||
Future<void> setAvoidanceDistance(String direction, double metres) =>
|
||
_methods.invokeMethod<void>('setAvoidanceDistance', <String, dynamic>{'direction': direction, 'value': metres});
|
||
|
||
/// How far the aircraft looks for obstacles in [direction].
|
||
Future<void> setMaxPerceptionDistance(String direction, double metres) =>
|
||
_methods.invokeMethod<void>('setMaxPerceptionDistance', <String, dynamic>{'direction': direction, 'value': metres});
|
||
|
||
// ── Fly zones (plan P8) ─────────────────────────────────────────────────────
|
||
/// GEO zones around the point, or around the aircraft when omitted.
|
||
Future<List<Map<String, dynamic>>> getFlyZonesNearby({double? latitude, double? longitude}) async {
|
||
final List<Object?>? l = await _methods.invokeMethod<List<Object?>>('getFlyZonesNearby', <String, dynamic>{
|
||
if (latitude != null) 'latitude': latitude,
|
||
if (longitude != null) 'longitude': longitude,
|
||
});
|
||
return <Map<String, dynamic>>[for (final Object? z in l ?? const <Object?>[]) if (z is Map) Map<String, dynamic>.from(z)];
|
||
}
|
||
|
||
/// Ids of the authorization zones unlocked on the connected aircraft.
|
||
Future<List<int>> getUnlockedFlyZones() async {
|
||
final List<Object?>? l = await _methods.invokeMethod<List<Object?>>('getUnlockedFlyZones');
|
||
return <int>[for (final Object? id in l ?? const <Object?>[]) if (id is int) id];
|
||
}
|
||
|
||
/// Self-unlocks an authorization zone; needs a logged-in DJI account.
|
||
Future<void> unlockAuthorizationFlyZone(int id) =>
|
||
_methods.invokeMethod<void>('unlockAuthorizationFlyZone', <String, dynamic>{'id': id});
|
||
|
||
/// The aircraft's licences (custom unlock zones): `{supported, zones: [...]}`.
|
||
Future<Map<String, dynamic>> getFlyZoneLicences() async {
|
||
final dynamic m = await _methods.invokeMethod('getFlyZoneLicences');
|
||
return Map<String, dynamic>.from(m as Map);
|
||
}
|
||
|
||
Future<void> setFlyZoneLicenceEnabled(int id, bool enabled) =>
|
||
_methods.invokeMethod<void>('setFlyZoneLicenceEnabled', <String, dynamic>{'id': id, 'enabled': enabled});
|
||
|
||
/// Downloads the logged-in DJI account's licences and writes them to the aircraft.
|
||
Future<void> syncFlyZoneLicences() => _methods.invokeMethod<void>('syncFlyZoneLicences');
|
||
|
||
Future<void> deleteAllFlyZoneLicences() => _methods.invokeMethod<void>('deleteAllFlyZoneLicences');
|
||
|
||
/// `{basicVersion, basicState, preciseVersion, preciseState}`; unknown parts are null.
|
||
Future<Map<String, dynamic>> getFlyZoneDatabase() async {
|
||
final dynamic m = await _methods.invokeMethod('getFlyZoneDatabase');
|
||
return Map<String, dynamic>.from(m as Map);
|
||
}
|
||
|
||
/// Downloads the basic GEO database; progress arrives as `flyZoneDbProgress` events.
|
||
Future<void> startFlyZoneDatabaseUpgrade() => _methods.invokeMethod<void>('startFlyZoneDatabaseUpgrade');
|
||
Future<void> stopFlyZoneDatabaseUpgrade() => _methods.invokeMethod<void>('stopFlyZoneDatabaseUpgrade');
|
||
|
||
/// A QuickShot by `QuickShotMode` name (DRONIE / ROCKET / CIRCLE / HELIX /
|
||
/// BOOMERANG / ASTEROID / DOLLY_ZOOM) on the subject boxed at [x], [y], [w],
|
||
/// [h] (0..1 of the live view); an aircraft without that shot answers a
|
||
/// typed error.
|
||
Future<void> startQuickShot(String name, {double? x, double? y, double? w, double? h}) =>
|
||
_methods.invokeMethod<void>('startQuickShot', <String, dynamic>{
|
||
'name': name,
|
||
if (x != null) 'x': x,
|
||
if (y != null) 'y': y,
|
||
if (w != null) 'w': w,
|
||
if (h != null) 'h': h,
|
||
});
|
||
|
||
/// ActiveTrack Circle's orbit speed (m/s, negative anticlockwise).
|
||
Future<void> setActiveTrackCircularSpeed(double speed) =>
|
||
_methods.invokeMethod<void>('setActiveTrackCircularSpeed', <String, dynamic>{'speed': speed});
|
||
Future<double?> getActiveTrackCircularSpeed() => _methods.invokeMethod<double>('getActiveTrackCircularSpeed');
|
||
|
||
/// Where the last waypoint mission was cut short: `{missionId, index, lat,
|
||
/// lon, altitude}`, or null when it was not.
|
||
Future<Map<String, dynamic>?> getWaypointInterruption() async {
|
||
final dynamic m = await _methods.invokeMethod('getWaypointInterruption');
|
||
return m == null ? null : Map<String, dynamic>.from(m as Map);
|
||
}
|
||
|
||
// ── Point of Interest (plan P7, `HotpointBridge.kt`) ────────────────────────
|
||
/// Circles [lat]/[lon] at [radius] m and [altitude] m; [angularVelocity] °/s.
|
||
/// startPoint: NORTH / SOUTH / WEST / EAST / NEAREST; heading:
|
||
/// TOWARDS_HOT_POINT / AWAY_FROM_HOT_POINT / ALONG_CIRCLE_LOOKING_FORWARDS /
|
||
/// ALONG_CIRCLE_LOOKING_BACKWARDS / CONTROLLED_BY_REMOTE_CONTROLLER / USING_INITIAL_HEADING.
|
||
Future<void> startHotpoint({
|
||
required double lat,
|
||
required double lon,
|
||
required double altitude,
|
||
required double radius,
|
||
required double angularVelocity,
|
||
required bool clockwise,
|
||
String startPoint = 'NEAREST',
|
||
String heading = 'TOWARDS_HOT_POINT',
|
||
}) =>
|
||
_methods.invokeMethod<void>('startHotpoint', <String, dynamic>{
|
||
'lat': lat,
|
||
'lon': lon,
|
||
'altitude': altitude,
|
||
'radius': radius,
|
||
'angularVelocity': angularVelocity,
|
||
'clockwise': clockwise,
|
||
'startPoint': startPoint,
|
||
'heading': heading,
|
||
});
|
||
Future<void> pauseHotpoint() => _methods.invokeMethod<void>('pauseHotpoint');
|
||
Future<void> resumeHotpoint() => _methods.invokeMethod<void>('resumeHotpoint');
|
||
Future<void> stopHotpoint() => _methods.invokeMethod<void>('stopHotpoint');
|
||
Future<void> setHotpointRadius(double radius) =>
|
||
_methods.invokeMethod<void>('setHotpointRadius', <String, dynamic>{'radius': radius});
|
||
|
||
/// °/s; the sign is the direction (positive clockwise).
|
||
Future<void> setHotpointAngularVelocity(double value) =>
|
||
_methods.invokeMethod<void>('setHotpointAngularVelocity', <String, dynamic>{'value': value});
|
||
Future<void> resetHotpointHeading() => _methods.invokeMethod<void>('resetHotpointHeading');
|
||
|
||
/// The fastest the aircraft circles at [radius] m, °/s.
|
||
Future<double?> getHotpointMaxAngularVelocity(double radius) =>
|
||
_methods.invokeMethod<double>('getHotpointMaxAngularVelocity', <String, dynamic>{'radius': radius});
|
||
|
||
// ── Follow Me (plan P7, `FollowMeBridge.kt`) ────────────────────────────────
|
||
/// heading: TOWARD_FOLLOW_POSITION | CONTROLLED_BY_REMOTE_CONTROLLER.
|
||
Future<void> startFollowMe({required double lat, required double lon, required double altitude, String heading = 'TOWARD_FOLLOW_POSITION'}) =>
|
||
_methods.invokeMethod<void>('startFollowMe', <String, dynamic>{'lat': lat, 'lon': lon, 'altitude': altitude, 'heading': heading});
|
||
Future<void> updateFollowMeTarget(double lat, double lon) =>
|
||
_methods.invokeMethod<void>('updateFollowMeTarget', <String, dynamic>{'lat': lat, 'lon': lon});
|
||
Future<void> stopFollowMe() => _methods.invokeMethod<void>('stopFollowMe');
|
||
|
||
// ── Orientation modes (plan P7) ─────────────────────────────────────────────
|
||
/// mode: AIRCRAFT_HEADING | COURSE_LOCK | HOME_LOCK.
|
||
Future<void> setFlightOrientationMode(String mode) =>
|
||
_methods.invokeMethod<void>('setFlightOrientationMode', <String, dynamic>{'mode': mode});
|
||
|
||
/// Course Lock's "forward" becomes the aircraft's current heading.
|
||
Future<void> lockCourseUsingCurrentHeading() => _methods.invokeMethod<void>('lockCourseUsingCurrentHeading');
|
||
|
||
// ── Remote controller extras (plan P6, `RemoteControllerBridge.kt`) ─────────
|
||
/// Looks for master remotes in range; they arrive as `rcMasters` events.
|
||
Future<void> searchMasters(bool on) => _methods.invokeMethod<void>('searchMasters', <String, dynamic>{'on': on});
|
||
|
||
/// Joins a master remote; answers `ConnectToMasterResult` (ACCEPTED, PASSWORD_ERROR, …).
|
||
Future<String?> connectToMaster({required int id, required String name, required String password}) =>
|
||
_methods.invokeMethod<String>('connectToMaster', <String, dynamic>{'id': id, 'name': name, 'password': password});
|
||
|
||
/// The slaves linked to this (master) remote: `[{id, name, signal}]`.
|
||
Future<List<Map<String, dynamic>>> getSlaves() async {
|
||
final List<Object?>? l = await _methods.invokeMethod<List<Object?>>('getSlaves');
|
||
return <Map<String, dynamic>>[for (final Object? s in l ?? const <Object?>[]) if (s is Map) Map<String, dynamic>.from(s)];
|
||
}
|
||
|
||
/// The video link's interference while on, as `spectrum` events.
|
||
Future<void> watchSpectrum(bool on) => _methods.invokeMethod<void>('watchSpectrum', <String, dynamic>{'on': on});
|
||
|
||
/// The Wi-Fi link's network name and/or password; [rebootWifi] applies them.
|
||
Future<void> setWifiCredentials({String? ssid, String? password}) => _methods.invokeMethod<void>('setWifiCredentials', <String, dynamic>{
|
||
if (ssid != null) 'ssid': ssid,
|
||
if (password != null) 'password': password,
|
||
});
|
||
Future<void> rebootWifi() => _methods.invokeMethod<void>('rebootWifi');
|
||
|
||
// ── Video cache (plan P6, `VideoCacheBridge.kt`) ────────────────────────────
|
||
Future<void> setVideoCacheRecording(bool on, {int? limitMb}) =>
|
||
_methods.invokeMethod<void>('setVideoCacheRecording', <String, dynamic>{'on': on, if (limitMb != null) 'limitMb': limitMb});
|
||
|
||
/// `{bytes, files, recording}`.
|
||
Future<Map<String, dynamic>> getVideoCacheInfo() async {
|
||
final dynamic m = await _methods.invokeMethod('getVideoCacheInfo');
|
||
return Map<String, dynamic>.from(m as Map);
|
||
}
|
||
|
||
Future<void> clearVideoCache() => _methods.invokeMethod<void>('clearVideoCache');
|
||
|
||
/// Records the lens's current focus as infinity (focus on a far subject first).
|
||
Future<void> calibrateInfinityFocus() => _methods.invokeMethod<void>('calibrateInfinityFocus');
|
||
|
||
// ── 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});
|
||
/// The camera's screen-sized preview of a photo (or a still of a video).
|
||
Future<String?> fetchMediaPreview(int index) =>
|
||
_methods.invokeMethod<String>('fetchMediaPreview', <String, dynamic>{'index': index});
|
||
|
||
/// Deletes the files off the card; answers how many the camera refused. A new
|
||
/// `mediaList` (re-indexed) follows.
|
||
Future<int> deleteMedia(List<int> indices) async =>
|
||
await _methods.invokeMethod<int>('deleteMedia', <String, dynamic>{'indices': indices}) ?? 0;
|
||
|
||
/// Plays a video on the aircraft, into the live video feed (plan P9);
|
||
/// `mediaPlayback` events follow its position.
|
||
Future<void> playMediaVideo(int index) =>
|
||
_methods.invokeMethod<void>('playMediaVideo', <String, dynamic>{'index': index});
|
||
Future<void> pauseMediaVideo() => _methods.invokeMethod<void>('pauseMediaVideo');
|
||
Future<void> resumeMediaVideo() => _methods.invokeMethod<void>('resumeMediaVideo');
|
||
Future<void> stopMediaVideo() => _methods.invokeMethod<void>('stopMediaVideo');
|
||
Future<void> seekMediaVideo(double seconds) =>
|
||
_methods.invokeMethod<void>('seekMediaVideo', <String, dynamic>{'seconds': seconds});
|
||
|
||
// ── Live streaming (plan P9, `LiveStreamBridge.kt`) ─────────────────────────
|
||
/// Streams the primary video feed to an RTMP [url]; `liveStream` events follow.
|
||
Future<void> startLiveStream(String url, {bool audio = false}) =>
|
||
_methods.invokeMethod<void>('startLiveStream', <String, dynamic>{'url': url, 'audio': audio});
|
||
Future<void> stopLiveStream() => _methods.invokeMethod<void>('stopLiveStream');
|
||
|
||
// ── DJI account (optional) ──────────────────────────────────────────────────
|
||
Future<void> djiLogin() => _methods.invokeMethod<void>('djiLogin');
|
||
Future<void> djiLogout() => _methods.invokeMethod<void>('djiLogout');
|
||
Future<void> refreshDjiAccountState() => _methods.invokeMethod<void>('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<String, dynamic> get args => <String, dynamic>{
|
||
'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;
|
||
}
|