From a7984d7f28eb35c1bdb46b14086eb15120d74de9 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:58:30 +0200 Subject: [PATCH] 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 --- Fly App/lib/fly/safety_gate.dart | 15 +++++++++++++-- Fly App/test/fly/p7_modes_test.dart | 20 ++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Fly App/lib/fly/safety_gate.dart b/Fly App/lib/fly/safety_gate.dart index 6c1c963..65f24c1 100644 --- a/Fly App/lib/fly/safety_gate.dart +++ b/Fly App/lib/fly/safety_gate.dart @@ -942,10 +942,21 @@ class SafetyGate { : 'Speed drops to about 1 m/s and the sticks become very gentle.'), ]; - Future 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 setSlowMode({required bool cinematic, required bool on, Duration settle = const Duration(milliseconds: 800)}) async { final List 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.delayed(settle); + final Object? now = await dji.getKey(key); + if (now is bool && now != on) { + final String name = cinematic ? 'Cinematic' : 'Tripod'; + throw MissionRefused([GateCheck.block('The aircraft kept $name ${now ? 'on' : 'off'}.')]); + } } // ── Hyperlapse (plan P7) ──────────────────────────────────────────────────── diff --git a/Fly App/test/fly/p7_modes_test.dart b/Fly App/test/fly/p7_modes_test.dart index 72873a5..d1a93e0 100644 --- a/Fly App/test/fly/p7_modes_test.dart +++ b/Fly App/test/fly/p7_modes_test.dart @@ -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().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> elevations(List points) async => List.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 getKey(DjiKey key) async => key.key == 'TRIPOD_MODE_ENABLED' ? false : super.getKey(key); +}