Files
PilotVault/Fly App/test/fly/fake_dji.dart
T
tajniak81andClaude Opus 5.5 e02bb0e5cd Give the Fly App GO 4-style obstacle radar, on-screen sticks, AirSense and fly zones
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>
2026-10-20 21:14:12 +02:00

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');
}