# Settings

Every setting a Kode Dot stores across restarts, which Active Object owns it, its values and default, and how the console verb config reads and changes it.

## What it is

kodeOS keeps some settings across a restart and a power cycle: the brightness, whether Wi-Fi and Bluetooth are wanted, the volume, the time zone and a few more. They all live in the `nvs` partition, the flash store ESP-IDF uses for small values. You read and change every one of them with one console verb, `config`.

**Each setting is read and written by the one Active Object that applies it**, and no other code touches it. So each stored value has one owner and the store needs no lock. The console never touches the store either. It sends a request for one key at a time, and the owner reads, applies, stores and answers.

Nothing in the store is a secret. Wi-Fi passwords and Bluetooth bonds live on the ESP32-C5. Every value here sits in plain flash, and any console and any app on the board can read it.

## How it is connected

```mermaid
sequenceDiagram
  autonumber
  participant Host as kcli.py
  box "On the Kode Dot"
    participant Con as AO_Console
    participant Own as owner AO
    participant Nvs as khal_nvs
  end
  Host->>Con: config key="bright" value=40
  Con->>Own: CONFIG_REQ_SIG (key, value)
  Own->>Own: apply
  Own->>Nvs: store
  Own->>Con: kode_config_answer()
  Con->>Host: >|config key="bright" value=40 stored=yes
```

*How config reaches a setting*

Each setting is a row of type `kode_config_t` in a registry (`kode_config.h`). Each Active Object registers the rows it owns as it starts, and kgui registers its six when AO_Gui has a queue. A table with one bad row is refused whole, with a log line naming the row.

An app can add rows of its own with `KODE_CONFIG_ADD()` and answer them from its own Active Object. They join the same listing. The registry holds at most `KODE_CONFIG_MAX` = 32 rows in `KODE_CONFIG_TABLES` = 4 tables. A key is lowercase letters, digits and `_`, at most 15 characters.

The console reads and sets in three forms:

- `config` lists every row, one record each.
- `config key="bright"` reads one.
- `config key="bright" value=40` sets one and answers with what the owner kept.

A record reads `>|config key="bright" value=40 stored=yes`. **`stored=no` means the key is not in the store and the value shown is the default.** Setting a value back to its default erases the key for `log`, for `scene` set to `auto`, for `kody_sleep` set to `lock` and for `mag_cal` set to `""`.

A value is typed like a function argument: a string is quoted and every other kind is bare, and the console refuses the wrong form with `>err code=3 msg=value`. A boolean is `on` or `off`. An integer outside its range, a word not in an enum, or a wrong form gets the same refusal. Strings hold at most 32 characters, and an enum lists at most six words (`KODE_VALUE_ENUMS_MAX`).

## Specifications

### The twenty-four settings

A row exists only in a build that has its feature. `config` prints the keys in the order their owners registered them. Each owner registers its table as it starts, so the order is the order of the start: in kodeOS `log`, `zone`, `off_after`, `bright`, `lock`, `scene`, `clock`, `kody_sleep`, `haptic_ui`, `usb_awake`, `wifi`, `ble`, `wifi_join`, `wifi_ps`, `chan`, `chan_keep`, `vol`, `mic_db`, `mute`, `mag_cal`, `haptic`, `led_activity`, `charge`, `charge_ma`. A build with Wi-Fi, Bluetooth, the Kode channel and low power lists 24 rows. An app built with `CONFIG_KODE_LOWPOWER_IDLE` also has `sleep_after`; kodeOS builds without it, because its Rest decides when the board rests. The tables below group them by what they control.

### Settings: display, log, audio and haptic

The panel, the console's log level, the codec, the compass and the motor.

| Key | Owner | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `bright` | AO_Display | Integer 5–100 | 100 | Panel brightness in per cent. 5 is the floor because 0 is black on this AMOLED and a settings screen must not turn off the only thing that can turn it back on. Applied at once and stored |
| `log` | AO_Log | `d`, `i`, `n`, `w`, `e`, `off` | The build's level, `CONFIG_KLOG_DEFAULT_LEVEL`, info | The console's global log level. Stored, so it survives the reset that opening the port causes. Setting the build's level erases the key. `log_level tag="<tag>"` sets one tag and is never stored |
| `vol` | AO_Audio | Integer 0–100 | 100 | Speaker level in per cent. Applied at once; the codec gets it at the next open |
| `mic_db` | AO_Audio | Integer 0–42, multiples of 6 | 24 | Microphone gain in dB. A value off the 6 dB grid is refused and nothing is stored |
| `mute` | AO_Audio | `on`, `off` | `off` | The DAC mute of the ES8311 |
| `mag_cal` | AO_Imu | A quoted calibration, `"ox oy oz sx sy sz"`, or `""` | None | The magnetometer's correction: an offset per axis in mG (up to 9999 either way) and a scale per axis in thousandths (250 to 4000). `""` forgets it. A text that does not read as six such numbers is refused and nothing is stored |
| `haptic` | AO_Haptic | Integer 0–100 | 100 | A scale in per cent on the gain of every buzz an app or the console asks for. The factory test row never uses it |

### Settings: radio and network

Wi-Fi, Bluetooth and the Kode channel. The rest of the board has its own table below.

| Key | Owner | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `wifi` | AO_Radio | `on`, `off` | `off` | Whether Wi-Fi is wanted. On starts the link to the C5 and the automatic join. Default is `CONFIG_KODEOS_WIFI_DEFAULT_ON`, off. Needs `CONFIG_KODEOS_WIFI` |
| `ble` | AO_Radio | `on`, `off` | `on` | Whether Bluetooth is wanted: `on` advertises from boot. Default is `CONFIG_KODEOS_BLE_DEFAULT_ON`, on. Needs `CONFIG_KODEOS_BLE` |
| `wifi_join` | AO_Radio | `on`, `off` | The build's `CONFIG_KODEOS_WIFI_AUTOJOIN`, on | Whether the link, when it comes up for a wanted Wi-Fi, scans and joins a remembered network by itself. A `wifi_join` command that AO_Radio accepts also sets `wifi` on. Needs `CONFIG_KODEOS_WIFI` |
| `wifi_ps` | AO_Radio | `on`, `off` | `on` | Whether the Wi-Fi station keeps its power save. Needs `CONFIG_KODEOS_WIFI` |
| `chan` | AO_Net | `on`, `off` | `on` | Whether the board joins the Kode channel (`internals/kode-channel`) whenever it has Wi-Fi. Off leaves it at once. Needs `CONFIG_KODEOS_CHANNEL` |
| `chan_keep` | AO_Net | `1h`, `6h`, `1d`, `3d`, `7d`, `never` | `1d` | How long the broker keeps the channel's session while the board is away, and so how long the commands sent meanwhile wait for it. `never` asks the broker for no expiry. A set is stored at once and used at the next connect, a renewal included. The channel is not reconnected for it. Needs `CONFIG_KODEOS_CHANNEL` |

### Settings: the rest

When the board rests (`sdk/kaos/lowpower`).

| Key | Owner | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `sleep_after` | AO_LowPower | `30s`, `1m`, `2m`, `5m`, `10m`, `never` | `2m` | The time without input after which the board goes from AWAKE to ASLEEP. A button, a touch or a console line from any port starts the count over, and `never` counts nothing. It does not depend on `lock` or `kody_sleep`. Needs `CONFIG_KODE_LOWPOWER` and `CONFIG_KODE_LOWPOWER_IDLE`, so kodeOS does not have it |
| `off_after` | AO_LowPower | `1h`, `6h`, `24h`, `3d`, `7d`, `never` | `1h` | The time spent ASLEEP after which the board goes on to OFF. Needs `CONFIG_KODE_LOWPOWER` |

### Settings: power

The LED and the charger.

| Key | Owner | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `led_activity` | AO_Power | `on`, `off` | `on` | Whether the LED pulses blue while a flash write runs |
| `charge` | AO_Power | `on`, `off` | `on` | Whether the cell charges. AO_Power writes it over the boot policy of the charger at every boot and after `pmic_reset` |
| `charge_ma` | AO_Power | Integer 64–896 | 512 | Fast-charge current in mA (`KBSP_CELL_ICHG_MA`, ceiling `KBSP_CELL_ICHG_MAX_MA`). Stored as the chip kept it on its 64 mA grid, so 300 is stored as 320 |

### Settings: the clock

The time zone belongs to AO_Boot, which applies it at boot before an app runs.

| Key | Owner | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `zone` | AO_Boot | A quoted POSIX TZ string, at most 32 characters | `"UTC0"` | The time zone of the face's clock, for example `"CET-1CEST,M3.5.0,M10.5.0/3"`. It also stamps FAT files in local time. The value must read as `std offset [dst [offset] [,start[/time],end[/time]]]`: a name of three or more letters or one in angle brackets, an offset of at most 24 hours, and rules in the `Jn`, `n` or `Mm.w.d` forms. Anything else is refused with `>err code=5 msg=value` and nothing changes |

### Settings: the face

Four of the six rows kgui owns: the clock's format, the lock and Kody.

| Key | Owner | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `lock` | kgui | `30s`, `1m`, `2m`, `5m`, `10m`, `never` | `1m` | The auto-lock time on the launcher and the settings screen |
| `scene` | kgui | A quoted scene name, or `"auto"` | `auto` | The face's scene. `auto` lets the hour draw one. A name the active pack does not have is refused. The official pack in the SDK has `tren`, `parque`, `camping`, `azotea`, `madrugada`, `desayuno`, `taller`, `lluvia`, `playa` and `reloj` |
| `clock` | kgui | `24h`, `12h` | `24h` | The face's hour format, with no AM or PM |
| `kody_sleep` | kgui | `lock`, `30s`, `1m`, `5m`, `10m`, `never` | `lock` | How long on the face without input before it dims to Rest, with Kody standing still (Sleep after, on the Settings list). `lock` follows the auto-lock time and erases the key. It is not `sleep_after`, which is the board's own time before ASLEEP |

### Settings: the interface

The other three rows of kgui. Each applies at once and is stored on every set, even when the value is the stored one. A byte the setting does not have reads as the default, with a warning.

| Key | Owner | Values | Default | What it does |
| --- | --- | --- | --- | --- |
| `haptic_ui` | kgui | `off`, `light`, `normal` | `light` | How much of the interface buzzes. `off` plays nothing, `light` plays the click, the thud, the tick and the mark, and `normal` adds a tick for each move of the selection, at most 14 a second. `haptic` set to 0 silences the interface too |
| `usb_awake` | kgui | `on`, `off` | `off` | `on` keeps the Lock out of Rest while a cable is in |

### How each one is stored

All keys are in the NVS namespace `kset`, except kgui's six (`lock`, `scene`, `clock`, `kody_sleep`, `haptic_ui`, `usb_awake`), which are in `kgui`. Namespaces and keys are at most 15 characters, NVS's own limit.

| Key | Stored as |
| --- | --- |
| `bright`, `vol`, `mic_db`, `haptic` | `u8` |
| `wifi`, `ble`, `wifi_join`, `wifi_ps`, `chan`, `mute`, `led_activity`, `charge` | `u8` as a boolean |
| `chan_keep` | A blob of one `uint32_t`, in seconds. 3600, 21600, 86400, 259200 or 604800, or 0xFFFFFFFF for never. Absent, another size or any other number means 86400 |
| `sleep_after`, `off_after` | A blob of one `uint32_t`, in seconds. 0 is never. Absent means 120 s and 3600 s |
| `log` | `u8`, a `kode_log_lvl_t`: 0 debug, 1 info, 2 notice, 3 warning, 4 error, 5 off. A byte above 5 is ignored |
| `charge_ma` | A blob of one `uint16_t`, in mA |
| `zone`, `scene`, `mag_cal` | A string |
| `lock`, `kody_sleep` | A blob of one `uint32_t`, in seconds. 0 is never. A missing or unreadable `lock` means 60 s |
| `clock` | `u8`, 1 for 12 h |
| `haptic_ui`, `usb_awake` | `u8`. `haptic_ui` is 0, 1 or 2 (off, light, normal); `usb_awake` is 1 for on. Absent means the default |

### Other things in the same partition

These have no row in `config`.

| Namespace and key | Owner | What it is |
| --- | --- | --- |
| `kgui/last` | AO_Gui | The row the launcher launched last, where the cursor starts after a restart |
| `kgui/recents`, `kgui/scene_hr`, `kgui/blename`, `kgui/volvib` | AO_Gui | The apps run last, by project name, latest first; the scene the hour drew and for which hour; the Bluetooth name typed on the Bluetooth page; and whether the volume bar stands on its vibrate stop |
| `kclock/wall`, `kclock/approx` | kdrv_clocks | The wall clock, kept every 60 s by AO_Power and before a switch-off, and the mark that it is approximate |
| `kset/lp_last` | AO_LowPower | The summary of the last rest, written at most once a minute while asleep and once when it ends. `lowpower_info last=on` prints it |
| `kcron/e<slot>`, `kcron/mqtt` | AO_Scheduler | One blob per entry made with `cron`, and one for the time of the built-in `mqtt` entry or its being off. A blob of another length or version is skipped. An app does not read or write them |
| `kident/c5mac` | `khal_identity` | The C5's MAC, six bytes, so the board is named before the link is up |
| `kode/app_cats` | AO_AppMgr | The category overrides of installed apps |
| `kapp/<key>` | Apps and operators | A provisioning store kodeOS never interprets. Key at most 15 characters (`KODE_NVS_KEY_MAX` is 16 with the terminator), value at most 191 characters (`KODE_NVS_VALUE_MAX` is 192 with the terminator), 24 keys in a listing. Reached by `kode_nvs_set()`, `kode_nvs_get()`, `kode_nvs_del()` and the verbs `nvs_set`, `nvs_get`, `nvs_del`, `nvs_list` |
| NimBLE bond store | NimBLE | Bluetooth bonds, in the same partition, outside `khal_nvs` |

The `nvs` partition is 64 KB at `0xB000`. `khal_nvs_init()` erases it and starts again when ESP-IDF reports no free pages or a newer version. A reader returns `KODE_DENIED` for an absent key and for a namespace never written, and that is not an error: the caller applies its default.

What survives a restart: the settings above, the last launched row, the wall clock (marked approximate), the entries of `cron`, the summary of the last rest, the category overrides, the `kapp` keys and the bonds.

What does not: the C5 link, the Wi-Fi connection and address, any MQTT session, the commands that wait in the queue of the Kode channel (RAM, already acknowledged by the broker), the scan tables and the selection in the launcher.

## Gotchas

> **NOTE**
>
> **A setting reads its default after a restart and `stored=no`.** The key was never written, or setting the default erased it. This is not a fault.

> **WARNING**
>
> **The setting you changed came back to what it was.** A sketch uploaded from the Arduino IDE shares the partition, and its `initArduino()` erases an NVS that it finds full or of a version it does not know. Every kodeOS setting is lost and the board starts with defaults. Nothing else is damaged.

> **WARNING**
>
> **A string value is refused with `>err code=3 msg=value`.** Strings are always quoted and every other kind is never quoted: `config key="zone" value="UTC0"` is right and `config key="bright" value="40"` is not.

> **NOTE**
>
> **`config` answers `>err code=4 msg=busy`.** The owner of the key has too little room left in its queue: the console keeps 4 of its slots free for the owner's own work. It happens under a burst of `config` lines, such as a script flipping `wifi` in a loop. Wait for the owner to catch up and send the line again. Nothing was changed.

> **WARNING**
>
> **The face reads UTC after a restart, although a zone was set.** The stored `zone` is not a POSIX TZ string, so AO_Boot ignores it at boot and the log says `zone: UTC0 (none stored)`. Set the zone again: a value the console accepts is one AO_Boot can read.

> **NOTE**
>
> **`config` takes a few seconds to list everything.** It asks each owner in turn, up to one second each, so a full listing can take as long as 24 seconds with a slow owner. `kcli.py` waits up to 40 seconds.

## Example code

List every setting, set the brightness to 40 and read it back. The second command prints a record with `value=40` and `stored=yes`. The last command writes all settings to `settings.json`, and `config-import` writes back only the keys that differ.

```sh
# settings.sh - 2026-10-28 - from Kode with love
#
# Lists every stored setting of a Kode Dot, dims the screen to 40 per cent,
# reads the value back, and saves all settings to a JSON file. The second
# command prints a record with value=40 and stored=yes. Opening the port
# resets the board, so the three config commands share one session.
python3 tools/kcli.py -p /dev/cu.usbmodem101 --settle 5 cmd 'config' 'config key="bright" value=40' 'config key="bright"'
python3 tools/kcli.py -p /dev/cu.usbmodem101 --settle 5 config-export -o settings.json
```

**TBD**: this example has not been run on a Kode Dot yet.

## In the SDK

- [`components/kaos/core/src/kset.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/src/kset.h): the names of the `kset` keys and their limits.
- [`components/kaos/core/include/kode_config.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/include/kode_config.h): the registry and `KODE_CONFIG_ADD()`.
- [`components/kgui/kgui_ao.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/kgui_ao.c): the six `kgui` rows and their owner.
