Take-off, land, RTH, the landing cancel/confirm and Stop now all go through one SafetyGate (lib/fly/safety_gate.dart). The gate sorts its preconditions into blockers and warnings: connection, flight state, battery against the thresholds, IMU warm-up, compass, critical diagnostics, GPS, home point, SD card and the RTH battery budget. It checks again right before sending. The left rail is now a state machine (lib/fly/action_rail_state.dart): Take-off becomes Land, then Cancel or Confirm landing. RTH becomes Cancel RTH. A Stop button shows while an intelligent mode is flying. Moving the aircraft needs a slide-to-confirm dialog, which can offer Precision Takeoff or set the RTH altitude. A blocked command shows why and has no slider. When the pilot is more than 30 m from home, RTH asks whether to return to them or to the original home point. The HUD gains an error stack. Its cards can be dismissed, it fits the space it has, and a new warning triggers haptic feedback. It also gets a smart-RTH countdown banner with Cancel / Go home now, a forced-landing banner, and an Aircraft Status sheet. The sheet opens from the status chip and appears before each connection's first take-off. It offers Calibrate compass/IMU and Format SD. On the bridge side this adds precisionTakeOff, setHomeLocation, compass and IMU calibration start/stop, formatStorage, and compass health in the telemetry tick. The classic flight screen stays available until the simulator run passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
3.8 KiB
Dart
103 lines
3.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../flight_model.dart';
|
|
import '../../../fly/action_rail_state.dart';
|
|
import '../../../theme.dart';
|
|
import '../../pv_icons.dart';
|
|
import '../flight_command_flow.dart';
|
|
import '../hud_kit.dart';
|
|
|
|
/// The left rail (plan P3): the [RailState] machine's buttons — Take-off ⇄
|
|
/// Land ⇄ Cancel / Confirm landing, RTH ⇄ Cancel RTH while airborne, Stop
|
|
/// while an intelligent mode flies — then the grid toggle. Every press goes
|
|
/// through [FlightCommandFlow], i.e. the SafetyGate and slide-to-confirm.
|
|
class ActionRail extends StatelessWidget {
|
|
const ActionRail({super.key, required this.model, required this.flow, required this.grid, required this.onGrid});
|
|
|
|
final FlightModel model;
|
|
final FlightCommandFlow flow;
|
|
final bool grid;
|
|
final VoidCallback onGrid;
|
|
|
|
static ({String icon, String label, Color color}) _look(FlightCommand c) => switch (c) {
|
|
FlightCommand.takeOff => (icon: 'takeoff', label: 'TAKE OFF', color: Glass.accent),
|
|
FlightCommand.land => (icon: 'land', label: 'LAND', color: Glass.pillStrong),
|
|
FlightCommand.cancelLanding => (icon: 'close', label: 'CANCEL LAND', color: HudColors.caution),
|
|
FlightCommand.confirmLanding => (icon: 'check', label: 'CONFIRM LAND', color: HudColors.caution),
|
|
FlightCommand.goHome => (icon: 'rth', label: 'RTH', color: Glass.pillStrong),
|
|
FlightCommand.cancelGoHome => (icon: 'close', label: 'CANCEL RTH', color: HudColors.caution),
|
|
FlightCommand.stopMode => (icon: 'stop', label: 'STOP', color: HudColors.danger),
|
|
};
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: Listenable.merge(<Listenable>[model.aircraft, model.mission]),
|
|
builder: (BuildContext context, _) {
|
|
final RailState rail = RailState.of(connected: model.connected, a: model.aircraft, mission: model.mission);
|
|
Widget button(FlightCommand c) {
|
|
final ({String icon, String label, Color color}) l = _look(c);
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: RailButton(
|
|
key: ValueKey<String>('rail-${c.name}'),
|
|
icon: l.icon,
|
|
label: l.label,
|
|
color: l.color,
|
|
enabled: rail.enabled,
|
|
onTap: () => flow.request(context, c),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
|
button(rail.primary),
|
|
if (rail.rth != null) button(rail.rth!),
|
|
if (rail.showStop) button(FlightCommand.stopMode),
|
|
// Four buttons would reach the minimap on a phone; Stop outranks the grid.
|
|
if (!rail.showStop)
|
|
RailButton(icon: 'grid', label: 'GRID', color: grid ? Glass.accent : Glass.pillStrong, onTap: onGrid),
|
|
]);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class RailButton extends StatelessWidget {
|
|
const RailButton({
|
|
super.key,
|
|
required this.icon,
|
|
required this.label,
|
|
required this.onTap,
|
|
this.color = Glass.pillStrong,
|
|
this.enabled = true,
|
|
});
|
|
|
|
final String icon;
|
|
final String label;
|
|
final VoidCallback onTap;
|
|
final Color color;
|
|
final bool enabled;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: enabled ? onTap : null,
|
|
child: Opacity(
|
|
opacity: enabled ? 1 : 0.45,
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle, border: Border.all(color: const Color(0x2EFFFFFF))),
|
|
alignment: Alignment.center,
|
|
child: PVIcon(icon, size: 20, color: Colors.white),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(label, style: HudText.label.copyWith(fontSize: 7.5)),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|