Plan P5: a value bubble on the pitch slider while dragging, press-and-drag on the video as a speed joystick (re-sent at 10 Hz, zero on release), and a gimbal settings sheet: mode, 30° extension, yaw follow, recenter / face down / yaw recenter, roll fine-tune, auto calibration and advanced speed/smoothness rows gated by the gimbal's capability ranges. GimbalBridge reports mode, trim, calibration and stop/overload state in the gimbal telemetry group, and getter-only settings in a new gimbalSettings slow group. The reported-plus-pending overlay moves out of CameraState into a SettingsOverlay mixin shared by camera and gimbal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
332 lines
18 KiB
Dart
332 lines
18 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`.
|
||
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');
|
||
Future<void> startIMUCalibration() => _methods.invokeMethod<void>('startIMUCalibration');
|
||
|
||
/// 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;
|
||
|
||
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 ────────────────────────────────────────────────────────────────
|
||
/// 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');
|
||
}
|
||
|
||
/// 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.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;
|
||
}
|