SDK

AO_Console

The Active Object that reads console commands: the command table, one command in flight, the watchdog, paced answers and the error codes.

What it is

AO_Console turns a line of text into an action. AO_Log owns the console port, builds each line from the bytes that arrive and publishes it (sdk/kaos/log). AO_Console receives that line, cuts it into words, checks the values and runs the command. To run it, AO_Console sends an event to the Active Object that owns the device and prints the answer when it comes back. The text goes out through AO_Log, so AO_Log stays the only writer of the console.

A few verbs have no owner to ask, and AO_Console runs them itself: help, ps, tasks, i2c_scan, clocks, exp, time, the nvs_* family, variable, function and config. One table, CMDS, drives the validator, the dispatcher and help, so the grammar, the help text and the error messages cannot disagree. The verbs themselves are listed on internals/command-set, and the text rules on internals/console-protocol. This page describes the code that runs them.

How it is connected

flowchart LR
log["AO_Log"] -- "CONSOLE_LINE_SIG" --> con["AO_Console"]
net["AO_Net"] -- "CONSOLE_LINE_SIG<br/>Kode channel" --> con
con -- "net_line_done()<br/>net_line_busy()" --> net
sched["AO_Scheduler"] -- "CONSOLE_LINE_SIG<br/>cron entry" --> con
con -- "sched_line_done()<br/>sched_line_busy()" --> sched
con -- "request event" --> owner["The owner AO"]
owner -- "answer event" --> con
con -- "kode_log_reply()" --> log
con -- "CONFIG_REQ_SIG<br/>GUI_DUMP_REQ_SIG" --> bus["Published"]
class log,net,sched,con,owner ao
From a finished line to the owner and back

AO_Console in numbers:

ThingValue
Priority11
Queue64 events
Stack8192 bytes, the largest tier
ClaimsNothing at boot
Compiled underKODEOS_CONSOLE, which selects KODEOS_APPMGR and KODEOS_AUTOTEST
StatesOne, Console_active

The stack is the largest tier because the AO formats every reply in its own frames and carries the payload verbs.

What is in flight is data, and the timers decide what happens next. The initial transition subscribes to every answer signal a command can wait for, CONSOLE_LINE_SIG first. Whole groups are compiled out when the radio, Wi-Fi, Bluetooth, the network or the mirror is not in the build.

Which line, from which port

ConsoleLineEvt.src says whether the line came from the cable, Bluetooth, the Kode channel or the scheduler (KODE_CONSOLE_SRC_SERIAL, _BLE, _NET, _SCHED). AO_Console copies it into src before it dispatches. cmd_src holds the port of the command in flight, and every reply goes out tagged with it, so AO_Log can copy a channel command's replies to the channel ring. A busy refusal goes to the port of the new line, not to the port of the command in flight.

A SCHED line has no port. AO_Scheduler runs it on its own account at a cron time, so nobody typed it and no wire carries it. reply() is the one function every reply leaves through, and for source SCHED it writes the reply as a notice, sched: <reply>, and sends nothing to a port. When the reply is a terminator, sched_line_done() follows and AO_Scheduler sends its next line. This covers every way a command ends: the handler, a refusal, an unknown verb, the watchdog and the low-power events. A blank line from SCHED has no reply, and console_exec() calls sched_line_done() for it, so the scheduler is told once per line. A SCHED line that arrives while a command is in flight is not refused either: console_exec() calls sched_line_busy(), answers nothing, and AO_Scheduler gives the line again a second later (sdk/kaos/scheduler).

A NET line tells AO_Net when it ends. AO_Net holds the lines that arrive on the Kode channel and gives the console one at a time, each with a sequence number in ConsoleLineEvt.seq. The console keeps the number and reply() returns it with net_line_done() after the reply that ends the command (>ok or >err), so the answers stay in order. AO_Net ignores an end with another number, so a command that ends after AO_Net gave up on it does not end the line that took its place. A line of source NET that arrives while a command is in flight is not refused: console_exec() calls net_line_busy() and returns, and AO_Net offers the line again after 100 ms. Both calls exist in a build with CONFIG_KODEOS_CHANNEL.

Which source may run what

Nearly every verb takes a line and gives lines, from any port. Three kinds ask more of their port, and src_allows() is the one place that decides. A refusal comes before anything is posted or begun.

KindVerbsRefused for
Not Bluetoothident_csr, ident_cert, ident_lock, c5_wipeBluetooth: >err code=5 msg=cable
Wireinstall with src="terminal", file_put, file_get, screenshot dst=console, mirror_startThe Kode channel: >err code=5 msg=net
Cableinstall with dst=c5Bluetooth: msg=cable. The channel: msg=net
Not the schedulercronA SCHED line: >err code=5 msg=sched

A SCHED line is held to the same policy as the channel: the Wire and Cable kinds answer it >err code=5 msg=sched, and an entry cannot edit the table it runs from, which is why cron is refused to it. The identity verbs of the first row run from SCHED as they run from the channel.

The channel carries lines and no raw bytes, so a verb that switches the port to raw mode or answers with >|d= payload records needs a wire (internals/kode-channel).

One command in flight

console_exec() splits the line into tokens, then runs these checks in order:

  1. Busy. From cmd_begin() to the terminator every new line answers >err code=4 msg=busy without being parsed. A line of source NET is the exception: the console hands it back to AO_Net, which offers it again.
  2. The token count.
  3. The verb, found in the table.
  4. cmd_validate().

Handlers receive values that are already checked. Many verbs answer >ok at once and never set the in-flight kind: led_*, sd_mount, app_run, radio_up, mqtt_pub. Their >ok means accepted, and the owner tells the outcome in ! log lines.

The watchdog

cmd_begin() arms the tmo timer for 5000 ms. cmd_end() disarms it and returns to idle. If the owner never answers, the watchdog closes the command with >err code=6, so a silent owner cannot leave the console refusing everything. Some commands change that deadline:

Paced answers

Every reply line is a LogEvt from the large pool of 48 blocks, and AO_Console outranks AO_Log. A listing emitted in one step would take the whole pool before one line reached the port. So multi-record output leaves in slices of KCLI_SLICE = 12 records, and the pacer timer spaces the slices 10 ms apart. The gap is a timer and not a self-post: a self-post keeps the queue full, and AO_Log would never run.

screenshot dst=console and file_get use the same pacer through one producer. It sends base64 records of 216 characters, which carry 162 source bytes, a whole number of base64 groups. It checks console_congested() before each record.

Variables, functions and settings

An app registers rows with kode_variables_add(), kode_functions_add() and kode_config_add() after kode_init(). The registry (kregistry.c, no QP code) holds the rows, and AO_Console does the console side. variable reads a row and function runs one, both on AO_Console's task, so a getter or a run must return at once.

variable name="x" watch=<ms> prints the value on a periodic timer; four watches can run at once, 100 to 60000 ms apart, and a watch skips a sample while the port is congested. A function with answer_ms holds the line open and prints the records its owner posts as FUNCTION_ANS_SIG. config lists, reads and sets the stored settings, one CONFIG_REQ_SIG at a time, with a 1 s bound per owner (KODE_CONFIG_ANSWER_MS). The first request for a key is published. Its answer names the owner (KodeConfigAnsEvt.owner), and from then on the console posts that key's requests straight to the owner, leaving 4 slots of its queue free (KODE_CONFIG_POST_MARGIN, the margin AO_Radio and AO_Power keep for their own doors). A queue that has no such room answers >err code=4 msg=busy, so a burst of config lines can never fill an owner's queue.

Self-tests

test asks AO_Autotest for a run with the client id AUTOTEST_CLIENT_CONSOLE and prints only the events that carry that id (sdk/kaos/autotest). test_report prints the report AO_Autotest kept.

Specifications

The error codes

Every failure is one line, >err code=<n> msg=<token>. The codes are kcli_err_t in kcli_proto.h.

CodeNameRaised when
1KCLI_ERR_VERBNo table entry matches the first token. msg is the verb
2KCLI_ERR_MISSINGA token has no =, or a required key is absent. msg is the key
3KCLI_ERR_VALUEAn unknown key, or a value that fails its type, range or list. Also a ninth token, an unclosed quote and bytes after a closing quote. msg is the key
4KCLI_ERR_BUSYA command is in flight (a line of source NET is handed back to AO_Net instead), or the owner is busy with another request. sleep and power_off raise it with msg="<who>: <why>" when an owner will not rest, and every form of sleep, power_off and cron raises it with msg=not_built in a build without the option
5KCLI_ERR_REJECTThe owner said no, or the request never reached it (an empty event pool included)
6KCLI_ERR_TIMEOUTThe watchdog expired, or the receiver of a payload sent RECV_STALL_SIG

The grammar

RuleValue
Tokens per line, verb included8 (KCLI_MAX_ARGS)
Keys per command6 (KCLI_MAX_KEYS). console_specs_ok() checks the table and Console_ctor() asserts it
Bare valuesKARG_UINT (bounded by the spec), KARG_BOOL (on, off), KARG_ENUM, KARG_RGB (r,g,b)
Quoted valuesKARG_STRING (not empty), KARG_TEXT (may be empty). Escapes \", \\, \n, and no other: console_token() returns CONSOLE_TOK_BAD for any other backslash pair, answered code 3 with the key
Repeated keysRefused: cmd_validate() answers code 3 with the key given twice
Either formKARG_ROW: the arg= of function and the value= of config, typed by the registry row the line names
Commands in CMDS156, with every option built in

A string value is always quoted and every other value bare. The wrong form is code=3.

Signals in

AO_Console subscribes to the answers its commands wait for. Each case checks the in-flight kind first, because other AOs publish entries and states for their own askers.

SignalsEventsFromUsed by
CONSOLE_LINE_SIGConsoleLineEvtAO_Log (publish), AO_Net and AO_Scheduler (post)Every command
SCHED_ENTRY_SIG, SCHED_END_SIGSchedEntryEvt, SchedEndEvtAO_Scheduler, postedcron: one cron record per entry, then >ok or the refusal that matches the reason. Read only while a cron is in flight
LOWPOWER_STATE_SIG, LOWPOWER_REFUSED_SIGLowPowerStateEvt, LowPowerRefusedEvtAO_LowPowersleep, power_off
LOWPOWER_ROUND_SIGLowPowerRoundEvtAO_LowPowerNot a command. The console has no device to rest, so it answers the round busy (console transfer) while a payload crosses the wire and ready otherwise
STORAGE_ENTRY_SIG, STORAGE_LIST_END_SIG, STORAGE_INFO_SIG, STORAGE_TEST_SIG, TEST_RESULT_SIGStorageEntryEvt, ListEndEvt, StorageInfoEvt, StorageTestEvt, TestResultEvtAO_Storagesd_ls, fs_ls, sd_info, fs_info, sd_verify
STORAGE_PATH_RSP_SIG, STORAGE_LINE_SIG, STORAGE_OPEN_RSP_SIG, STORAGE_DATA_SIG, STORAGE_WRITE_RSP_SIG, STORAGE_DESC_RSP_SIG, STORAGE_FIELD_RSP_SIGThe matching eventsAO_Storage, postedThe file tree, file_get, screenshot dst=file, file_desc, sd
APPDATA_FIELDS_SIG, APPDATA_ICON_SIG, APPDATA_END_SIG, APPDATA_RM_RSP_SIGApp-data eventsAO_Storage, postedappdata_ls, appdata_info, appdata_rm
APP_ENTRY_SIG, APP_LIST_END_SIG, APP_CAT_RSP_SIGAppEntryEvt, ListEndEvt, AppCatRspEvtAO_AppMgrapp_list, app_cat
RECV_READY_SIG, RECV_ACK_SIG, RECV_DONE_SIG, RECV_FAIL_SIG, RECV_STALL_SIGnoneAO_AppMgr, AO_Storage, AO_Radioinstall src="terminal", file_put. Become >|ready chunk=4096, >|ack and the terminator
AUTOTEST_BEGIN_SIG, AUTOTEST_ROW_SIG, AUTOTEST_HOLD_SIG, AUTOTEST_END_SIG, AUTOTEST_REPORT_SIGAutotest*EvtAO_Autotesttest, test_report
BTN_TEST_SIG, TOUCH_TEST_SIG, AUDIO_TEST_SIG, IMU_TEST_SIG, MAG_TEST_SIG, HAPTIC_TEST_SIG, DISPLAY_TEST_SIG, LED_TEST_SIG, POWER_TEST_SIG, PSRAM_TEST_SIGStep eventsThe ownersThe steps of the guided rows, printed as they come
POWER_EVT_SIG, POWER_FIELD_SIG, POWER_CTRL_SIGPowerEvt, PowerFieldEvt, PowerCtrlEvtAO_Powerbat_info, pmic, gauge, boost, pmic_reset
BTN_STATE_SIG, BOOT_STATE_SIG, GUI_STATE_SIG, IR_RX_SIG, DISPLAY_MIRROR_INFO_SIGState eventsAO_Buttons, AO_Boot, AO_Gui, AO_Ir, AO_Displaybtn_read, boot_info, gui_info, ir_rx, mirror_info
AUDIO_FIELD_SIG, AUDIO_FILE_RSP_SIG, HAPTIC_FIELD_SIGField and file eventsAO_Audio, AO_Hapticaudio, audio_rec, haptic
CONFIG_ANS_SIGKodeConfigAnsEvtThe owner of the keyconfig
FUNCTION_ANS_SIGFunctionAnsEvtThe owner of a function that answers, postedfunction
RADIO_STATE_SIG, WIFI_*_SIG, BLE_*_SIGRadio eventsAO_RadioThe radio_*, wifi_*, ble_* verbs
NET_PING_*_SIG, NET_HTTP_*_SIG, NET_UDP_DONE_SIG, NET_INFO_SIG, MQTT_STATE_SIG, CHANNEL_STATE_SIGNetwork eventsAO_NetThe net_*, mqtt_* verbs, channel_info

The posted answers (*_RSP_SIG to a named asker) reach AO_Console only because it is the asker. It does not subscribe to MQTT_MSG_SIG on purpose: a message nobody asked for belongs in a ! line, never in the middle of a command's answer. The IOEXP_FIELD_RSP_SIG, TOUCH_FIELD_RSP_SIG, RADIO_FIELD_RSP_SIG, NFC_*_RSP_SIG, RFID_*_RSP_SIG and IR_INFO_RSP_SIG answers come back the same way.

Signals out

SignalEventRangeMeaning
CONFIG_REQ_SIGKodeConfigReqEvtpublish, then postRead or set one key. The owner answers with kode_config_answer(). Published until the key's owner has answered once, then posted to it with KODE_CONFIG_POST_MARGIN slots left; a refused post answers >err code=4 msg=busy
GUI_DUMP_REQ_SIGnonepublishgui_info. Published because kgui is above kaos and kaos cannot name AO_Gui
Requests to ownersPer ownerpostLED_* to AO_LedMgr, STORAGE_* to AO_Storage, appmgr_*_request() and FLASH_REQ_SIG to AO_AppMgr, power_*_request() to AO_Power (ship_mode is power_ship_request()), lowpower_request() to AO_LowPower (sleep, power_off), sched_request_list() and sched_request_set() to AO_Scheduler (cron), and the matching helpers for AO_Radio, AO_Net, AO_Nfc, AO_Rfid, AO_Ir, AO_Audio, AO_Haptic, AO_IoExp, AO_Touch, AO_Display and AO_Boot
Buttons and touch injectionButtonEvt, TouchEvtpublishbtn, tap, touch and swipe publish what AO_Buttons and AO_Touch publish

Replies go through kode_log_reply(), on the same path as log lines.

Timers

TimerSignalKindValue
tmoCONSOLE_TIMEOUT_SIGWatchdog of the command in flight5000 ms (KCLI_CMD_TIMEOUT_MS), extended per command
pacerCONSOLE_SLICE_SIGOne-shot between slices10 ms
inj_tmoKCLI_INJ_TMO_SIGAn injected tap or swipe has a sample dueTap hold 60 ms (KCLI_TAP_HOLD_MS), retry 60 ms
four watch timersCONSOLE_WATCH_SIGPeriodic, one per followed variableThe watch= period

Settings

AO_Console owns no stored setting. It only mediates config between the registry and the owners (internals/settings).

Gotchas

Example code

Register one variable and one function so the console can read and drive your app. With the app running, type variable name="uptime_s" to read the seconds since boot, and function name="green" to turn the LED green.

/*
 * console_hooks.c - 2026-10-28 - from Kode with love
 *
 * Gives the console one variable and one function. Type variable
 * name="uptime_s" to read the uptime, and function name="green" to light
 * the LED green. Both run on AO_Console's task and return at once.
 */
#include <stdio.h>
#include "kode.h"
 
/* Spell the uptime in seconds as one word. */
static kode_status_t get_uptime(char *buf, size_t cap)
{
    (void)snprintf(buf, cap, "%lu", (unsigned long)(kode_time_ms() / 1000U));
    return KODE_OK;
}
 
/* Publish the colour and return: AO_LedMgr does the work. */
static kode_status_t run_green(const kode_value_t *arg)
{
    (void)arg;
    return kode_led_rgb(0, 64, 0);
}
 
static const kode_variable_t VARIABLES[] = {
    { .name = "uptime_s", .desc = "Seconds since boot", .get = get_uptime },
};
static const kode_function_t FUNCTIONS[] = {
    { .name = "green", .desc = "LED green", .run = run_green },
};
 
/* Bring the system up, register the rows, then idle. */
void app_main(void)
{
    kode_init();
    (void)KODE_VARIABLES_ADD(VARIABLES);
    (void)KODE_FUNCTIONS_ADD(FUNCTIONS);
    for (;;) { kode_delay_ms(1000); }
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK