The GO 4-style HUD needs far more live data than the bridge sent, and pushing every SDK callback straight to Dart would rebuild the screen dozens of times a second. This is the telemetry foundation (plan P1). On the Kotlin side, every source now writes into a TelemetryHub that emits one telemetry event per tick: flight-controller state, gimbal and vision at up to 10 Hz, battery, links, RC, storage and camera settings folded in at up to 2 Hz. Each event carries the full latest snapshot, so a consumer that drops packets never loses a slow group. New bridges cover batteries (every pack, cell voltages, warning thresholds), link quality and RC battery, diagnostics, and obstacle sensing per sensor. KeyBridge adds generic getKey/setKey/listenKey over KeyManager for the settings rows to come. The flight controller also gains smart-RTH confirmation and simulator start/stop. On the Dart side, FlightModel now owns one ChangeNotifier per subsystem (aircraft, battery, links, camera, gimbal, vision, alerts, mission), so a HUD widget can listen to just the one it draws. TelemetryReducer routes events into them, only notifies groups that changed, and gives FlightModel one notification per tick. Flat getters keep the existing screens working. Diagnostics become graded, de-duplicated alerts. Settings > About > Telemetry inspector shows every field live and can start the simulator. Battery no longer arrives as its own event, so the API Server reads the percentage from the telemetry snapshot (the old event is still accepted). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
12 KiB
Dart
237 lines
12 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`, `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');
|
|
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');
|
|
|
|
/// 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});
|
|
Future<void> setHistogramEnabled(bool on) =>
|
|
_methods.invokeMethod<void>('setHistogramEnabled', <String, dynamic>{'value': on});
|
|
|
|
// ── Gimbal ──────────────────────────────────────────────────────────────────
|
|
Future<void> rotateGimbalPitch(double deg) =>
|
|
_methods.invokeMethod<void>('rotateGimbalPitch', <String, dynamic>{'pitch': deg});
|
|
Future<void> resetGimbal() => _methods.invokeMethod<void>('resetGimbal');
|
|
|
|
// ── Missions ────────────────────────────────────────────────────────────────
|
|
/// points: [{lat, lon, altitude}], finishAction: NO_ACTION|GO_HOME|AUTO_LAND|GO_FIRST_WAYPOINT
|
|
Future<void> uploadWaypointMission(
|
|
List<Map<String, dynamic>> points, {
|
|
double speed = 8,
|
|
String finishAction = 'NO_ACTION',
|
|
}) =>
|
|
_methods.invokeMethod<void>('uploadWaypointMission', <String, dynamic>{
|
|
'points': points,
|
|
'speed': speed,
|
|
'finishAction': finishAction,
|
|
});
|
|
Future<void> startWaypointMission() => _methods.invokeMethod<void>('startWaypointMission');
|
|
Future<void> stopWaypointMission() => _methods.invokeMethod<void>('stopWaypointMission');
|
|
Future<void> pauseWaypointMission() => _methods.invokeMethod<void>('pauseWaypointMission');
|
|
Future<void> resumeWaypointMission() => _methods.invokeMethod<void>('resumeWaypointMission');
|
|
|
|
/// Rect is normalized 0..1 in the live view. mode: TRACE|PROFILE|SPOTLIGHT|QUICK_SHOT
|
|
Future<void> startActiveTrack(
|
|
double x,
|
|
double y,
|
|
double w,
|
|
double h, {
|
|
String mode = 'TRACE',
|
|
}) =>
|
|
_methods.invokeMethod<void>('startActiveTrack', <String, dynamic>{
|
|
'x': x,
|
|
'y': y,
|
|
'w': w,
|
|
'h': h,
|
|
'mode': mode,
|
|
});
|
|
Future<void> stopActiveTrack() => _methods.invokeMethod<void>('stopActiveTrack');
|
|
|
|
/// A DJI-Fly QuickShot by name (Dronie/Rocket/Circle/Helix/Boomerang/Asteroid).
|
|
/// The native side maps each to the closest real operator or returns a typed
|
|
/// "unsupported on this aircraft" error.
|
|
Future<void> startQuickShot(String name) =>
|
|
_methods.invokeMethod<void>('startQuickShot', <String, dynamic>{'name': name});
|
|
|
|
// ── Media (MediaManager) ────────────────────────────────────────────────────
|
|
Future<void> refreshMediaList() => _methods.invokeMethod<void>('refreshMediaList');
|
|
Future<String?> fetchThumbnail(int index) =>
|
|
_methods.invokeMethod<String>('fetchThumbnail', <String, dynamic>{'index': index});
|
|
Future<String?> downloadMedia(int index) =>
|
|
_methods.invokeMethod<String>('downloadMedia', <String, dynamic>{'index': index});
|
|
Future<void> deleteMedia(int index) =>
|
|
_methods.invokeMethod<void>('deleteMedia', <String, dynamic>{'index': index});
|
|
|
|
// ── DJI account (optional) ──────────────────────────────────────────────────
|
|
Future<void> djiLogin() => _methods.invokeMethod<void>('djiLogin');
|
|
Future<void> djiLogout() => _methods.invokeMethod<void>('djiLogout');
|
|
Future<void> refreshDjiAccountState() => _methods.invokeMethod<void>('getDjiAccountState');
|
|
}
|
|
|
|
/// 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;
|
|
}
|