Files
PilotVault/Fly App/lib/fly/hud_features.dart
T
tajniak81andClaude Opus 5.5 e49f318a0e Finish the Fly App's ActiveTrack and TapFly extras and letterbox the feed
Auto-detect: on aircraft that can sense subjects themselves, the
ActiveTrack panel offers Auto-detect; the subjects it finds show as dashed
boxes on the video, and tapping one then GO follows it by its index.

Trace gets a Backward switch (fly ahead of a subject walking towards the
aircraft, obstacle-sensing aircraft only) and TapFly Free a Reset heading
button. Settings > Vision gains Gesture mode and Palm control where the
flight assistant supports them, and the HUD shows when palm control is
flying the aircraft.

The live view used to stretch the feed over the screen. It now fits the
picture to the decoder's own size with black bars, and reports that size,
so the ActiveTrack / TapFly overlays, tap-to-focus / meter and the grid
all map into the picture and ignore taps on the bars.

Not yet run on a simulator or aircraft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:23:37 +02:00

249 lines
13 KiB
Dart

import 'package:flutter/widgets.dart';
import 'product_profile.dart';
/// One capability-gated piece of UI: a HUD element, settings row or mode tile.
///
/// Its [requires] predicate is the only place that decides whether the element
/// exists on the connected product — GO 4 does the same with per-product `gone`
/// views. Gate a widget with [Requires]; list every feature in
/// [HudFeatures.all] so the debug screen and the widget tests see it.
class HudFeature {
const HudFeature(this.id, this.label, this.group, this.requires);
final String id;
final String label;
/// Where the element lives (`Status bar`, `Camera`, …), for the debug screen.
final String group;
final bool Function(ProductProfile p) requires;
bool enabledFor(ProductProfile p) => requires(p);
}
abstract final class HudFeatures {
// ── Status bar ────────────────────────────────────────────────────────────
static final HudFeature rcHdSignal = HudFeature(
'status.rcHdSignal', 'RC + HD signal bars', 'Status bar', (ProductProfile p) => p.hasRcLink);
static final HudFeature wifiSignal = HudFeature(
'status.wifiSignal', 'Wi-Fi signal', 'Status bar', (ProductProfile p) => p.hasWifiLink);
static final HudFeature visionIndicator = HudFeature(
'status.vision', 'Vision system icon', 'Status bar', (ProductProfile p) => p.hasVps);
static final HudFeature masterSlaveBadge = HudFeature(
'status.masterSlave', 'Master / slave RC badge', 'Status bar', (ProductProfile p) => p.hasMasterSlaveRc);
static final HudFeature rtkStatus = HudFeature(
'status.rtk', 'RTK status', 'Status bar', (ProductProfile p) => p.hasRtk);
// ── Battery ───────────────────────────────────────────────────────────────
static final HudFeature singleBattery = HudFeature(
'battery.single', 'Single battery bar', 'Battery', (ProductProfile p) => p.batteryCount == 1);
static final HudFeature dualBattery = HudFeature(
'battery.dual', 'Dual battery bars', 'Battery', (ProductProfile p) => p.batteryCount == 2);
static final HudFeature multiBattery = HudFeature(
'battery.multi', 'Multi-battery summary', 'Battery', (ProductProfile p) => p.batteryCount > 2);
static final HudFeature nonSmartBattery = HudFeature(
'battery.nonSmart', 'Cell-voltage actions (non-smart)', 'Battery',
(ProductProfile p) => p.batteryKind == BatteryKind.nonSmart);
// ── Camera ────────────────────────────────────────────────────────────────
static final HudFeature cameraControls = HudFeature(
'camera.controls', 'Shutter + mode column', 'Camera', (ProductProfile p) => p.hasCamera);
static final HudFeature apertureReadout = HudFeature(
'camera.aperture', 'Aperture readout + wheel', 'Camera',
(ProductProfile p) => p.camera?.adjustableApertureSupported == true);
static final HudFeature zoomColumn = HudFeature(
'camera.zoom', 'Zoom column', 'Camera', (ProductProfile p) => p.hasZoom);
static final HudFeature ssdStatus = HudFeature(
'camera.ssd', 'SSD status', 'Camera', (ProductProfile p) => p.hasSsd);
static final HudFeature thermalReadout = HudFeature(
'camera.thermal', 'Thermal readouts', 'Camera', (ProductProfile p) => p.hasThermal);
static final HudFeature spotMetering = HudFeature(
'camera.metering', 'Tap-to-meter', 'Camera', (ProductProfile p) => p.camera?.meteringSupported == true);
static final HudFeature tapFocus = HudFeature(
'camera.focus', 'Tap-to-focus', 'Camera', (ProductProfile p) => p.camera?.adjustableFocalPointSupported == true);
static final HudFeature playback = HudFeature('camera.playback', 'Playback button', 'Camera',
(ProductProfile p) => p.camera?.mediaDownloadSupported == true || p.camera?.playbackSupported == true);
static final HudFeature histogram = HudFeature(
'camera.histogram', 'Histogram', 'Camera', (ProductProfile p) => p.hasCamera);
// ── Gimbal ────────────────────────────────────────────────────────────────
static final HudFeature gimbalPitch = HudFeature(
'gimbal.pitch', 'Gimbal pitch slider', 'Gimbal', (ProductProfile p) => p.gimbal?.pitchAdjustable == true);
static final HudFeature gimbalYaw = HudFeature(
'gimbal.yaw', 'Gimbal yaw (drag, recenter, Free mode)', 'Gimbal', (ProductProfile p) => p.gimbal?.yawAdjustable == true);
static final HudFeature gimbalPitchExtension = HudFeature(
'gimbal.pitchExtension', 'Extend pitch to +30°', 'Gimbal',
(ProductProfile p) => p.gimbal?.pitchRangeExtensionSupported == true);
static final HudFeature gimbalDrag = HudFeature(
'gimbal.drag', 'Drag on video to aim', 'Gimbal', (ProductProfile p) => p.gimbal?.pitchAdjustable == true);
static final HudFeature gimbalSettings = HudFeature(
'gimbal.settings', 'Gimbal settings (mode, recenter)', 'Gimbal', (ProductProfile p) => p.hasGimbal);
static final HudFeature gimbalRollAdjust = HudFeature(
'gimbal.rollAdjust', 'Adjust gimbal roll', 'Gimbal', (ProductProfile p) => p.hasGimbal);
static final HudFeature gimbalCalibration = HudFeature(
'gimbal.calibration', 'Auto calibrate gimbal', 'Gimbal', (ProductProfile p) => p.hasGimbal);
static final HudFeature gimbalAdvanced = HudFeature('gimbal.advanced', 'Gimbal speed / smoothness', 'Gimbal',
(ProductProfile p) => p.gimbal?.controller.isNotEmpty == true);
// ── Vision / control ──────────────────────────────────────────────────────
static final HudFeature vpsReadout = HudFeature(
'telemetry.vps', 'VPS height', 'Telemetry', (ProductProfile p) => p.hasVps);
static final HudFeature visionRadar = HudFeature(
'overlay.radar', 'Vision radar', 'Overlays', (ProductProfile p) => p.hasObstacleSensing);
static final HudFeature virtualSticks = HudFeature(
'control.virtualSticks', 'On-screen sticks', 'Control', (ProductProfile p) => p.supportsVirtualStick);
/// Precision take-off records the spot with the downward cameras.
static final HudFeature precisionTakeoff = HudFeature(
'control.precisionTakeoff', 'Precision Takeoff option', 'Control', (ProductProfile p) => p.hasVps);
static final HudFeature simulator = HudFeature(
'control.simulator', 'Simulator toggle', 'Control', (ProductProfile p) => p.hasSimulator);
// ── Settings sheet (plan P6) ──────────────────────────────────────────────
static final HudFeature settingsVision = HudFeature(
'settings.vision', 'Vision settings tab', 'Settings', (ProductProfile p) => p.hasVps);
static final HudFeature settingsObstacle = HudFeature(
'settings.obstacle', 'Obstacle avoidance settings', 'Settings', (ProductProfile p) => p.hasObstacleSensing);
/// Transmission rows address the link's own key class, so each set is gated
/// on the link type.
/// Gesture and palm control ride on ActiveTrack's vision.
static final HudFeature settingsGestures = HudFeature('settings.gestures', 'Gesture & palm control', 'Settings',
(ProductProfile p) => p.supportsMode(IntelligentMode.activeTrack));
static final HudFeature linkOcuSync = HudFeature(
'settings.ocusync', 'OcuSync transmission settings', 'Settings', (ProductProfile p) => p.link == LinkType.ocusync);
static final HudFeature linkLightbridge = HudFeature('settings.lightbridge', 'Lightbridge transmission settings', 'Settings',
(ProductProfile p) => p.link == LinkType.lightbridge);
static final HudFeature linkWifi = HudFeature(
'settings.wifi', 'Wi-Fi transmission settings', 'Settings', (ProductProfile p) => p.link == LinkType.wifi);
// ── Calibration & maintenance wizards (plan P6b) ──────────────────────────
// Runtime support (`CalibrationState`) can still drop a wizard the profile allows.
static final HudFeature calibrationCompass = HudFeature(
'calibration.compass', 'Compass calibration', 'Calibration', (ProductProfile p) => p.isAircraft);
static final HudFeature calibrationImu = HudFeature(
'calibration.imu', 'IMU calibration', 'Calibration', (ProductProfile p) => p.isAircraft);
static final HudFeature calibrationRc = HudFeature(
'calibration.rc', 'Remote controller calibration', 'Calibration', (ProductProfile p) => p.remoteController != null);
static final HudFeature gimbalAdjust = HudFeature(
'calibration.gimbalAdjust', 'Adjust gimbal (horizon)', 'Calibration', (ProductProfile p) => p.hasGimbal);
static final HudFeature calibrationLens = HudFeature('calibration.lens', 'Lens calibration', 'Calibration',
(ProductProfile p) => p.camera?.interchangeableLensSupported == true);
static final HudFeature sensorCleaning = HudFeature('calibration.sensorCleaning', 'Sensor cleaning', 'Calibration',
(ProductProfile p) => p.camera?.interchangeableLensSupported == true);
static final HudFeature calibrationGravityCenter = HudFeature(
'calibration.gravityCenter', 'Center of gravity calibration', 'Calibration', (ProductProfile p) => p.isAircraft);
static final HudFeature calibrationPropeller = HudFeature('calibration.propeller', 'Propeller calibration', 'Calibration',
(ProductProfile p) => p.flightController?.propellerCalibrationSupported == true);
static final HudFeature visionSensorGuide = HudFeature(
'calibration.vision', 'Vision sensor calibration (guide)', 'Calibration', (ProductProfile p) => p.hasVps);
// ── Intelligent flight modes (plan P7) ────────────────────────────────────
static final HudFeature modePicker = HudFeature(
'mode.picker', 'Intelligent Flight Modes button', 'Intelligent modes', (ProductProfile p) => p.modes.isNotEmpty);
static final List<HudFeature> modes = <HudFeature>[
for (final IntelligentMode m in IntelligentMode.values)
HudFeature('mode.${m.name}', _modeLabel(m), 'Intelligent modes', (ProductProfile p) => p.supportsMode(m)),
];
static final List<HudFeature> all = <HudFeature>[
rcHdSignal,
wifiSignal,
visionIndicator,
masterSlaveBadge,
rtkStatus,
singleBattery,
dualBattery,
multiBattery,
nonSmartBattery,
cameraControls,
apertureReadout,
zoomColumn,
ssdStatus,
thermalReadout,
spotMetering,
tapFocus,
playback,
histogram,
gimbalPitch,
gimbalYaw,
gimbalPitchExtension,
gimbalDrag,
gimbalSettings,
gimbalRollAdjust,
gimbalCalibration,
gimbalAdvanced,
vpsReadout,
visionRadar,
virtualSticks,
precisionTakeoff,
simulator,
settingsVision,
settingsObstacle,
settingsGestures,
linkOcuSync,
linkLightbridge,
linkWifi,
calibrationCompass,
calibrationImu,
calibrationRc,
gimbalAdjust,
calibrationLens,
sensorCleaning,
calibrationGravityCenter,
calibrationPropeller,
visionSensorGuide,
modePicker,
...modes,
];
static String _modeLabel(IntelligentMode m) => switch (m) {
IntelligentMode.waypoint => 'Waypoints',
IntelligentMode.waypointV2 => 'Waypoints V2',
IntelligentMode.pointOfInterest => 'Point of Interest',
IntelligentMode.activeTrack => 'ActiveTrack',
IntelligentMode.tapFly => 'TapFly',
IntelligentMode.followMe => 'Follow Me',
IntelligentMode.quickShots => 'QuickShots',
IntelligentMode.courseLock => 'Course Lock',
IntelligentMode.homeLock => 'Home Lock',
IntelligentMode.tripod => 'Tripod',
IntelligentMode.hyperlapse => 'Hyperlapse',
};
}
/// Makes the active [ProductProfile] available to every [Requires] below it.
class ProductProfileScope extends InheritedWidget {
const ProductProfileScope({super.key, required this.profile, required super.child});
final ProductProfile profile;
/// The nearest scope's profile, or [ProductProfile.disconnected] when none.
static ProductProfile of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<ProductProfileScope>()?.profile ??
ProductProfile.disconnected;
@override
bool updateShouldNotify(ProductProfileScope oldWidget) => !identical(profile, oldWidget.profile);
}
/// Shows [child] only when [feature] is enabled for the scope's profile.
///
/// With no product connected there is nothing to be capable *of*, so the gate
/// is open by default and the HUD keeps its full layout as a preview; pass
/// `whenDisconnected: false` for elements that must never appear without a
/// product.
class Requires extends StatelessWidget {
const Requires(this.feature, {super.key, required this.child, this.fallback, this.whenDisconnected = true});
final HudFeature feature;
final Widget child;
final Widget? fallback;
final bool whenDisconnected;
@override
Widget build(BuildContext context) {
final ProductProfile p = ProductProfileScope.of(context);
final bool on = p.connected ? feature.enabledFor(p) : whenDisconnected;
return on ? child : (fallback ?? const SizedBox.shrink());
}
}