Files
PilotVault/Fly App/lib/dji_service.dart
T
tajniak81andClaude Opus 5.5 60c2fd7f64 Give the Fly App GO 4-style gimbal controls
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>
2026-10-19 20:22:42 +02:00

332 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}