Plan P5: a value bubble on the pitch slider while dragging, press-and-drag on the video as a speed joystick (re-sent at 10 Hz, zero on release), and a gimbal settings sheet: mode, 30° extension, yaw follow, recenter / face down / yaw recenter, roll fine-tune, auto calibration and advanced speed/smoothness rows gated by the gimbal's capability ranges. GimbalBridge reports mode, trim, calibration and stop/overload state in the gimbal telemetry group, and getter-only settings in a new gimbalSettings slow group. The reported-plus-pending overlay moves out of CameraState into a SettingsOverlay mixin shared by camera and gimbal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
9.8 KiB
Dart
193 lines
9.8 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);
|
|
|
|
// ── Intelligent flight modes ──────────────────────────────────────────────
|
|
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,
|
|
...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());
|
|
}
|
|
}
|