Firmware was already half-wired: the bridge read BaseProduct.getFirmwarePackageVersion() once, at connect. The SDK returns null there until it has finished handshaking with the aircraft, so the About panel almost always showed "-" instead. The serial was never fetched at all. Resolve both asynchronously after connect. fetchIdentity() reads the serial from FlightController.getSerialNumber() and the firmware from the product package version, falling back to the component-level getFirmwareVersion() for aircraft that only report the latter. It re-checks every 2s (max 6 attempts) and emits each value on a new `identity` event as it lands, so the serial still appears when firmware never resolves. Values are cached to answer getProductInfo without re-fetching, and cleared on disconnect. The SDK delivers lifecycle callbacks on arbitrary threads, so all identity mutation hops onto the main thread, guarded by a generation counter that strands retries queued for a product that has since changed or dropped - otherwise a reconnect could race a stale chain and report the previous aircraft. Verified via flutter analyze and an APK build (which type-checks the new MSDK calls). Runtime timing and the reported values still need a physical aircraft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
8.5 KiB
Dart
152 lines
8.5 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`, `battery`, `camera`, `exposure`, `gimbal`, `mission`,
|
|
/// `mediaList`, `mediaDownload`, `djiAccount`, `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);
|
|
}
|
|
|
|
// ── 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');
|
|
Future<void> setHomeToCurrent() => _methods.invokeMethod<void>('setHomeToCurrent');
|
|
|
|
// ── 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});
|
|
|
|
// ── 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');
|
|
}
|