Report a Tripod or Cinematic switch the aircraft did not take

On the Mavic Pro the TRIPOD_MODE_ENABLED write succeeds but the flight
controller keeps Tripod off, and the panel showed "Tripod on" for a moment
before silently flipping back. The gate now reads the key back after the write
and refuses with "The aircraft kept Tripod off." when it did not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-22 09:58:30 +02:00
1 parent 789818679a
commit a7984d7f28
2 files changed
+31 -4

No files matched your search

+13 -2
View File
@@ -942,10 +942,21 @@ class SafetyGate {
: 'Speed drops to about 1 m/s and the sticks become very gentle.'),
];
Future<void> setSlowMode({required bool cinematic, required bool on}) async {
/// The write can succeed while the flight controller keeps the old value (a
/// Mavic Pro kept Tripod off on the ground and in the simulator), so it is
/// read back after [settle] and a refusal is reported instead of the switch
/// silently flipping back.
Future<void> setSlowMode({required bool cinematic, required bool on, Duration settle = const Duration(milliseconds: 800)}) async {
final List<GateCheck> checks = checkSlowMode(cinematic: cinematic, on: on);
if (checks.any((GateCheck c) => c.blocks)) throw MissionRefused(checks);
await dji.setKey(DjiKey.flightController(cinematic ? 'CINEMATIC_MODE_ENABLED' : 'TRIPOD_MODE_ENABLED'), on);
final DjiKey key = DjiKey.flightController(cinematic ? 'CINEMATIC_MODE_ENABLED' : 'TRIPOD_MODE_ENABLED');
await dji.setKey(key, on);
await Future<void>.delayed(settle);
final Object? now = await dji.getKey(key);
if (now is bool && now != on) {
final String name = cinematic ? 'Cinematic' : 'Tripod';
throw MissionRefused(<GateCheck>[GateCheck.block('The aircraft kept $name ${now ? 'on' : 'off'}.')]);
}
}
// ── Hyperlapse (plan P7) ────────────────────────────────────────────────────
+18 -2
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart' show LatLng;
import 'package:pilotvault_fly/dji_service.dart';
import 'package:pilotvault_fly/flight_model.dart';
import 'package:pilotvault_fly/fly/action_rail_state.dart';
import 'package:pilotvault_fly/fly/fly_prefs.dart';
@@ -146,12 +147,21 @@ void main() {
test('Tripod and Cinematic are key writes behind the gate', () async {
final FakeDjiService dji = FakeDjiService();
final SafetyGate g = SafetyGate(model: _model(telemetryTick()), dji: dji);
await g.setSlowMode(cinematic: false, on: true);
await g.setSlowMode(cinematic: false, on: true, settle: Duration.zero);
expect(dji.calls.last, 'setKey(TRIPOD_MODE_ENABLED, true)');
await g.setSlowMode(cinematic: true, on: false);
await g.setSlowMode(cinematic: true, on: false, settle: Duration.zero);
expect(dji.calls.last, 'setKey(CINEMATIC_MODE_ENABLED, false)');
});
test('a Tripod write the aircraft ignores is reported, not silent', () async {
final _KeepsTripodOff dji = _KeepsTripodOff();
final SafetyGate g = SafetyGate(model: _model(telemetryTick()), dji: dji);
await expectLater(
g.setSlowMode(cinematic: false, on: true, settle: Duration.zero),
throwsA(isA<MissionRefused>().having((MissionRefused e) => e.toString(), 'message', contains('kept Tripod off'))),
);
});
test('Free hyperlapse: frames from the video length, then the camera\'s interval mode', () async {
const HyperlapseSpec h = HyperlapseSpec(intervalSeconds: 3, videoSeconds: 10);
expect(h.frames, 250);
@@ -339,3 +349,9 @@ class _FlatGround implements ElevationSource {
@override
Future<List<double>> elevations(List<LatLng> points) async => List<double>.filled(points.length, 12);
}
/// A flight controller that accepts the Tripod write and keeps it off (Mavic Pro, 2026-10-22).
class _KeepsTripodOff extends FakeDjiService {
@override
Future<Object?> getKey(DjiKey key) async => key.key == 'TRIPOD_MODE_ENABLED' ? false : super.getKey(key);
}