Files
PilotVault/Fly App/lib/fly/camera_exposure.dart
T
tajniak81andClaude Opus 5.5 c1770fff73 Give the Fly App GO 4-style camera controls
One settings table in CameraBridge watches every camera value into the
telemetry tick and backs a generic setCameraValue, so the tabbed camera
menu (Exposure / Photo / Video / Other) is built from row specs and the
camera's own capability ranges. Writes show at once and fall back to what
the camera reports. Adds the quick exposure panel (Auto/A/S/M, ISO slider,
shutter and aperture wheels, EV strip, metered EV in M), a new camera
column with record timer and interval/pano progress, a zoom column for
zoom cameras, tap-to-focus / tap-to-meter, and a live histogram.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:57:38 +02:00

143 lines
5.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'product_profile.dart';
import 'state/camera_state.dart';
/// GO 4's four exposure modes (plan P4 quick exposure panel). The wire value is
/// the SDK's `ExposureMode` name, sent through `setCameraValue('exposureMode')`.
enum ExposureMode {
auto('PROGRAM', 'Auto'),
aperture('APERTURE_PRIORITY', 'A'),
shutter('SHUTTER_PRIORITY', 'S'),
manual('MANUAL', 'M');
const ExposureMode(this.wire, this.label);
final String wire;
final String label;
static ExposureMode? fromWire(String? wire) {
for (final ExposureMode m in values) {
if (m.wire == wire) return m;
}
return null;
}
/// What the pilot may set by hand in this mode. In M the EV readout is the
/// meter's verdict rather than a setting.
bool get isoEditable => this != auto;
bool get shutterEditable => this == shutter || this == manual;
bool get apertureEditable => this == aperture || this == manual;
bool get evEditable => this != manual;
}
/// The option lists the exposure controls offer, from the camera's own
/// capability ranges (P0) and a conservative fallback while those are still
/// empty (before the camera's handshake, or with nothing connected).
abstract final class ExposureOptions {
static const List<String> _isos = <String>['100', '200', '400', '800', '1600', '3200'];
static const List<String> _shutters = <String>[
'1/8000', '1/4000', '1/2000', '1/1000', '1/500', '1/240', '1/120', '1/60', '1/30', '1/15', '1/8', '1/4', '1/2', '1',
];
static const List<String> _evs = <String>[
'-3.0', '-2.7', '-2.3', '-2.0', '-1.7', '-1.3', '-1.0', '-0.7', '-0.3', '0.0', //
'+0.3', '+0.7', '+1.0', '+1.3', '+1.7', '+2.0', '+2.3', '+2.7', '+3.0',
];
/// The modes the segmented control shows: the camera's own list, with A only
/// on adjustable-aperture lenses.
static List<ExposureMode> modes(CameraProfile? c) {
final List<ExposureMode> reported = <ExposureMode>[
for (final String w in c?.exposureModes ?? const <String>[])
if (ExposureMode.fromWire(w) case final ExposureMode m) m,
];
final List<ExposureMode> all = reported.isEmpty ? ExposureMode.values : reported;
final bool aperture = c == null || c.adjustableApertureSupported;
return <ExposureMode>[
for (final ExposureMode m in ExposureMode.values)
if (all.contains(m) && (m != ExposureMode.aperture || aperture)) m,
];
}
/// Numeric ISOs for the slider; `AUTO` is its own toggle.
static List<String> isos(CameraProfile? c) {
final List<String> l = <String>[for (final String v in c?.isos ?? const <String>[]) if (int.tryParse(v) != null) v];
return l.isEmpty ? _isos : l;
}
static bool isoAutoAvailable(CameraProfile? c) => c == null || c.isos.isEmpty || c.isos.contains('AUTO');
/// Fastest first, as the SDK lists them.
static List<String> shutters(CameraProfile? c) =>
(c?.shutterSpeeds.isNotEmpty ?? false) ? c!.shutterSpeeds : _shutters;
static List<String> apertures(CameraProfile? c) => c?.apertures ?? const <String>[];
/// Lowest first.
static List<String> evs(CameraProfile? c) {
final List<String> l = (c?.exposureCompensations.isNotEmpty ?? false) ? c!.exposureCompensations : _evs;
return List<String>.of(l)..sort((String a, String b) => (evValue(a) ?? 0).compareTo(evValue(b) ?? 0));
}
/// The EV one press of − / + lands on, or null at the end of the range.
static String? stepEv(String? current, List<String> evs, int direction) {
if (evs.isEmpty) return null;
final double now = evValue(current) ?? 0;
if (direction > 0) {
for (final String e in evs) {
if ((evValue(e) ?? 0) > now + 0.05) return e;
}
} else {
for (final String e in evs.reversed) {
if ((evValue(e) ?? 0) < now - 0.05) return e;
}
}
return null;
}
/// `+0.7` → 0.7, `0.0` / `0` → 0.
static double? evValue(String? label) => label == null ? null : double.tryParse(label.replaceFirst('+', ''));
/// The index in [values] closest to [current], for a wheel to start on.
static int nearest(List<String> values, String? current) {
if (values.isEmpty) return 0;
final int exact = current == null ? -1 : values.indexOf(current);
if (exact >= 0) return exact;
final double? v = _numeric(current);
if (v == null) return 0;
int best = 0;
double bestD = double.infinity;
for (int i = 0; i < values.length; i++) {
final double? x = _numeric(values[i]);
if (x == null) continue;
final double d = (x - v).abs();
if (d < bestD) {
bestD = d;
best = i;
}
}
return best;
}
/// `1/240` → 0.0041, `2.5` → 2.5, `f/2.8` → 2.8, `400` → 400.
static double? _numeric(String? s) {
if (s == null) return null;
final String t = s.replaceFirst('f/', '').replaceAll('"', '');
final int slash = t.indexOf('/');
if (slash > 0) {
final double? n = double.tryParse(t.substring(0, slash)), d = double.tryParse(t.substring(slash + 1));
return n == null || d == null || d == 0 ? null : n / d;
}
return double.tryParse(t);
}
}
/// How far the meter is from a correct exposure in M, for the indicator:
/// within a third of a stop is fine, past two-thirds is worth a colour.
enum MeterVerdict { ok, under, over }
MeterVerdict meterVerdict(CameraState c) {
final double ev = ExposureOptions.evValue(c.ev) ?? 0;
if (ev <= -0.7) return MeterVerdict.under;
if (ev >= 0.7) return MeterVerdict.over;
return MeterVerdict.ok;
}