Intelligent modes: Point of Interest (orbit on the map, live radius and speed), Follow Me (follows the phone), QuickShots on a boxed subject, Course Lock / Home Lock, Tripod and Cinematic, a Free hyperlapse, and Waypoints 2.0 for V2-only aircraft. ActiveTrack gains Spotlight Pro and Circle. Every mode has a first-use tutorial and stops from the rail. Waypoints can resume an interrupted mission, change cruise speed in flight and check terrain clearance (Open-Meteo, on tap). Settings: units, C1/C2 actions, dual-operator master/slave, HDMI/SDI output, a link spectrum chart, Wi-Fi name and password, aircraft name, video cache, battery warning history and pairing, non-smart cell voltages. Calibration: compass interference meter, per-IMU calibration, lens infinity focus and a first-manual-focus prompt. Also an RTK chip, gimbal movement presets and motor tuning, Precision Takeoff gated on the flight controller, and the precise GEO database's server version. Built and unit/widget-tested only; nothing has flown yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
824 lines
36 KiB
Dart
824 lines
36 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');
|
|
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<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]),
|
|
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<SettingSpec> _vision = <SettingSpec>[
|
|
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<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,
|
|
),
|
|
GroupSpec('BUTTONS', requires: HudFeatures.customButtons),
|
|
CustomSpec(id: customButtons, requires: HudFeatures.customButtons, keys: const <DjiKey>[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<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),
|
|
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<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),
|
|
// 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 <int>[1, 2]) ...<SettingSpec>[
|
|
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<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')),
|
|
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<SettingSpec> _general = <SettingSpec>[
|
|
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: <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);
|
|
}
|