kodeOS internals

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

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:

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.

KeyOwnerValuesDefaultWhat it does
brightAO_DisplayInteger 5–100100Panel 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
logAO_Logd, i, n, w, e, offThe build's level, CONFIG_KLOG_DEFAULT_LEVEL, infoThe 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
volAO_AudioInteger 0–100100Speaker level in per cent. Applied at once; the codec gets it at the next open
mic_dbAO_AudioInteger 0–42, multiples of 624Microphone gain in dB. A value off the 6 dB grid is refused and nothing is stored
muteAO_Audioon, offoffThe DAC mute of the ES8311
mag_calAO_ImuA quoted calibration, "ox oy oz sx sy sz", or ""NoneThe 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
hapticAO_HapticInteger 0–100100A 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.

KeyOwnerValuesDefaultWhat it does
wifiAO_Radioon, offoffWhether 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
bleAO_Radioon, offonWhether Bluetooth is wanted: on advertises from boot. Default is CONFIG_KODEOS_BLE_DEFAULT_ON, on. Needs CONFIG_KODEOS_BLE
wifi_joinAO_Radioon, offThe build's CONFIG_KODEOS_WIFI_AUTOJOIN, onWhether 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_psAO_Radioon, offonWhether the Wi-Fi station keeps its power save. Needs CONFIG_KODEOS_WIFI
chanAO_Neton, offonWhether the board joins the Kode channel (internals/kode-channel) whenever it has Wi-Fi. Off leaves it at once. Needs CONFIG_KODEOS_CHANNEL
chan_keepAO_Net1h, 6h, 1d, 3d, 7d, never1dHow 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).

KeyOwnerValuesDefaultWhat it does
sleep_afterAO_LowPower30s, 1m, 2m, 5m, 10m, never2mThe 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_afterAO_LowPower1h, 6h, 24h, 3d, 7d, never1hThe time spent ASLEEP after which the board goes on to OFF. Needs CONFIG_KODE_LOWPOWER

Settings: power

The LED and the charger.

KeyOwnerValuesDefaultWhat it does
led_activityAO_Poweron, offonWhether the LED pulses blue while a flash write runs
chargeAO_Poweron, offonWhether the cell charges. AO_Power writes it over the boot policy of the charger at every boot and after pmic_reset
charge_maAO_PowerInteger 64–896512Fast-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.

KeyOwnerValuesDefaultWhat it does
zoneAO_BootA 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.

KeyOwnerValuesDefaultWhat it does
lockkgui30s, 1m, 2m, 5m, 10m, never1mThe auto-lock time on the launcher and the settings screen
scenekguiA quoted scene name, or "auto"autoThe 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
clockkgui24h, 12h24hThe face's hour format, with no AM or PM
kody_sleepkguilock, 30s, 1m, 5m, 10m, neverlockHow 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.

KeyOwnerValuesDefaultWhat it does
haptic_uikguioff, light, normallightHow 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_awakekguion, offoffon 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.

KeyStored as
bright, vol, mic_db, hapticu8
wifi, ble, wifi_join, wifi_ps, chan, mute, led_activity, chargeu8 as a boolean
chan_keepA 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_afterA blob of one uint32_t, in seconds. 0 is never. Absent means 120 s and 3600 s
logu8, a kode_log_lvl_t: 0 debug, 1 info, 2 notice, 3 warning, 4 error, 5 off. A byte above 5 is ignored
charge_maA blob of one uint16_t, in mA
zone, scene, mag_calA string
lock, kody_sleepA blob of one uint32_t, in seconds. 0 is never. A missing or unreadable lock means 60 s
clocku8, 1 for 12 h
haptic_ui, usb_awakeu8. 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 keyOwnerWhat it is
kgui/lastAO_GuiThe row the launcher launched last, where the cursor starts after a restart
kgui/recents, kgui/scene_hr, kgui/blename, kgui/volvibAO_GuiThe 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/approxkdrv_clocksThe wall clock, kept every 60 s by AO_Power and before a switch-off, and the mark that it is approximate
kset/lp_lastAO_LowPowerThe 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/mqttAO_SchedulerOne 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/c5mackhal_identityThe C5's MAC, six bytes, so the board is named before the link is up
kode/app_catsAO_AppMgrThe category overrides of installed apps
kapp/<key>Apps and operatorsA 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 storeNimBLEBluetooth 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

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.

# 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