Vision radar: graded obstacle sectors around the centre of the video, the nearest distance per side, the obstacle overhead, and BLOCKED on a side whose sensors cannot see once the motors run. On aircraft with 360° sensing, sides without their own readings are filled from the perception ring. The same sectors sit on the attitude ball's rim, turned by the heading. Settings > Vision gains Display radar chart and the braking / sensing distances per direction. On-screen sticks: the rail's STICKS button hands the aircraft to two stick pads after a slide-to-confirm. The 25 Hz stream runs natively and sends zeros once Dart's last command is 300 ms old, so a stalled UI leaves the aircraft hovering. The sticks close when the remote takes over, on RTH or on landing. They are simulator-only on aircraft with a remote unless Settings > General > Developer allows real flights; Spark and Mavic Air on Wi-Fi may fly them for real. Which MSDK axis is forward in velocity + body mode is coded from DJI's documentation and must be checked in the simulator first. AirSense: a banner for manned aircraft at warning level 2 and up, with distance, direction and ICAO code; the planes on the map; the full list in a sheet and Settings > Safety; and the aircraft's own avoidance action with its countdown. Fly zones: DJI GEO zones around the aircraft (or the phone) on the map with their height limits, a banner for FlySafe notices and in/near-zone states, take-off refused in restricted and locked authorization zones, self-unlock of authorization zones, the aircraft's licences (enable, sync from the DJI account, remove all; ground only), and the GEO database version with an update button. Tested with unit and widget tests and screenshot renders only; nothing has run on an aircraft or the simulator yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
275 lines
13 KiB
Dart
275 lines
13 KiB
Dart
import 'package:pilotvault_fly/dji_service.dart';
|
|
import 'package:pilotvault_fly/fly/state/key_state.dart';
|
|
|
|
/// Records every bridge call instead of reaching the platform channel.
|
|
class FakeDjiService extends DjiService {
|
|
final List<String> calls = <String>[];
|
|
Object? failWith;
|
|
|
|
Future<void> _record(String call) async {
|
|
calls.add(call);
|
|
if (failWith != null) throw failWith!;
|
|
}
|
|
|
|
@override
|
|
Future<void> takeOff() => _record('takeOff');
|
|
@override
|
|
Future<void> precisionTakeOff() => _record('precisionTakeOff');
|
|
@override
|
|
Future<void> land() => _record('land');
|
|
@override
|
|
Future<void> cancelLanding() => _record('cancelLanding');
|
|
@override
|
|
Future<void> confirmLanding() => _record('confirmLanding');
|
|
@override
|
|
Future<void> startGoHome() => _record('startGoHome');
|
|
@override
|
|
Future<void> cancelGoHome() => _record('cancelGoHome');
|
|
@override
|
|
Future<void> confirmSmartRTH(bool goHome) => _record('confirmSmartRTH($goHome)');
|
|
@override
|
|
Future<void> setGoHomeHeight(int m) => _record('setGoHomeHeight($m)');
|
|
@override
|
|
Future<void> setHomeLocation(double latitude, double longitude) => _record('setHomeLocation');
|
|
@override
|
|
Future<void> stopWaypointMission() => _record('stopWaypointMission');
|
|
|
|
// ── Missions (plan P7) ─────────────────────────────────────────────────────
|
|
/// The last `uploadWaypointMission` payload.
|
|
Map<String, Object?>? uploaded;
|
|
|
|
@override
|
|
Future<void> uploadWaypointMission(Map<String, Object?> mission) {
|
|
uploaded = mission;
|
|
return _record('uploadWaypointMission(${(mission['waypoints'] as List<Object?>).length})');
|
|
}
|
|
|
|
@override
|
|
Future<void> startWaypointMission() => _record('startWaypointMission');
|
|
@override
|
|
Future<void> pauseWaypointMission() => _record('pauseWaypointMission');
|
|
@override
|
|
Future<void> resumeWaypointMission() => _record('resumeWaypointMission');
|
|
|
|
// ── ActiveTrack / TapFly (plan P7b) ────────────────────────────────────────
|
|
@override
|
|
Future<void> startActiveTrack(double x, double y, double w, double h, {String mode = 'TRACE'}) =>
|
|
_record('startActiveTrack(${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)}, ${w.toStringAsFixed(2)}, ${h.toStringAsFixed(2)}, $mode)');
|
|
@override
|
|
Future<void> acceptActiveTrackConfirmation() => _record('acceptActiveTrackConfirmation');
|
|
@override
|
|
Future<void> rejectActiveTrackConfirmation() => _record('rejectActiveTrackConfirmation');
|
|
@override
|
|
Future<void> switchActiveTrackMode(String mode) => _record('switchActiveTrackMode($mode)');
|
|
@override
|
|
Future<void> startTapFly(double x, double y, {String mode = 'FORWARD', double speed = 5, bool avoidance = true}) =>
|
|
_record('startTapFly(${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)}, $mode, ${speed.toStringAsFixed(0)}, $avoidance)');
|
|
@override
|
|
Future<void> stopTapFly() => _record('stopTapFly');
|
|
@override
|
|
Future<void> setTapFlySpeed(double speed) => _record('setTapFlySpeed(${speed.toStringAsFixed(0)})');
|
|
@override
|
|
Future<void> resetTapFlyHeading() => _record('resetTapFlyHeading');
|
|
@override
|
|
Future<void> setActiveTrackAutoSensing(bool on) => _record('setActiveTrackAutoSensing($on)');
|
|
@override
|
|
Future<void> startActiveTrackSubject(int index, {String mode = 'TRACE'}) => _record('startActiveTrackSubject($index, $mode)');
|
|
@override
|
|
Future<void> setActiveTrackRetreat(bool on) => _record('setActiveTrackRetreat($on)');
|
|
@override
|
|
Future<bool?> getActiveTrackRetreat() async => false;
|
|
@override
|
|
Future<void> stopActiveTrack() => _record('stopActiveTrack');
|
|
|
|
// ── Sensing distances / fly zones (plan P8) ────────────────────────────────
|
|
Map<String, dynamic> sensing = <String, dynamic>{};
|
|
List<Map<String, dynamic>> flyZones = <Map<String, dynamic>>[];
|
|
List<int> unlockedZones = <int>[];
|
|
|
|
/// Zone fetches, counted apart from [calls]: the flight screen fetches on its own.
|
|
int zoneFetches = 0;
|
|
|
|
@override
|
|
Future<Map<String, dynamic>> getSensingDistances() async => sensing;
|
|
@override
|
|
Future<void> setAvoidanceDistance(String direction, double metres) => _record('setAvoidanceDistance($direction, $metres)');
|
|
@override
|
|
Future<void> setMaxPerceptionDistance(String direction, double metres) => _record('setMaxPerceptionDistance($direction, $metres)');
|
|
@override
|
|
Future<List<Map<String, dynamic>>> getFlyZonesNearby({double? latitude, double? longitude}) async {
|
|
zoneFetches++;
|
|
return flyZones;
|
|
}
|
|
|
|
@override
|
|
Future<List<int>> getUnlockedFlyZones() async => unlockedZones;
|
|
@override
|
|
Future<void> unlockAuthorizationFlyZone(int id) async {
|
|
await _record('unlockAuthorizationFlyZone($id)');
|
|
unlockedZones = <int>[...unlockedZones, id];
|
|
}
|
|
|
|
/// What `getFlyZoneLicences` / `getFlyZoneDatabase` answer.
|
|
Map<String, dynamic> licences = <String, dynamic>{'supported': false, 'zones': <Object?>[]};
|
|
Map<String, dynamic> flyZoneDb = <String, dynamic>{};
|
|
|
|
@override
|
|
Future<Map<String, dynamic>> getFlyZoneLicences() async => licences;
|
|
@override
|
|
Future<void> setFlyZoneLicenceEnabled(int id, bool enabled) => _record('setFlyZoneLicenceEnabled($id, $enabled)');
|
|
@override
|
|
Future<void> syncFlyZoneLicences() => _record('syncFlyZoneLicences');
|
|
@override
|
|
Future<void> deleteAllFlyZoneLicences() => _record('deleteAllFlyZoneLicences');
|
|
@override
|
|
Future<Map<String, dynamic>> getFlyZoneDatabase() async => flyZoneDb;
|
|
@override
|
|
Future<void> startFlyZoneDatabaseUpgrade() => _record('startFlyZoneDatabaseUpgrade');
|
|
@override
|
|
Future<void> stopFlyZoneDatabaseUpgrade() => _record('stopFlyZoneDatabaseUpgrade');
|
|
|
|
// ── Virtual sticks (plan P8) ───────────────────────────────────────────────
|
|
@override
|
|
Future<void> setVirtualStickEnabled(bool on) => _record('setVirtualStickEnabled($on)');
|
|
@override
|
|
Future<void> sendVirtualStick({required double forward, required double right, required double yaw, required double up}) =>
|
|
_record('sendVirtualStick(${forward.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${yaw.toStringAsFixed(0)}, ${up.toStringAsFixed(1)})');
|
|
@override
|
|
Future<void> startCompassCalibration() => _record('startCompassCalibration');
|
|
|
|
// ── Calibration (plan P6b) ─────────────────────────────────────────────────
|
|
@override
|
|
Future<void> stopCompassCalibration() => _record('stopCompassCalibration');
|
|
@override
|
|
Future<void> startIMUCalibration({int? index}) => _record('startIMUCalibration');
|
|
@override
|
|
Future<void> startGravityCenterCalibration() => _record('startGravityCenterCalibration');
|
|
@override
|
|
Future<void> stopGravityCenterCalibration() => _record('stopGravityCenterCalibration');
|
|
@override
|
|
Future<void> startPropellerCalibration() => _record('startPropellerCalibration');
|
|
@override
|
|
Future<void> stopPropellerCalibration() => _record('stopPropellerCalibration');
|
|
@override
|
|
Future<void> startLensCalibration() => _record('startLensCalibration');
|
|
@override
|
|
Future<void> stopLensCalibration() => _record('stopLensCalibration');
|
|
@override
|
|
Future<void> initSensorCleaning() => _record('initSensorCleaning');
|
|
@override
|
|
Future<void> startSensorCleaning() => _record('startSensorCleaning');
|
|
@override
|
|
Future<void> exitSensorCleaning() => _record('exitSensorCleaning');
|
|
@override
|
|
Future<void> setRcCalibrationStep(String step) => _record('setRcCalibrationStep($step)');
|
|
@override
|
|
Future<void> watchRcHardware(bool on) async => calls.add('watchRcHardware($on)');
|
|
@override
|
|
Future<void> formatStorage({String location = 'SDCARD'}) => _record('formatStorage');
|
|
@override
|
|
Future<void> setHomeToCurrent() => _record('setHomeToCurrent');
|
|
|
|
// ── KeyManager (plan P6) ───────────────────────────────────────────────────
|
|
/// Keys `isKeySupported` answers false for (by [DjiKey.key] name).
|
|
final Set<String> unsupportedKeys = <String>{};
|
|
|
|
/// When set, a successful `setKey` reports the new value back, as the
|
|
/// native listener would.
|
|
KeyState? echo;
|
|
|
|
@override
|
|
Future<bool> isKeySupported(DjiKey key) async => !unsupportedKeys.contains(key.key);
|
|
@override
|
|
Future<String> listenKey(DjiKey key) async {
|
|
calls.add('listenKey(${key.key})');
|
|
return key.id;
|
|
}
|
|
|
|
@override
|
|
Future<void> cancelKey(String id) async => calls.add('cancelKey');
|
|
@override
|
|
Future<void> setKey(DjiKey key, Object? value) async {
|
|
await _record('setKey(${key.key}, $value)');
|
|
echo?.apply(key.id, value);
|
|
}
|
|
|
|
@override
|
|
Future<void> performKeyAction(DjiKey key) => _record('performKeyAction(${key.key})');
|
|
|
|
@override
|
|
Future<Object?> getKey(DjiKey key) async => switch (key.key) {
|
|
'MAX_FLIGHT_HEIGHT' => 120,
|
|
'MAX_FLIGHT_RADIUS' => 500,
|
|
'MAX_FLIGHT_RADIUS_ENABLED' => true,
|
|
_ => null,
|
|
};
|
|
|
|
// ── Camera (plan P4) ───────────────────────────────────────────────────────
|
|
@override
|
|
Future<void> setCameraMode(String mode) => _record('setCameraMode($mode)');
|
|
@override
|
|
Future<void> startShootPhoto() => _record('startShootPhoto');
|
|
@override
|
|
Future<void> stopShootPhoto() => _record('stopShootPhoto');
|
|
@override
|
|
Future<void> startRecordVideo() => _record('startRecordVideo');
|
|
@override
|
|
Future<void> stopRecordVideo() => _record('stopRecordVideo');
|
|
@override
|
|
Future<void> setCameraValue(String id, Object value) => _record('setCameraValue($id, $value)');
|
|
@override
|
|
Future<void> setISO(String value) => _record('setISO($value)');
|
|
@override
|
|
Future<void> setShutterSpeed(String value) => _record('setShutterSpeed($value)');
|
|
@override
|
|
Future<void> setAperture(String value) => _record('setAperture($value)');
|
|
@override
|
|
Future<void> setEV(String value) => _record('setEV($value)');
|
|
@override
|
|
Future<void> setWhiteBalance(String value) => _record('setWhiteBalance($value)');
|
|
@override
|
|
Future<void> setVideoResolutionAndFrameRate(String resolution, String frameRate) =>
|
|
_record('setVideoResolutionAndFrameRate($resolution, $frameRate)');
|
|
@override
|
|
Future<void> setPhotoTimeInterval({required int count, required int seconds}) => _record('setPhotoTimeInterval($count, $seconds)');
|
|
@override
|
|
Future<void> setPictureStyle({required int sharpness, required int contrast, required int saturation}) =>
|
|
_record('setPictureStyle($sharpness, $contrast, $saturation)');
|
|
@override
|
|
Future<void> setFocusTarget(double x, double y) => _record('setFocusTarget(${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)})');
|
|
@override
|
|
Future<void> setSpotMeteringTarget(double x, double y) =>
|
|
_record('setSpotMeteringTarget(${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)})');
|
|
@override
|
|
Future<void> startContinuousZoom(String direction) => _record('startContinuousZoom($direction)');
|
|
@override
|
|
Future<void> stopContinuousZoom() => _record('stopContinuousZoom');
|
|
@override
|
|
Future<void> setZoomFocalLength(int value) => _record('setZoomFocalLength($value)');
|
|
@override
|
|
Future<void> restoreCameraDefaults() => _record('restoreCameraDefaults');
|
|
@override
|
|
Future<void> setHistogramEnabled(bool on) => _record('setHistogramEnabled($on)');
|
|
|
|
// ── Gimbal (plan P5) ───────────────────────────────────────────────────────
|
|
@override
|
|
Future<void> rotateGimbalPitch(double deg) => _record('rotateGimbalPitch(${deg.round()})');
|
|
@override
|
|
Future<void> rotateGimbalSpeed({double? pitch, double? yaw}) =>
|
|
_record('rotateGimbalSpeed(${pitch?.toStringAsFixed(1)}, ${yaw?.toStringAsFixed(1)})');
|
|
@override
|
|
Future<void> setGimbalMode(String mode) => _record('setGimbalMode($mode)');
|
|
@override
|
|
Future<void> resetGimbal() => _record('resetGimbal');
|
|
@override
|
|
Future<void> resetGimbalYaw() => _record('resetGimbalYaw');
|
|
@override
|
|
Future<void> fineTuneGimbal(String axis, double degrees) => _record('fineTuneGimbal($axis, ${degrees.toStringAsFixed(1)})');
|
|
@override
|
|
Future<void> startGimbalCalibration() => _record('startGimbalCalibration');
|
|
@override
|
|
Future<void> setGimbalSetting(String id, Object value) => _record('setGimbalSetting($id, $value)');
|
|
@override
|
|
Future<void> resetGimbalControlParameters() => _record('resetGimbalControlParameters');
|
|
}
|