# 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

```mermaid
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
```

*Everything that reaches the console passes AO_Log*

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:

1. It claims the console.
2. It applies the stored log level.
3. It opens the transport with `khal_console_init()`.
4. It installs `log_sink` as 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

> **WARNING**
>
> **A long answer loses its tail and its `>ok`.** A producer that emits all its records in one step allocates the whole large pool before AO_Log writes one line, because it outranks AO_Log. The pool drops the rest in silence. Emit in slices of a dozen records a tick apart, and check `console_congested()` before each record. The driver's own ring cannot be asked: it reads empty because AO_Log has not run yet.

> **WARNING**
>
> **Opening the cable port resets the board.** The USB-Serial/JTAG peripheral resets the chip on attach. A sequence that builds state, such as a Wi-Fi join, runs inside one open session. The stored log level exists for this reason: without it every session starts at the build's level again.

> **NOTE**
>
> **The stored level does not filter the first lines.** The ROM, the bootloader and the first lines of `kode_init()` print before AO_Log reads `kset/log`. Check `info` for `log=<x> source=stored`.

> **NOTE**
>
> **The reader task is the only consumer of the cable.** Nothing else may read the transport. AO_Log asks the reader to enter raw mode and never touches its input side.

## 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`.

```c
/*
 * 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`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/log): `log_ao.c`, the whole Active Object.
- [`components/klog`](https://github.com/kodediy/kode-sdk/tree/main/components/klog): the macros and the sink AO_Log installs (`sdk/klog`).
- [`kaos.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/include/kaos.h): `console_congested()`, `log_dropped()`, `log_level_request()`.
