Files
PilotVault/Fly App/lib/fly/product_matrix.dart
T
tajniak81andClaude Opus 5.5 606838a0b1 Teach the Fly App what the connected aircraft can do
The GO 4-parity HUD has to work on every aircraft MSDK V4 drives, so no
screen may assume one model. This adds the capability layer the rest of
the plan builds on (P0).

ProductProfileBridge reads what the SDK can report at runtime (cameras
and their ISO/shutter/aperture/EV ranges, gimbal axes, air link, FC and
RC feature flags, batteries) and emits it as a productProfile event on
connect and component change. Capability ranges stay empty until the
camera handshakes, so it re-probes at 3, 10 and 30 s and emits only
when something changed.

On the Dart side, ProductProfile merges that with a static
product_matrix keyed by the MSDK Model enum name, for facts the SDK
cannot report: vision sensor layout, battery bay count, intelligent
modes. SDK answers win and every fact records its source. HudFeatures
lists each capability-gated element with its predicate, and Requires
hides a widget the profile rules out. The flight page now gates its
gimbal slider, camera column and vision pill, labels the link Wi-Fi or
HD, and takes the gimbal pitch range from the profile.

Settings > About > Capability profile shows the active profile, fact
sources and which features are on. Matrix rows are seeded from published
specs until runtime capture confirms them; nothing has run on a real
aircraft yet.

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

205 lines
13 KiB
Dart

/// Static per-model facts the SDK does not report (plan §1.7 / P0).
///
/// `ProductProfileBridge.kt` publishes everything MSDK V4 can answer at runtime
/// — cameras and their ranges, gimbal axes, the air link, RC and FC feature
/// flags, the batteries it can see. This table fills the rest: the vision sensor
/// layout, the usual link before the air link has enumerated, how many battery
/// bays a model has, and which intelligent flight modes its firmware offers.
///
/// Keys are `dji.common.product.Model` enum names exactly as MSDK 4.18 spells
/// them (the bridge publishes `model` raw), so `Phantom_3_4K` and `Spark` keep
/// their odd casing.
///
/// Every row says where its facts came from ([MatrixSource]). The first seed is
/// [MatrixSource.publishedSpec]; runtime capture against GO 4 on real aircraft
/// (research track R2) upgrades rows to [MatrixSource.runtimeCapture] as it
/// corrects them. Where the SDK reports the same fact at runtime, the SDK wins —
/// see `ProductProfile.fromBridge`.
library;
/// The transmission system between the aircraft and the pilot.
enum LinkType { wifi, lightbridge, ocusync, unknown }
/// The vision sensor layout, coarsest to richest. `downward` is VPS only (no
/// obstacle sensing); `front`, `frontBack` and `omni` all include the downward
/// VPS as well.
enum VisionSensors { unknown, none, downward, front, frontBack, omni }
enum BatteryKind { smart, nonSmart, unknown }
/// Intelligent flight modes, as the IFM picker (P7) will offer them.
enum IntelligentMode {
waypoint,
waypointV2,
pointOfInterest,
activeTrack,
tapFly,
followMe,
quickShots,
courseLock,
homeLock,
tripod,
hyperlapse,
}
/// Where a matrix row's facts come from.
enum MatrixSource {
/// Checked against the MSDK 4.18 jar / runtime SDK answers.
verifiedSdk,
/// Read off GO 4's per-product layouts and strings.
go4Layouts,
/// Observed in GO 4 running against the real aircraft (track R2).
runtimeCapture,
/// DJI's published product specifications; the first seed, still to be
/// confirmed by [runtimeCapture].
publishedSpec,
}
class ProductMatrixEntry {
const ProductMatrixEntry({
required this.model,
required this.family,
required this.link,
required this.vision,
this.batteryCount = 1,
this.batteryKind = BatteryKind.smart,
this.modes = const <IntelligentMode>{},
this.virtualStickPrimary = false,
this.go4Supported = true,
this.source = MatrixSource.publishedSpec,
});
/// MSDK `Model` enum name.
final String model;
/// Human family name, used to group rows and in the debug screen.
final String family;
/// The link the model ships with. Products with more than one (a Mavic Pro
/// flown over Wi-Fi) are corrected at runtime by the SDK's answer.
final LinkType link;
final VisionSensors vision;
final int batteryCount;
final BatteryKind batteryKind;
final Set<IntelligentMode> modes;
/// True for aircraft commonly flown with no RC at all (phone over Wi-Fi), where
/// on-screen virtual sticks are the primary control rather than a test tool.
final bool virtualStickPrimary;
/// False for models MSDK V4 drives but DJI GO 4 itself never supported (DJI
/// Fly / DJI Pilot products). They stay in the table so a connect still
/// resolves to sensible facts.
final bool go4Supported;
final MatrixSource source;
}
const Set<IntelligentMode> _gpsClassic = <IntelligentMode>{
IntelligentMode.waypoint,
IntelligentMode.pointOfInterest,
IntelligentMode.followMe,
IntelligentMode.courseLock,
IntelligentMode.homeLock,
};
const Set<IntelligentMode> _visionClassic = <IntelligentMode>{
..._gpsClassic,
IntelligentMode.activeTrack,
IntelligentMode.tapFly,
};
const Set<IntelligentMode> _visionTripod = <IntelligentMode>{
..._visionClassic,
IntelligentMode.tripod,
};
const Set<IntelligentMode> _mavic2 = <IntelligentMode>{
..._visionTripod,
IntelligentMode.quickShots,
IntelligentMode.hyperlapse,
};
const Set<IntelligentMode> _enterprise = <IntelligentMode>{
IntelligentMode.waypoint,
IntelligentMode.pointOfInterest,
IntelligentMode.activeTrack,
IntelligentMode.tapFly,
};
/// Spark and Mavic Air: MSDK V4 offers no waypoint or POI missions on them.
const Set<IntelligentMode> _wifiConsumer = <IntelligentMode>{
IntelligentMode.activeTrack,
IntelligentMode.tapFly,
IntelligentMode.quickShots,
};
const Set<IntelligentMode> _matrice = <IntelligentMode>{
IntelligentMode.waypoint,
IntelligentMode.pointOfInterest,
IntelligentMode.followMe,
};
abstract final class ProductMatrix {
static const List<ProductMatrixEntry> entries = <ProductMatrixEntry>[
// ── Phantom 3 ──────────────────────────────────────────────────────────────
ProductMatrixEntry(model: 'PHANTOM_3_STANDARD', family: 'Phantom 3', link: LinkType.wifi, vision: VisionSensors.none, modes: _gpsClassic),
ProductMatrixEntry(model: 'Phantom_3_4K', family: 'Phantom 3', link: LinkType.wifi, vision: VisionSensors.downward, modes: _gpsClassic),
ProductMatrixEntry(model: 'Phantom_3_SE', family: 'Phantom 3', link: LinkType.wifi, vision: VisionSensors.downward, modes: _gpsClassic),
ProductMatrixEntry(model: 'PHANTOM_3_ADVANCED', family: 'Phantom 3', link: LinkType.lightbridge, vision: VisionSensors.downward, modes: _gpsClassic),
ProductMatrixEntry(model: 'PHANTOM_3_PROFESSIONAL', family: 'Phantom 3', link: LinkType.lightbridge, vision: VisionSensors.downward, modes: _gpsClassic),
// ── Phantom 4 ──────────────────────────────────────────────────────────────
ProductMatrixEntry(model: 'PHANTOM_4', family: 'Phantom 4', link: LinkType.lightbridge, vision: VisionSensors.front, modes: _visionClassic),
ProductMatrixEntry(model: 'PHANTOM_4_ADVANCED', family: 'Phantom 4 Pro', link: LinkType.lightbridge, vision: VisionSensors.front, modes: _visionTripod),
ProductMatrixEntry(model: 'PHANTOM_4_PRO', family: 'Phantom 4 Pro', link: LinkType.lightbridge, vision: VisionSensors.frontBack, modes: _visionTripod),
ProductMatrixEntry(model: 'PHANTOM_4_PRO_V2', family: 'Phantom 4 Pro', link: LinkType.ocusync, vision: VisionSensors.frontBack, modes: _visionTripod),
ProductMatrixEntry(model: 'PHANTOM_4_RTK', family: 'Phantom 4 RTK', link: LinkType.ocusync, vision: VisionSensors.frontBack, modes: <IntelligentMode>{IntelligentMode.waypoint}),
ProductMatrixEntry(model: 'P_4_MULTISPECTRAL', family: 'Phantom 4 RTK', link: LinkType.ocusync, vision: VisionSensors.frontBack, modes: <IntelligentMode>{IntelligentMode.waypoint}, go4Supported: false),
// ── Mavic ──────────────────────────────────────────────────────────────────
ProductMatrixEntry(model: 'MAVIC_PRO', family: 'Mavic Pro', link: LinkType.ocusync, vision: VisionSensors.front, modes: _visionTripod),
ProductMatrixEntry(model: 'MAVIC_AIR', family: 'Mavic Air', link: LinkType.wifi, vision: VisionSensors.frontBack, modes: _wifiConsumer, virtualStickPrimary: true),
ProductMatrixEntry(model: 'MAVIC_2_PRO', family: 'Mavic 2', link: LinkType.ocusync, vision: VisionSensors.omni, modes: _mavic2),
ProductMatrixEntry(model: 'MAVIC_2_ZOOM', family: 'Mavic 2', link: LinkType.ocusync, vision: VisionSensors.omni, modes: _mavic2),
ProductMatrixEntry(model: 'MAVIC_2', family: 'Mavic 2', link: LinkType.ocusync, vision: VisionSensors.omni, modes: _mavic2),
ProductMatrixEntry(model: 'MAVIC_2_ENTERPRISE', family: 'Mavic 2 Enterprise', link: LinkType.ocusync, vision: VisionSensors.omni, modes: _enterprise),
ProductMatrixEntry(model: 'MAVIC_2_ENTERPRISE_DUAL', family: 'Mavic 2 Enterprise', link: LinkType.ocusync, vision: VisionSensors.omni, modes: _enterprise),
ProductMatrixEntry(model: 'MAVIC_2_ENTERPRISE_ADVANCED', family: 'Mavic 2 Enterprise', link: LinkType.ocusync, vision: VisionSensors.omni, modes: _enterprise, go4Supported: false),
// ── Spark / Mini (Wi-Fi consumer) ──────────────────────────────────────────
ProductMatrixEntry(model: 'Spark', family: 'Spark', link: LinkType.wifi, vision: VisionSensors.front, modes: _wifiConsumer, virtualStickPrimary: true),
ProductMatrixEntry(model: 'MAVIC_MINI', family: 'Mavic Mini', link: LinkType.wifi, vision: VisionSensors.downward, virtualStickPrimary: true, go4Supported: false),
ProductMatrixEntry(model: 'DJI_MINI_SE', family: 'Mavic Mini', link: LinkType.wifi, vision: VisionSensors.downward, virtualStickPrimary: true, go4Supported: false),
ProductMatrixEntry(model: 'DJI_MINI_2', family: 'Mavic Mini', link: LinkType.ocusync, vision: VisionSensors.downward, go4Supported: false),
ProductMatrixEntry(model: 'MAVIC_AIR_2', family: 'Air 2', link: LinkType.ocusync, vision: VisionSensors.frontBack, go4Supported: false),
ProductMatrixEntry(model: 'DJI_AIR_2S', family: 'Air 2', link: LinkType.ocusync, vision: VisionSensors.frontBack, go4Supported: false),
// ── Inspire ────────────────────────────────────────────────────────────────
ProductMatrixEntry(model: 'INSPIRE_1', family: 'Inspire 1', link: LinkType.lightbridge, vision: VisionSensors.downward, modes: _gpsClassic),
ProductMatrixEntry(model: 'INSPIRE_1_PRO', family: 'Inspire 1', link: LinkType.lightbridge, vision: VisionSensors.downward, modes: _gpsClassic),
ProductMatrixEntry(model: 'INSPIRE_1_RAW', family: 'Inspire 1', link: LinkType.lightbridge, vision: VisionSensors.downward, modes: _gpsClassic),
ProductMatrixEntry(model: 'INSPIRE_2', family: 'Inspire 2', link: LinkType.lightbridge, vision: VisionSensors.front, batteryCount: 2, modes: _visionClassic),
// ── Matrice ────────────────────────────────────────────────────────────────
ProductMatrixEntry(model: 'MATRICE_100', family: 'Matrice 100', link: LinkType.lightbridge, vision: VisionSensors.none, modes: _matrice),
ProductMatrixEntry(model: 'MATRICE_200', family: 'Matrice 200', link: LinkType.lightbridge, vision: VisionSensors.front, batteryCount: 2, modes: _enterprise),
ProductMatrixEntry(model: 'MATRICE_210', family: 'Matrice 200', link: LinkType.lightbridge, vision: VisionSensors.front, batteryCount: 2, modes: _enterprise),
ProductMatrixEntry(model: 'MATRICE_210_RTK', family: 'Matrice 200', link: LinkType.lightbridge, vision: VisionSensors.front, batteryCount: 2, modes: _enterprise),
ProductMatrixEntry(model: 'MATRICE_200_V2', family: 'Matrice 200', link: LinkType.ocusync, vision: VisionSensors.front, batteryCount: 2, modes: _enterprise, go4Supported: false),
ProductMatrixEntry(model: 'MATRICE_210_V2', family: 'Matrice 200', link: LinkType.ocusync, vision: VisionSensors.front, batteryCount: 2, modes: _enterprise, go4Supported: false),
ProductMatrixEntry(model: 'MATRICE_210_RTK_V2', family: 'Matrice 200', link: LinkType.ocusync, vision: VisionSensors.front, batteryCount: 2, modes: _enterprise, go4Supported: false),
ProductMatrixEntry(model: 'MATRICE_600', family: 'Matrice 600', link: LinkType.lightbridge, vision: VisionSensors.none, batteryCount: 6, modes: _matrice),
ProductMatrixEntry(model: 'MATRICE_600_PRO', family: 'Matrice 600', link: LinkType.lightbridge, vision: VisionSensors.none, batteryCount: 6, modes: _matrice),
ProductMatrixEntry(model: 'MATRICE_300_RTK', family: 'Matrice 300', link: LinkType.ocusync, vision: VisionSensors.omni, batteryCount: 2, modes: <IntelligentMode>{IntelligentMode.waypointV2}, go4Supported: false),
// ── Flight controllers on custom frames (A3 / N3) ──────────────────────────
ProductMatrixEntry(model: 'A3', family: 'A3 / N3', link: LinkType.unknown, vision: VisionSensors.none, batteryKind: BatteryKind.nonSmart, modes: _matrice),
ProductMatrixEntry(model: 'N3', family: 'A3 / N3', link: LinkType.unknown, vision: VisionSensors.none, batteryKind: BatteryKind.nonSmart, modes: _matrice),
];
static final Map<String, ProductMatrixEntry> _byModel = <String, ProductMatrixEntry>{
for (final ProductMatrixEntry e in entries) e.model: e,
};
/// The row for an MSDK `Model` enum name, or null for unknown / handheld /
/// placeholder models (`UNKNOWN_AIRCRAFT`, `DISCONNECT`, the Osmo family).
static ProductMatrixEntry? lookup(String? model) => model == null ? null : _byModel[model];
}