Meshtastic-Apple/docs/settings-validation.md
copilot-swe-agent[bot] b0142d0937
docs: add settings validation reference markdown
Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/93cf8d9c-f165-4067-a258-420caf50bc86

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
2026-04-19 00:05:19 +00:00

419 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Meshtastic-Apple — Settings Validation Reference
This document describes every field presented in the **Config** and **Module Config** settings screens, the validation rules applied in the SwiftUI forms, and the underlying constraints from the protobuf definitions.
---
## Shared Components
The following reusable components are used across all config forms.
### `UpdateIntervalPicker` (`Views/Settings/UpdateIntervalPicker.swift`)
A reusable picker bound to an `UpdateInterval` value. Each usage specifies an `IntervalConfiguration` that restricts which time values appear. If the device's current value does not match a predefined option an orange ⚠️ warning is shown: *"The configured value: (X) is not one of the optimized options."* Manual/arbitrary values are accepted and transmitted but are flagged.
| Configuration | Allowed values (seconds) |
|---|---|
| `broadcastShort` | 0, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200, never |
| `broadcastMedium` | 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200, never |
| `broadcastLong` | 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200, never |
| `detectionSensorMinimum` | 0, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `detectionSensorState` | 0, 900, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `nagTimeout` | 0, 1, 5, 10, 15, 30, 60 |
| `paxCounter` | 900, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 64800, 86400, 129600, 172800, 259200 |
| `rangeTestSender` | 0, 15, 30, 45, 60, 300, 600, 900, 1800, 3600 |
| `smartBroadcastMinimum` | 15, 30, 45, 60, 300, 600, 900, 1800, 3600 |
### `SecureInput` (`Views/Helpers/SecureInput.swift`)
A text field with a visibility-toggle button and an `isValid: Bool` binding. When `isValid` is `false` the field is decorated with a red `RoundedRectangle` stroke. Used for all cryptographic-key fields in Security Config.
### `FloatField` (local to `PowerConfig.swift`)
A generic float text field that accepts an `isValid: (Float) -> Bool` closure. On every change the closure is evaluated; if the new value is invalid the field reverts to the previous value.
---
## Config Screens
### 1. LoRa Config (`Config/LoRaConfig.swift`)
Protobuf message: `Config.LoRaConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `region` | Picker (enum) | Values from `RegionCode` enum (unset, US, EU_433, EU_868, CN, JP, ANZ, KR, TW, RU, IN, NZ_865, TH, LORA_24, UA_433, UA_868, MY_433, MY_919, SG_923) |
| `usePreset` | Toggle | Boolean |
| `modemPreset` | Picker (enum) | Values from `ModemPresets` enum; disabled when `usePreset` is false |
| `hopLimit` | Picker (`ForEach 0..<8`) | Integer **07** inclusive. Proto: *"Maximum number of hops. This can't be greater than 7."* |
| `txEnabled` | Toggle | Boolean |
| `txPower` | `Stepper(in: 1...30, step: 1)` | Integer **130 dBm**. Proto: *"If zero, then use default max legal continuous power."* (0 = default; UI starts at 1 to prevent accidental override) |
| `channelNum` | `TextField` (integer `NumberFormatter`, no grouping separator) | `UInt32`; disabled when `overrideFrequency > 0` |
| `bandwidth` | Picker (enum `BandwidthCodes`) | Enum-constrained |
| `spreadFactor` | Picker (`ForEach 7..<13`) | Integer **712**. Proto: *"A number from 7 to 12. Indicates number of chirps per symbol as 1<<spread_factor."* Value 0 maps internally to 12 |
| `codingRate` | Picker (`ForEach 5..<9`) | Integer **58**. Proto: *"The denominator of the coding rate. ie for 4/5, the value is 5."* Value 0 maps internally to 8 |
| `rxBoostedGain` | Toggle | Boolean |
| `overrideFrequency` | `TextField` (`NumberFormatter`, `maximumFractionDigits: 4`, no grouping separator) | `Float`; disables `channelNum` field when > 0 |
| `ignoreMqtt` | Toggle | Boolean |
| `okToMqtt` | Toggle | Boolean |
**Dirty-tracking:** All fields use `.onChange` comparing new value against `node?.loRaConfig?.field ?? -1`; `hasChanges` is set to `true` on any discrepancy.
---
### 2. Device Config (`Config/DeviceConfig.swift`)
Protobuf message: `Config.DeviceConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `deviceRole` | Picker (enum) | Values from `DeviceRoles` enum (client, clientMute, router, routerLate, repeater, tracker, sensor, tak, clientHidden, lostAndFound, takTracker). Selecting a router/routerLate/clientBase role triggers a confirmation dialog |
| `rebroadcastMode` | Picker (enum `RebroadcastModes`) | Enum-constrained |
| `nodeInfoBroadcastSecs` | `UpdateIntervalPicker(config: .broadcastLong)` | Predefined options ≥ 3 hours (10 800 s). On `setInitialValues`: if stored value < 10 800, it is **clamped to 10 800** |
| `doubleTapAsButtonPress` | Toggle | Boolean |
| `tripleClickAsAdHocPing` | Toggle | Boolean |
| `ledHeartbeatEnabled` | Toggle | Boolean |
| `tzdef` | `TextField` | **Max 63 UTF-8 bytes.** `.onChange` trims trailing characters until `utf8.count <= 63` |
| `buttonGPIO` | Picker (GPIO list 048) | Integer |
| `buzzerGPIO` | Picker (GPIO list 048) | Integer |
---
### 3. Display Config (`Config/DisplayConfig.swift`)
Protobuf message: `Config.DisplayConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `screenOnSeconds` | `UpdateIntervalPicker(config: .broadcastShort)` | Predefined interval options |
| `screenCarouselInterval` | Picker (enum `ScreenCarouselIntervals`) | Enum-constrained |
| `compassNorthTop` | Toggle | Boolean |
| `wakeOnTapOrMotion` | Toggle | Boolean |
| `flipScreen` | Toggle | Boolean |
| `oledType` | Picker (enum `OledTypes`) | Enum-constrained |
| `displayMode` | Picker (enum `DisplayModes`) | Enum-constrained |
| `units` | Picker (enum `Units`) | Enum-constrained (metric / imperial) |
| `use12HourClock` | Toggle | Boolean |
| `headingBold` | Toggle | Boolean |
All fields use standard `.onChange` dirty-tracking only; no custom range or byte-length validation.
---
### 4. Power Config (`Config/PowerConfig.swift`)
Protobuf message: `Config.PowerConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `isPowerSaving` | Toggle | Boolean; only shown for ESP32/ESP32S3 devices or specific roles |
| `shutdownOnPowerLoss` | Toggle | Boolean |
| `shutdownAfterSecs` | `UpdateIntervalPicker(config: .all)` | Any `FixedUpdateIntervals` value |
| `adcOverride` | Toggle | Boolean; enables the `adcMultiplier` field |
| `adcMultiplier` | `FloatField(isValid: { (2.0 ... 6.0).contains($0) })` | **Float 2.06.0 inclusive.** Invalid input is silently reverted to the previous value. When `adcOverride` is false the field is hidden and `0` is sent |
| `waitBluetoothSecs` | `UpdateIntervalPicker` | Predefined interval options |
| `lsSecs` | `UpdateIntervalPicker` | Predefined interval options |
| `minWakeSecs` | `UpdateIntervalPicker` | Predefined interval options |
---
### 5. Network Config (`Config/NetworkConfig.swift`)
Protobuf message: `Config.NetworkConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `wifiEnabled` | Toggle | Boolean; only shown on devices where `hasWifi == true` |
| `wifiSsid` | `TextField` | **Max 32 UTF-8 bytes.** `.onChange` trims trailing characters until `utf8.count <= 32` |
| `wifiPsk` | `TextField` (`.keyboardType(.asciiCapable)`) | **Max 63 UTF-8 bytes.** `.onChange` trims trailing characters until `utf8.count <= 63` |
| `ethEnabled` | Toggle | Boolean; only shown on devices where `hasEthernet == true` |
| `udpEnabled` | Toggle | Boolean |
---
### 6. Bluetooth Config (`Config/BluetoothConfig.swift`)
Protobuf message: `Config.BluetoothConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `mode` | Picker (enum `BluetoothModes`) | randomPin, fixedPin, noPin |
| `fixedPin` | `TextField` (`.keyboardType(.decimalPad)`) | **Exactly 6 digits, no leading zeros.** `.onChange`: (1) removes all `"0"` characters if the first character is `"0"`; (2) truncates to 6 characters with `.prefix(6)` if over-length; (3) sets `shortPin = true` and shows red error text *"BLE Pin must be 6 digits long."* if under-length. Only shown when `mode == .fixedPin` |
Proto field: `fixedPin: UInt32` the app transmits the string converted to `UInt32`.
---
### 7. Security Config (`Config/SecurityConfig.swift`)
Protobuf message: `Config.SecurityConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `publicKey` | Read-only `Text` display | Auto-derived from `privateKey` (Curve25519); not editable |
| `privateKey` | `SecureInput(isValid: $hasValidPrivateKey)` | **32 bytes when decoded from Base64.** `.onChange`: decodes as Base64; if `Data.count == 32` valid and the public key display is refreshed; otherwise invalid with red border |
| `adminKey` | `SecureInput(isValid: $hasValidAdminKey)` | **32 bytes when decoded from Base64, or empty.** Empty string valid; `Data.count == 32` valid; otherwise invalid with red border |
| `adminKey2` | `SecureInput(isValid: $hasValidAdminKey2)` | Same rule as `adminKey` |
| `adminKey3` | `SecureInput(isValid: $hasValidAdminKey3)` | Same rule as `adminKey` |
| `isManaged` | Toggle | Boolean |
| `serialEnabled` | Toggle | Boolean |
| `debugLogApiEnabled` | Toggle | Boolean |
**Save guard:** The save action includes a `guard hasValidPrivateKey && hasValidAdminKey && hasValidAdminKey2 && hasValidAdminKey3 else { return }` a save is silently blocked if any key field is invalid.
---
### 8. Position Config (`Config/PositionConfig.swift`)
Protobuf message: `Config.PositionConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `smartPositionEnabled` | Toggle | Boolean; controls visibility of smart-position sub-fields |
| `deviceGpsEnabled` | Toggle | Boolean |
| `gpsMode` | Picker (enum `GpsModes`) | Enum-constrained |
| `fixedPosition` | Toggle | Requires firmware version check and shows a confirmation dialog |
| `positionBroadcastSeconds` | `UpdateIntervalPicker(config: .broadcastShort)` | Predefined options: 30 min72 hours or never |
| `broadcastSmartMinimumDistance` | `TextField` (integer) | `UInt32`, no additional validation |
| `broadcastSmartMinimumIntervalSecs` | `UpdateIntervalPicker(config: .smartBroadcastMinimum)` | Predefined options: 15 s1 hour |
| `gpsUpdateInterval` | Picker (enum `GpsUpdateIntervals`) | Enum-constrained |
| `positionFlags` (bitfield) | Multiple Toggles | Each toggle controls one bit (altitude, speed, heading, satellite count, sequence number, timestamp, heading, name). Combined into a single `UInt32` on save |
---
## Module Config Screens
### 1. MQTT Config (`Config/Module/MQTTConfig.swift`)
Protobuf message: `ModuleConfig.MQTTConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `proxyToClientEnabled` | Toggle | Boolean |
| `address` | `TextField` | **Max 62 UTF-8 bytes.** `.onChange` trims trailing characters |
| `username` | `TextField` | **Max 62 UTF-8 bytes.** `.onChange` trims trailing characters |
| `password` | `TextField` | **Max 30 UTF-8 bytes.** `.onChange` trims trailing characters |
| `root` | `TextField` | **Max 30 UTF-8 bytes.** `.onChange` trims trailing characters. Proto default: `"msh"` |
| `encryptionEnabled` | Toggle | Boolean |
| `jsonEnabled` | Toggle | Boolean |
| `tlsEnabled` | Toggle | Boolean |
| `mapReportingEnabled` | Toggle | Boolean; requires a consent toggle before it can be enabled |
| `mapPublishIntervalSecs` | `UpdateIntervalPicker(config: .broadcastMedium)` | Predefined options: 172 hours |
| `mapPositionPrecision` | `Slider(in: 12...15, step: 1)` | Integer **1215** inclusive |
**Duty cycle warning:** If the connected node's duty cycle is between 0 and 100 (exclusive), an orange warning about duty cycle restrictions is shown.
---
### 2. Telemetry Config (`Config/Module/TelemetryConfig.swift`)
Protobuf message: `ModuleConfig.TelemetryConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `deviceUpdateInterval` | `UpdateIntervalPicker(config: .broadcastShort)` | Predefined options: 30 min72 hours |
| `deviceTelemetryEnabled` | Toggle | Boolean; only shown on firmware 2.7.12 |
| `environmentUpdateInterval` | `UpdateIntervalPicker(config: .broadcastShort)` | Predefined options: 30 min72 hours |
| `environmentMeasurementEnabled` | Toggle | Boolean |
| `environmentScreenEnabled` | Toggle | Boolean |
| `environmentDisplayFahrenheit` | Toggle | Boolean |
| `powerMeasurementEnabled` | Toggle | Boolean |
| `powerUpdateInterval` | `UpdateIntervalPicker(config: .broadcastShort)` | Predefined options: 30 min72 hours |
| `powerScreenEnabled` | Toggle | Boolean |
No range or byte-length validation beyond the predefined picker options.
---
### 3. Canned Messages Config (`Config/Module/CannedMessagesConfig.swift`)
Protobuf message: `ModuleConfig.CannedMessageConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `sendBell` | Toggle | Boolean |
| `rotary1Enabled` | Toggle | Boolean; **mutually exclusive** with `updown1Enabled` (the other is disabled while this is on) |
| `updown1Enabled` | Toggle | Boolean; **mutually exclusive** with `rotary1Enabled` |
| `inputbrokerPinA` | Picker (GPIO list) | Integer |
| `inputbrokerPinB` | Picker (GPIO list) | Integer |
| `inputbrokerPinPress` | Picker (GPIO list) | Integer |
| `inputbrokerEventCw` | Picker (enum `InputEventSets`) | Enum-constrained |
| `inputbrokerEventCcw` | Picker (enum `InputEventSets`) | Enum-constrained |
| `inputbrokerEventPress` | Picker (enum `InputEventSets`) | Enum-constrained |
| `messages` | `TextField` (multiline) | **Max 198 UTF-8 bytes.** `.onChange` trims trailing characters until `utf8.count <= 198` |
---
### 4. Detection Sensor Config (`Config/Module/DetectionSensorConfig.swift`)
Protobuf message: `ModuleConfig.DetectionSensorConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `role` | Segmented Picker (sensor / client) | Stored in `AppStorage`; controls which sub-form sections are visible |
| `sendBell` | Toggle | Boolean; sensor role only |
| `name` | `TextField` | **Max 20 UTF-8 bytes.** `.onChange` trims trailing characters |
| `triggerType` | Picker (enum) | Enum-constrained |
| `usePullup` | Toggle | Boolean |
| `minimumBroadcastSecs` | `UpdateIntervalPicker(config: .detectionSensorMinimum)` | Predefined options: 072 hours |
| `stateBroadcastSecs` | `UpdateIntervalPicker(config: .detectionSensorState)` | Predefined options: 072 hours |
| `monitorPin` | Picker (GPIO list) | Integer |
---
### 5. External Notification Config (`Config/Module/ExternalNotificationConfig.swift`)
Protobuf message: `ModuleConfig.ExternalNotificationConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `alertBell` | Toggle | Boolean |
| `alertBellBuzzer` | Toggle | Boolean |
| `alertBellVibra` | Toggle | Boolean |
| `alertMessage` | Toggle | Boolean |
| `alertMessageBuzzer` | Toggle | Boolean |
| `alertMessageVibra` | Toggle | Boolean |
| `active` | Toggle | Boolean (active-high vs active-low) |
| `output` | Picker (GPIO list 049) | Integer (`UInt32`) |
| `outputBuzzer` | Picker (GPIO list) | Integer |
| `outputVibra` | Picker (GPIO list) | Integer |
| `outputMilliseconds` | Picker (`OutputIntervals` enum) | Values (ms): 0, 1000, 2000, 3000, 4000, 5000, 10000, 15000, 30000, 60000 |
| `nagTimeout` | `UpdateIntervalPicker(config: .nagTimeout)` | Predefined options: 0, 1, 5, 10, 15, 30, 60 seconds |
| `usePWM` | Toggle | Boolean |
| `useI2SAsBuzzer` | Toggle | Boolean |
No string-length or numeric-range validation beyond the predefined picker options.
---
### 6. Store and Forward Config (`Config/Module/StoreForwardConfig.swift`)
Protobuf message: `ModuleConfig.StoreAndForwardConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `isServer` | Toggle | Boolean |
| `heartbeat` | Toggle | Boolean |
| `records` | Picker | Values: 0, 25, 50, 75, 100 |
| `historyReturnMax` | Picker | Values: 0, 25, 50, 75, 100 |
| `historyReturnWindow` | Picker | Values (seconds): 0, 60, 300, 600, 900, 1800, 3600, 7200 |
All options are predefined; no custom validation.
---
### 7. Serial Config (`Config/Module/SerialConfig.swift`)
Protobuf message: `ModuleConfig.SerialConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `echo` | Toggle | Boolean |
| `rxd` | Picker (GPIO list 049) | Integer |
| `txd` | Picker (GPIO list 049) | Integer |
| `baudRate` | Picker (enum `SerialBaudRates`) | Enum-constrained standard baud rates |
| `timeout` | Picker | Predefined timeout values |
| `overrideConsoleSerialPort` | Toggle | Boolean |
| `mode` | Picker (enum `SerialModes`) | Enum-constrained (default, simple, proto, textmsg, nmea, caltopo) |
---
### 8. Range Test Config (`Config/Module/RangeTestConfig.swift`)
Protobuf message: `ModuleConfig.RangeTestConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `sender` | `UpdateIntervalPicker(config: .rangeTestSender)` | Predefined options: 0, 15, 30, 45, 60, 300, 600, 900, 1800, 3600 seconds |
| `save` | Toggle | Boolean |
---
### 9. RTTTL Config (`Config/Module/RtttlConfig.swift`)
Protobuf: transmitted as a plain string via `saveRtttlConfig`.
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `ringtone` | `TextField` | **Max 228 UTF-8 bytes.** `.onChange` trims trailing characters until `utf8.count <= 228`. Value is stripped of leading/trailing whitespace before being sent |
---
### 10. Ambient Lighting Config (`Config/Module/AmbientLightingConfig.swift`)
Protobuf message: `ModuleConfig.AmbientLightingConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `ledState` | Toggle | Boolean |
| `current` | `Stepper(in: 0...31, step: 1)` | Integer **031** (LED current level) |
| `color` | `ColorPicker` | SwiftUI color picker; converted to RGB `UInt32` on save |
---
### 11. TAK Module Config (`Config/Module/TAKModuleConfig.swift`)
Protobuf message: `ModuleConfig.TAKConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean; entire form only shown for roles with TAK access (`tak`, `takTracker`) |
| `team` | Picker (enum `TakTeams`) | Enum-constrained (cyan, white, yellow, orange, magenta, red, maroon, purple, dark blue, blue, green, dark green, brown, …) |
| `role` | Picker (enum `TakRoles`) | Enum-constrained (team member, team lead, HQ, sniper, medic, RTO, FO, casevac, transport, ground, air, control, Blue Force, EOD, intelligence, recon, belligerent, sus, exercise participant) |
---
### 12. Pax Counter Config (`Config/Module/PaxCounterConfig.swift`)
Protobuf message: `ModuleConfig.PaxcounterConfig`
| Field | UI Control | Constraint / Validation |
|---|---|---|
| `enabled` | Toggle | Boolean |
| `paxcounterUpdateInterval` | `UpdateIntervalPicker(config: .paxCounter)` | Predefined options: 15 min72 hours |
---
## Validation Patterns Summary
| Pattern | Where Used | Details |
|---|---|---|
| **UTF-8 byte truncation** | All string `TextField` fields | `.onChange` loop: `while utf8.count > limit { dropLast() }` |
| **Numeric stepper range** | `txPower` (130), `current` (031) | Enforced by SwiftUI `Stepper(in:)` |
| **Enum picker** | All categorical fields | Statically typed; only valid enum cases selectable |
| **Predefined interval picker** | All timing fields | `UpdateIntervalPicker` with `IntervalConfiguration`; out-of-range values shown with warning |
| **Slider range** | `mapPositionPrecision` | `Slider(in: 12...15, step: 1)` |
| **Float closure validation** | `adcMultiplier` | `FloatField(isValid:)` reverts to previous value on invalid input |
| **Base64 + byte-count key validation** | `privateKey`, `adminKey` ×3 | `Data(base64Encoded:)?.count == 32`; `SecureInput` shows red border when `isValid == false` |
| **Guard-on-save** | Security Config | Save is blocked until all four key fields are valid |
| **PIN digit enforcement** | `fixedPin` (Bluetooth) | Leading-zero removal; `.prefix(6)` truncation; `shortPin` flag triggers red error text |
| **Minimum value clamp on load** | `nodeInfoBroadcastSecs` | Stored value < 10 800 s is clamped to 10 800 s when the view initialises |
| **Conditional field visibility** | Many screens | Fields hidden/disabled based on device capabilities (`hasWifi`, `hasEthernet`), firmware version (`checkIsVersionSupported`), device role, or toggle state |
| **Mutual exclusion** | `rotary1Enabled` / `updown1Enabled` | Each disables the other's toggle |
| **Confirmation dialog on change** | Device Config `deviceRole`, Position Config `fixedPosition` | Destructive/special roles require user confirmation |
| **Dirty-tracking (`hasChanges`)** | All screens | `@State var hasChanges = false` set via `.onChange` comparing new value against `node?.config?.field ?? -1`; controls `SaveConfigButton` enabled state |
---
## String Field Byte Limits at a Glance
| Field | Screen | Max UTF-8 Bytes |
|---|---|---|
| `wifiSsid` | Network Config | 32 |
| `wifiPsk` | Network Config | 63 |
| `tzdef` | Device Config | 63 |
| `address` (MQTT) | MQTT Config | 62 |
| `username` (MQTT) | MQTT Config | 62 |
| `root` (MQTT topic) | MQTT Config | 30 |
| `password` (MQTT) | MQTT Config | 30 |
| `messages` (canned) | Canned Messages Config | 198 |
| `name` (detection sensor) | Detection Sensor Config | 20 |
| `ringtone` (RTTTL) | RTTTL Config | 228 |