Keep the screen on in flight, time out silent vision starts, gate Tripod on the ground
From the Mavic Pro simulator run on 2026-10-22: - The tablet's 2-minute sleep blanked the flight screen mid-flight. The flight screen now sets FLAG_KEEP_SCREEN_ON while it is open. - TapFly's GO got no answer from the aircraft and no error, so the button looked dead. ActiveTrack/TapFly starts now give up after 15 s with "the aircraft did not answer". - Tripod works in the air but the flight controller ignores it on the ground, so the gate asks to take off first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a7984d7f28
commit
8f259339fc
7 files changed
+76
-1
No files matched your search
@@ -4,10 +4,12 @@ import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
// FlutterFragmentActivity (not FlutterActivity) is required by local_auth so the
|
||||
// platform BiometricPrompt can attach to a FragmentActivity host.
|
||||
@@ -21,6 +23,16 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
// Live DJI video feed rendered behind the Flight Control HUD.
|
||||
flutterEngine.platformViewsController.registry
|
||||
.registerViewFactory(DjiVideoView.VIEW_TYPE, DjiVideoViewFactory())
|
||||
// The flight screen keeps the display on (see ScreenAwake in Dart).
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "pilotvault/screen").setMethodCallHandler { call, result ->
|
||||
if (call.method == "keepOn") {
|
||||
if (call.arguments == true) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
result.success(null)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -936,6 +936,8 @@ class SafetyGate {
|
||||
if (!model.connected) const GateCheck.block('No aircraft connected.'),
|
||||
if (model.connected && on && RailState.activeMode(_a, model.mission) != ActiveMode.none)
|
||||
const GateCheck.block('Stop the intelligent flight mode that is running first.'),
|
||||
// A Mavic Pro accepts the write on the ground and keeps Tripod off.
|
||||
if (model.connected && on && !cinematic && _a.isFlying != true) const GateCheck.block('Take off first: Tripod works in flight.'),
|
||||
if (model.connected && on)
|
||||
GateCheck.warn(cinematic
|
||||
? 'The aircraft brakes and turns slowly: give it room to stop.'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Keeps the tablet's screen on while the flight screen is open: Android's
|
||||
/// sleep timeout (2 min on the dev tablet) otherwise blanks the HUD mid-flight.
|
||||
abstract final class ScreenAwake {
|
||||
static const MethodChannel _channel = MethodChannel('pilotvault/screen');
|
||||
|
||||
/// Best-effort: a missing host (widget tests) just leaves the screen alone.
|
||||
static Future<void> keepOn(bool on) async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('keepOn', on);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:latlong2/latlong.dart' show LatLng;
|
||||
|
||||
import '../../dji_service.dart';
|
||||
import '../../flight_model.dart';
|
||||
import '../../fly/screen_awake.dart';
|
||||
import '../../fly/calibration.dart' show CalibrationKind;
|
||||
import '../../fly/flight_trail.dart';
|
||||
import '../../fly/fly_zone_loader.dart';
|
||||
@@ -188,6 +189,7 @@ class _FlyScreenState extends State<FlyScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ScreenAwake.keepOn(true);
|
||||
SystemChrome.setPreferredOrientations(<DeviceOrientation>[
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
@@ -549,6 +551,7 @@ class _FlyScreenState extends State<FlyScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ScreenAwake.keepOn(false);
|
||||
_m.removeListener(_onModel);
|
||||
_m.hudHidden.removeListener(_onHudHidden);
|
||||
_m.mission.removeListener(_onMission);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../flight_model.dart';
|
||||
@@ -33,12 +35,19 @@ Future<void> startVisionMode(
|
||||
if (r == null) return;
|
||||
}
|
||||
try {
|
||||
await start();
|
||||
// A start the aircraft never answers (TapFly on a Mavic Pro in the
|
||||
// simulator) would otherwise leave GO looking dead.
|
||||
await start().timeout(startTimeout);
|
||||
} on TimeoutException {
|
||||
onError(title, 'the aircraft did not answer; try again or fly by hand');
|
||||
} catch (e) {
|
||||
onError(title, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// How long [startVisionMode] waits for the aircraft to accept a start.
|
||||
const Duration startTimeout = Duration(seconds: 15);
|
||||
|
||||
/// The ActiveTrack / TapFly panel at the head of the left column: what the
|
||||
/// operator is doing, the follow style or direction, TapFly's speed, Stop
|
||||
/// while it flies, and Exit.
|
||||
|
||||
@@ -153,6 +153,13 @@ void main() {
|
||||
expect(dji.calls.last, 'setKey(CINEMATIC_MODE_ENABLED, false)');
|
||||
});
|
||||
|
||||
test('Tripod waits for take-off; Cinematic does not', () {
|
||||
final SafetyGate g = SafetyGate(model: _model(telemetryTick()..['isFlying'] = false), dji: FakeDjiService());
|
||||
expect(_blockers(g.checkSlowMode(cinematic: false, on: true)), contains(contains('Take off first')));
|
||||
expect(_blockers(g.checkSlowMode(cinematic: true, on: true)), isEmpty);
|
||||
expect(_blockers(g.checkSlowMode(cinematic: false, on: false)), isEmpty, reason: 'turning it off is always allowed');
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pilotvault_fly/flight_model.dart';
|
||||
@@ -11,6 +13,7 @@ import 'package:pilotvault_fly/fly/state/mission_state.dart';
|
||||
import 'package:pilotvault_fly/fly/telemetry_reducer.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/dialogs/slide_to_confirm.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/fly_screen.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/modes/vision/vision_panel.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'fake_dji.dart';
|
||||
@@ -444,4 +447,29 @@ void main() {
|
||||
expect(dji.calls.last, 'stopTapFly');
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('a vision start the aircraft never answers reports an error', (WidgetTester tester) async {
|
||||
final List<String> errors = <String>[];
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Builder(
|
||||
builder: (BuildContext context) => TextButton(
|
||||
onPressed: () => startVisionMode(
|
||||
context,
|
||||
title: 'TapFly · Forward',
|
||||
icon: 'chevronsRight',
|
||||
description: '',
|
||||
checks: const <GateCheck>[],
|
||||
start: () => Completer<void>().future,
|
||||
onError: (String label, Object e) => errors.add('$label: $e'),
|
||||
),
|
||||
child: const Text('GO'),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.tap(find.text('GO'));
|
||||
await tester.pump(const Duration(seconds: 14));
|
||||
expect(errors, isEmpty);
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
expect(errors.single, contains('did not answer'));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user