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>
510 lines
22 KiB
Dart
510 lines
22 KiB
Dart
import '../../dji_service.dart';
|
|
import '../hud_features.dart';
|
|
import '../product_profile.dart';
|
|
import '../state/key_state.dart';
|
|
import '../state/parse.dart';
|
|
import 'settings_spec.dart';
|
|
|
|
/// The settings sheet's tabs (plan P6, GO 4's MC / Vision / RC / HD / Battery /
|
|
/// Gimbal / General). Each row is one spec bound to a KeyManager key; rows
|
|
/// whose key the aircraft does not support hide themselves, so the same list
|
|
/// serves every product.
|
|
///
|
|
/// Out of scope here, with no public MSDK V4 endpoint (see the matrix): gain &
|
|
/// expo, max descent speed, the RC speed bar, RC type / factory reset.
|
|
abstract final class SettingsKeys {
|
|
// Flight controller
|
|
static const DjiKey goHomeHeight = DjiKey.flightController('GO_HOME_HEIGHT_IN_METERS');
|
|
static const DjiKey smartRth = DjiKey.flightController('SMART_RETURN_TO_HOME_ENABLED');
|
|
static const DjiKey failSafe = DjiKey.flightController('CONNECTION_FAIL_SAFE_BEHAVIOR');
|
|
static const DjiKey maxHeight = DjiKey.flightController('MAX_FLIGHT_HEIGHT');
|
|
static const DjiKey maxHeightRange = DjiKey.flightController('MAX_FLIGHT_HEIGHT_RANGE');
|
|
static const DjiKey maxRadiusEnabled = DjiKey.flightController('MAX_FLIGHT_RADIUS_ENABLED');
|
|
static const DjiKey maxRadius = DjiKey.flightController('MAX_FLIGHT_RADIUS');
|
|
static const DjiKey maxRadiusRange = DjiKey.flightController('MAX_FLIGHT_RADIUS_RANGE');
|
|
static const DjiKey novice = DjiKey.flightController('NOVICE_MODE_ENABLED');
|
|
static const DjiKey multiMode = DjiKey.flightController('MULTI_MODE_OPEN');
|
|
static const DjiKey leds = DjiKey.flightController('LEDS_ENABLED_SETTINGS');
|
|
static const DjiKey escBeep = DjiKey.flightController('ESC_BEEPING_ENABLED');
|
|
static const DjiKey imuCount = DjiKey.flightController('IMU_COUNT');
|
|
static const DjiKey compassCount = DjiKey.flightController('COMPASS_COUNT');
|
|
static const DjiKey lowBattery = DjiKey.flightController('LOW_BATTERY_WARNING_THRESHOLD');
|
|
static const DjiKey criticalBattery = DjiKey.flightController('SERIOUS_LOW_BATTERY_WARNING_THRESHOLD');
|
|
static const DjiKey fcSerial = DjiKey.flightController('SERIAL_NUMBER');
|
|
|
|
// Flight assistant (vision)
|
|
static const DjiKey collision = DjiKey.flightAssistant('COLLISION_AVOIDANCE_ENABLED');
|
|
static const DjiKey activeAvoidance = DjiKey.flightAssistant('ACTIVE_OBSTACLE_AVOIDANCE_ENABLED');
|
|
static const DjiKey upwardAvoidance = DjiKey.flightAssistant('UPWARDS_AVOIDANCE_ENABLED');
|
|
static const DjiKey apas = DjiKey.flightAssistant('IS_ADVANCED_PILOT_ASSISTANT_SYSTEM_ENABLED');
|
|
static const DjiKey rthAvoidance = DjiKey.flightAssistant('RTH_OBSTACLE_AVOIDANCE_ENABLED');
|
|
static const DjiKey rthRemoteAvoidance = DjiKey.flightAssistant('RTH_REMOTE_OBSTACLE_AVOIDANCE_ENABLED');
|
|
static const DjiKey vps = DjiKey.flightAssistant('VISION_ASSISTED_POSITIONING_ENABLED');
|
|
static const DjiKey landingProtection = DjiKey.flightAssistant('LANDING_PROTECTION_ENABLED');
|
|
static const DjiKey precisionLanding = DjiKey.flightAssistant('PRECISION_LANDING_ENABLED');
|
|
static const DjiKey bottomLight = DjiKey.flightAssistant('BOTTOM_AUXILIARY_LIGHT_MODE');
|
|
static const DjiKey gestureMode = DjiKey.flightAssistant('ACTIVE_TRACK_GESTURE_MODE_ENABLED');
|
|
static const DjiKey palmControl = DjiKey.flightAssistant('ADVANCED_GESTURE_CONTROL_ENABLED');
|
|
|
|
// Remote controller
|
|
static const DjiKey stickMode = DjiKey.remoteController('AIRCRAFT_MAPPING_STYLE');
|
|
static const DjiKey chargeMobile = DjiKey.remoteController('CHARGE_MOBILE_MODE');
|
|
static const DjiKey gimbalDialSpeed = DjiKey.remoteController('GIMBAL_CONTROL_SPEED_COEFFICIENT');
|
|
static const DjiKey pairingState = DjiKey.remoteController('PAIRING_STATE');
|
|
static const DjiKey startPairing = DjiKey.remoteController('START_PAIRING');
|
|
static const DjiKey stopPairing = DjiKey.remoteController('STOP_PAIRING');
|
|
static const DjiKey rcName = DjiKey.remoteController('DISPLAY_NAME');
|
|
static const DjiKey rcSerial = DjiKey.remoteController('SERIAL_NUMBER');
|
|
static const DjiKey rcFirmware = DjiKey.remoteController('FIRMWARE_VERSION');
|
|
|
|
// Image transmission
|
|
static const DjiKey uplink = DjiKey.airLink('UPLINK_SIGNAL_QUALITY');
|
|
static const DjiKey downlink = DjiKey.airLink('DOWNLINK_SIGNAL_QUALITY');
|
|
static const DjiKey osChannelMode = DjiKey.ocuSync('CHANNEL_SELECTION_MODE');
|
|
static const DjiKey osBand = DjiKey.ocuSync('OCUSYNC_FREQUENCY_BAND');
|
|
static const DjiKey osBands = DjiKey.ocuSync('OCUSYNC_SUPPORTED_FREQUENCY_BANDS');
|
|
static const DjiKey osBandwidth = DjiKey.ocuSync('BANDWIDTH');
|
|
static const DjiKey osChannel = DjiKey.ocuSync('FREQUENCY_POINT_INDEX');
|
|
static const DjiKey osChannelRange = DjiKey.ocuSync('FREQUENCY_POINT_INDEX_VALID_RANGE');
|
|
static const DjiKey osDataRate = DjiKey.ocuSync('DYNAMIC_DATA_RATE');
|
|
static const DjiKey lbChannelMode = DjiKey.lightbridge('CHANNEL_SELECTION_MODE');
|
|
static const DjiKey lbBand = DjiKey.lightbridge('LB_FREQUENCY_BAND');
|
|
static const DjiKey lbBands = DjiKey.lightbridge('LB_SUPPORTED_FREQUENCY_BANDS');
|
|
static const DjiKey lbChannel = DjiKey.lightbridge('LB_CHANNEL');
|
|
static const DjiKey lbChannelRange = DjiKey.lightbridge('CHANNEL_RANGE');
|
|
static const DjiKey lbDataRate = DjiKey.lightbridge('LB_DATA_RATE');
|
|
static const DjiKey wifiSsid = DjiKey.wifi('SSID');
|
|
static const DjiKey wifiBand = DjiKey.wifi('WIFI_FREQUENCY_BAND');
|
|
static const DjiKey wifiChannel = DjiKey.wifi('CHANNEL_NUMBER');
|
|
|
|
// Product
|
|
static const DjiKey firmwarePackage = DjiKey.product('FIRMWARE_PACKAGE_VERSION');
|
|
|
|
// Battery (per index)
|
|
static DjiKey battery(String key, int i) => DjiKey.battery(key, index: i);
|
|
}
|
|
|
|
abstract final class SettingsTabs {
|
|
static const String safety = 'safety';
|
|
static const String vision = 'vision';
|
|
static const String rc = 'rc';
|
|
static const String transmission = 'transmission';
|
|
static const String battery = 'battery';
|
|
static const String gimbal = 'gimbal';
|
|
static const String general = 'general';
|
|
|
|
/// Custom row ids the sheet draws itself.
|
|
static const String homePoint = 'homePoint';
|
|
static const String leds = 'leds';
|
|
static const String sensors = 'sensors';
|
|
static const String stickDiagram = 'stickDiagram';
|
|
static const String gimbalList = 'gimbalList';
|
|
static const String showRoute = 'showRoute';
|
|
static const String clearRoute = 'clearRoute';
|
|
static const String about = 'about';
|
|
static const String developer = 'developer';
|
|
|
|
static bool _isOn(KeyState s, DjiKey k) => asBool(s.value(k)) == true;
|
|
static bool _manual(KeyState s, DjiKey k) => asString(s.value(k)) == 'MANUAL';
|
|
|
|
static const List<(String, String)> _channelModes = <(String, String)>[('AUTO', 'Auto'), ('MANUAL', 'Custom')];
|
|
|
|
/// The tabs for [p]; battery groups repeat per battery.
|
|
static List<SettingsTab> all(ProductProfile p) => <SettingsTab>[
|
|
SettingsTab(id: safety, label: 'Safety', icon: 'shield', rows: _safety),
|
|
SettingsTab(id: vision, label: 'Vision', icon: 'obstacle', requires: HudFeatures.settingsVision, rows: _vision),
|
|
SettingsTab(id: rc, label: 'Remote', icon: 'rc', rows: _rc),
|
|
SettingsTab(id: transmission, label: 'Video link', icon: 'radio', rows: _transmission),
|
|
SettingsTab(id: battery, label: 'Battery', icon: 'battery', rows: _battery(p)),
|
|
SettingsTab(
|
|
id: gimbal,
|
|
label: 'Gimbal',
|
|
icon: 'gimbal',
|
|
requires: HudFeatures.gimbalSettings,
|
|
rows: const <SettingSpec>[CustomSpec(id: gimbalList)],
|
|
),
|
|
SettingsTab(id: general, label: 'General', icon: 'settings', rows: _general),
|
|
];
|
|
|
|
// ── Safety / MC ─────────────────────────────────────────────────────────────
|
|
static final List<SettingSpec> _safety = <SettingSpec>[
|
|
const GroupSpec('RETURN TO HOME'),
|
|
const CustomSpec(id: homePoint),
|
|
const KeyNumberSpec(
|
|
id: 'rthAltitude',
|
|
label: 'RTH altitude',
|
|
key: SettingsKeys.goHomeHeight,
|
|
min: 20,
|
|
max: 500,
|
|
step: 10,
|
|
unit: 'm',
|
|
feet: true,
|
|
),
|
|
const KeySwitchSpec(
|
|
id: 'smartRth',
|
|
label: 'Smart RTH',
|
|
hint: 'Return home on its own when the battery only just covers the way back',
|
|
key: SettingsKeys.smartRth,
|
|
),
|
|
const KeyChoiceSpec(
|
|
id: 'failSafe',
|
|
label: 'RC signal lost',
|
|
key: SettingsKeys.failSafe,
|
|
options: <(String, String)>[('HOVER', 'Hover'), ('LANDING', 'Land'), ('GO_HOME', 'Return home')],
|
|
),
|
|
const GroupSpec('LIMITS'),
|
|
const KeyNumberSpec(
|
|
id: 'maxAltitude',
|
|
label: 'Max altitude',
|
|
key: SettingsKeys.maxHeight,
|
|
rangeKey: SettingsKeys.maxHeightRange,
|
|
min: 20,
|
|
max: 500,
|
|
step: 10,
|
|
unit: 'm',
|
|
feet: true,
|
|
),
|
|
const KeySwitchSpec(id: 'distanceLimit', label: 'Distance limit', key: SettingsKeys.maxRadiusEnabled),
|
|
KeyNumberSpec(
|
|
id: 'maxDistance',
|
|
label: 'Max distance',
|
|
key: SettingsKeys.maxRadius,
|
|
rangeKey: SettingsKeys.maxRadiusRange,
|
|
min: 15,
|
|
max: 8000,
|
|
step: 50,
|
|
unit: 'm',
|
|
feet: true,
|
|
visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.maxRadiusEnabled),
|
|
),
|
|
const KeySwitchSpec(
|
|
id: 'beginner',
|
|
label: 'Beginner mode',
|
|
hint: 'Slow, and held within 30 m of home',
|
|
key: SettingsKeys.novice,
|
|
groundOnly: true,
|
|
),
|
|
const GroupSpec('FLIGHT'),
|
|
const KeySwitchSpec(
|
|
id: 'multiMode',
|
|
label: 'Multiple flight modes',
|
|
hint: 'Lets the RC switch reach Sport / Atti / Tripod',
|
|
key: SettingsKeys.multiMode,
|
|
),
|
|
const CustomSpec(id: leds, keys: <DjiKey>[SettingsKeys.leds]),
|
|
const KeySwitchSpec(id: 'escBeep', label: 'ESC beeping', key: SettingsKeys.escBeep),
|
|
const GroupSpec('SENSORS'),
|
|
const CustomSpec(id: sensors, keys: <DjiKey>[SettingsKeys.imuCount, SettingsKeys.compassCount]),
|
|
];
|
|
|
|
// ── Vision ──────────────────────────────────────────────────────────────────
|
|
static final List<SettingSpec> _vision = <SettingSpec>[
|
|
GroupSpec('OBSTACLE AVOIDANCE', requires: HudFeatures.settingsObstacle),
|
|
KeySwitchSpec(
|
|
id: 'collision',
|
|
label: 'Obstacle avoidance',
|
|
key: SettingsKeys.collision,
|
|
requires: HudFeatures.settingsObstacle,
|
|
),
|
|
KeySwitchSpec(
|
|
id: 'activeAvoidance',
|
|
label: 'Active obstacle avoidance',
|
|
hint: 'Back away from obstacles that approach the aircraft',
|
|
key: SettingsKeys.activeAvoidance,
|
|
requires: HudFeatures.settingsObstacle,
|
|
),
|
|
KeySwitchSpec(
|
|
id: 'upwardAvoidance',
|
|
label: 'Upward obstacle avoidance',
|
|
key: SettingsKeys.upwardAvoidance,
|
|
requires: HudFeatures.settingsObstacle,
|
|
),
|
|
KeySwitchSpec(
|
|
id: 'apas',
|
|
label: 'APAS',
|
|
hint: 'Fly around obstacles instead of braking',
|
|
key: SettingsKeys.apas,
|
|
requires: HudFeatures.settingsObstacle,
|
|
),
|
|
KeySwitchSpec(
|
|
id: 'rthAvoidance',
|
|
label: 'RTH obstacle check',
|
|
key: SettingsKeys.rthAvoidance,
|
|
requires: HudFeatures.settingsObstacle,
|
|
),
|
|
KeySwitchSpec(
|
|
id: 'rthRemoteAvoidance',
|
|
label: 'RTH remote obstacle check',
|
|
key: SettingsKeys.rthRemoteAvoidance,
|
|
requires: HudFeatures.settingsObstacle,
|
|
),
|
|
const GroupSpec('POSITIONING & LANDING'),
|
|
const KeySwitchSpec(id: 'vps', label: 'Vision positioning', key: SettingsKeys.vps),
|
|
const KeySwitchSpec(
|
|
id: 'landingProtection',
|
|
label: 'Landing protection',
|
|
hint: 'Checks the ground before landing',
|
|
key: SettingsKeys.landingProtection,
|
|
),
|
|
const KeySwitchSpec(
|
|
id: 'precisionLanding',
|
|
label: 'Precision landing',
|
|
hint: 'RTH lands on the recorded take-off spot',
|
|
key: SettingsKeys.precisionLanding,
|
|
),
|
|
const KeyChoiceSpec(
|
|
id: 'bottomLight',
|
|
label: 'Bottom auxiliary light',
|
|
key: SettingsKeys.bottomLight,
|
|
options: <(String, String)>[('AUTO', 'Auto'), ('ON', 'On'), ('OFF', 'Off')],
|
|
),
|
|
// Rows hide on aircraft whose flight assistant does not support the key.
|
|
GroupSpec('GESTURES', requires: HudFeatures.settingsGestures),
|
|
KeySwitchSpec(
|
|
id: 'gestureMode',
|
|
label: 'Gesture mode',
|
|
hint: 'Raise your arms to be tracked; make a frame with your fingers for a photo',
|
|
key: SettingsKeys.gestureMode,
|
|
requires: HudFeatures.settingsGestures,
|
|
),
|
|
KeySwitchSpec(
|
|
id: 'palmControl',
|
|
label: 'Palm control',
|
|
hint: 'Guide the aircraft with your palm, a couple of metres away',
|
|
key: SettingsKeys.palmControl,
|
|
requires: HudFeatures.settingsGestures,
|
|
),
|
|
];
|
|
|
|
// ── Remote controller ───────────────────────────────────────────────────────
|
|
static final List<SettingSpec> _rc = <SettingSpec>[
|
|
const GroupSpec('STICKS'),
|
|
const KeyChoiceSpec(
|
|
id: 'stickMode',
|
|
label: 'Stick mode',
|
|
key: SettingsKeys.stickMode,
|
|
options: <(String, String)>[('STYLE_1', 'Mode 1'), ('STYLE_2', 'Mode 2'), ('STYLE_3', 'Mode 3')],
|
|
groundOnly: true,
|
|
),
|
|
const CustomSpec(id: stickDiagram, keys: <DjiKey>[SettingsKeys.stickMode]),
|
|
const KeyNumberSpec(
|
|
id: 'gimbalDialSpeed',
|
|
label: 'Gimbal dial speed',
|
|
key: SettingsKeys.gimbalDialSpeed,
|
|
min: 0,
|
|
max: 100,
|
|
step: 5,
|
|
),
|
|
const GroupSpec('CONTROLLER'),
|
|
const KeyChoiceSpec(
|
|
id: 'chargeMobile',
|
|
label: 'Charge mobile device',
|
|
key: SettingsKeys.chargeMobile,
|
|
options: <(String, String)>[('NEVER', 'Never'), ('INTELLIGENT', 'Smart'), ('ALWAYS', 'Always')],
|
|
),
|
|
const KeyReadoutSpec(id: 'pairingState', label: 'Linking', key: SettingsKeys.pairingState),
|
|
KeyActionSpec(
|
|
id: 'startPairing',
|
|
label: 'Link remote controller',
|
|
hint: 'Then press the link button on the aircraft',
|
|
key: SettingsKeys.startPairing,
|
|
button: 'Link',
|
|
groundOnly: true,
|
|
confirmTitle: 'Link remote controller',
|
|
confirmText: 'The current link drops until the aircraft is linked again. '
|
|
'After sliding, press the aircraft\'s link button.',
|
|
visibleWhen: (SettingsContext c) => asString(c.keys.value(SettingsKeys.pairingState)) != 'PAIRING',
|
|
),
|
|
KeyActionSpec(
|
|
id: 'stopPairing',
|
|
label: 'Linking…',
|
|
key: SettingsKeys.stopPairing,
|
|
button: 'Stop',
|
|
visibleWhen: (SettingsContext c) => asString(c.keys.value(SettingsKeys.pairingState)) == 'PAIRING',
|
|
),
|
|
const KeyReadoutSpec(id: 'rcName', label: 'Name', key: SettingsKeys.rcName),
|
|
const KeyReadoutSpec(id: 'rcSerial', label: 'Serial number', key: SettingsKeys.rcSerial),
|
|
const KeyReadoutSpec(id: 'rcFirmware', label: 'Firmware', key: SettingsKeys.rcFirmware),
|
|
];
|
|
|
|
// ── Image transmission ──────────────────────────────────────────────────────
|
|
static final List<SettingSpec> _transmission = <SettingSpec>[
|
|
const KeyReadoutSpec(id: 'uplink', label: 'Control signal', key: SettingsKeys.uplink, format: _percent),
|
|
const KeyReadoutSpec(id: 'downlink', label: 'Video signal', key: SettingsKeys.downlink, format: _percent),
|
|
// OcuSync
|
|
GroupSpec('OCUSYNC', requires: HudFeatures.linkOcuSync),
|
|
KeyReadoutSpec(id: 'osDataRate', label: 'Video bitrate', key: SettingsKeys.osDataRate, format: _mbps, requires: HudFeatures.linkOcuSync),
|
|
KeyChoiceSpec(
|
|
id: 'osChannelMode',
|
|
label: 'Channel mode',
|
|
key: SettingsKeys.osChannelMode,
|
|
options: _channelModes,
|
|
requires: HudFeatures.linkOcuSync,
|
|
),
|
|
KeyChoiceSpec(
|
|
id: 'osBand',
|
|
label: 'Frequency band',
|
|
key: SettingsKeys.osBand,
|
|
supportedKey: SettingsKeys.osBands,
|
|
options: const <(String, String)>[
|
|
('FREQUENCY_BAND_DUAL', 'Dual'),
|
|
('FREQUENCY_BAND_2_DOT_4_GHZ', '2.4 GHz'),
|
|
('FREQUENCY_BAND_5_DOT_8_GHZ', '5.8 GHz'),
|
|
('FREQUENCY_BAND_5_DOT_7_GHZ', '5.7 GHz'),
|
|
],
|
|
requires: HudFeatures.linkOcuSync,
|
|
),
|
|
KeyChoiceSpec(
|
|
id: 'osBandwidth',
|
|
label: 'Bandwidth',
|
|
key: SettingsKeys.osBandwidth,
|
|
options: const <(String, String)>[('Bandwidth10MHz', '10 MHz'), ('Bandwidth20MHz', '20 MHz'), ('Bandwidth40MHZ', '40 MHz')],
|
|
requires: HudFeatures.linkOcuSync,
|
|
visibleWhen: (SettingsContext c) => _manual(c.keys, SettingsKeys.osChannelMode),
|
|
),
|
|
KeyNumberSpec(
|
|
id: 'osChannel',
|
|
label: 'Channel',
|
|
key: SettingsKeys.osChannel,
|
|
rangeKey: SettingsKeys.osChannelRange,
|
|
min: 0,
|
|
max: 100,
|
|
requires: HudFeatures.linkOcuSync,
|
|
visibleWhen: (SettingsContext c) => _manual(c.keys, SettingsKeys.osChannelMode),
|
|
),
|
|
// Lightbridge
|
|
GroupSpec('LIGHTBRIDGE', requires: HudFeatures.linkLightbridge),
|
|
KeyChoiceSpec(
|
|
id: 'lbChannelMode',
|
|
label: 'Channel mode',
|
|
key: SettingsKeys.lbChannelMode,
|
|
options: _channelModes,
|
|
requires: HudFeatures.linkLightbridge,
|
|
),
|
|
KeyChoiceSpec(
|
|
id: 'lbBand',
|
|
label: 'Frequency band',
|
|
key: SettingsKeys.lbBand,
|
|
supportedKey: SettingsKeys.lbBands,
|
|
options: const <(String, String)>[
|
|
('FREQUENCY_BAND_2_DOT_4_GHZ', '2.4 GHz'),
|
|
('FREQUENCY_BAND_5_DOT_8_GHZ', '5.8 GHz'),
|
|
('FREQUENCY_BAND_5_DOT_7_GHZ', '5.7 GHz'),
|
|
],
|
|
requires: HudFeatures.linkLightbridge,
|
|
),
|
|
KeyNumberSpec(
|
|
id: 'lbChannel',
|
|
label: 'Channel',
|
|
key: SettingsKeys.lbChannel,
|
|
rangeKey: SettingsKeys.lbChannelRange,
|
|
min: 0,
|
|
max: 40,
|
|
requires: HudFeatures.linkLightbridge,
|
|
visibleWhen: (SettingsContext c) => _manual(c.keys, SettingsKeys.lbChannelMode),
|
|
),
|
|
KeyChoiceSpec(
|
|
id: 'lbDataRate',
|
|
label: 'Video bitrate',
|
|
key: SettingsKeys.lbDataRate,
|
|
options: const <(String, String)>[
|
|
('BANDWIDTH_4_MBPS', '4 Mbps'),
|
|
('BANDWIDTH_6_MBPS', '6 Mbps'),
|
|
('BANDWIDTH_8_MBPS', '8 Mbps'),
|
|
('BANDWIDTH_10_MBPS', '10 Mbps'),
|
|
],
|
|
requires: HudFeatures.linkLightbridge,
|
|
),
|
|
// Wi-Fi
|
|
GroupSpec('WI-FI', requires: HudFeatures.linkWifi),
|
|
KeyReadoutSpec(id: 'wifiSsid', label: 'Network', key: SettingsKeys.wifiSsid, requires: HudFeatures.linkWifi),
|
|
KeyChoiceSpec(
|
|
id: 'wifiBand',
|
|
label: 'Frequency band',
|
|
key: SettingsKeys.wifiBand,
|
|
options: const <(String, String)>[
|
|
('FREQUENCY_BAND_2_DOT_4_GHZ', '2.4 GHz'),
|
|
('FREQUENCY_BAND_5_GHZ', '5 GHz'),
|
|
('FREQUENCY_BAND_DUAL', 'Dual'),
|
|
],
|
|
requires: HudFeatures.linkWifi,
|
|
// 2.4-only hardware reports ONLY_2_DOT_4: nothing to choose.
|
|
visibleWhen: (SettingsContext c) => asString(c.keys.value(SettingsKeys.wifiBand)) != 'FREQUENCY_BAND_ONLY_2_DOT_4',
|
|
),
|
|
KeyReadoutSpec(id: 'wifiChannel', label: 'Channel', key: SettingsKeys.wifiChannel, requires: HudFeatures.linkWifi),
|
|
];
|
|
|
|
// ── Battery ─────────────────────────────────────────────────────────────────
|
|
static List<SettingSpec> _battery(ProductProfile p) {
|
|
final int n = p.batteryCount.clamp(1, 6);
|
|
return <SettingSpec>[
|
|
const GroupSpec('WARNINGS'),
|
|
KeyNumberSpec(
|
|
id: 'lowBattery',
|
|
label: 'Low battery warning',
|
|
key: SettingsKeys.lowBattery,
|
|
min: 15,
|
|
max: 50,
|
|
unit: '%',
|
|
bounds: (KeyState s, int lo, int hi) {
|
|
final int? critical = asInt(s.value(SettingsKeys.criticalBattery));
|
|
return (critical == null ? lo : (critical + 5).clamp(lo, hi), hi);
|
|
},
|
|
),
|
|
KeyNumberSpec(
|
|
id: 'criticalBattery',
|
|
label: 'Critical battery warning',
|
|
hint: 'The aircraft lands on its own at this level',
|
|
key: SettingsKeys.criticalBattery,
|
|
min: 10,
|
|
max: 45,
|
|
unit: '%',
|
|
bounds: (KeyState s, int lo, int hi) {
|
|
final int? low = asInt(s.value(SettingsKeys.lowBattery));
|
|
return (lo, low == null ? hi : (low - 5).clamp(lo, hi));
|
|
},
|
|
),
|
|
for (int i = 0; i < n; i++) ..._batteryRows(i, n),
|
|
];
|
|
}
|
|
|
|
static List<SettingSpec> _batteryRows(int i, int n) {
|
|
DjiKey k(String key) => SettingsKeys.battery(key, i);
|
|
return <SettingSpec>[
|
|
GroupSpec(n == 1 ? 'BATTERY' : 'BATTERY ${i + 1}'),
|
|
KeyReadoutSpec(id: 'cells$i', label: 'Cell voltages', key: k('CELL_VOLTAGES'), format: formatCells),
|
|
KeyReadoutSpec(id: 'temperature$i', label: 'Temperature', key: k('TEMPERATURE'), format: _celsius),
|
|
KeyReadoutSpec(id: 'discharges$i', label: 'Charge cycles', key: k('NUMBER_OF_DISCHARGES')),
|
|
KeyReadoutSpec(id: 'capacity$i', label: 'Full charge capacity', key: k('FULL_CHARGE_CAPACITY'), format: _mah),
|
|
KeyReadoutSpec(id: 'lifetime$i', label: 'Battery life', key: k('LIFETIME_REMAINING'), format: _percent),
|
|
KeyNumberSpec(
|
|
id: 'selfDischarge$i',
|
|
label: 'Self-discharge after',
|
|
hint: 'Storage: the battery drains to ~60 % after this many idle days',
|
|
key: k('SELF_DISCHARGE_IN_DAYS'),
|
|
min: 1,
|
|
max: 10,
|
|
unit: 'd',
|
|
),
|
|
KeyReadoutSpec(id: 'batterySerial$i', label: 'Serial number', key: k('SERIAL_NUMBER')),
|
|
KeyReadoutSpec(id: 'batteryFirmware$i', label: 'Firmware', key: k('FIRMWARE_VERSION')),
|
|
];
|
|
}
|
|
|
|
// ── General ─────────────────────────────────────────────────────────────────
|
|
static final List<SettingSpec> _general = <SettingSpec>[
|
|
const GroupSpec('MAP'),
|
|
const CustomSpec(id: showRoute),
|
|
const CustomSpec(id: clearRoute),
|
|
const GroupSpec('ABOUT'),
|
|
const CustomSpec(id: about, keys: <DjiKey>[SettingsKeys.firmwarePackage, SettingsKeys.fcSerial]),
|
|
const GroupSpec('DEVELOPER'),
|
|
const CustomSpec(id: developer),
|
|
];
|
|
|
|
static String _percent(Object? v) => v is num ? '${v.round()} %' : formatValue(v);
|
|
static String _mbps(Object? v) => v is num ? '${v.toStringAsFixed(1)} Mbps' : formatValue(v);
|
|
static String _celsius(Object? v) => v is num ? '${v.toStringAsFixed(1)} °C' : formatValue(v);
|
|
static String _mah(Object? v) => v is num ? '${v.round()} mAh' : formatValue(v);
|
|
}
|