# Settings Screen Validation Reference

This document describes the validation rules enforced on each radio configuration and module
configuration settings screen. Constraints are sourced from two layers:

1. **Protobuf schema** — field types, enums, and `max_size` annotations defined in the
   `meshtastic/protobufs` submodule.
2. **UI form layer** — additional range checks, conditional visibility, and interval pickers
   implemented in `feature/settings/src/commonMain/.../radio/component/`.

> **Reusable components** live in `core/ui/.../component/`. Each component enforces its own
> validation strategy (see [UI Component Validation](#ui-component-validation) at the bottom).

---

## Table of Contents

- [Device Config Screens](#device-config-screens)
  - [User](#user-configuserconfig)
  - [Device](#device-configdeviceconfig)
  - [Position](#position-configpositionconfig)
  - [Power](#power-configpowerconfig)
  - [Network](#network-confignetworkconfig)
  - [Display](#display-configdisplayconfig)
  - [LoRa](#lora-configloraconfig)
  - [Bluetooth](#bluetooth-configbluetoothconfig)
  - [Security](#security-configsecurityconfig)
- [Module Config Screens](#module-config-screens)
  - [MQTT](#mqtt-moduleconfigmqttconfig)
  - [Serial](#serial-moduleconfigserialconfig)
  - [External Notification](#external-notification-moduleconfigexternalnotificationconfig)
  - [Store & Forward](#store--forward-moduleconfigstoreforwardconfig)
  - [Range Test](#range-test-moduleconfigrangetestconfig)
  - [Telemetry](#telemetry-moduleconfigtelemetryconfig)
  - [Canned Message](#canned-message-moduleconfigcannedmessageconfig)
  - [Audio](#audio-moduleconfigaudioconfig)
  - [Remote Hardware](#remote-hardware-moduleconfigremotehardwareconfig)
  - [Neighbor Info](#neighbor-info-moduleconfigneighborinfoconfig)
  - [Ambient Lighting](#ambient-lighting-moduleconfigambientlightingconfig)
  - [Detection Sensor](#detection-sensor-moduleconfigdetectionsensorconfig)
  - [Paxcounter](#paxcounter-moduleconfigpaxcounterconfig)
  - [Status Message](#status-message-moduleconfigstatusmessageconfig)
  - [Traffic Management](#traffic-management-moduleconfigtrafficmanagementconfig)
  - [TAK](#tak-moduleconfigtakconfig)
- [Channel Config](#channel-config)
- [Interval Configurations](#interval-configurations)
- [UI Component Validation](#ui-component-validation)

---

## Device Config Screens

### User (`Config.UserConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `long_name` | String | maxSize: 39 bytes (proto max_size: 40) | Required (non-blank) |
| `short_name` | String | maxSize: 4 bytes (proto max_size: 5) | Required (non-blank) |
| `is_licensed` | Boolean | Toggle | — |
| `is_unmessagable` | Boolean | Toggle | Enabled only when device capability `canToggleUnmessageable` is true or role is unmessageable |

### Device (`Config.DeviceConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `role` | Enum | Dropdown: `DeviceConfig.Role` entries | Router/ROUTER_LATE/REPEATER roles trigger a confirmation dialog |
| `rebroadcast_mode` | Enum | Dropdown: `RebroadcastMode` entries | ALL, ALL_SKIP_DECODING, LOCAL_ONLY, KNOWN_ONLY, NONE, CORE_PORTNUMS_ONLY |
| `node_info_broadcast_secs` | Interval | Dropdown: `NODE_INFO_BROADCAST` intervals | See [Interval Configurations](#interval-configurations) |
| `double_tap_as_button_press` | Boolean | Toggle | — |
| `disable_triple_click` | Boolean | Toggle (inverted) | UI shows "enabled" when proto field is `false` |
| `led_heartbeat_disabled` | Boolean | Toggle (inverted) | UI shows "enabled" when proto field is `false` |
| `tzdef` | String | maxSize: 64 bytes (proto max_size: 65) | Clearable |
| `button_gpio` | Integer | Numeric input | — |
| `buzzer_gpio` | Integer | Numeric input | — |

### Position (`Config.PositionConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `position_broadcast_secs` | Interval | Dropdown: `POSITION_BROADCAST` intervals | — |
| `position_broadcast_smart_enabled` | Boolean | Toggle | Controls visibility of smart broadcast fields |
| `broadcast_smart_minimum_interval_secs` | Interval | Dropdown: `SMART_BROADCAST_MINIMUM` intervals | Visible only when smart broadcast enabled |
| `broadcast_smart_minimum_distance` | Integer | Numeric input | Visible only when smart broadcast enabled |
| `fixed_position` | Boolean | Toggle | Controls visibility of lat/lon/alt vs GPS fields |
| `latitude` | Double | Range: −90.0 to +90.0 | Visible only when `fixed_position = true` |
| `longitude` | Double | Range: −180.0 to +180.0 | Visible only when `fixed_position = true` |
| `altitude` | Integer | Numeric input | Visible only when `fixed_position = true` |
| `gps_mode` | Enum | Dropdown: `GpsMode` entries | Visible only when `fixed_position = false` |
| `gps_update_interval` | Interval | Dropdown: `GPS_UPDATE` intervals | Visible only when `fixed_position = false` |
| `position_flags` | Bitwise | Checkbox set: `PositionFlags` entries (excl. UNSET) | Bitwise OR of selected flags |
| `rx_gpio` | Integer | Dropdown: GPIO pins 0–48 | — |
| `tx_gpio` | Integer | Dropdown: GPIO pins 0–48 | — |
| `gps_en_gpio` | Integer | Dropdown: GPIO pins 0–48 | — |

### Power (`Config.PowerConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `is_power_saving` | Boolean | Toggle | — |
| `on_battery_shutdown_after_secs` | Interval | Dropdown: `ALL` intervals | — |
| `adc_multiplier_override` | Float | Must be > 0.0 when enabled | Checkbox + float input; float visible only when enabled |
| `wait_bluetooth_secs` | Interval | Dropdown: `NAG_TIMEOUT` intervals | 0s–60s |
| `sds_secs` | Interval | Dropdown: `ALL` intervals | — |
| `min_wake_secs` | Interval | Dropdown: `NAG_TIMEOUT` intervals | 0s–60s |
| `device_battery_ina_address` | Integer | Numeric input (I2C address) | — |

### Network (`Config.NetworkConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `wifi_enabled` | Boolean | Toggle | Controls visibility of Wi-Fi fields |
| `wifi_ssid` | String | maxSize: 32 bytes (proto max_size: 33) | Visible when `wifi_enabled = true` |
| `wifi_psk` | Password | maxSize: 64 bytes (proto max_size: 65) | Visible when `wifi_enabled = true` |
| `eth_enabled` | Boolean | Toggle | Visible only when device `hasEthernet = true` |
| `ntp_server` | String | maxSize: 32 bytes (proto max_size: 33) | — |
| `rsyslog_server` | String | maxSize: 32 bytes (proto max_size: 33) | — |
| `address_mode` | Enum | Dropdown: `AddressMode` (DHCP, STATIC) | Controls visibility of IPv4 fields |
| `ipv4_config.ip` | IPv4 | Regex: `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}` | Visible when `address_mode = STATIC` |
| `ipv4_config.gateway` | IPv4 | Same regex | Visible when `address_mode = STATIC` |
| `ipv4_config.subnet` | IPv4 | Same regex | Visible when `address_mode = STATIC` |
| `ipv4_config.dns` | IPv4 | Same regex | Visible when `address_mode = STATIC` |

### Display (`Config.DisplayConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `screen_on_secs` | Interval | Dropdown: `DISPLAY_SCREEN_ON` intervals | 15s – Always On |
| `auto_screen_carousel_secs` | Interval | Dropdown: `DISPLAY_CAROUSEL` intervals | 0 (disabled) – 15 min |
| `compass_north_top` | Boolean | Toggle | — |
| `use_12h_clock` | Boolean | Toggle | — |
| `heading_bold` | Boolean | Toggle | — |
| `units` | Enum | Dropdown: `DisplayUnits` entries | Metric / Imperial |
| `wake_on_tap_or_motion` | Boolean | Toggle | — |
| `flip_screen` | Boolean | Toggle | — |
| `displaymode` | Enum | Dropdown: `DisplayMode` entries | — |
| `oled` | Enum | Dropdown: `OledType` entries | — |
| `compass_orientation` | Enum | Dropdown: `CompassOrientation` entries | — |

### LoRa (`Config.LoRaConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `region` | Enum | Dropdown: `RegionInfo` entries | Regional frequency plans |
| `use_preset` | Boolean | Toggle | Controls manual vs preset LoRa settings visibility |
| `modem_preset` | Enum | Dropdown: `ChannelOption` entries | Visible only when `use_preset = true` |
| `bandwidth` | Integer | Numeric input | Visible only when `use_preset = false` |
| `spread_factor` | Integer | Numeric input | Visible only when `use_preset = false` |
| `coding_rate` | Integer | Numeric input | Visible only when `use_preset = false` |
| `hop_limit` | Integer | Dropdown: 0–7 | — |
| `channel_num` | Integer | Numeric input; must be ≤ `numChannels` | — |
| `tx_enabled` | Boolean | Toggle | — |
| `tx_power` | Integer | Signed integer input (dBm) | — |
| `override_duty_cycle` | Boolean | Toggle | — |
| `override_frequency` | Float | MHz value | — |
| `sx126x_rx_boosted_gain` | Boolean | Toggle | — |
| `ignore_mqtt` | Boolean | Toggle | — |
| `config_ok_to_mqtt` | Boolean | Toggle | — |
| `pa_fan_disabled` | Boolean | Toggle | Visible only when device `hasPaFan = true` |

### Bluetooth (`Config.BluetoothConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `mode` | Enum | Dropdown: `PairingMode` entries (excl. UNRECOGNIZED) | — |
| `fixed_pin` | String | Exactly 6 digits | — |

### Security (`Config.SecurityConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `public_key` | Base64 | 32 bytes; read-only | Auto-derived from private key |
| `private_key` | Base64 | 32 bytes | Key generation applies bit masking: `f[0] &= 0xF8`, `f[31] = (f[31] & 0x7F) ǀ 0x40` |
| `admin_key` | Base64 list | maxCount: 3; each entry 32 bytes | — |
| `serial_enabled` | Boolean | Toggle | — |
| `debug_log_api_enabled` | Boolean | Toggle | — |
| `admin_channel_enabled` | Boolean | Toggle | — |
| `is_managed` | Boolean | Toggle | Enabled only when `admin_key` list is non-empty |

---

## Module Config Screens

### MQTT (`ModuleConfig.MQTTConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `address` | String | maxSize: 63 bytes (proto max_size: 64) | Non-blank required for connection test |
| `username` | String | maxSize: 63 bytes (proto max_size: 64) | — |
| `password` | Password | maxSize: 63 bytes (proto max_size: 64) | — |
| `encryption_enabled` | Boolean | Toggle | — |
| `json_enabled` | Boolean | Toggle | — |
| `tls_enabled` | Boolean | Toggle | Auto-enforced when using default server with proxy |
| `root` | String | maxSize: 31 bytes (proto max_size: 32) | MQTT root topic |
| `proxy_to_client_enabled` | Boolean | Toggle | — |
| `map_reporting_enabled` | Boolean | Toggle | Controls visibility of map report fields |
| `map_report_settings.publish_interval_secs` | Integer | Minimum: 3600 seconds (1 hour) | — |
| `map_report_settings.position_precision` | Integer | Slider: 12–15 bits | — |

### Serial (`ModuleConfig.SerialConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `echo` | Boolean | Toggle | — |
| `rxd` | Integer | Numeric input (GPIO) | — |
| `txd` | Integer | Numeric input (GPIO) | — |
| `baud` | Enum | Dropdown: `Serial_Baud` entries | — |
| `timeout` | Integer | Numeric input | — |
| `mode` | Enum | Dropdown: `Serial_Mode` entries | — |
| `override_console_serial_port` | Boolean | Toggle | — |

### External Notification (`ModuleConfig.ExternalNotificationConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `alert_message` | Boolean | Toggle | — |
| `alert_message_buzzer` | Boolean | Toggle | — |
| `alert_message_vibra` | Boolean | Toggle | — |
| `alert_bell` | Boolean | Toggle | — |
| `alert_bell_buzzer` | Boolean | Toggle | — |
| `alert_bell_vibra` | Boolean | Toggle | — |
| `output` | Integer | Dropdown: GPIO pins 0–48 | Shows `active` field when ≠ 0 |
| `active` | Boolean | Toggle | Visible when `output ≠ 0` |
| `output_buzzer` | Integer | Dropdown: GPIO pins 0–48 | Shows `use_pwm` field when ≠ 0 |
| `use_pwm` | Boolean | Toggle | Visible when `output_buzzer ≠ 0` |
| `output_vibra` | Integer | Dropdown: GPIO pins 0–48 | — |
| `output_ms` | Interval | Dropdown: `OUTPUT` intervals | 0s–10s |
| `nag_timeout` | Interval | Dropdown: `NAG_TIMEOUT` intervals | 0s–60s |
| `ringtone` | String | maxSize: 230 bytes | RTTTL format |
| `use_i2s_as_buzzer` | Boolean | Toggle | — |

### Store & Forward (`ModuleConfig.StoreForwardConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `heartbeat` | Boolean | Toggle | — |
| `records` | Integer | Numeric input | — |
| `history_return_max` | Integer | Numeric input | — |
| `history_return_window` | Integer | Numeric input (seconds) | — |
| `is_server` | Boolean | Toggle | — |

### Range Test (`ModuleConfig.RangeTestConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `sender` | Interval | Dropdown: `RANGE_TEST_SENDER` intervals | 0 (disabled) – 1 hour |
| `save` | Boolean | Toggle | — |

### Telemetry (`ModuleConfig.TelemetryConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `device_telemetry_enabled` | Boolean | Toggle | Visible only when `canToggleTelemetryEnabled` capability |
| `device_update_interval` | Interval | Dropdown: `BROADCAST_SHORT` intervals | 30 min – 72 hours |
| `environment_measurement_enabled` | Boolean | Toggle | — |
| `environment_update_interval` | Interval | Dropdown: `BROADCAST_SHORT` intervals | 30 min – 72 hours |
| `environment_screen_enabled` | Boolean | Toggle | — |
| `environment_display_fahrenheit` | Boolean | Toggle | — |
| `air_quality_enabled` | Boolean | Toggle | — |
| `air_quality_interval` | Interval | Dropdown: `BROADCAST_SHORT` intervals | 30 min – 72 hours |
| `power_measurement_enabled` | Boolean | Toggle | — |
| `power_update_interval` | Interval | Dropdown: `BROADCAST_SHORT` intervals | 30 min – 72 hours |
| `power_screen_enabled` | Boolean | Toggle | — |

### Canned Message (`ModuleConfig.CannedMessageConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `rotary1_enabled` | Boolean | Toggle | — |
| `inputbroker_pin_a` | Integer | Numeric input (GPIO) | — |
| `inputbroker_pin_b` | Integer | Numeric input (GPIO) | — |
| `inputbroker_pin_press` | Integer | Numeric input (GPIO) | — |
| `inputbroker_event_press` | Enum | Dropdown: `InputEventChar` entries | — |
| `inputbroker_event_cw` | Enum | Dropdown: `InputEventChar` entries | — |
| `inputbroker_event_ccw` | Enum | Dropdown: `InputEventChar` entries | — |
| `updown1_enabled` | Boolean | Toggle | — |
| `allow_input_source` | String | maxSize: 63 bytes (proto max_size: 16; UI intentionally allows more) | — |
| `send_bell` | Boolean | Toggle | — |
| `messages` | String | maxSize: 200 bytes (proto max_size: 201) | Pipe-delimited canned messages |

### Audio (`ModuleConfig.AudioConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `codec2_enabled` | Boolean | Toggle | — |
| `ptt_pin` | Integer | Numeric input (GPIO) | — |
| `bitrate` | Enum | Dropdown: `Audio_Baud` entries | — |
| `i2s_ws` | Integer | Numeric input (GPIO) | — |
| `i2s_sd` | Integer | Numeric input (GPIO) | — |
| `i2s_din` | Integer | Numeric input (GPIO) | — |
| `i2s_sck` | Integer | Numeric input (GPIO) | — |

### Remote Hardware (`ModuleConfig.RemoteHardwareConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `allow_undefined_pin_access` | Boolean | Toggle | — |
| `available_pins` | Pin list | maxCount: 4 | Each pin: gpio 0–255, name maxSize 14 bytes, type `RemoteHardwarePinType` enum |

### Neighbor Info (`ModuleConfig.NeighborInfoConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `update_interval` | Integer | Numeric input (seconds) | — |
| `transmit_over_lora` | Boolean | Toggle | — |

### Ambient Lighting (`ModuleConfig.AmbientLightingConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `led_state` | Boolean | Toggle | — |
| `current` | Integer | Numeric input | — |
| `red` | Integer | Numeric input (0–255) | — |
| `green` | Integer | Numeric input (0–255) | — |
| `blue` | Integer | Numeric input (0–255) | — |

### Detection Sensor (`ModuleConfig.DetectionSensorConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `minimum_broadcast_secs` | Interval | Dropdown: `DETECTION_SENSOR_MINIMUM` intervals | 0 (unset) – 72 hours |
| `state_broadcast_secs` | Interval | Dropdown: `DETECTION_SENSOR_STATE` intervals | 0 (unset) – 72 hours |
| `send_bell` | Boolean | Toggle | — |
| `name` | String | maxSize: 19 bytes (proto max_size: 20) | — |
| `monitor_pin` | Integer | Dropdown: GPIO pins 0–48 | — |
| `detection_trigger_type` | Enum | Dropdown: `TriggerType` entries | — |
| `use_pullup` | Boolean | Toggle | — |

### Paxcounter (`ModuleConfig.PaxcounterConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | — |
| `paxcounter_update_interval` | Interval | Dropdown: `PAX_COUNTER` intervals | 15 min – 72 hours |
| `wifi_threshold` | Integer | Signed integer (RSSI dBm) | Default: −80 |
| `ble_threshold` | Integer | Signed integer (RSSI dBm) | Default: −80 |

### Status Message (`ModuleConfig.StatusMessageConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `node_status` | String | maxSize: 80 bytes | Clearable; requires `supportsStatusMessage` capability |

### Traffic Management (`ModuleConfig.TrafficManagementConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `enabled` | Boolean | Toggle | Requires `supportsTrafficManagementConfig` capability |
| `position_dedup_enabled` | Boolean | Toggle | — |
| `position_precision_bits` | Integer | Numeric input | — |
| `position_min_interval_secs` | Integer | Numeric input (seconds) | — |
| `nodeinfo_direct_response` | Boolean | Toggle | — |
| `nodeinfo_direct_response_max_hops` | Integer | Numeric input | — |
| `rate_limit_enabled` | Boolean | Toggle | — |
| `rate_limit_window_secs` | Integer | Numeric input (seconds) | — |
| `rate_limit_max_packets` | Integer | Numeric input | — |
| `drop_unknown_enabled` | Boolean | Toggle | — |
| `unknown_packet_threshold` | Integer | Numeric input | — |
| `exhaust_hop_telemetry` | Boolean | Toggle | — |
| `exhaust_hop_position` | Boolean | Toggle | — |
| `router_preserve_hops` | Boolean | Toggle | — |

### TAK (`ModuleConfig.TAKConfig`)

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `team` | Enum | Dropdown with custom colors/labels | Only shown when device role is `TAK` or `TAK_TRACKER` |
| `role` | Enum | Dropdown with custom labels | Only shown when device role is `TAK` or `TAK_TRACKER` |

---

## Channel Config

Channel editing is handled by `EditChannelDialog`.

| Field | Type | Validation | Notes |
|-------|------|------------|-------|
| `name` | String | maxSize: 11 bytes | Empty name falls back to modem preset name |
| `psk` | ByteString | Must be exactly 0, 16, or 32 bytes | Pre-shared key for encryption |

---

## Interval Configurations

Time-based fields use predefined interval sets from `IntervalConfiguration`. Each configuration
context restricts the available choices to a specific subset of `FixedUpdateIntervals`.

| Configuration | Range | Values (seconds) |
|---------------|-------|------------------|
| `ALL` | 0 – 2147483647 | 0, 1, 2, 3, 4, 5, 8, 10, 15, 20, 30, 40, 45, 60, 80, 90, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200, 2147483647 |
| `BROADCAST_SHORT` | 30 min – 72 hr | 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `BROADCAST_MEDIUM` | 1 hr – 72 hr | 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `BROADCAST_LONG` | 3 hr – 72 hr | 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `NODE_INFO_BROADCAST` | 0 (unset), 3 hr – 72 hr | 0, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `NAG_TIMEOUT` | 0 – 60 s | 0, 1, 5, 10, 15, 30, 60 |
| `OUTPUT` | 0 – 10 s | 0, 1, 2, 3, 4, 5, 10 |
| `PAX_COUNTER` | 15 min – 72 hr | 900, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `POSITION` | 1 s – 1 hr | 1, 2, 5, 10, 15, 20, 30, 45, 60, 120, 300, 600, 900, 1800, 3600 |
| `POSITION_BROADCAST` | 0 (unset), 1 min – 72 hr | 0, 60, 90, 300, 900, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `GPS_UPDATE` | 0 (unset), 8 s – 24 hr | 0, 8, 20, 40, 60, 80, 120, 300, 600, 900, 1800, 3600, 21600, 43200, 86400 |
| `RANGE_TEST_SENDER` | 0 (disabled) – 1 hr | 0, 15, 30, 45, 60, 300, 600, 900, 1800, 3600 |
| `SMART_BROADCAST_MINIMUM` | 15 s – 1 hr | 15, 30, 45, 60, 300, 600, 900, 1800, 3600 |
| `DETECTION_SENSOR_MINIMUM` | 0 (unset), 15 s – 72 hr | 0, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `DETECTION_SENSOR_STATE` | 0 (unset), 15 min – 72 hr | 0, 900, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `DISPLAY_SCREEN_ON` | 15 s – Always On | 15, 30, 60, 300, 600, 900, 1800, 3600, 2147483647 |
| `DISPLAY_CAROUSEL` | 0 (disabled) – 15 min | 0, 15, 30, 60, 300, 600, 900 |

### Shared Constants

- **GPIO Pins:** 0–48 (inclusive)
- **Hop Limits:** 0–7 (inclusive)

---

## UI Component Validation

The reusable Compose components in `core/ui/.../component/` provide the actual enforcement
layer. Each component accepts validation parameters and rejects or flags invalid input.

### EditTextPreference

- **String mode:** Enforces `maxSize` in UTF-8 bytes via `encodeToByteArray().size <= maxSize`.
  Shows a live byte counter when focused.
- **Int mode:** Validates with `toIntOrNull()`; sets error icon on failure.
- **Float mode:** Validates with `toFloatOrNull()`.
- **Double mode:** Validates with `toDoubleOrNull()`; supports international decimal separators
  (`.`, `,`, `٫`, `、`, `·`).

### SignedIntegerEditTextPreference

- Validates with `toIntOrNull()`; shows error icon on invalid input.

### DropDownPreference

- Closed-set selection only — no free-form input.
- Automatically filters `UNRECOGNIZED` and deprecated enum entries.

### SwitchPreference

- Binary toggle; inherently valid. Disabled state shown at reduced opacity.

### SliderPreference

- Discrete steps only; values clamped with `coerceIn()` and rounded to the nearest valid index.

### EditIPv4Preference

- Regex validation: `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`.
- Stored as a little-endian `Int`; displayed as dotted-decimal.

### EditPasswordPreference

- Delegates to `EditTextPreference` with `maxSize` enforcement and password visibility toggle.

### EditBase64Preference

- Validates Base64 encoding on input.
- Flags all-zero 32-byte values as errors (empty/unset key).

### BitwisePreference

- Closed checkbox set; toggles individual bit flags via XOR.
- "Clear" button resets to 0.

### EditListPreference

- Enforces `maxCount` for list length.
- Per-element validation varies by type: `Int` range 0–255 for GPIO pins,
  `ByteString` via Base64 parsing, composite `RemoteHardwarePin` with nested field rules.

### PositionPrecisionPreference

- State machine with three modes: disabled (0), precision range (10–19), full precision (32).
- Slider constrained to 10–19 with 8 discrete steps.

### ConfigState

- Generic wrapper that tracks `isDirty` (current value ≠ initial value).
- Changes are only persisted on explicit save; cancel reverts to the initial snapshot.
