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 variable is a value the app shows, such as a temperature. The verb
variablereads it once or follows it. It never runs anything. - A function is something the app does, and it can change a value. The verb
functionruns it. It never reads anything.
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
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 takes time answers
KODE_OKas an accept, raises a flag or posts to the Active Object of the app, and tells the outcome through a variable or in records. - A reading that is slow to fetch is sampled by an Active Object or a worker at the rate the part likes, and the getter only spells the last sample.
- A getter can read one aligned 32-bit value another task writes, because the P4 loads it in one instruction.
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.
| File | What it holds |
|---|---|
kaos/core/include/kode_variables.h | kode_variable_t, KODE_VARIABLES_ADD() and the limits |
kaos/core/include/kode_functions.h | kode_function_t, kode_value_t, KODE_FUNCTIONS_ADD() |
kaos/core/include/kode_config.h | kode_config_t and KODE_CONFIG_ADD() for settings |
kaos/core/src/kregistry.c | The private registry: validation, lookup, parsing and spelling of both kinds |
kaos/console/src/console_ao.c | A_VARIABLE, A_FUNCTION, h_variable, h_function, the list verbs and the four watch slots |
examples/console_actions | A counter Active Object with a variable and two functions |
All three headers are included by kode.h.
Specifications
The verbs
| Verb | What it does | The answer |
|---|---|---|
variable_list | Lists the variables | variable watch desc per row in registration order, then >ok. watch is the period in ms, or 0 |
function_list | Lists the functions | function 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 variable | variable 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
| Thing | Value |
|---|---|
| Name | [a-z0-9_], 1 to 15 characters (KODE_ROW_NAME_MAX), unique over every variable and function |
| Description | At most 48 characters (KODE_ROW_DESC_MAX). NULL prints - |
| Caps | Variables: 32 rows (KODE_VARIABLES_MAX) in 4 tables. Functions: 64 rows (KODE_FUNCTIONS_MAX) in 4 tables. Settings: 32 rows in 4 tables |
| Registration | After 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 word | A 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 result | KODE_OK. Any other status refuses the read and ends a watch |
| Watch | 4 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 |
| Scaling | No 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_t | Wire form | Notes |
|---|---|---|
KODE_TYPE_NONE | no arg= | run receives NULL |
KODE_TYPE_INT | signed decimal | min and max bound it when they differ. The console refuses a value outside them before run is called |
KODE_TYPE_BOOL | on or off | |
KODE_TYPE_ENUM | one of up to 6 words (KODE_VALUE_ENUMS_MAX) | Each word is a valid name. The index travels to run |
KODE_TYPE_STRING | quoted 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.
| Call | What 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.
| Function | Argument | Job |
|---|---|---|
assets_info | none | Source, scenes and roles of the pack |
assets_check | enum sd, fat | Checks the pack on that medium, activates nothing |
assets_reload | none | Reads the pack again |
assets_begin | int 1 to 2147483647 | Stages an install of this many bytes |
assets_commit | none | Checks the staged pack and makes it active. Holds the console up to 120 s |
assets_update | string, a URL or none | Fetches the pack. Holds the console up to 240 s |
assets_try | string | Plays 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
components/kaos/core/include:kode_variables.h,kode_functions.handkode_config.h.examples/console_actions: a counter Active Object with a variable and two functions, the LED showing its parity.console_ao.c: the handlersh_variableandh_function.