kodeOS internals

Variables and functions

Add your own verbs to the console: a variable a host can read or follow, a function a host can run, and a setting the board stores.

What it is

An app is a whole firmware and carries the console with it, but the console knows only the verbs of kodeOS. Variables and functions let an app add verbs of its own, with the same grammar, the same help and the same errors as every other verb.

A value a host can both see and change is a variable plus a function under two names, such as gain and set_gain. A third kind of row, a setting, is a value the board stores, owned by one Active Object and read and changed with config.

How it is connected

flowchart LR
host["host line"] --> con["AO_Console"]
subgraph kodeos["In kodeOS"]
  con --> reg["kregistry"]
end
subgraph yours["In your app"]
  getter["your getter"]
  run["your run callback"]
  ao["your AO"]
  run -- "post event" --> ao
end
reg -- "get" --> getter
reg -- "run" --> run
ao -. "function_record,<br/>function_end" .-> con
class host ext
class con ao
class getter,run,ao app
From a verb to your callback

The app declares tables of rows. The registry keeps each table by pointer, so a table must be static const. The console turns the rows into variable, function, variable_list and function_list, and a handler looks a row up by name and calls its callback.

Every callback runs on the task of AO_Console, inside one step. Return promptly and never block: no kode_delay_ms(), no kode_net_* call that waits for an answer, no kode_fs_*, no kode_imu_read(). A blocked callback stops the console and every verb after it, and the watchdog does not catch it, since the command is synchronous.

A function that changes state the app's Active Object owns should post an event instead of writing the state, so two tasks never own one number. The example console_actions does it that way: reset posts a static event, add posts a pooled event, and the Active Object applies both in a step of its own.

FileWhat it holds
kaos/core/include/kode_variables.hkode_variable_t, KODE_VARIABLES_ADD() and the limits
kaos/core/include/kode_functions.hkode_function_t, kode_value_t, KODE_FUNCTIONS_ADD()
kaos/core/include/kode_config.hkode_config_t and KODE_CONFIG_ADD() for settings
kaos/core/src/kregistry.cThe private registry: validation, lookup, parsing and spelling of both kinds
kaos/console/src/console_ao.cA_VARIABLE, A_FUNCTION, h_variable, h_function, the list verbs and the four watch slots
examples/console_actionsA counter Active Object with a variable and two functions

All three headers are included by kode.h.

Specifications

The verbs

VerbWhat it doesThe answer
variable_listLists the variablesvariable watch desc per row in registration order, then >ok. watch is the period in ms, or 0
function_listLists the functionsfunction arg desc per row. arg is none, int, bool, enum or string, with min max or enum=a,b where they apply
variable name="<row>"Reads one variablevariable value, then >ok
variable name="<row>" watch=<ms>Follows it>ok at once, then the same variable record every period. watch=0 stops it and answers >ok for any row
function name="<row>" [arg=<value>]Runs one function>ok and nothing else. A row with answer_ms answers in records first

The names and the desc of a record are quoted. The lists go out in slices of 12 records, like help.

Rows

ThingValue
Name[a-z0-9_], 1 to 15 characters (KODE_ROW_NAME_MAX), unique over every variable and function
DescriptionAt most 48 characters (KODE_ROW_DESC_MAX). NULL prints -
CapsVariables: 32 rows (KODE_VARIABLES_MAX) in 4 tables. Functions: 64 rows (KODE_FUNCTIONS_MAX) in 4 tables. Settings: 32 rows in 4 tables
RegistrationAfter kode_init(). A table with one bad row is refused whole and a log line names the row. The result is KODE_EXISTS for a taken name, KODE_NO_RESOURCES past a cap and KODE_DENIED for any other rule, and always in a build without CONFIG_KODEOS_CONSOLE
Value wordA getter writes one word of at most 32 characters (KODE_VALUE_STR_MAX), bare. Whitespace, control characters and quotes go out as _. An empty or longer word is refused, never cut
Getter resultKODE_OK. Any other status refuses the read and ends a watch
Watch4 slots (KODE_VARIABLE_WATCH_MAX). Period 100 to 60000 ms. A busy port skips a sample. A failing getter ends the watch with one warning
ScalingNo float and no unsigned type. Decimals are scaled integers with the unit in the name, such as temp_dc or vbat_mv. A percentage is an int from 0

Function arguments

The form on the wire follows the type: a string is quoted (arg="hello there") and every other kind is bare (arg=-5, arg=on, arg=fast). The other form answers code 3 msg=arg.

kode_type_tWire formNotes
KODE_TYPE_NONEno arg=run receives NULL
KODE_TYPE_INTsigned decimalmin and max bound it when they differ. The console refuses a value outside them before run is called
KODE_TYPE_BOOLon or off
KODE_TYPE_ENUMone of up to 6 words (KODE_VALUE_ENUMS_MAX)Each word is a valid name. The index travels to run
KODE_TYPE_STRINGquoted text, 1 to 256 characters (KODE_ARG_STR_MAX)Valid for the call only: copy what you keep. The line itself is 240 bytes, so the longest argument the console can carry is shorter

run gets NULL when the line has no arg=. It returns KODE_OK to accept, KODE_DENIED to refuse the argument or its absence (the console answers code 3 msg=arg), or any other status for the failure of the row (code 5 with the row name). Nothing runs on a parse failure. The console refuses value= and watch= on a function, arg= and value= on a variable, and a name of the other kind, all with code 3.

A function that answers in records

A row with answer_ms set (at most 600000, ten minutes) keeps the line open. run still returns at once. Then the Active Object that got the request says what came of it. A ! log line reaches the cable and Bluetooth and never the Kode channel, so an outcome a remote host must see is a record.

CallWhat it does
function_ticket()Called by run. Returns the number of this run. Hand it to the Active Object you post to
function_record(ticket, record)Sends one record, without the greater-than and bar prefix. Returns false for NULL, for a record of KODE_FUNCTION_REC_MAX (192) characters or more, or when the queue is full. A record is never cut
function_quote()Writes a text the way every record does
function_end(ticket, status, msg)Ends the function. KODE_OK closes with >ok, KODE_DENIED with code 3 msg=arg, anything else with code 5 and msg, or the function name when msg is NULL. False leaves the line open, so post it again

The console compares the ticket with the run in flight and drops every record or end that carries another one, so a late answer cannot close the next function's line. While the line is open every other command answers busy. When the owner never ends the function the console closes it with code 6 after answer_ms. The records go to the port the command came from, so the cable, Bluetooth and the channel read the same lines. These calls are in kaos.h.

The functions of kodeOS

kodeOS registers one table the same way, from kgui: seven assets_ functions for Kody's pack, no variables. Each answers in >|assets records and then >ok or >err.

FunctionArgumentJob
assets_infononeSource, scenes and roles of the pack
assets_checkenum sd, fatChecks the pack on that medium, activates nothing
assets_reloadnoneReads the pack again
assets_beginint 1 to 2147483647Stages an install of this many bytes
assets_commitnoneChecks the staged pack and makes it active. Holds the console up to 120 s
assets_updatestring, a URL or noneFetches the pack. Holds the console up to 240 s
assets_trystringPlays one file of the pack on the face

Settings

An app registers settings with KODE_CONFIG_ADD() and answers each request from its own Active Object with kode_config_answer(). The value is typed like a function argument, and a key is [a-z0-9_] up to 15 characters. The rows appear in config after the ones of kodeOS. internals/command-set lists the nineteen settings of kodeOS.

Gotchas

Example code

The variable count shows a counter and the function add changes it. After flashing the app, type function name="add" arg=5 and then variable name="count" on the console: the answer reads 5. The callbacks run on the console task and so do both users of the counter, so no lock is needed.

/*
 * console_counter.c - 2026-10-28 - from Kode with love
 *
 * Adds a variable "count" and a function "add" to the console. Type
 * function name="add" arg=5, then variable name="count": the answer reads 5.
 */
#include <stdio.h>
#include "kode.h"
 
static int32_t s_count; /* only the console task touches it */
 
/* Spell the counter as the one word the console sends. */
static kode_status_t get_count(char *buf, size_t cap)
{
    (void)snprintf(buf, cap, "%ld", (long)s_count);
    return KODE_OK;
}
 
/* Add the argument to the counter; a line with no arg= is refused. */
static kode_status_t run_add(const kode_value_t *arg)
{
    if (arg == NULL) { return KODE_DENIED; }
    s_count += arg->i;
    return KODE_OK;
}
 
static const kode_variable_t VARS[] = {
    { .name = "count", .get = get_count, .desc = "The counter" } };
static const kode_function_t FUNCS[] = {
    { .name = "add", .arg = KODE_TYPE_INT, .min = -100, .max = 100,
      .run = run_add, .desc = "Add arg to the counter" } };
 
/* Bring the system up, register both tables, keep the app alive. */
void app_main(void)
{
    kode_init();
    (void)KODE_VARIABLES_ADD(VARS);
    (void)KODE_FUNCTIONS_ADD(FUNCS);
    while (1) { 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