SDK
Overview
How the Kode Dot's firmware is built: the layers of the SDK, what each one holds, and which way they talk to each other.
What it is
The SDK is the code every Kode Dot firmware is built from: kodeOS, the
examples, and your own app. It lives in one repository,
kodediy/kode-sdk, as a set of
ESP-IDF components. You add it to a project, call kode_init(), and the
whole system runs inside your image: the drivers, the owners of every chip,
the console, the radio link.
Each component is a layer. A layer uses the layers under it and never one above it, so a change to the screen code cannot break a driver, and a firmware with no screen can leave the screen layers out.
The build files enforce the order. Each component names in REQUIRES the
layers it may use, and a script, tools/check_layers.sh, checks every
#include against the rules before every merge.
kodeOS uses the SDK the same way your app does. It has no private door into it. Its own code, the screens and the launcher, sits above the SDK in the kodeOS repository.
The ideas behind it
The whole SDK is built on a few ideas. Every other page uses their names, so they are explained here once, in plain words.
Active Objects own the chips
An Active Object, or AO, is a small program inside the firmware with three things of its own:
- a task, so it runs by itself, side by side with the others;
- a queue where messages for it wait their turn;
- a state machine, which decides what to do with a message from the state the AO is in. The same button press can mean "unlock" on one screen and "go back" on another.
An AO takes one message from its queue, handles it from start to finish, and only then takes the next. Nothing else touches its data halfway, so that data needs no lock.
Every chip on the board has one owner, and the owner is an AO: AO_Power owns the charger, AO_LedMgr the LED, AO_Storage the microSD card. Nobody else touches that chip. Whoever wants something from it sends the owner a message. Two owners for one chip stop the board at boot, naming both.
Events are the messages
A message between AOs is an event. It has a name, its signal (for
example BUTTON_EVT_SIG), and it may carry data: a copy, not a pointer into
someone else's memory (two events too big to copy are the exceptions,
listed in sdk/design-rules). An event travels one of two ways:
| Way | Goes to | Used for |
|---|---|---|
| Post | One AO, by name | A request ("set the LED blue") and its answer |
| Publish | Every AO that subscribed to the signal | News ("a button was pressed", "the battery changed") |
The sender never waits: it leaves the event in a queue and carries on. The SDK is built on QP/C, a framework for Active Objects, which moves the events and runs the state machines.
Workers do the waiting
Some jobs make you wait: a page from the network, a file on the card, a chip that is slow to answer. An AO must never wait, because while it waits it reads no messages, and its queue fills up.
So an AO that has slow jobs gets a worker: a helper task of its own that runs one job at a time and posts the result back to its AO as an event. The worker decides nothing. Which job comes next, how long to wait, whether to try again: the AO decides all of it, in its state machine.
sequenceDiagram autonumber participant app as Your app participant ao as The owner AO participant wk as Its worker participant chip as The chip or the network app->>ao: request (post) ao->>wk: one job wk->>chip: ask, then wait chip-->>wk: data wk-->>ao: done event (post) ao-->>app: answer (post or publish) Note over ao: keeps reading its queue while the worker waits
Your app makes calls, the facade sends events
You do not have to send events yourself. Your app calls plain functions such
as kode_led_rgb(), and the facade, the kode layer, turns each call
into the right event for the right owner. An app needs Active Objects of its
own only when it wants to react to events, and the examples show how.
The words, in one table
| Word | In one sentence | More |
|---|---|---|
| Active Object (AO) | A task with its own queue and state machine, handling one event at a time | sdk/kaos/overview |
| Owner | The one AO that may use a chip; it claims the chip at boot | sdk/design-rules |
| Event | A message with a signal and a copy of its data | sdk/events |
| Signal | The name of an event, such as BUTTON_EVT_SIG | sdk/events |
| Post | Send an event to one AO | sdk/kqp |
| Publish, subscribe | Send an event to every AO that asked for that signal | sdk/kqp |
| State machine | The part of an AO that decides what an event means now | sdk/kqp |
| Worker | An AO's helper task: runs one slow job, posts the result back, decides nothing | sdk/kqp |
| Facade | The kode_* calls an app makes; each becomes an event | sdk/kode |
| Layer | One component of the SDK; it uses the layers below it, never above | sdk/overview |
How it is connected
flowchart TB app["Your app<br/>or kodeOS"] subgraph sdk["The SDK"] kode["kode<br/>the facade"] kui["kui + kscreen<br/>the UI kit"] kaos["kaos<br/>the Active Objects"] kdrv["kdrv<br/>the chip drivers"] kqp["kqp<br/>QP/C in this system"] qpc["qpc<br/>QP/C itself"] khal["khal<br/>shared services"] klog["klog<br/>the log"] kbsp["kbsp<br/>the board"] kdefs["kdefs<br/>kode_status_t"] end app --> kode app --> kui kode --> kaos kaos --> kdrv kaos --> kqp kdrv --> khal kqp --> qpc khal --> klog khal --> kbsp khal --> kdefs class app app class kaos ao
Two kinds of arrow run between the layers:
- A function call goes down: the caller waits for the answer.
- An event is a message left in a queue, and the sender does not wait.
A command goes down like this:
- Your app calls
kode_led_rgb(). - The facade turns that into an event for AO_LedMgr, the Active Object that owns the LED.
- AO_LedMgr calls the LED driver.
What happens comes back up the same way. An Active Object publishes a button press, and any Active Object of yours that subscribed receives it.
Only four layers may include ESP-IDF headers (esp_*, driver/,
freertos/): kbsp, khal, kdrv and klog. Everything above them sees the
hardware through Kode's own types, and every public function returns
kode_status_t.
The radio chip, the ESP32-C5, runs a firmware of its own. Its source is a
second repository, kodediy/kode_slave_c5, included in the SDK as the
submodule c5/.
The layers
One row per layer, with what it holds and the page that documents it.
| Layer | What it holds | Page |
|---|---|---|
| kdefs | One type, kode_status_t, that every public function returns | sdk/kdefs |
| kbsp | The board described as constants: pins, I2C addresses, screen size, the battery, who owns what | sdk/kbsp |
| klog | The log: macros, levels, and the path that still works inside a crash | sdk/klog |
| khal | What several drivers share: the I2C bus, the console bytes, time, NVS, PSRAM | sdk/khal |
| kqp, qpc | QP/C, the Active Object framework, and how this system uses it: pools, priorities, the tick. Every signal is listed in sdk/events | sdk/kqp |
| kdrv | One driver per chip, and the only layer that knows a part number | sdk/kdrv/overview |
| kaos | The Active Objects: one owner per device, each deciding when its device is used | sdk/kaos/overview |
| kode | The facade: kode_init(), and every call an app makes | sdk/kode |
| kui | The UI kit: colours, fonts, the pill and the sheet, and Kody's player | sdk/kui |
| kscreen | LVGL on the screen, for an Active Object of your own | sdk/kscreen |
Two folders in the SDK are other people's code, kept under their own names:
qpc, Quantum Leaps' QP/C, and rfal, ST's library for the NFC chip. Only
kqp includes the first, and only kdrv the second.
The rules that keep the layers apart, and the checks that enforce them, are
in sdk/design-rules.
Where a new file goes
Ask in order. The first yes is the answer.
- It defines a pin or an I2C address: kbsp.
- Several drivers share it, or it could have a second implementation: khal.
- It is the code for one chip: kdrv.
- It owns a device or decides when something happens: kaos.
- It is what an app calls: kode.
- It is a program on top of the system: a component of its own, above the SDK.
A type goes with the layer that defines it. A type no layer owns goes in kdefs, which is why kdefs exists at all.
In the SDK
components/: one folder per layer.tools/check_layers.sh: the include rules, as a script.examples/: small apps, one Active Object per thing they do.