# Command set

Every verb of the kodeOS console, grouped by the Active Object that owns it, with its arguments, its answer and its limits.

## What it is

This page lists every command of the console of a Kode Dot: its arguments, what it answers and how long the console waits for it. The grammar of a line and the shape of each answer are on `internals/console-protocol`. Commands that an app adds are on `internals/variables-and-functions`.

The board has one list of commands, the array `CMDS[]` near the end of `console_ao.c`. The parser reads it, `help` prints it and this page follows it. If this page and `help` ever differ, believe `help`: it prints the verbs of your build, and a Kconfig option can remove a group. The groups below follow the parts of the board. The order of `help` is the order of `CMDS[]`.

Try these first:

- `help` prints every verb with its arguments.
- `ping` answers `pong`, the liveness check.
- `info` says what the board is and what runs on it. `ps` lists the Active Objects and the memory.
- `led_rgb rgb=0,0,200` sets the LED to blue.
- `bat_info` shows the battery. `ls path="/"` lists the files. `screenshot` takes a picture of the glass.
- `config` lists every stored setting.

## How it is connected

```mermaid
flowchart LR
  host["Host line<br/>cable or Bluetooth"]
  subgraph board["On the Kode Dot"]
    con["AO_Console<br/>CMDS[]"]
    own["Owning AO"]
    dev["kdrv driver"]
    con -- "publish or post" --> own
    own --> dev
    own -. "answer event" .-> con
  end
  host -- "command" --> con
  con -- "records, >ok" --> host
  class host ext
  class con,own ao
```

*Where a verb goes*

AO_Console parses and validates. It owns no device. A handler posts a request to the Active Object that owns the device (`led_*` to AO_LedMgr, `app_*` to AO_AppMgr, `wifi_*` to AO_Radio) and the owner answers by event.

A line comes from one of four sources: the cable (and the charging base), Bluetooth, the Kode channel and the scheduler. A line from the channel waits in a queue when the console is busy and is not answered `busy` (`internals/kode-channel`). A line from the scheduler is a cron entry running on its own account, and its replies go to the log. `sdk/kaos/console` has the policy of the sources.

A few bench verbs have no owner to ask and run inside the AO_Console step: `i2c_scan`, `clocks`, `temp`, `display_info`, `time`, `exp`, `exp_help`, `pwd`, the `nvs_*` verbs and the `_help` tables of the chips.

A Kconfig option can remove a verb. This table lists which option guards which verbs:

| Option | Verbs it guards |
| --- | --- |
| `KODE_LOWPOWER` | `sleep`, `power_off` and `lowpower_info`. In a build without it `sleep` and `power_off` answer `>err code=4 msg=not_built`, and `lowpower_info` does not exist |
| `KODE_SCHEDULER` | `cron`. In a build without it every form answers `>err code=4 msg=not_built` |
| `KODEOS_MIRROR` | The `mirror_*` verbs |
| `KODEOS_RADIO` | The radio, identity and `exp_power` verbs |
| `KODEOS_WIFI` | The `wifi_*` verbs |
| `KODEOS_BLE` | The `ble_*` verbs |
| `KODEOS_NET` | The network verbs |
| `KODEOS_MQTT` | The MQTT verbs and `channel_info` |
| `KODEOS_SCREENSHOT` | The capture |
| `KODE_TEST_DOT` | `test component=all` and the `test_dot_*` verbs, the E5 final test. In a build without it `test component=all` answers `>err code=5 msg=not_built` and no `test_dot_*` verb exists |

See `internals/console-protocol` for the in-flight rule and the watchdog classes.

These files hold the console:

| File | What it holds |
| --- | --- |
| `kaos/console/src/console_ao.c` | `CMDS[]`, the key tables `A_*`, every handler and the replies of the owners |
| `kaos/console/src/console_fs.c` and `console_fs.h` | The working directory and the path commands: `ls`, `cd`, `stat`, `cat`, `mkdir`, `rm`, `rmdir`, `mv`, `cp`, `write`, `file_put` |
| `kaos/console/src/console_grammar.c` | Tokenizer and quoting |
| `kaos/autotest/src/autotest_ao.c` | The table `TESTS[]` that `test` walks |

## Specifications

Argument cells write the key, its type and whether it is required. Optional keys show their default after `=`. An enum lists its words after "one of". A record cell names the keys of each `>|` record without the prefix.

### System and liveness

No Active Object owns these verbs. They answer from the AO_Console task.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `info` | none | Records `serial` (with `product` and `board`), `mac_p4` (carries the C5's MAC), one `slot` per kodeOS partition, `coredump`, `c5` (when the radio is built in), `log` (with `source` stored or default) and `temp`, then `>ok`. A board that never met its C5 answers `-` for serial and MAC |
| `help` | none | One `cmd` record per verb with `args`, then `>ok`. Sliced |
| `ping` | none | The record `pong`, then `>ok` |
| `ps` | none | `prio ao owns` per Active Object and task, `worker name stack free` per worker, `res owner` per ownership claim, `exp` when a header position is in use, then `heap int_free int_max int_low alloc_fail log_dropped rx_dropped`. Sliced |
| `tasks` | none | `task prio stack_free` per live task, then `tasks shown`. The snapshot holds 32 tasks and the list 28, so a board with more than 32 shows none (`shown=0`) |
| `hcf` | none | None. Fires `Q_ERROR()`, the board resets and leaves a core dump |
| `i2c_scan` | none | `addr` for each of the addresses 0x08 to 0x77 that answers, then `found`. Probes 8 addresses per step, 10 ms each |

What the records say:

- `slot` carries `state` as running, inactive, empty or absent.
- `coredump` reads none, or present with `owner` kodeos or other, and the task and PC when known.
- `recog` in the `c5` record says what the P4 recognises on the C5: kode, stock, old or nolink.
- In `ps`, `int_low` is the lowest the internal pool has been since boot and the number to judge a build by.
- `alloc_fail` counts refused allocations, `log_dropped` counts console lines the event pool could not carry and `rx_dropped` counts bytes lost on input.

### LED

Owner: AO_LedMgr. Each verb publishes one event and answers `>ok` at once.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `led_on`, `led_off` | none | `>ok` |
| `led_toggle` | none | `>ok` |
| `led_blink` | `period` uint 0 to 65535 ms, required | `>ok`. Also turns the heartbeat on |
| `led_hb` | `state` bool, required | `>ok`. Heartbeat on or off |
| `led_rgb` | `rgb` rgb, required | `>ok` |

### Storage media

Owner: AO_Storage. The `sd_*` and `fs_*` verbs differ only in the medium: the microSD card or the internal FAT partition.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `sd_mount`, `fs_mount` | none | `>ok`, an accept. The outcome is in `!` lines |
| `sd_unmount`, `fs_unmount` | none | `>ok`, an accept |
| `sd_ls`, `fs_ls` | none | `name size medium` per file at the top of the medium, then `>ok` |
| `sd_info`, `fs_info` | none | `medium mounted`, plus `total_kb free_kb` when readable, then `>ok` |

### The file tree

Owner: AO_Storage, through `console_fs.c`. These verbs take a path, absolute or relative to one working directory that AO_Console keeps (it starts at `/`, moves only on `cd` and is lost at reset). `/` exists only in the console and has two children, `sdcard` and `storage`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `ls` | `path` string, optional | `name type size medium` per entry, then `>ok` |
| `cd` | `path` string, required | `>ok`. Refuses a file with `notdir` |
| `pwd` | none | `path`, then `>ok` |
| `stat` | `path` string, required | `path type size medium`, then `>ok` |
| `cat` | `path` string, `max` uint 1 to 32768 default 4096 | `line` per line of the file, then `>ok`. Refuses a directory with `isdir`. Watchdog 30 s once the stat confirms a file |
| `mkdir` | `path` string, required | `>ok`. One level only: `noparent`, `exists`, `notdir`, `iofail` |
| `rm` | `path` string, required | `>ok`. Files only, `isdir` otherwise |
| `rmdir` | `path` string, required | `>ok`. Empty directories only, `notempty` otherwise |
| `mv` | `src` string, `dst` string, both required | `>ok`. One volume only: `path` (code 3) across two, `src` for a typo |
| `cp` | `src` string, `dst` string, both required | `>ok` when the whole copy is written. Crosses volumes. Watchdog 60 s |
| `write` | `path` string, `append` bool default off, `line` uint 0 to 65535 default 0, `text` text, `path` and `text` required | `>ok`. See below |
| `file_put` | `path` string, `size` uint, `crc32` uint, all required | The record `ready`, the payload flow, then `>ok` |

How `write` behaves:

- `line=0` writes the whole file, truncating it or appending with `append=on`.
- `line=<n>` replaces line `n` through a temporary file and three renames. It answers `noline` (code 3) for a line the file lacks and `toolarge` when the file or the result passes 32 KB.
- `append=on` with a non-zero `line` answers `appendline` at once.
- `text=""` writes an empty file.
- A `\n` inside the quotes writes a newline.

Missing volumes answer `volume`. `file_put` answers code 3 `msg=path` for a path that names no volume and runs on the cable, the base and Bluetooth.

### Files and payloads

Owners: AO_Storage for the files, AO_Display for the capture. The bulk answers are base64 `d` records, described on `internals/console-protocol`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `file_get` | `name` string, required | The record `file name size medium`, base64 `d` records, then `>ok`. Searches the card first, then the internal partition. Not over the channel |
| `file_desc` | `name` string, required | `name project category display` from the first bytes of a `.bin`, then `>ok`. A file with no manifest has category other |
| `screenshot` | `dst` one of console, file, http default console. `name` string default shot.bmp. `token` string, `fps` uint 1 to 10, `secs` uint 1 to 300, `stop` bool, all for `dst=http` only | Console: `shot w h fmt len`, `d` records, `>ok`, refused over the channel. File: `shot name bytes medium`, `>ok`. Http: see below |

`screenshot` freezes the shadow copy of the glass in AO_Display and takes the rectangles the panel received, so it captures LVGL and raw pixel writers alike. A black screen is not an error and logs a warning. The enum word `http` exists in a build with `KODEOS_SHOT_HTTP`.

`dst=http` uploads a JPEG over the Kode channel. `token` is required (1 to 128 characters of letters, digits, `_` and `-`) and refused with another destination.

- With none of `fps`, `secs`, the answer is `screenshot dst=http bytes id seq`.
- With either, a stream starts and the answer is `screenshot dst=http mode=stream fps secs` at once, with defaults of 2 frames a second and 60 s.
- `stop=on` ends a stream and takes no other key.

The refusals are in `internals/kode-channel`.

### Apps

Owner: AO_AppMgr. See `internals/apps-region` and `internals/boot-model`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `app_list` | none | `name version size_kb category display` per installed app, then `>ok`. Read in pages |
| `app_run` | `name` string, required | `>ok`, an accept, and the board resets. A `.bin` name is flashed and then run |
| `app_erase` | `name` string, required | `>ok`, an accept |
| `app_cat` | `name` string, `set` string, `clear` bool | With no keys, `cat project cats` per stored override. With `name`, exactly one of `set` or `clear=on` |
| `install` | `src` string, `dst` one of os, app, c5, pending, `run` bool default off, `size` uint, `crc32` uint, `name` string | See below |
| `appdata_ls` | none | `project medium json icon app` per project, then `>ok`. 1 s per row, up to 48 projects |
| `appdata_info` | `name` string, required | `project medium`, then `field src value` per field, then `>ok` |
| `appdata_rm` | `name` string, required | `>ok` after removing the internal folder. `missing`, `busy` or `notempty` otherwise. Never touches the card |

`install` takes its image from `src`: `fs:` or `sd:` (the prefix is removed and the name is searched on the card first), `http://`, `https://`, `pending` (the mailbox) or `terminal`. `terminal` streams the image down the port the command arrived on and needs `size` and `crc32`; every other source refuses them with code 3. The destinations:

- `os` writes the inactive kodeOS slot, switches to it and resets. The new image has to confirm itself or the board returns to the old one.
- `app` writes the apps region. It runs the app only with `run=on`. `run` is refused with any other destination. From the terminal, `name` is the image's project name: the region makes room before the image arrives, and with `name` the old copy of that app goes first instead of the oldest app. Each app the install evicts prints `>|evicted name="<project>" kept=off` before `>|ready`. `name` is refused with code 3 outside `src="terminal" dst=app`.
- `c5` updates the ESP32-C5 firmware. It runs from the cable and the base only.
- `pending` parks the image undecoded in the mailbox. `src="pending" dst=pending` is refused.

From a medium `>ok` is an accept. From the terminal it is a result. `kcli.py install` and `kble.py install` run the exchange for you.

`app_run`, `install dst=os` and `hcf` reset the board.

### The app's variables, functions and settings

Four verbs and `config` exist in every build with a console. `internals/variables-and-functions` explains them.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `variable_list` | none | `variable watch desc` per row, then `>ok` |
| `function_list` | none | `function arg desc` per row, with `min max` or `enum` where they apply, then `>ok` |
| `variable` | `name` string, required. `watch` uint 100 to 60000 ms, or 0 | `variable value`, then `>ok`. With `watch`, `>ok` first and then a `variable` record each period. 0 stops it. 4 watches at once, else `slots` |
| `function` | `name` string, required. `arg` row | `>ok`. A row with `answer_ms` sends records first and then `>ok` or `>err` |
| `config` | `key` string, `value` row | See settings below |

Refusals of `variable` and `function`:

- Unknown row, `value=` or `watch=` on a function, and `arg=` on a variable are code 3.
- A watch period from 1 to 99 is code 3 with `msg=watch`.
- An `arg` in the wrong form, out of range or refused by the row is code 3 with `msg=arg`.
- A getter or `run` that fails is code 5 with the row name.

### Settings

`config` with no keys lists every setting in registration order. With `key` it reads one. With `key` and `value` it sets one, and the answer is the record with the new value. A string setting is quoted and every other kind is bare. `stored=yes` means the key is in NVS and `stored=no` means the owner's default applies. The console never touches NVS: it sends a request per key and the owner answers within 1 s. The first request for a key is published; once the owner has answered, the console posts to it directly and keeps 4 slots of its queue free.

| Key | Owner | Type and values | Default |
| --- | --- | --- | --- |
| `bright` | AO_Display | int 5 to 100 | 100 |
| `wifi`, `ble` | AO_Radio | bool. The wish that NVS stores, not the link state | The build default |
| `log` | AO_Log | one of d, i, n, w, e, off | The build level |
| `vol` | AO_Audio | int 0 to 100 | 100 |
| `mic_db` | AO_Audio | int 0 to 42 in steps of 6 | 24 |
| `mute` | AO_Audio | bool | off |
| `mag_cal` | AO_Imu | string, the magnetometer's calibration `"ox oy oz sx sy sz"` (offsets in mG, scales in thousandths), `""` to forget it | none |
| `haptic` | AO_Haptic | int 0 to 100, a scale on every buzz an app or `haptic_play` asks for | 100 |
| `led_activity` | AO_Power | bool, the blue pulse while a flash writes | on |
| `charge` | AO_Power | bool | on |
| `charge_ma` | AO_Power | int 64 to 896, read back on the chip's 64 mA grid | 512 |
| `wifi_join` | AO_Radio | bool, join a remembered network when the link comes up | The build default |
| `wifi_ps` | AO_Radio | bool, Wi-Fi power save | on |
| `chan` | AO_Net | bool, join the Kode channel | on |
| `chan_keep` | AO_Net | one of 1h, 6h, 1d, 3d, 7d, never | 1d |
| `sleep_after` | AO_LowPower | one of 30s, 1m, 2m, 5m, 10m, never | 2m |
| `off_after` | AO_LowPower | one of 1h, 6h, 24h, 3d, 7d, never | 1h |
| `zone` | AO_Boot | string, a valid POSIX TZ string of at most 32 characters | `"UTC0"` |
| `lock` | AO_Gui | one of 30s, 1m, 2m, 5m, 10m, never | 1m |
| `scene` | AO_Gui | string, a scene name or `auto` | `"auto"` |
| `clock` | AO_Gui | one of 24h, 12h | 24h |
| `kody_sleep` | AO_Gui | one of lock, 30s, 1m, 5m, 10m, never | lock |
| `haptic_ui` | AO_Gui | one of off, light, normal | light |
| `usb_awake` | AO_Gui | bool, a cable keeps the Lock out of Rest | off |

`wifi_join`, `wifi_ps`, `chan` and `chan_keep` exist only in a build with Wi-Fi or the channel, `off_after` only with `KODE_LOWPOWER`, `sleep_after` only with `KODE_LOWPOWER_IDLE` as well, and `wifi` and `ble` only with their radio service. `mag_cal` exists with the IMU's owner. kodeOS builds without `KODE_LOWPOWER_IDLE` and lists 24 rows.

The refusals:

- Unknown key: code 3 `msg=key`.
- `value` without `key`: code 2.
- A wrong form or range: code 3 `msg=value`.
- A value the owner refuses: code 5 `msg=value`. A `zone` that is not a POSIX TZ string is one of these.
- An owner whose queue has fewer than 4 free slots: code 4 `msg=busy`. Nothing is sent and nothing changes.
- An owner that does not answer in 1 s: code 6.

In a listing, a silent owner gives `value=- stored=-` and the listing goes on. An app adds rows with `KODE_CONFIG_ADD()`, up to 32 rows in 4 tables.

Rules worth knowing:

- `ble=on` means advertise from boot.
- `charge` and `charge_ma` replace the boot policy of the charger.
- `chan=off` leaves the channel at once.
- `chan_keep` is how long the broker keeps the channel's session while the board is away, so how long the commands sent meanwhile wait. A set is stored at once and used at the next connect, a renewal included. `channel_info` shows the live age as `keep=`.
- `sleep_after` is the time without input before the board goes from AWAKE to ASLEEP, and `off_after` the time ASLEEP before OFF. A button, a touch or a console line from any port restarts the count, and `never` counts nothing. `sleep_after` does not depend on `lock` or `kody_sleep`.
- `scene=auto` erases the chosen scene.
- `kody_sleep=lock` erases its key.

### Buttons, power and display

| Verb | Arguments | Answer |
| --- | --- | --- |
| `btn_read` | none | `btn pressed` per button, then `>ok`. Owner AO_Buttons. Code 5 if the expander does not answer |
| `bat_info` | none | `vbat_mv vbus_mv vbus boost current_ma soc charging fault`, then `>ok`. With no battery it times out with code 6 |
| `sleep` | none | `>ok` when the board is ASLEEP, or `>err code=4 msg="<who>: <why>"` |
| `power_off` | none | `>ok` when the board is going OFF, or `>err code=4 msg="<who>: <why>"` |
| `ship_mode` | none | `>ok`. Ship mode. On USB the board cannot switch off |
| `lowpower_info` | `last` bool, optional | A `lowpower` record and a `lowpower_stats` record, then `>ok`. With `last=on`, a `lowpower_last` record and one `lp_sample` per sample, then `>ok` |
| `boost` | `state` bool, required | `op=boost boost=on` or off, then `>ok`. Refused with code 3 when USB is present, no reading exists, or the cell is low |
| `pmic_reset` | none | `>ok` once `REG_RST` has cleared and the boot policy is written again |

The brightness is `config key="bright"`. The charger is `charge` and `charge_ma`. `charge_limit` is not a verb.

- **`sleep`** takes the board to ASLEEP: light sleep with the radios kept up and the time on the dimmed glass. AO_LowPower asks every owner to put its device at rest, and `>ok` comes when the state has changed. A refusal is quoted: `cable: external power` while VBUS is present, or the name of the owner that is busy, for example `storage: file write`. A busy or silent owner is not final. The request is begun again every second for 10 s and the verb is answered once, at the end. A cable is refused at once. The verb works from every port, and a line on Bluetooth or the Kode channel wakes a board that sleeps. The console waits up to 17 s for the answer.
- **`power_off`** takes the board to OFF: deep sleep with the C5 held in reset. `>ok` comes when the state has changed, a moment before the P4 stops. It is refused for the same reasons as `sleep`. Only the bottom button wakes the board, and the wake is a boot.
- **`ship_mode`** puts the board into ship mode and is the only door to it: no app and no other verb reaches it. On battery nothing could answer afterwards. To switch the board on again, press the bottom button, which is wired to the QON pin of the charger chip. On USB the board cannot switch off.
- **`lowpower_info`** prints `>|lowpower state=awake|asleep|off for_s= cause= sleep_after= off_after= wakes_bot= wakes_ble= wakes_mqtt= wakes_timer= last_busy=` and `>|lowpower_stats slept_pct= sleeps= wakes_c5= wakes_tx= tx_sta= tx_serial= tx_hci= knocks_max= deaf=`. The cause is `request`, `idle`, `off_after`, `bot`, `ble`, `mqtt`, `cable`, `timer` or `boot`. The two times are seconds and 0 is never. `wakes_bot`, `wakes_ble` and `wakes_mqtt` count the wakes from ASLEEP by the bottom button, a Bluetooth line and the Kode channel, and `wakes_timer` the wakes for an owner's time (the radio wakes, in which the Kode channel works with the glass dark). `last_busy` is the last refusal, quoted, or `-`. The `tx_` fields are `-` until a wake has been noted. `slept_pct` is the share of the time since boot spent in light sleep.
- **`lowpower_info last=on`** prints the summary the board keeps of its last rest, so a run on battery can be read back on the cable: `>|lowpower_last start= for_s= slept_pct= sleeps= wakes_c5= wakes_tx= deaf= end= samples=` (`start` in Unix seconds, `-` when the clock was not set; `end` is a cause word, or `asleep` when nothing ended it), then one `>|lp_sample t= ma= mv=` per minute, oldest first: the seconds from the start, the cell current in mA (negative out of the cell) and the cell voltage in mV. It goes out in slices like `help`. A board with no summary prints `>|lowpower_last present=no`.

### Scheduled commands

Owner: AO_Scheduler (`sdk/kaos/scheduler`). `cron` keeps the table of entries. An entry is a name, a time in the five fields of cron and a console line to run at that time. It exists with `KODE_SCHEDULER`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `cron` | `name` string, `when` row, `cmd` string, all optional | One `cron` record per entry (`name when cmd owner next stored`), then `>ok`; or one of the refusals below |

`when` is a string like every other, with one bare word: an expression is quoted and `off` is bare. A quoted `"off"` and a bare expression are both refused, so each has one way to be typed.

```text
cron                                          list every entry, mqtt first
cron name="riego"                             show one
cron name="riego" when="0 7 * * *" cmd="ping" add an entry, or change it
cron name="riego" when=off                    remove it
cron name="mqtt" when="*/5 * * * *"           change the built-in entry
cron name="mqtt" when=off                     switch the safety net off
```

- **The records** come in the order of the table, `mqtt` first, then `>ok`. A set answers with the entry as it now is, then `>ok`. A removal, and `mqtt` switched off, answer `>ok` alone.
- **`owner`** is `console`, `code` or `builtin`. `next` is the next minute as Unix seconds in UTC, or `-` when the clock is not set or the entry is off. `cmd` is `-` for an entry with no line (an entry of code, and `mqtt`). `stored` says whether the entry survives a reset. A switched-off `mqtt` lists with `when="off"` and `next=-`, and a time given to it switches it on again.
- **The time** is the UTC calendar of the epoch clock in five fields: minute, hour, day of month, month, day of week. Each takes `*`, numbers, lists (`1,15`), ranges (`9-17`) and steps (`*/5`, `9-17/2`). The least period is one minute.
- **The line** runs with the source SCHED when the minute comes. It must start with a verb this build has, other than `cron`, or the entry is refused when it is made. The payload verbs are accepted then and refused only when they run (`>err code=5 msg=sched` in the log). If the console is in the middle of another command at that minute, AO_Scheduler gives the line again every second, for up to 10 s from the start of the minute's work; past that it drops the line with a warning, `the entry '<name>' did not run: the work of its minute ran out of time`, and the entry runs again at its next time. Its replies go to the log as notices, `sched: <reply>`. `config key="log" value=w` hides them. A record whose line does not fit one console line with the other fields is cut, after a `cmd: cut to fit one line` warning. The entry keeps the whole line.
- **From the Kode channel** `cron` is accepted like every other verb, and the entry it leaves is stored (`internals/kode-channel`).
- **`mqtt`** takes no `cmd`. Its work is the look at the sessions of the Kode channel in a radio wake.

The console refuses before it posts anything:

| Typed | Answer |
| --- | --- |
| `when=` bare and not `off`, or `when="off"` quoted | `>err code=3 msg=when` |
| `when=off` with a `cmd=` | `>err code=3 msg=cmd` |
| `cmd=` that does not start with a verb of this build, or starts with `cron` | `>err code=3 msg=cmd` |
| `cmd=` without `when=` | `>err code=2 msg=when` |
| `when=` without `name=` | `>err code=2 msg=name` |
| `cron` run by a SCHED line | `>err code=5 msg=sched` |
| The request could not be posted | `>err code=5 msg=cron` |
| AO_Scheduler never answered in 5 s | `>err code=6 msg=cron` |

AO_Scheduler refuses with one of these reasons, and the console turns it into the answer:

| Reason | Answer | Meaning |
| --- | --- | --- |
| `when`, `fields`, `range`, `step`, `syntax`, `never` | `>err code=3 msg=when` | The expression is not five fields, a value is out of range, a step is bad, the text is not cron, or no minute in four years matches. A notice says `cron: the expression is refused: <reason>` |
| `name`, `unknown` | `>err code=3 msg=name` | The name is empty, over 15 characters, or has a character outside `A-Z a-z 0-9 _ - .`; or no such entry exists |
| `cmd` | `>err code=3 msg=cmd` | The line is empty, too long, or given to `mqtt` |
| `full` | `>err code=4 msg=full` | The table holds 16 entries, `mqtt` included |
| `owner` | `>err code=5 msg=owner` | The name belongs to an entry of code |
| `partial` | `>err code=5 msg=partial` | A list lost an entry because the console's queue was full. The records that arrived come first |
| `memory` | `>err code=5 msg=memory` | The table could not be taken from PSRAM |
| any other | `>err code=5 msg=cron` | |

Removing a name that no console entry holds answers `msg=name`.

### Chip fields

Five chips share one shape. `pmic` and `gauge` (AO_Power), `ioexp` (AO_IoExp), `tp` (AO_Touch) and `sd` (AO_Storage) read and write the fields of one chip. `audio`, `haptic` and `c5` have the same shape in their own sections.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `pmic`, `gauge`, `ioexp`, `tp`, `sd`, `audio`, `haptic`, `c5` | `field` string, `value` uint 0 to 0x7FFFFFFF | No keys: a dump in `!` lines, then `>ok`. `field`: the record `chip field value`. Both: writes, and the record carries what the chip kept |
| `pmic_help`, `gauge_help`, `ioexp_help`, `tp_help`, `sd_help`, `audio_help`, `haptic_help`, `c5_help` | none | `field unit min max rw desc` per field, then `>ok`. No board is touched. Sliced |
| `sd_verify` | `confirm` bool, required | `step of sd` for probe and format, then `verify=sd result`, then `>ok`. **Erases the card.** Watchdog 46 s |

An unknown field is code 3 `msg=field`. `value` without `field` is code 2. A read-only field, or a value the field cannot hold, is code 3 with the field name. A chip that does not answer is code 5. Writes are read back, so `ichg_ma` set to 500 answers 512.

Notes per chip:

- `pmic` refuses `batfet_dis` (ship mode is `ship_mode`) and `reg_rst` (use `pmic_reset`).
- `gauge` values are unsigned.
- `ioexp` writes to `c7` or `o7` change the supply of the panel.
- `tp` with `dis_auto_sleep` set to 0 puts the controller to sleep until a finger touches the glass or the board resets.
- `sd` fields from the card are read-only. `width_req` (1 or 4) and `freq_req_khz` (400 to 50000) set the next mount.
- `c5` has no writable row and answers from the snapshot of the last link start.
- `audio` reads the register back, since `mic_db` rounds.

The field tables are on the pages of the drivers, for example `sdk/kdrv/bq25896`.

### Clocks, time and temperature

No owner. These answer immediately.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `clocks` | none | `clk=x1`, `slow_mux`, `x3` (state ok with `millihz ppm`, or silent), `x1_from_x3`, `rc_slow`, `cpu`, `apb`, `rtc_slow`, `rtc_fast`, then `>ok`. Blocks the console for about 92 ms |
| `temp` | none | `temp=p4 c=<degrees>`, one decimal. `c=-` and code 5 when the sensor is silent |
| `display_info` | none | `display frames underruns dpi ack phy other st0 st1`. Counters of the DSI link since the panel came up. `underruns` are frames PSRAM could not feed in time |
| `time` | `set` string, `sync` bool | No keys: `time state utc source`, state known, approx or unset, source rtc, set, sntp or saved. `set="YYYY-MM-DDThh:mm:ssZ"` sets UTC and refuses anything else and any date before 2026. `sync=on` asks a time server and answers "asked", not "got" |

### Audio

Owner: AO_Audio.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `audio_tone` | `freq` uint 400 to 3400 default 1020, `ms` uint 10 to 2000 default 300, `amp` uint 1 to 50 default 25 | `tone ms amp`, then `>ok` at once. The frequency printed is the one snapped to a detector bin |
| `audio_rec` | `ms` uint 100 to 30000 default 1000, `file` string, required | `rec ms bytes`, then `>ok`. Writes a WAV to the path. Watchdog is `ms` plus 30 s |

`audio` and `audio_help` are in the chip fields. `dac_vol_db` and `adc_vol_db` use the chip's dB scale where 0 is the loudest clean level. `adc_scale_db` and `pga` are two different microphone gains. `reg32`, `reg34` and `drc_en` are read-only.

### Infrared

Owner: AO_Ir. The block is on the ESP32-C5.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `ir` | none | Five `ir` records: `tx`, `rx` (with `deaf`), `carrier hz=38000`, `level` and `last`, then `>ok` |
| `ir_help` | none | `field desc` per field, then `>ok` |
| `ir_send` | `proto` one of nec (the default), `addr` uint 0 to 0xFFFF default 0, `cmd` uint 0 to 0xFFFF, required | `>ok` once queued, not once sent |
| `ir_rx` | `ms` uint 100 to 30000 default 5000 | `ir=rx` per frame heard, with `nec addr cmd symbols`, then `>ok` when the window ends |

### NFC

Owner: AO_Nfc, the ST25R3916B on the antenna board. The reader field is on only while a verb waits. `nfc_field` and `nfc_emulate` keep it off.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `nfc` | none | Five `nfc` records: `chip`, `field`, `emulate`, `counts`, `last`, then `>ok` |
| `nfc_help` | none | `field unit min max step rw` per field, then `>ok` |
| `nfc_read` | `ms` uint 100 to 10000 default 1500 | `nfc=tag` with `uid atqa sak type model`, then `nfc=ndef len text` when there is a message, then `>ok`. Code 4 with `msg="no tag in the window"` when empty |
| `nfc_write` | `url` string, `text` text. Exactly one | `nfc=tag`, then `>ok`. Code 2 for none or both |
| `nfc_keys` | `block` uint 0 to 255 default 0 | `nfc=keys block result tried`, then `>ok`. Tries 2038 public keys and takes minutes |
| `nfc_dump` | none | `nfc=dump sectors blocks`, then `>ok`. Sectors that no known key opens are skipped |
| `nfc_field` | `ms` uint 1 to 200, required | `nfc min mean peak mod ext n`, then `>ok`. Code 4 busy, code 5 `absent` or `error` |
| `nfc_emulate` | `uid`, `atqa`, `sak` hex strings, `uri` string, `text` text. At least one of `uid`, `uri`, `text` | `>ok reads=0` when the window is up |
| `nfc_emulate_off` | none | `>ok reads=<n>`. Code 4 when nothing emulates |

`uid` has 4, 7 or 10 bytes, `atqa` two, `sak` one. A coherent `uid atqa sak` set clones a card. `uri` or `text` presents a Type-2 tag a phone reads.

When a tag is in the window but the exchange fails, `nfc_read` and `nfc_write` put the raw `kode_status_t` in `code`:

- 3: no key opened it.
- 1: no NDEF message.
- 2: the tag stopped answering.

`nfc_read` and `nfc_write` try the twenty most common Mifare Classic keys. kodeOS reads cards with keys it holds and does not recover a key that is on no list.

### RFID, 125 kHz

Owner: AO_Rfid. The carrier is on only while a read, a clone or a test window runs.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `rfid` | none | `rfid=field up`, `rfid=last` and `rfid=counts`, then `>ok` |
| `rfid_help` | none | `field desc` per field, then `>ok` |
| `rfid_read` | `ms` uint 100 to 10000 default 2000 | `rfid id rf`, then `>ok`. `id` is five bytes of an EM4100 tag. Code 4 with `msg="no tag in the window"` when empty |
| `rfid_write` | `id` string, ten hex digits, colons optional | `rfid=clone id match=yes`, then `>ok`. Writes a T5577 and reads it back. Code 4 with `msg="clone not verified"` when the read-back differs |
| `rfid_field` | `ms` uint 1 to 150, required | `rfid min mean peak ac n` in ADC counts, then `>ok`. Reads the demodulator with the carrier off |

A job that holds the front end makes the others answer code 3 `msg="rfid busy or not fitted"` for a read or a clone, and code 4 `busy` for `rfid_field`. A board with no antenna board closes its windows empty, since the board cannot sense the antenna. `id` that is not ten hex digits is code 3 `msg=id`.

### Haptic

Owner: AO_Haptic, the AW86233 and its LRA. `haptic` and `haptic_help` follow the chip fields.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `haptic_play` | `ms` uint 1 to 2000 default 200, `gain` uint 0 to 100 default 50 | `play=1 ms gain`, then `>ok` at once. The buzz is a 235 Hz sine fed to the chip's RTP FIFO. Code 5 `msg=busy` during another buzz |

The record states what was asked for, not what was felt. `gain` is a percent of full scale, clamped to the rating of the LRA.

### The expansion header

Owner: whoever holds `KBSP_RES_EXP`, or nobody. The fourteen positions of J3 are served by `khal_exp`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `exp` | `pos` uint 1 to 14, `mode` one of free, in, out, adc, pwm, `pull` one of none, up, down default none, `level` uint 0 to 1 default 0, `hz` uint 1 to 40000 default 1000, `duty` uint 0 to 1000 default 500 | No keys: one `exp` record per position (mode free, in, out, pwm with `level`; adc with `raw mv cal`; lent with `tag`), then one `exp_ctrl` per controller an app may take, then `>ok`. With `pos` and `mode`: acts and answers `>ok` |
| `exp_help` | none | `exp gpio caps may lent` per position, four `syntax` records, `kodeos_ctrl`, then `>ok` |
| `exp_power` | `state` bool, required | `>ok`. Switches `+3.3V_EXT` of J3, a pin of the C5. `c5 field="pwr_ext"` reads it back. Needs the radio |

A write needs both `pos` and `mode`:

- Any key without `pos` is code 2 `msg=pos`.
- `pos` without `mode` is code 2 `msg=mode`.
- The first write claims `KBSP_RES_EXP` for "console (bench)" until the next boot.
- If an app holds the header, every write is code 5 `msg=owned`, while reads still work.
- A request `khal_exp` refuses is code 5 `msg=exp`.

`duty` is in tenths of a percent. `caps` letters are d digital, m matrix, a adc, l low power. The tag of a lent position is clipped to 8 characters.

### Injected input

No owner. These verbs publish the events that AO_Touch and AO_Buttons publish, so no receiver can tell them from a finger or a thumb. All answer `>ok` as accepts.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `touch` | `x` uint 0 to 409, `y` uint 0 to 501, `state` one of down, up, move, all required | `>ok`. Code 4 while a tap is armed |
| `tap` | `x` uint 0 to 409, `y` uint 0 to 501, both required | `>ok` after the press. The release is published about 60 ms later. Code 4 while another gesture is armed |
| `swipe` | `x0 y0 x1 y1` uint in the panel, `ms` uint 10 to 5000, `hold` uint 0 to 5000 default 0 | `>ok`. The console replays the path at the sensor's rate of one sample per 10 ms, rests `hold` ms, then lifts. Code 4 while a gesture is armed |
| `btn` | `id` one of up, down, left, right, top, bot. `state` one of press, release, hold, double. Both required | `>ok` |

Coordinates outside the panel are refused, never clamped.

- `btn` with `hold` is for `top` and `bot` only, and `double` for `top` only. Other ids answer code 3 `msg=id`.
- An injected press never starts the 600 ms hold clock of AO_Buttons, so a host that wants a held thumb sends press, hold, release. The hold of `bot` is the HOME signal of AO_Boot.
- `press` and `release` go through the same door as a real button, so `kode_buttons_read()` follows them.

### The mirror

Owner: AO_Display. Built with `KODEOS_MIRROR`. See `internals/mirror`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `mirror_start` | `scale` uint 1 to 2 default 2, `q` uint 1 to 100 default 70 | `>ok`, then the `m` stream. The board lowers `q` above 70 to 70. Not over the channel |
| `mirror_stop` | none | `>ok`. A frame in progress is cut with `mabort` |
| `mirror_ack` | `seq` uint 0 to 65535, required | `>ok`. Opens the credit window and wakes a paused session |
| `mirror_info` | none | `mirror paused scale q window in_flight sent stalls`, then `>ok` |

### GUI and boot

| Verb | Arguments | Answer |
| --- | --- | --- |
| `gui_info` | none | A record with `state lvgl pills installed files writing`, a `gui` record with `state sel level group sheet kody lock_s stack_free`, a `gesture` record with `drag taps claims commits shorts diagonal axis zone blocked`, then `>ok`. Owner AO_Gui. Code 6 means AO_Gui is stuck |
| `boot_info` | none | `running addr size_kb otadata reason`, then `>ok`. Owner AO_Boot |
| `factory_reset` | `confirm` bool, required | Cable only, and only in the kodeOS role. AO_Boot removes every app but the welcome app, wipes the C5's saved networks and keys and this chip's bonds, and erases every NVS namespace but the identity and the station lock. It then writes the welcome app's boot request and restarts the board. The answer is the record `factory_reset result=done` and `>ok` just before the restart. A step that fails answers the record `factory_reset step=<name> status=<n>` and `>err code=5 msg=<name>`, and the board stays up. `>err code=4 msg=busy` while an E5 run goes. The watchdog is 290 s |

What the `gui` record says:

- `sel` is the selection of the launcher in quotes, or `-` for none: the project name of an installed app, the file name of a file, and `/<group>` for a group.
- `level` is `root` or `group`, and `group` is the name of the open group in quotes, or `-` at the root.
- `sheet` is the card that is up, or `-`. `state` is `face` while the Lock is awake and `rest` at Rest, and `returning` while the first frame after an app waits for the list.
- `kody` is `playing`, `paused`, `asleep` or `none`. `lock_s` is the auto-lock time in seconds, or `never`. `stack_free` is the bytes of AO_Gui's stack that were never used.
- The `gesture` record has `drag` (`idle`, `pending`, `follow` or `settle`) and counters since boot that each stop at 65535: the presses that ended as a tap, the drags the recognizer claimed, the followed drags that landed (`commits`) and those released short (`shorts`), and the presses it rejected by reason (`diagonal`, `axis`, `zone`, `blocked`).
- The record reaches the Kode channel as it reaches the cable. The log lines that `gui_info` also writes do not. One of those lines starts with `ui:` and gives the interface settings and the buzzes asked and held back (`internals/gui/buttons-and-gestures`).

### The NVS store

No owner. A value is read by the next app through `kode_nvs_get()`. Values sit in plain flash.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `nvs_set` | `key` string, `value` string, both required | `>ok`. Code 3 for a key of 16 characters or more or a value too long |
| `nvs_get` | `key` string, required | `key value`, or `key state=unset`, then `>ok` |
| `nvs_del` | `key` string, required | `>ok`, also when the key was absent |
| `nvs_list` | none | `key` per stored name, up to 24, then `>ok` |

### The radio and the identity

Owner: AO_Radio. Built with `KODEOS_RADIO`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `radio_up` | none | `>ok`, an accept. Starting the link resets the C5 and takes about 10 s |
| `radio_down` | none | `>ok`, an accept. The link ends and the C5 goes into reset when no service is wanted. Does not clear the `wifi` and `ble` flags |
| `radio_info` | none | `link fw sd`, link off, starting, up, stopping or failed, then `>ok` |
| `id_sign` | `nonce` string, 1 to 200 characters of letters, digits, `.`, `_`, `-` | `sig der="<base64>"`, then `>ok`. The C5 signs `kode-auth/v1`, a newline and the nonce with ECDSA P-256 over SHA-256 |
| `id_cert` | none | `id serial`, `id part of der` in parts of 144 bytes, `id unit_len len`, then `>ok` |
| `ident_csr` | none | `csr part of der` per part, then `>ok`. Creates the key when none exists. Factory only |
| `ident_cert` | `part` uint 1 to 11, `of` uint 1 to 11, `der` string, all required | `>ok`. Parts in order, the unit's certificate first. Factory only |
| `ident_lock` | `confirm` bool, required | `>ok`. Closes `ident_csr` and `ident_cert` for good. Factory only |
| `c5_wipe` | `confirm` bool, required | `>ok`. Forgets the Wi-Fi networks and BLE keys of the user, then the bonds of the P4. The identity stays |

The identity verbs answer code 5 `identity` with no key or certificate, `locked` after `ident_lock`, and `refused` with the link down, another request in flight or no answer in 3 s.

`ident_csr`, `ident_cert`, `ident_lock` and `c5_wipe` refuse Bluetooth with `msg=cable`. `confirm=on` is required for the last two: without it the answer is code 3 `msg=confirm` and nothing happens. `c5_wipe` stops at the first step that fails with `vault`, `btkeys` or `ble`.

`install dst=c5` verifies the image before it asks the C5 for anything: CRC, container digest, ESP header and chip id, so an image for the P4 is refused by name. Its `>ok` means the C5 took and activated the image. The update ends with the link off, so run `radio_up` and read `c5`. The C5 has no rollback: a bad image leaves it unreachable.

### Wi-Fi

Owner: AO_Radio. Built with `KODEOS_WIFI`. Most verbs hand their deadline to the owner. The state record is `wifi` (idle, connecting, got_ip or disconnected) with `ssid ip rssi en`, where `en` is the stored flag and not the association.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `wifi_up` | none | State record, then `>ok`. Stores the wish, starts the link, joins a remembered network when `wifi_join` is on |
| `wifi_down` | none | State record, then `>ok`. Clears the flag and stops the station |
| `wifi_scan` | none | `ssid rssi ch auth bssid` per network as the sweep finds it, then `>ok`. The sweep takes about 38 s |
| `wifi_join` | `ssid` string, `pass` string, `remember` bool default on | State record, then `>ok`. Working credentials go to the vault in the C5. `remember=off` keeps nothing |
| `wifi_auto` | none | State record, then `>ok`. Scans and joins the strongest remembered network |
| `wifi_list` | none | `ssid` per remembered network, most recent first, then `>ok`. Names only |
| `wifi_forget` | `ssid` string, required | `>ok`. Removes the network from the vault, does not disconnect |
| `wifi_leave` | none | State record, then `>ok`. The network stays remembered |
| `wifi_info` | none | State record, then `>ok` |

### Bluetooth

Owner: AO_Radio. Built with `KODEOS_BLE`. See `internals/ble-console`.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `ble_up` | none | `ble addr`, then `>ok`. Stores the wish, starts NimBLE and advertises |
| `ble_down` | none | `>ok`. Stops advertising and disconnects. Does not stop NimBLE. Over Bluetooth the `>ok` never arrives |
| `ble_scan` | `ms` uint 500 to 30000 default 5000 | `addr rssi name type mfg svc` per device, then `>ok`. `mfg` is hex with the company id first, `svc` the 16-bit service UUIDs |
| `ble_info` | none | `ble addr console tx_drop bonds backlog retry hidden pair`, then `>ok` |
| `ble_unpair` | none | State record, then `>ok`. Does nothing while a console is attached, and logs a warning |
| `ble_pair` | none | State record, then `>ok`. Pair new device: a bonded board is visible to new devices for 5 minutes or until one pairs. With Bluetooth off nothing changes and the record reads `pair=off` |

### Network and MQTT

Owner: AO_Net. `KODEOS_NET` builds the network verbs and `KODEOS_MQTT` the MQTT verbs. lwIP runs on the P4.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `net_ping` | `host` string, `count` uint 1 to 10 default 4 | `seq time_ms ttl` per reply, `seq timeout=on` per loss, then `ip tx rx min_ms avg_ms max_ms`, then `>ok` |
| `net_info` | none | `ip gw mask dns mqtt rx_dropped`, then `>ok` |
| `http_get` | `url` string, required | `body` records of printable text in slices of 191 bytes, up to 8, then `status content_len body_len shown truncated`, then `>ok`. A 404 is `>ok` |
| `udp_send` | `host` string, `port` uint 1 to 65535, `data` string, all required | `sent`, then `>ok` |
| `mqtt_up` | `uri` string, `user` string, `pass` string | Closed by the state record. `mqtt://` or `mqtts://host[:port]`. The host is a name of letters, digits, `-`, `_` and `.` that neither starts nor ends with `.` or `-`, or an IPv6 address in brackets. Any other URI is `>err code=3 msg=uri`. With no network it answers `>err code=5 msg=no_net` |
| `mqtt_down` | none | Closed by the state record |
| `mqtt_pub` | `topic` string, `data` string, both required | `>ok` at QoS 0, written to the socket at once. `data` is limited by a 240-byte line. With no network, `>err code=5 msg=no_net` |
| `mqtt_sub`, `mqtt_unsub` | `topic` string, required | `>ok`. Messages arrive as `!` lines. With no network, `>err code=5 msg=no_net` |
| `mqtt_info` | none | `mqtt uri why`, then `>ok`. State down, connecting or up. `why` none, no_net, refused or timeout |
| `channel_info` | none | `channel serial broker since_s why next_s lost fast shot sent fps_got left_s keep session`, then `>ok`. State off, no_net, no_identity, joining, up, waiting or refused. `keep` is the session age in seconds that the live connect asked for, `never` or `-`; `session` is `resumed`, `new` or `-` |

The five `mqtt_*` verbs answer code 4 `busy` while the Kode channel holds the one MQTT client. `mqtt_down` waits for the esp-mqtt task and has no console deadline. `net_ping` is the ICMP verb because `ping` is the liveness check. The ceiling of `count` is the ceiling of the listing, since every echo is an event. `rx_dropped` of `net_info` counts the MQTT messages the driver dropped since boot because its receive ring was full. It is not the `rx_dropped` of `ps`, which counts console input bytes that were lost. See `internals/kode-channel` for the fields of `channel_info`.

### Logging

| Verb | Arguments | Answer |
| --- | --- | --- |
| `log_level` | `tag` string, `level` one of the log levels d, i, n, w, e, off | `>ok`. Sets one tag for this boot. `*` and `all` are code 3 `msg=tag`. Not stored |
| `log_color` | `state` bool, required | `>ok`. ANSI colour on or off |

The global level is `config key="log"`. It is stored, and setting it back to the build level erases the key. Answers are never filtered.

- At `n` the console shows answers, notices, warnings and errors.
- At `off` it shows answers only.
- The guided tests print their prompts at notice level, so `e` hides them on the console and not on the glass.
- A line that the build removed with `CONFIG_KLOG_MAXIMUM_LEVEL` cannot come back.

### Self-tests

`test` runs one row or a group. `test_report` prints the report that the last whole `user` run kept in NVS.

| Verb | Arguments | Answer |
| --- | --- | --- |
| `test` | `component` one of the rows below or a group (`all`, `main`, `user`), default `all` | For a row, `main` and `user`: `test result` per row with result pass, fail, timeout or skipped, then `summary pass fail timeout`, with `skipped` when a row was skipped, then `>ok`. Watchdog is the budget of each row plus 5 s. For `all`, the E5 final test, see below |
| `test_report` | none | `row result` per row, then `when rows problems`, then `>ok`. `report=none` when no whole `user` run exists. `when` is UTC seconds, 0 for an unset clock |

A `timeout` is a row nobody judged. `fail` counts every row that did not pass, timeouts included. A `skipped` row found nothing to judge, such as a slot with no card. Rows run one at a time in the order of `TESTS[]`. A verdict block of log lines with the tag `kcli` comes before the summary.

The groups:

| Group | Rows |
| --- | --- |
| `all` | The E5 final test, run by AO_TestDot on a station build: sd, imu, the camera's checks of the glass and the LED, the operator's buttons and touch, then audio, haptic, mag, pmic, gauge, ioexp, clocks, ir, nfc, rfid and mem. Cable only. See below |
| `main` | The rows a main board answers alone: pmic, gauge, ioexp, clocks, imu, mag |
| `user` | The owner's profile that the Self-test page runs: ioexp, clocks, imu, mag, haptic, sd_user, c5_user, ir, nfc_field, rfid_field, audio, then `hold=continue`, then power_plug, buttons, led, display, tp, nfc_card, rfid_card, psram, mem |

Every row also runs by name. Some run only by name: screenshot, jpeg (with the mirror), fs, boot, apps, kfw, id, ble_console and net (with their options), c5, psram, and the user rows sd_user, c5_user, nfc_field, rfid_field, power_plug, nfc_card and rfid_card.

The rows that need a person:

- `buttons`: six prompts, 20 s each, a press let go within 3 s.
- `led` (three colours) and `display` (five fields): TOP is yes and BOT is no.
- `tp`: two targets, 64 px tolerance, 20 s each.

The criteria of each row are on `internals/production-test`.

### The E5 final test

`test component=all` is the final test of a finished Kode Dot on the E5 station. A station computer starts it over the cable, and the Kode Dot runs the rows, the camera's checks of the glass and the LED, and the operator's presses and touches. Then the computer decides the verdict. AO_Console hands the line to AO_TestDot, which answers the records on the open line and closes it with `>ok` when the camera part ends. The rest arrives through `test_dot_poll`.

`all` is refused in this order: `not_built`, the source (`cable`, `net` or `sched`), `locked`, `busy`, then the fixture: `camera`, then `station_i2c`. On a Kode Dot without the station, `test` therefore ends in `>err code=4 msg=camera` and leaves the glass, the radio and every owner as they were.

The verbs of the test are cable only, and the station's computer is their only client. Their keys, records and refusals are on `internals/e5-wire-contract`.

| Verb | What it does |
| --- | --- |
| `test_dot_place` | Looks for the four marks under the camera |
| `test_dot_poll` | Reads what the run said since its line closed |
| `test_dot_screen` | Puts the connecting or the result screen on the glass |
| `test_dot_photos`, `test_dot_blob` | Send the kept photos and pictures as blobs |
| `test_dot_relays`, `test_dot_adc` | Drive the station's relays and read its ADC |
| `test_dot_loop` | Sends frames round the dock's pads |
| `test_dot_radio` | Checks the C5 and two networks and a BLE name |
| `test_dot_boost` | Switches the pads delivery test's boost |
| `test_dot_result` | Tells the Kode Dot the verdict. A pass runs the factory reset |
| `test_dot_lock` | Locks the station verbs for good |
| `test_dot_end` | Gives back everything the test took |
| `test_dot_info` | Says where AO_TestDot is |
| `test_dot_camera` | Runs the camera part alone |

The owner is AO_TestDot (`sdk/kaos/test-dot`). The console's `factory_reset` is refused as `busy` while a run goes.

## Gotchas

> **WARNING**
>
> **`sd_verify` and `install dst=os` destroy data.** `sd_verify confirm=on` formats the card. `install dst=os` replaces the inactive kodeOS slot and resets. Both are single lines with no second prompt. The Kode channel runs them too, since it has no allow-list.

> **WARNING**
>
> **`value` on `pmic`, `tp` and `ioexp` writes registers.** Setting `c7` on `ioexp` switches the panel supply off. Setting `dis_auto_sleep` to 0 on `tp` puts the controller to sleep and every later `tp` answers code 5 until a finger touches the glass. Read the `_help` table first.

> **NOTE**
>
> **A verb in this page can be missing from your build.** A Kconfig option removes a group, and an unknown verb answers code 1. Run `help` to see what your firmware has.

> **NOTE**
>
> **Strings are quoted, everything else is bare.** `app_run name="snake"` works and `app_run name=snake` is code 3. `ms=100` works and `ms="100"` is code 3. When you send a line from a shell, put the whole line in single quotes so the shell keeps the double quotes.

## Example code

Send commands from a terminal with `tools/kcli.py`. It prints the records and checks the terminator.

1. Connect the Kode Dot over the USB-C cable and load ESP-IDF in your terminal, which provides pyserial.
2. Run `python3 tools/kcli.py -p PORT --settle 5 cmd 'ls path="/"'`. It prints one record for `sdcard`, one for `storage` and then `ok`.
3. Run `python3 tools/kcli.py -p PORT cmd 'led_rgb rgb=0,0,200'`. The LED turns blue.
4. Run `python3 tools/kcli.py -p PORT cmd 'config key="bright" value=40'`. The record shows `value=40 stored=yes` and the glass dims.
5. For a verb that works for a while, add `--gap 10` after `cmd` so `kcli.py` keeps listening for the `!` lines of the outcome.

The verbs that carry a payload (`install` with `src="terminal"`, `file_put`, `file_get`, `screenshot`) have their own `kcli.py` subcommands: `install`, `file-put`, `file-get` and `screenshot -o shot.png`. Do not send them with `cmd`.

## In the SDK

- [`components/kaos/console`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/console): `console_ao.c` holds `CMDS[]` and every handler.
- [`components/kaos/autotest`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/autotest): the `TESTS[]` table behind `test`.
- [`components/kaos/test_dot`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/test_dot): AO_TestDot and the `test_dot_*` verbs.
- [`tools/kcli.py`](https://github.com/kodediy/KodeOS/blob/main/tools/kcli.py): the reference host program.
