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'); static const DjiKey aircraftName = DjiKey.flightController('AIRCRAFT_NAME'); // 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'); static const DjiKey customButton1 = DjiKey.remoteController('CUSTOM_BUTTON_1'); // Dual operator (master / slave) static const DjiKey rcRole = DjiKey.remoteController('MODE'); static const DjiKey rcLinkName = DjiKey.remoteController('NAME'); static const DjiKey rcLinkPassword = DjiKey.remoteController('PASSWORD'); static const DjiKey masterSlaveSupported = DjiKey.remoteController('IS_MASTER_SLAVE_MODE_SUPPORTED'); static const DjiKey masterSlaveState = DjiKey.remoteController('MASTER_SLAVE_STATE'); // Secondary video output (Lightbridge remotes / CrystalSky HDMI, Cendence SDI) static const DjiKey secondaryOut = DjiKey.remoteController('SECONDARY_VIDEO_OUTPUT_ENABLED'); static const DjiKey secondaryPort = DjiKey.remoteController('SECONDARY_VIDEO_OUTPUT_PORT'); static const DjiKey secondaryDisplay = DjiKey.remoteController('SECONDARY_VIDEO_DISPLAY_MODE'); static const DjiKey secondaryOsd = DjiKey.remoteController('SECONDARY_VIDEO_OSD_ENABLED'); static const DjiKey secondaryOsdUnit = DjiKey.remoteController('SECONDARY_VIDEO_OSD_UNIT'); static const DjiKey hdmiFormat = DjiKey.remoteController('HDMI_OUTPUT_FORMAT'); static const DjiKey sdiFormat = DjiKey.remoteController('SDI_OUTPUT_FORMAT'); static const DjiKey pipPosition = DjiKey.remoteController('PIP_POSITION'); static DjiKey osdMargin(String side) => DjiKey.remoteController('SECONDARY_VIDEO_OSD_${side}_MARGIN'); // 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'); static const DjiKey wifiPassword = DjiKey.wifi('PASSWORD'); static const DjiKey wifiReboot = DjiKey.wifi('REBOOT'); // 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 radarChart = 'radarChart'; static const String stickPads = 'stickPads'; static const String airSenseList = 'airSenseList'; static const String sensingDistances = 'sensingDistances'; static const String flyZoneList = 'flyZoneList'; static const String flyZoneLicences = 'flyZoneLicences'; static const String flyZoneDatabase = 'flyZoneDatabase'; static const String clearRoute = 'clearRoute'; static const String liveStream = 'liveStream'; static const String about = 'about'; static const String developer = 'developer'; static const String units = 'units'; static const String customButtons = 'customButtons'; static const String masterSearch = 'masterSearch'; static const String slaveList = 'slaveList'; static const String spectrum = 'spectrum'; static const String videoCache = 'videoCache'; static const String flightLogs = 'flightLogs'; static const String batteryHistory = 'batteryHistory'; static bool _isOn(KeyState s, DjiKey k) => asBool(s.value(k)) == true; static bool _manual(KeyState s, DjiKey k) => asString(s.value(k)) == 'MANUAL'; /// The remote's dual-operator role; `NORMAL` until it says otherwise. static String _role(KeyState s) => asString(s.value(SettingsKeys.rcRole)) ?? 'NORMAL'; static const List<(String, String)> _channelModes = <(String, String)>[('AUTO', 'Auto'), ('MANUAL', 'Custom')]; static const List<(String, String)> _videoFormats = <(String, String)>[ ('RESOLUTION_1080P_60FPS', '1080p60'), ('RESOLUTION_1080P_50FPS', '1080p50'), ('RESOLUTION_1080P_30FPS', '1080p30'), ('RESOLUTION_1080P_25FPS', '1080p25'), ('RESOLUTION_1080P_24FPS', '1080p24'), ('RESOLUTION_1080I_60FPS', '1080i60'), ('RESOLUTION_1080I_50FPS', '1080i50'), ('RESOLUTION_720P_60FPS', '720p60'), ('RESOLUTION_720P_50FPS', '720p50'), ('RESOLUTION_720P_30FPS', '720p30'), ('RESOLUTION_720P_25FPS', '720p25'), ('RESOLUTION_720P_24FPS', '720p24'), ]; /// The tabs for [p]; battery groups repeat per battery. static List all(ProductProfile p) => [ 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 [CustomSpec(id: gimbalList)], ), SettingsTab(id: general, label: 'General', icon: 'settings', rows: _general), ]; // ── Safety / MC ───────────────────────────────────────────────────────────── static final List _safety = [ 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: [SettingsKeys.leds]), const KeySwitchSpec(id: 'escBeep', label: 'ESC beeping', key: SettingsKeys.escBeep), const GroupSpec('SENSORS'), const CustomSpec(id: sensors, keys: [SettingsKeys.imuCount, SettingsKeys.compassCount]), GroupSpec('AIRSENSE', requires: HudFeatures.airSense), CustomSpec(id: airSenseList, requires: HudFeatures.airSense), GroupSpec('FLY ZONES', requires: HudFeatures.flyZones), CustomSpec(id: flyZoneList, requires: HudFeatures.flyZones), CustomSpec(id: flyZoneLicences, requires: HudFeatures.flyZones), CustomSpec(id: flyZoneDatabase, requires: HudFeatures.flyZones), ]; // ── Vision ────────────────────────────────────────────────────────────────── static final List _vision = [ GroupSpec('OBSTACLE AVOIDANCE', requires: HudFeatures.settingsObstacle), CustomSpec(id: radarChart, requires: HudFeatures.visionRadar), 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, ), CustomSpec(id: sensingDistances, 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 _rc = [ 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: [SettingsKeys.stickMode]), const KeyNumberSpec( id: 'gimbalDialSpeed', label: 'Gimbal dial speed', key: SettingsKeys.gimbalDialSpeed, min: 0, max: 100, step: 5, ), GroupSpec('BUTTONS', requires: HudFeatures.customButtons), CustomSpec(id: customButtons, requires: HudFeatures.customButtons, keys: const [SettingsKeys.customButton1]), // ── Dual operator ───────────────────────────────────────────────────────── GroupSpec('DUAL OPERATOR', requires: HudFeatures.masterSlaveBadge), KeyChoiceSpec( id: 'rcRole', label: 'Remote role', hint: 'Master flies; slave runs the gimbal and camera', key: SettingsKeys.rcRole, options: const <(String, String)>[('NORMAL', 'Single'), ('MASTER', 'Master'), ('SLAVE', 'Slave')], requires: HudFeatures.masterSlaveBadge, groundOnly: true, ), KeyTextSpec( id: 'rcLinkName', label: 'Remote name', hint: 'How the other remote finds this one', key: SettingsKeys.rcLinkName, maxLength: 6, requires: HudFeatures.masterSlaveBadge, visibleWhen: (SettingsContext c) => _role(c.keys) != 'NORMAL', ), KeyTextSpec( id: 'rcLinkPassword', label: 'Connection password', hint: 'Four digits the slave types to join', key: SettingsKeys.rcLinkPassword, secret: true, maxLength: 4, requires: HudFeatures.masterSlaveBadge, visibleWhen: (SettingsContext c) => _role(c.keys) == 'MASTER', ), CustomSpec( id: masterSearch, requires: HudFeatures.masterSlaveBadge, visibleWhen: (SettingsContext c) => _role(c.keys) == 'SLAVE', ), CustomSpec( id: slaveList, requires: HudFeatures.masterSlaveBadge, visibleWhen: (SettingsContext c) => _role(c.keys) == 'MASTER', ), // ── Secondary video output (HDMI / SDI) ─────────────────────────────────── GroupSpec('VIDEO OUTPUT', requires: HudFeatures.linkLightbridge), KeySwitchSpec( id: 'secondaryOut', label: 'HDMI / SDI output', hint: "The remote's video out to a monitor or recorder", key: SettingsKeys.secondaryOut, requires: HudFeatures.linkLightbridge, ), KeyChoiceSpec( id: 'secondaryPort', label: 'Output port', key: SettingsKeys.secondaryPort, options: const <(String, String)>[('HDMI', 'HDMI'), ('SDI', 'SDI')], requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut), ), KeyChoiceSpec( id: 'hdmiFormat', label: 'HDMI format', key: SettingsKeys.hdmiFormat, options: _videoFormats, requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut) && asString(c.keys.value(SettingsKeys.secondaryPort)) != 'SDI', ), KeyChoiceSpec( id: 'sdiFormat', label: 'SDI format', key: SettingsKeys.sdiFormat, options: _videoFormats, requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut) && asString(c.keys.value(SettingsKeys.secondaryPort)) == 'SDI', ), KeyChoiceSpec( id: 'secondaryDisplay', label: 'Picture', hint: 'Which feed, and which one inset (dual-camera aircraft)', key: SettingsKeys.secondaryDisplay, options: const <(String, String)>[ ('SOURCE_1_ONLY', 'Feed 1'), ('SOURCE_2_ONLY', 'Feed 2'), ('SOURCE_1_MAIN', 'Feed 1 + inset'), ('SOURCE_2_MAIN', 'Feed 2 + inset'), ], requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut), ), KeyChoiceSpec( id: 'pipPosition', label: 'Inset position', key: SettingsKeys.pipPosition, options: const <(String, String)>[ ('TOP_LEFT', 'Top left'), ('TOP_RIGHT', 'Top right'), ('BOTTOM_LEFT', 'Bottom left'), ('BOTTOM_RIGHT', 'Bottom right'), ], requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut) && (asString(c.keys.value(SettingsKeys.secondaryDisplay)) ?? '').endsWith('_MAIN'), ), KeySwitchSpec( id: 'secondaryOsd', label: 'Show flight data (OSD)', key: SettingsKeys.secondaryOsd, requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut), ), KeyChoiceSpec( id: 'secondaryOsdUnit', label: 'OSD units', key: SettingsKeys.secondaryOsdUnit, options: const <(String, String)>[('METRIC', 'Metric'), ('IMPERIAL', 'Imperial')], requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut) && _isOn(c.keys, SettingsKeys.secondaryOsd), ), for (final (String side, String label) in const <(String, String)>[('TOP', 'top'), ('BOTTOM', 'bottom'), ('LEFT', 'left'), ('RIGHT', 'right')]) KeyNumberSpec( id: 'osdMargin$side', label: 'OSD margin, $label', key: SettingsKeys.osdMargin(side), min: 0, max: 50, unit: 'px', requires: HudFeatures.linkLightbridge, visibleWhen: (SettingsContext c) => _isOn(c.keys, SettingsKeys.secondaryOut) && _isOn(c.keys, SettingsKeys.secondaryOsd), ), 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 _transmission = [ const KeyReadoutSpec(id: 'uplink', label: 'Control signal', key: SettingsKeys.uplink, format: _percent), const KeyReadoutSpec(id: 'downlink', label: 'Video signal', key: SettingsKeys.downlink, format: _percent), const CustomSpec(id: spectrum), // 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), KeyTextSpec( id: 'wifiSsid', label: 'Network name', hint: 'Takes effect after Restart Wi-Fi', key: SettingsKeys.wifiSsid, requires: HudFeatures.linkWifi, groundOnly: true, send: (DjiService dji, String v) => dji.setWifiCredentials(ssid: v), ), KeyTextSpec( id: 'wifiPassword', label: 'Wi-Fi password', hint: 'At least 8 characters; takes effect after Restart Wi-Fi', key: SettingsKeys.wifiPassword, secret: true, maxLength: 63, requires: HudFeatures.linkWifi, groundOnly: true, send: (DjiService dji, String v) { if (v.length < 8) throw ArgumentError('The password needs at least 8 characters'); return dji.setWifiCredentials(password: v); }, ), KeyActionSpec( id: 'wifiReboot', label: 'Restart Wi-Fi', hint: 'Applies a new name or password; the phone has to join the network again', key: SettingsKeys.wifiReboot, button: 'Restart', requires: HudFeatures.linkWifi, groundOnly: true, confirmTitle: 'Restart the aircraft Wi-Fi', confirmText: 'The connection drops. Join the network again in the phone\'s Wi-Fi settings, with the new name or password if you changed them.', ), 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 _battery(ProductProfile p) { final int n = p.batteryCount.clamp(1, 6); return [ 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), // Inspire 2 / M200: the two packs fly as a pair and must be matched. GroupSpec('BATTERY PAIR', requires: HudFeatures.dualBattery), KeyReadoutSpec( id: 'pairingState', label: 'Pairing', key: SettingsKeys.battery('PAIRING_STATE', 0), requires: HudFeatures.dualBattery, ), KeyActionSpec( id: 'pairBatteries', label: 'Pair the two batteries', hint: 'After swapping a pack: the pair then ages and balances together', key: SettingsKeys.battery('PAIR_BATTERIES', 0), button: 'Pair', requires: HudFeatures.dualBattery, groundOnly: true, confirmTitle: 'Pair batteries', confirmText: 'The two batteries in the aircraft are recorded as a pair. Use two packs of similar age and charge.', visibleWhen: (SettingsContext c) => asString(c.keys.value(SettingsKeys.battery('PAIRING_STATE', 0))) != 'PAIRED', ), // Non-smart packs (A3 / N3 / M600 on plain LiPos): the FC judges cells by voltage. GroupSpec('CELL VOLTAGE', requires: HudFeatures.nonSmartBattery), KeyNumberSpec( id: 'cellCount', label: 'Number of cells', key: SettingsKeys.battery('NUMBER_OF_CELLS', 0), min: 3, max: 12, unit: 'S', requires: HudFeatures.nonSmartBattery, groundOnly: true, ), for (final int level in const [1, 2]) ...[ KeyNumberSpec( id: 'cellLevel$level', label: level == 1 ? 'Low cell voltage' : 'Critical cell voltage', key: SettingsKeys.battery('LEVEL_${level}_CELL_VOLTAGE_THRESHOLD', 0), min: level == 1 ? 3500 : 3300, max: level == 1 ? 3900 : 3700, step: 10, unit: 'mV', requires: HudFeatures.nonSmartBattery, bounds: level == 1 ? (KeyState s, int lo, int hi) { final int? l2 = asInt(s.value(SettingsKeys.battery('LEVEL_2_CELL_VOLTAGE_THRESHOLD', 0))); return (l2 == null ? lo : (l2 + 50).clamp(lo, hi), hi); } : (KeyState s, int lo, int hi) { final int? l1 = asInt(s.value(SettingsKeys.battery('LEVEL_1_CELL_VOLTAGE_THRESHOLD', 0))); return (lo, l1 == null ? hi : (l1 - 50).clamp(lo, hi)); }, ), KeyChoiceSpec( id: 'cellBehavior$level', label: level == 1 ? 'At low voltage' : 'At critical voltage', key: SettingsKeys.battery('LEVEL_${level}_CELL_VOLTAGE_BEHAVIOR', 0), options: const <(String, String)>[('FLASH_LED', 'Flash LEDs'), ('GO_HOME', 'Return home'), ('LAND', 'Land')], requires: HudFeatures.nonSmartBattery, ), ], ]; } static List _batteryRows(int i, int n) { DjiKey k(String key) => SettingsKeys.battery(key, i); return [ 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')), KeyReadoutSpec(id: 'batteryLatest$i', label: 'Latest warning', key: k('LATEST_WARNING_RECORD'), format: formatWarningRecord), KeyReadoutSpec( id: 'batteryHistory$i', label: 'Warning history', hint: 'What the pack recorded over its last flights', key: k('HISTORICAL_WARNING_RECORDS'), format: formatWarningHistory, ), ]; } // ── General ───────────────────────────────────────────────────────────────── static final List _general = [ const GroupSpec('UNITS'), const CustomSpec(id: units), const GroupSpec('MAP'), const CustomSpec(id: showRoute), const CustomSpec(id: clearRoute), GroupSpec('GEO UNLOCKS', requires: HudFeatures.flyZones), CustomSpec(id: flyZoneLicences, requires: HudFeatures.flyZones), GroupSpec('VIDEO CACHE', requires: HudFeatures.cameraControls), CustomSpec(id: videoCache, requires: HudFeatures.cameraControls), const GroupSpec('FLIGHT RECORDS'), const CustomSpec(id: flightLogs), GroupSpec('LIVE STREAMING', requires: HudFeatures.liveStream), CustomSpec(id: liveStream, requires: HudFeatures.liveStream), GroupSpec('ON-SCREEN STICKS', requires: HudFeatures.virtualSticks), CustomSpec(id: stickPads, requires: HudFeatures.virtualSticks), const GroupSpec('ABOUT'), const KeyTextSpec( id: 'aircraftName', label: 'Aircraft name', hint: 'Shown in the logbook and to other DJI apps', key: SettingsKeys.aircraftName, maxLength: 32, ), const CustomSpec(id: about, keys: [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); }