Give the Fly App GO 4-style calibration wizards
P6b: a new CalibrationBridge owns every calibration call and publishes a `calibration` telemetry group (IMU per-unit and per-side progress, gravity centre, lens, sensor cleaning, RC calibration state and live sticks). Guided full-screen wizards cover compass, IMU, remote controller, gimbal auto calibration, lens, sensor cleaning, centre of gravity, propellers and a vision-sensor guide; gimbal adjust is a horizon-line overlay on the live video. Every start goes through SafetyGate.runMaintenance, and closing mid-run stops what the SDK can stop. Reachable from Settings > Safety > Calibration & maintenance, the Aircraft Status calibrate buttons and the gimbal sheet. Not yet run on an aircraft; the RC key sequence still needs checking per remote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
284de5442a
commit
a892bfb209
20 files changed
+3119
-77
No files matched your search
@@ -0,0 +1,499 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pilotvault_fly/flight_model.dart';
|
||||
import 'package:pilotvault_fly/fly/calibration.dart';
|
||||
import 'package:pilotvault_fly/fly/product_profile.dart';
|
||||
import 'package:pilotvault_fly/fly/safety_gate.dart';
|
||||
import 'package:pilotvault_fly/fly/telemetry_reducer.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/panels/aircraft_status_sheet.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/panels/settings/calibration/calibration_hub.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/panels/settings/calibration/wizards.dart';
|
||||
|
||||
import 'fake_dji.dart';
|
||||
import 'profile_fixtures.dart';
|
||||
import 'telemetry_fixtures.dart';
|
||||
|
||||
/// A connected aircraft on the ground, fed through the real reducer; [tick]
|
||||
/// sends the next telemetry snapshot with [patch] on top.
|
||||
class Rig {
|
||||
Rig({Map<String, dynamic>? profile, bool flying = false}) {
|
||||
model
|
||||
..connected = true
|
||||
..profile = ProductProfile.fromBridge(profile ?? phantom4);
|
||||
tick(flying ? const <String, Object?>{} : const <String, Object?>{'isFlying': false, 'areMotorsOn': false});
|
||||
}
|
||||
|
||||
final FlightModel model = FlightModel();
|
||||
late final TelemetryReducer reducer = TelemetryReducer(model);
|
||||
final Map<String, Object?> _state = <String, Object?>{};
|
||||
|
||||
void tick(Map<String, Object?> patch) {
|
||||
_state.addAll(patch);
|
||||
reducer.apply(<String, dynamic>{...telemetryTick(), ..._state});
|
||||
}
|
||||
|
||||
void calibration(Map<String, Object?> group) => tick(<String, Object?>{'calibration': group});
|
||||
}
|
||||
|
||||
Map<String, dynamic> withLens(Map<String, dynamic> p) => <String, dynamic>{
|
||||
...p,
|
||||
'cameras': <dynamic>[
|
||||
for (final Object? c in p['cameras'] as List<dynamic>) <String, dynamic>{...(c as Map<String, dynamic>), 'interchangeableLensSupported': true},
|
||||
],
|
||||
};
|
||||
|
||||
void main() {
|
||||
group('State', () {
|
||||
test('the calibration group lands in CalibrationState and clears when absent', () {
|
||||
final Rig r = Rig();
|
||||
r.calibration(<String, Object?>{
|
||||
'support': <String, Object?>{'compassCount': 2, 'imuCount': 2, 'gravityCenter': false, 'rc': true},
|
||||
'imu': <String, Object?>{
|
||||
'units': <Object?>[
|
||||
<String, Object?>{'index': 0, 'state': 'CALIBRATING', 'progress': 40, 'connected': true},
|
||||
],
|
||||
'multiSide': true,
|
||||
'toCalibrate': <Object?>['NOSE_DOWN'],
|
||||
'calibrated': <Object?>['BOTTOM_DOWN'],
|
||||
},
|
||||
'rc': <String, Object?>{'state': 'MIDDLE', 'segments': 4, 'axes': <String, Object?>{'A': 2}},
|
||||
'rcHardware': <String, Object?>{'lx': 100, 'ly': -660, 'rx': 0, 'ry': 0, 'leftDial': 10, 'rightDial': -5},
|
||||
});
|
||||
final c = r.model.calibration;
|
||||
expect(c.compassCount, 2);
|
||||
expect(c.gravityCenterSupported, isFalse);
|
||||
expect(c.imuUnits.single.progress, 40);
|
||||
expect(c.imuToCalibrate, <String>['NOSE_DOWN']);
|
||||
expect(c.rcState, 'MIDDLE');
|
||||
expect(c.rcAxes['A'], 2);
|
||||
expect(c.rcHardware!.ly, -660);
|
||||
|
||||
r.tick(<String, Object?>{'calibration': null});
|
||||
expect(c.compassCount, isNull);
|
||||
expect(c.imuUnits, isEmpty);
|
||||
expect(c.rcHardware, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('Availability', () {
|
||||
test('profile features, then runtime support, decide the hub', () {
|
||||
final Rig r = Rig();
|
||||
List<CalibrationKind> kinds() => CalibrationKinds.available(r.model.profile, r.model.calibration);
|
||||
expect(kinds(), containsAll(<CalibrationKind>[
|
||||
CalibrationKind.compass,
|
||||
CalibrationKind.imu,
|
||||
CalibrationKind.rc,
|
||||
CalibrationKind.gimbal,
|
||||
CalibrationKind.gimbalAdjust,
|
||||
CalibrationKind.visionSensors,
|
||||
]));
|
||||
expect(kinds(), isNot(contains(CalibrationKind.lens)), reason: 'fixed lens');
|
||||
expect(kinds(), isNot(contains(CalibrationKind.sensorCleaning)));
|
||||
expect(kinds(), contains(CalibrationKind.gravityCenter), reason: 'unknown until the aircraft answers');
|
||||
|
||||
r.calibration(<String, Object?>{
|
||||
'support': <String, Object?>{'gravityCenter': false, 'rc': false, 'compassCount': 1},
|
||||
});
|
||||
expect(kinds(), isNot(contains(CalibrationKind.gravityCenter)));
|
||||
expect(kinds(), isNot(contains(CalibrationKind.rc)));
|
||||
expect(kinds(), contains(CalibrationKind.compass));
|
||||
});
|
||||
|
||||
test('interchangeable-lens cameras get lens calibration and sensor cleaning; Spark has no RC wizard', () {
|
||||
final Rig x5s = Rig(profile: withLens(inspire2X5s));
|
||||
final List<CalibrationKind> k = CalibrationKinds.available(x5s.model.profile, x5s.model.calibration);
|
||||
expect(k, containsAll(<CalibrationKind>[CalibrationKind.lens, CalibrationKind.sensorCleaning]));
|
||||
|
||||
final Rig spark = Rig(profile: sparkWifi);
|
||||
expect(CalibrationKinds.available(spark.model.profile, spark.model.calibration), isNot(contains(CalibrationKind.rc)));
|
||||
});
|
||||
});
|
||||
|
||||
group('SafetyGate', () {
|
||||
test('ground work is refused in the air and with the motors running', () {
|
||||
final Rig r = Rig(flying: true);
|
||||
final SafetyGate gate = SafetyGate(model: r.model, dji: FakeDjiService());
|
||||
for (final CalibrationKind k in <CalibrationKind>[
|
||||
CalibrationKind.compass,
|
||||
CalibrationKind.imu,
|
||||
CalibrationKind.rc,
|
||||
CalibrationKind.gimbal,
|
||||
CalibrationKind.lens,
|
||||
CalibrationKind.sensorCleaning,
|
||||
CalibrationKind.propeller,
|
||||
]) {
|
||||
expect(gate.checkMaintenance(k).where((GateCheck c) => c.blocks).map((GateCheck c) => c.message), contains('Land first.'), reason: k.name);
|
||||
}
|
||||
r.tick(<String, Object?>{'isFlying': false, 'areMotorsOn': true});
|
||||
expect(gate.checkMaintenance(CalibrationKind.compass).single.message, 'Stop the motors first.');
|
||||
r.tick(<String, Object?>{'areMotorsOn': false});
|
||||
expect(gate.checkMaintenance(CalibrationKind.compass), isEmpty);
|
||||
});
|
||||
|
||||
test('center of gravity is the exception: it needs a hover', () {
|
||||
final Rig ground = Rig();
|
||||
final SafetyGate g1 = SafetyGate(model: ground.model, dji: FakeDjiService());
|
||||
expect(g1.checkMaintenance(CalibrationKind.gravityCenter).any((GateCheck c) => c.blocks), isTrue);
|
||||
final Rig air = Rig(flying: true);
|
||||
final SafetyGate g2 = SafetyGate(model: air.model, dji: FakeDjiService());
|
||||
expect(g2.checkMaintenance(CalibrationKind.gravityCenter).any((GateCheck c) => c.blocks), isFalse);
|
||||
expect(g2.checkMaintenance(CalibrationKind.gimbalAdjust), isEmpty, reason: 'trim is fine in flight');
|
||||
});
|
||||
|
||||
test('runMaintenance re-checks and refuses without sending', () async {
|
||||
final Rig r = Rig(flying: true);
|
||||
final FakeDjiService dji = FakeDjiService();
|
||||
final SafetyGate gate = SafetyGate(model: r.model, dji: dji);
|
||||
await expectLater(gate.runMaintenance(CalibrationKind.imu, dji.startIMUCalibration), throwsA(isA<MaintenanceRefused>()));
|
||||
expect(dji.calls, isEmpty);
|
||||
});
|
||||
|
||||
test('a low battery and a cold IMU only warn', () {
|
||||
final Rig r = Rig();
|
||||
r.tick(<String, Object?>{
|
||||
'isIMUPreheating': true,
|
||||
'battery': <String, Object?>{'percent': 20},
|
||||
});
|
||||
final List<GateCheck> checks = SafetyGate(model: r.model, dji: FakeDjiService()).checkMaintenance(CalibrationKind.imu);
|
||||
expect(checks, isNotEmpty);
|
||||
expect(checks.any((GateCheck c) => c.blocks), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Runs', () {
|
||||
test('compass: a stale result never ends a new run; steps, then success', () {
|
||||
final Rig r = Rig();
|
||||
r.tick(<String, Object?>{'compassCalibrationState': 'SUCCESSFUL'});
|
||||
final CompassRun run = CompassRun()..begin();
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.starting, reason: 'SUCCESSFUL from before the start');
|
||||
|
||||
r.tick(<String, Object?>{'compassCalibrating': true, 'compassCalibrationState': 'HORIZONTAL'});
|
||||
run.observe(r.model);
|
||||
expect((run.phase, run.step), (RunPhase.running, CompassStep.horizontal));
|
||||
r.tick(<String, Object?>{'compassCalibrationState': 'VERTICAL'});
|
||||
run.observe(r.model);
|
||||
expect(run.step, CompassStep.vertical);
|
||||
r.tick(<String, Object?>{'compassCalibrating': false, 'compassCalibrationState': 'SUCCESSFUL'});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.succeeded);
|
||||
});
|
||||
|
||||
test('compass: an end without a verdict counts by the error flag', () {
|
||||
final Rig r = Rig();
|
||||
final CompassRun run = CompassRun()..begin();
|
||||
r.tick(<String, Object?>{'compassCalibrating': true, 'compassCalibrationState': 'VERTICAL'});
|
||||
run.observe(r.model);
|
||||
r.tick(<String, Object?>{'compassCalibrating': false, 'compassCalibrationState': 'NOT_CALIBRATING', 'compassHasError': true});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.failed);
|
||||
expect(run.failure, isNotNull);
|
||||
});
|
||||
|
||||
test('IMU: multi-side sequence walks the sides in order and averages progress', () {
|
||||
final Rig r = Rig();
|
||||
final ImuRun run = ImuRun()..begin();
|
||||
Map<String, Object?> unit(int i, String state, int progress) =>
|
||||
<String, Object?>{'index': i, 'state': state, 'progress': progress, 'connected': true};
|
||||
r.calibration(<String, Object?>{
|
||||
'imu': <String, Object?>{
|
||||
'units': <Object?>[unit(0, 'CALIBRATING', 20), unit(1, 'CALIBRATING', 40)],
|
||||
'multiSide': true,
|
||||
'orientationState': 'CALIBRATING',
|
||||
'toCalibrate': <Object?>['NOSE_DOWN', 'LEFT_DOWN', 'BOTTOM_DOWN'],
|
||||
'calibrated': <Object?>['BOTTOM_DOWN'],
|
||||
},
|
||||
});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.running);
|
||||
expect(run.progress, closeTo(0.3, 1e-9));
|
||||
expect(run.orientation, 'NOSE_DOWN');
|
||||
expect(run.sides, <(String, bool)>[('BOTTOM_DOWN', true), ('NOSE_DOWN', false), ('LEFT_DOWN', false)]);
|
||||
|
||||
r.calibration(<String, Object?>{
|
||||
'imu': <String, Object?>{
|
||||
'units': <Object?>[unit(0, 'SUCCESSFUL', 100), unit(1, 'SUCCESSFUL', 100)],
|
||||
'multiSide': true,
|
||||
'orientationState': 'COMPLETED',
|
||||
},
|
||||
});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.succeeded);
|
||||
});
|
||||
|
||||
test('IMU: one failed unit fails the run', () {
|
||||
final Rig r = Rig();
|
||||
final ImuRun run = ImuRun()..begin();
|
||||
r.calibration(<String, Object?>{
|
||||
'imu': <String, Object?>{
|
||||
'units': <Object?>[<String, Object?>{'index': 0, 'state': 'CALIBRATING', 'progress': 10}],
|
||||
},
|
||||
});
|
||||
run.observe(r.model);
|
||||
r.calibration(<String, Object?>{
|
||||
'imu': <String, Object?>{
|
||||
'units': <Object?>[<String, Object?>{'index': 0, 'state': 'FAILED'}],
|
||||
},
|
||||
});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.failed);
|
||||
});
|
||||
|
||||
test('RC: MIDDLE → LIMITS → finish; a timeout fails', () {
|
||||
final Rig r = Rig();
|
||||
final RcRun run = RcRun()..begin();
|
||||
r.calibration(<String, Object?>{'rc': <String, Object?>{'state': 'MIDDLE'}});
|
||||
run.observe(r.model);
|
||||
expect((run.phase, run.step), (RunPhase.running, RcStep.middle));
|
||||
r.calibration(<String, Object?>{'rc': <String, Object?>{'state': 'LIMITS'}});
|
||||
run.observe(r.model);
|
||||
expect(run.step, RcStep.limits);
|
||||
run
|
||||
..finishing()
|
||||
..observe(r.model);
|
||||
expect(run.phase, RunPhase.succeeded);
|
||||
|
||||
run.begin();
|
||||
r.calibration(<String, Object?>{'rc': <String, Object?>{'state': 'MIDDLE'}});
|
||||
run.observe(r.model);
|
||||
r.calibration(<String, Object?>{'rc': <String, Object?>{'state': 'TIMEOUT'}});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.failed);
|
||||
});
|
||||
|
||||
test('sensor cleaning ends well only after leaving the mode', () {
|
||||
final Rig r = Rig();
|
||||
final SensorCleaningRun run = SensorCleaningRun()..begin();
|
||||
for (final String s in <String>['INITIATING', 'WAITING_FOR_LENS_REMOVAL', 'READY', 'CLEANING', 'WAITING_FOR_LENS_REMOUNT']) {
|
||||
r.calibration(<String, Object?>{'sensorCleaning': s});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.running, reason: s);
|
||||
}
|
||||
expect(run.step, CleaningStep.remountLens);
|
||||
run.exiting();
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.running, reason: 'still in the mode');
|
||||
r.calibration(<String, Object?>{'sensorCleaning': 'IDLE'});
|
||||
run.observe(r.model);
|
||||
expect(run.phase, RunPhase.succeeded);
|
||||
});
|
||||
|
||||
test('lens and center of gravity read their verdicts', () {
|
||||
final Rig r = Rig();
|
||||
final LensRun lens = LensRun()..begin();
|
||||
r.calibration(<String, Object?>{'lens': <String, Object?>{'state': 'CALIBRATING', 'result': 'IDLE'}});
|
||||
lens.observe(r.model);
|
||||
r.calibration(<String, Object?>{'lens': <String, Object?>{'state': 'CALIBRATED', 'result': 'DATA_ERROR'}});
|
||||
lens.observe(r.model);
|
||||
expect(lens.phase, RunPhase.failed);
|
||||
|
||||
final GravityCenterRun gc = GravityCenterRun()..begin();
|
||||
r.calibration(<String, Object?>{'gravityCenter': <String, Object?>{'state': 'CALCULATING'}});
|
||||
gc.observe(r.model);
|
||||
r.calibration(<String, Object?>{'gravityCenter': <String, Object?>{'state': 'SUCCESSFUL'}});
|
||||
gc.observe(r.model);
|
||||
expect(gc.phase, RunPhase.succeeded);
|
||||
});
|
||||
});
|
||||
|
||||
group('Wizards', () {
|
||||
// The compass illustration spins forever, so pumpAndSettle never would.
|
||||
Future<void> settle(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
Future<FakeDjiService> open(WidgetTester tester, Rig r, CalibrationKind kind) async {
|
||||
tester.view.physicalSize = const Size(844, 390);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final FakeDjiService dji = FakeDjiService();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Builder(
|
||||
builder: (BuildContext context) => Scaffold(
|
||||
body: Center(
|
||||
child: TextButton(
|
||||
onPressed: () => openCalibration(context, kind, model: r.model, dji: dji, onError: (_, __) {}),
|
||||
child: const Text('open'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.tap(find.text('open'));
|
||||
await settle(tester);
|
||||
return dji;
|
||||
}
|
||||
|
||||
String heading(WidgetTester tester) => tester.widget<Text>(find.byKey(const ValueKey<String>('wizard-heading'))).data!;
|
||||
|
||||
testWidgets('compass: start, the two steps follow telemetry, done', (WidgetTester tester) async {
|
||||
final Rig r = Rig();
|
||||
final FakeDjiService dji = await open(tester, r, CalibrationKind.compass);
|
||||
expect(heading(tester), 'Calibrate the compass');
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-start')));
|
||||
await tester.pump();
|
||||
expect(dji.calls, <String>['startCompassCalibration']);
|
||||
expect(heading(tester), 'Starting…');
|
||||
|
||||
r.tick(<String, Object?>{'compassCalibrating': true, 'compassCalibrationState': 'HORIZONTAL'});
|
||||
await tester.pump();
|
||||
expect(find.text('STEP 1 OF 2'), findsOneWidget);
|
||||
r.tick(<String, Object?>{'compassCalibrationState': 'VERTICAL'});
|
||||
await tester.pump();
|
||||
expect(find.text('STEP 2 OF 2'), findsOneWidget);
|
||||
r.tick(<String, Object?>{'compassCalibrating': false, 'compassCalibrationState': 'SUCCESSFUL'});
|
||||
await tester.pump();
|
||||
expect(heading(tester), 'Compass calibrated');
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-done')));
|
||||
await settle(tester);
|
||||
expect(find.byType(CompassWizard), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('compass: closing mid-run asks, then stops the calibration', (WidgetTester tester) async {
|
||||
final Rig r = Rig();
|
||||
final FakeDjiService dji = await open(tester, r, CalibrationKind.compass);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-start')));
|
||||
await tester.pump();
|
||||
r.tick(<String, Object?>{'compassCalibrating': true, 'compassCalibrationState': 'HORIZONTAL'});
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-close')));
|
||||
await settle(tester);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-confirm-stop')));
|
||||
await settle(tester);
|
||||
expect(dji.calls, contains('stopCompassCalibration'));
|
||||
expect(find.byType(CompassWizard), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a blocked start says why and sends nothing', (WidgetTester tester) async {
|
||||
final Rig r = Rig(flying: true);
|
||||
final FakeDjiService dji = await open(tester, r, CalibrationKind.imu);
|
||||
expect(find.text('Land first.'), findsOneWidget);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-start')));
|
||||
await tester.pump();
|
||||
expect(dji.calls, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('IMU: shows the side to put down; leaving does not try to stop it', (WidgetTester tester) async {
|
||||
final Rig r = Rig();
|
||||
final FakeDjiService dji = await open(tester, r, CalibrationKind.imu);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-start')));
|
||||
await tester.pump();
|
||||
r.calibration(<String, Object?>{
|
||||
'imu': <String, Object?>{
|
||||
'units': <Object?>[<String, Object?>{'index': 0, 'state': 'CALIBRATING', 'progress': 50}],
|
||||
'multiSide': true,
|
||||
'toCalibrate': <Object?>['NOSE_DOWN', 'TAIL_DOWN'],
|
||||
'calibrated': <Object?>['BOTTOM_DOWN'],
|
||||
},
|
||||
});
|
||||
await tester.pump();
|
||||
expect(heading(tester), 'Put it nose down');
|
||||
expect(find.text('SIDE 2 OF 3'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey<String>('imu-side-TAIL_DOWN')), findsOneWidget);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-close')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ImuWizard), findsNothing, reason: 'no stop to confirm');
|
||||
expect(dji.calls, <String>['startIMUCalibration']);
|
||||
});
|
||||
|
||||
testWidgets('RC: watches the sticks, steps MIDDLE → LIMITS → QUIT, unwatches on the way out', (WidgetTester tester) async {
|
||||
final Rig r = Rig();
|
||||
final FakeDjiService dji = await open(tester, r, CalibrationKind.rc);
|
||||
expect(dji.calls, <String>['watchRcHardware(true)']);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-start')));
|
||||
await tester.pump();
|
||||
expect(dji.calls.last, 'setRcCalibrationStep(MIDDLE)');
|
||||
r.calibration(<String, Object?>{'rc': <String, Object?>{'state': 'MIDDLE'}});
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-next')));
|
||||
await tester.pump();
|
||||
expect(dji.calls.last, 'setRcCalibrationStep(LIMITS)');
|
||||
r.calibration(<String, Object?>{
|
||||
'rc': <String, Object?>{'state': 'LIMITS'},
|
||||
'rcHardware': <String, Object?>{'lx': 660, 'ly': 660, 'rx': -660, 'ry': -660},
|
||||
});
|
||||
await tester.pump();
|
||||
expect(heading(tester), 'Move every stick and dial to its limits');
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-finish')));
|
||||
await tester.pump();
|
||||
expect(dji.calls.last, 'setRcCalibrationStep(QUIT)');
|
||||
expect(heading(tester), 'Remote controller calibrated');
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-done')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(dji.calls.last, 'watchRcHardware(false)');
|
||||
});
|
||||
|
||||
testWidgets('sensor cleaning: the buttons follow the camera, Stop leaves the mode', (WidgetTester tester) async {
|
||||
final Rig r = Rig(profile: withLens(inspire2X5s));
|
||||
final FakeDjiService dji = await open(tester, r, CalibrationKind.sensorCleaning);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-start')));
|
||||
await tester.pump();
|
||||
r.calibration(<String, Object?>{'sensorCleaning': 'READY'});
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-clean')));
|
||||
await tester.pump();
|
||||
expect(dji.calls, <String>['initSensorCleaning', 'startSensorCleaning']);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-stop')));
|
||||
await tester.pump();
|
||||
expect(dji.calls.last, 'exitSensorCleaning');
|
||||
expect(heading(tester), 'Clean the image sensor', reason: 'back at the start');
|
||||
});
|
||||
|
||||
testWidgets('the hub lists the offered wizards and opens one', (WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(844, 390);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final Rig r = Rig();
|
||||
r.calibration(<String, Object?>{
|
||||
'support': <String, Object?>{'gravityCenter': false},
|
||||
});
|
||||
await tester.pumpWidget(MaterialApp(home: CalibrationHubPage(model: r.model, dji: FakeDjiService(), onError: (_, __) {})));
|
||||
expect(find.byKey(const ValueKey<String>('hub-compass')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey<String>('hub-gravityCenter')), findsNothing);
|
||||
expect(find.byKey(const ValueKey<String>('hub-lens')), findsNothing);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('hub-imu')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ImuWizard), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Aircraft Status: Calibrate opens the compass wizard instead of starting at once', (WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(844, 390);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final Rig r = Rig();
|
||||
final FakeDjiService dji = FakeDjiService();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Builder(
|
||||
builder: (BuildContext context) => TextButton(
|
||||
onPressed: () => showAircraftStatusSheet(context, model: r.model, dji: dji, onError: (_, __) {}),
|
||||
child: const Text('status'),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.tap(find.text('status'));
|
||||
await settle(tester);
|
||||
final Finder calibrate = find.byKey(const ValueKey<String>('status-action-compass'));
|
||||
await tester.scrollUntilVisible(calibrate, 60, scrollable: find.byType(Scrollable).last);
|
||||
await tester.tap(calibrate);
|
||||
await settle(tester);
|
||||
expect(find.byType(CompassWizard), findsOneWidget);
|
||||
expect(dji.calls, isNot(contains('startCompassCalibration')));
|
||||
});
|
||||
|
||||
testWidgets('gimbal adjust lays the horizon over the screen and nudges each axis', (WidgetTester tester) async {
|
||||
final Rig r = Rig(profile: inspire2X5s);
|
||||
final FakeDjiService dji = await open(tester, r, CalibrationKind.gimbalAdjust);
|
||||
expect(find.byType(GimbalAdjustOverlay), findsOneWidget);
|
||||
expect(find.text('open'), findsOneWidget, reason: 'the page underneath stays visible');
|
||||
await tester.tap(find.byKey(const ValueKey<String>('step-adjustRoll-plus')));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const ValueKey<String>('step-adjustPitch-minus')));
|
||||
await tester.pump();
|
||||
expect(dji.calls, <String>['fineTuneGimbal(roll, 0.1)', 'fineTuneGimbal(pitch, -0.1)']);
|
||||
await tester.tap(find.byKey(const ValueKey<String>('adjust-done')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(GimbalAdjustOverlay), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -37,6 +37,34 @@ class FakeDjiService extends DjiService {
|
||||
Future<void> stopActiveTrack() => _record('stopActiveTrack');
|
||||
@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
|
||||
|
||||
@@ -6,10 +6,10 @@ import 'package:pilotvault_fly/fly/gimbal_control.dart';
|
||||
import 'package:pilotvault_fly/fly/hud_features.dart';
|
||||
import 'package:pilotvault_fly/fly/product_profile.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/panels/camera_write.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/panels/gimbal_sheet.dart';
|
||||
import 'package:pilotvault_fly/ui/fly/panels/settings/calibration/wizards.dart';
|
||||
|
||||
import 'fake_dji.dart';
|
||||
import 'profile_fixtures.dart';
|
||||
@@ -250,7 +250,7 @@ void main() {
|
||||
expect(dji.calls, containsAllInOrder(<String>['fineTuneGimbal(roll, 0.1)', 'fineTuneGimbal(roll, -0.1)']));
|
||||
});
|
||||
|
||||
testWidgets('calibration is blocked in flight and slide-confirmed on the ground', (WidgetTester tester) async {
|
||||
testWidgets('calibration is blocked in flight and runs through its wizard on the ground', (WidgetTester tester) async {
|
||||
final FlightModel m = gimbalModel(phantom4);
|
||||
final FakeDjiService dji = await pump(tester, m);
|
||||
await openSheet(tester);
|
||||
@@ -260,8 +260,8 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(find.text('Land and stop the motors first'), findsOneWidget);
|
||||
await tester.tap(calibrate);
|
||||
await tester.pump();
|
||||
expect(find.byType(SlideConfirmDialog), findsNothing);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(GimbalCalibrationWizard), findsNothing);
|
||||
|
||||
m.aircraft
|
||||
..isFlying = false
|
||||
@@ -269,12 +269,11 @@ void main() {
|
||||
..notifyListeners();
|
||||
await tester.pump();
|
||||
await tester.tap(calibrate);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(GimbalCalibrationWizard), findsOneWidget);
|
||||
expect(dji.calls, isNot(contains('startGimbalCalibration')));
|
||||
await tester.drag(find.byKey(SlideToConfirmTrack.thumbKey), const Offset(600, 0));
|
||||
await tester.tap(find.byKey(const ValueKey<String>('wizard-start')));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(dji.calls, contains('startGimbalCalibration'));
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user