SDK
AO_Log
The Active Object that owns the console of the Kode Dot: every log line, the line editor, raw mode for file transfers, and the stored log level.
What it is
The console is the text channel between a Kode Dot and a computer. A host types a command and the board answers with lines. The board also prints log lines whenever a part of the firmware calls KLOG_I() or a sibling. AO_Log is the only code that writes to the console. Log lines, typed characters and the answers to commands all reach it as events in one queue, and it writes them one after the other, so two lines never mix.
It serves two ports: the cable (USB-C, plus the charging base when KHAL_CONSOLE_DOCK is on) and Bluetooth. A third way in, the Kode channel, belongs to AO_Net (sdk/kaos/net), and AO_Log only keeps the replies that go back to it. AO_Log also builds each command line from the bytes that arrive and echoes them. It publishes the finished line and does not read it. AO_Console does that (sdk/kaos/console).
How it is connected
flowchart LR subgraph inp["Into AO_Log"] klog["klog<br/>KLOG_x lines"] reader["Reader task<br/>priority 4"] ble["NimBLE task"] end klog -- "LOG_LINE_SIG" --> log["AO_Log"] reader -- "CONSOLE_BYTE_SIG" --> log ble -- "lines, raw chunks" --> log subgraph outp["Out of AO_Log"] cable["USB-C port"] blep["Bluetooth port"] con["AO_Console"] ring["PSRAM ring<br/>drained by AO_Net"] end log -- "khal_console_write" --> cable log -- "kdrv_ble_console_write" --> blep log -- "CONSOLE_LINE_SIG,<br/>published" --> con log -. "reply lines of the channel" .-> ring class log,con ao class cable,blep chip class ring store
AO_Log runs at priority 6 with a queue of 64 events and a 4096-byte stack. It claims KBSP_RES_CONSOLE and has one state, Log_active.
The reader task is not an AO. It blocks on the cable for a byte, which an AO must never do, and it posts each byte to AO_Log at priority 4. Six sits above four, so a keystroke is echoed the moment it arrives. It sits below everything with real work, so a slow terminal cannot hold the system up. The AOs below it are AO_Storage, AO_Radio, AO_Gui and AO_Net.
The initial transition does four things:
- It claims the console.
- It applies the stored log level.
- It opens the transport with
khal_console_init(). - It installs
log_sinkas the sink of klog (sdk/klog).
Until then every line leaves through klog's rescue path, which is how the first lines of the boot get printed at all. With KODEOS_BLE_CONSOLE it also installs the two Bluetooth callbacks here and not when the radio starts, so the radio is never up with nobody listening.
Output
log_sink() runs on the thread that logged. It takes a LogEvt from the pool without waiting, counts it as pending and posts LOG_LINE_SIG. When the pool is empty it drops the line and counts the drop.
The handler erases the prompt, writes the line to the cable, copies the whole line to Bluetooth, and, for a reply to a Kode channel command, appends it without its newline to a ring in PSRAM that AO_Net drains. It then redraws the prompt if a half-typed line needs its echo, and lowers the pending count.
The prompt, the echo and the erase sequence go to the cable only, because a machine does not need them.
Input
Each byte is a CONSOLE_BYTE_SIG. The reader task retries a failed allocation every 1 ms, up to 200 times, and then drops the byte and counts it. A printable byte is echoed and added to the line, which holds 239 characters.
Enter publishes a non-empty line as CONSOLE_LINE_SIG with src set to the serial port, and no prompt follows, so the answer starts at column 0. A longer line sets an overflow flag and is answered with >err code=3 msg=line_too_long. Delete and backspace clear the flag.
Raw mode
install src="terminal" and file_put send their payload over the same wire as the command, and one event per byte would exhaust the pool. An AO that expects a payload (AO_AppMgr, AO_Storage, AO_Radio) posts CONSOLE_RAW_REQ_SIG with the byte count and itself as the receiver. AO_Log drops any half-typed line, and the reader task drains what is in the input and switches the port to raw.
Chunks go straight from the reader task to the receiver as RECV_DATA_SIG. When the count reaches zero, raw mode ends by itself. On Bluetooth the driver hands the writes to AO_Log, which gathers them into 4 KB chunks. CONSOLE_RAW_END_SIG aborts raw mode on both ports.
The pending count
console_congested() tells a producer to wait for the next tick. The count compares lines handed to AO_Log and not yet written against half the large pool, 24, or against 4 when a Bluetooth console is attached. AO_Console checks it before each record of a long answer (sdk/kaos/console).
Specifications
Signals in
| Signal | Event | From | What AO_Log does |
|---|---|---|---|
LOG_LINE_SIG | LogEvt (src, len, line) | log_sink, any thread | Writes the line to the cable and to Bluetooth, and to the channel ring when src is the Kode channel |
CONSOLE_BYTE_SIG | ConsoleByteEvt | The reader task | Edits the line and echoes |
CONSOLE_RAW_REQ_SIG | ConsoleRawReqEvt (size, who, src) | AO_AppMgr, AO_Storage, AO_Radio | Starts raw mode on the port named by src. A refusal is answered when who is set |
CONSOLE_RAW_END_SIG | none | The same three, on an abort | Ends raw mode on both ports |
LOG_LEVEL_SET_SIG | LogLevelEvt (tag, level) | log_level_request() | Applies the level to a tag or globally |
CONFIG_REQ_SIG | KodeConfigReqEvt | AO_Console, subscribed | Answers the key log only |
Signals out
| Signal | Event | Range | Meaning |
|---|---|---|---|
CONSOLE_LINE_SIG | ConsoleLineEvt (src, up to 240 bytes) | publish | A finished command line of the cable or Bluetooth. A firmware without AO_Console still has a console to log to. AO_Net posts the lines of the Kode channel and AO_Scheduler those of cron to AO_Console itself, with the sources NET and SCHED, so AO_Log never sees them (sdk/kaos/console) |
CONFIG_ANS_SIG | KodeConfigAnsEvt | publish | The level in force, and whether it is stored |
CONSOLE_RAW_RSP_SIG | ConsoleRawRspEvt | post to the requester | Whether raw mode is armed |
RECV_DATA_SIG | RecvDataEvt (up to KODE_RECV_CHUNK, 4096 bytes) | post to the receiver | One raw chunk, from the reader task or the NimBLE task |
NET_CHAN_OUT_SIG | none | post to AO_Net | The channel ring holds reply lines |
AO_Log has no timer. Pacing belongs to AO_Console.
The log level
The level has one stored value. config key="log" value=d|i|n|w|e|off sets the console's global level and stores it in the NVS key kset/log, a single byte, so it survives the reset that opening the cable port causes. A level equal to the build's default (CONFIG_KLOG_DEFAULT_LEVEL, INFO) erases the key, so a missing key and the default are one state. A stored byte that is not a level of this build is ignored with a warning. At boot the level applies before the port opens, then one NOTICE line says log: level n (stored).
| Level word | Name | Used for |
|---|---|---|
d | Debug | Bench detail |
i | Info | What happened, for someone reading later |
n | Notice | Outcomes of requests, readings a command asked for, state changes |
w | Warning | Wrong but survivable |
e | Error | The call failed and somebody has to know |
off | Off | Drops every line; answers still go out |
log_level tag="<tag>" level=<word> sets one tag, is not stored and refuses * and all with code=3 msg=tag. A bench script that greps !I lines sees nothing on a board left at n or e: set value=i first.
The calls
| Call | What it does |
|---|---|
log_dropped() | Lines the pool could not carry. ps prints it; above 0 means too many lines arrived at once |
console_rx_dropped() | Input lost: a byte given up after the 200 retries, or a finished line that found no event. ps prints it as rx_dropped= |
console_congested() | True while the writer is behind and a producer should skip its next slice |
log_level_request(tag, level) | Posts LOG_LEVEL_SET_SIG. A NULL or empty tag is the global level |
log_start_reader() | Called last by kode_init(): draws the first prompt and starts the reader task at priority 4 |
The limits are a command line of 240 bytes (KODE_CONSOLE_LINE_MAX), a log line of 256 bytes (KODE_LOG_LINE_MAX), a raw read window of 200 ms and a Bluetooth backlog threshold of 512 bytes. CONFIG_KLOG_MAXIMUM_LEVEL is a compile-time ceiling: a line below it is not in the image and no log_level brings it back.
Gotchas
Example code
Print one line at each level, then raise the debug level for one tag. With the app running and the console left at its default level, the up and ready lines print and the debug line prints once it is raised for the tag demo.
/*
* log_demo.c - 2026-10-28 - from Kode with love
*
* Writes lines at three levels through klog, which hands them to AO_Log.
* The debug line prints only after the level of the tag "demo" is raised. On
* a Kode Dot the console shows the info and notice lines at the default
* level, then the debug line.
*/
#include "kode.h"
#define TAG "demo"
/* Bring the system up and log at three levels. */
void app_main(void)
{
kode_init();
KLOG_I(TAG, "up after %u ms", (unsigned)kode_time_ms());
KLOG_D(TAG, "hidden: the default level is info");
kode_log_set_level(TAG, KODE_LOG_DEBUG);
KLOG_D(TAG, "shown: this tag now prints debug");
KLOG_N(TAG, "ready");
}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/log:log_ao.c, the whole Active Object.components/klog: the macros and the sink AO_Log installs (sdk/klog).kaos.h:console_congested(),log_dropped(),log_level_request().