kodeOS internals
kgui
AO_Gui, the Active Object that draws every screen of kodeOS: its task, its states, its doors, the settings it keeps, its files, and the page for each screen.
What it is
kgui is the component that draws everything you see on the glass of a Kode Dot running kodeOS. It holds one Active Object, AO_Gui, and the screens it shows:
- The face is the lock screen and home. It shows the time, the date and the battery over Kody's scene.
- Apps (the launcher) is a rail of folders and apps. The face opens it with a tap, TOP, the pad's right or a swipe to either side.
- Settings is a control centre: Wi-Fi, Bluetooth and Settings, the battery and Turn off, then a brightness level and a volume level. It comes down over the face.
- The loading screen and the touch test serve the factory tests and the self-test.
A full-glass card, the sheet, rises over these whenever a page, a question, a write or a pairing code needs the whole display.
AO_Gui owns LVGL and the widget tree and owns no device. Fingers arrive from sdk/kaos/touch, buttons from sdk/kaos/buttons, the lists of apps from sdk/kaos/storage and sdk/kaos/appmgr, and pixels leave through sdk/kaos/display. kgui sits above the SDK and is not part of it: it includes kode.h and kode_ao.h like any app, the SDK never includes it, and a firmware without a screen leaves it out and carries no LVGL. The kit it draws with is sdk/kui. An app that wants a screen of its own uses sdk/kscreen.
The look follows kshell, Luismi's shell for the Kode Dot: a black page, dark rows, one yellow ring that shows where the cursor is. Colours, type and glyphs live in sdk/kui.
How it is connected
flowchart LR subgraph inputs["Inputs"] touch["AO_Touch"] btn["AO_Buttons"] boot["AO_Boot"] end subgraph owners["Owners"] lists["AO_Storage<br/>AO_AppMgr"] state["Wi-Fi, BLE, power,<br/>display owners"] end touch -- "TOUCH_EVT_SIG" --> gui["AO_Gui<br/>LVGL, the screens"] btn -- "BUTTON_EVT_SIG" --> gui boot -- "HOME_SIG" --> gui lists -- "app and file lists" --> gui state -- "published state" --> gui gui -- "request functions" --> state gui -- "DISPLAY_FLUSH_SIG" --> disp["AO_Display"] gui -- "kode_haptic_play()" --> hap["AO_Haptic"] class gui ao class disp ao class hap ao
Priority and stack
AO_Gui runs at priority 3 (KODE_PRIO_USER_GUI), above only AO_Net. A repaint spends most of its time waiting for the panel's DMA, up to 500 ms inside one step, and at this level that wait delays no other Active Object.
It has a 64-event queue and a 24 KB stack in static internal RAM, because ThorVG rasterises Kody on that stack. Every lv_* call in kodeOS runs on this one task, so LVGL needs no mutex and a widget callback may post events like any other code.
The tick
The tick is a one-shot timer. AO_Gui arms it again after each pass, for the time LVGL says its next timer is due and never more than 10 ms (KODE_GUI_TICK_MS). A periodic timer would post on every expiry whether AO_Gui had run or not, and one slow step elsewhere, such as an SD card mount, would fill the queue with stale ticks. Re-arming keeps at most one tick waiting, so a busy board lowers the frame rate and overflows nothing.
One tick does these jobs:
- Applies a back that a swipe recorded.
- Repeats a held pad button on the rail.
- Feeds LVGL one input read while a finger rests.
- Steps whatever moves: a curtain, a card, the rail, the launch scene, a system card's rise, the face's red arc.
- Calls
lv_timer_handler(). - Counts the lock, the auto-lock, the red arc, the wait for Remove and the two waits of a launch.
At Rest the tick waits 1,000 ms (KGUI_REST_TICK_MS) instead, and never past the next brightness step or the end of a hint.
Events and requests
AO_Gui asks owners by event. It never calls a driver. What it shows (Wi-Fi on, battery percent, brightness) comes from events it subscribed to:
POWER_EVT_SIGWIFI_STATE_SIGBLE_STATE_SIGDISPLAY_STATE_SIGRADIO_STATE_SIG
What it wants done it asks for with a request function that posts to the owner:
wifi_start_request()ble_unpair_request()display_brightness_request()appmgr_run_request()appmgr_flash_request()appmgr_erase_request()autotest_run_request()boot_factory_reset_request()test_dot_screen_done()kode_power_off()kode_haptic_play()
A list arrives as one event per entry and an end marker, in pages of 24, and AO_Gui asks the next page itself with storage_list_page() or appmgr_list_page().
Drawing
LVGL's heap lives in PSRAM through the kit's kui_mem.c, with no fallback to internal RAM. When the panel exposes its two framebuffers, LVGL draws straight into them in direct mode. A panel with one framebuffer gets two draw buffers of 41,000 bytes each in PSRAM.
AO_Gui posts each rendered chunk to AO_Display as DISPLAY_FLUSH_SIG, and waits on a semaphore for the panel to take it. That semaphore is one of the three documented exceptions to "everything is an event" (sdk/design-rules).
No screen load is animated. A door between the face, Apps and Settings is a curtain: two still pictures of the screens, drawn on a screen of their own and moved by a spring or by a finger. The curtain is described on internals/gui/settings-screen and internals/gui/face.
Start
main/main.c calls kode_init() and then kgui_start(). The panel was announced from inside kode_init(), before AO_Gui existed, so AO_Gui asks for the announcement again with display_state_request(). When the panel answers, AO_Gui:
- Starts LVGL.
- Builds the screens, the sheet, the hint and the players of Kody.
- Reads its stored settings.
- Asks both owners for their lists.
- Enters the face, which is the first lit frame. A board that comes back from an app enters Apps instead.
If the panel is down or LVGL fails to start, AO_Gui stays in Gui_waiting and the board is console-only.
The return from an app
An app ends in a reset, so every return to kodeOS is a boot. A boot that comes back from an app lights Apps, in the group of the app that ran and with that app in focus, and shows no face. The state Gui_returning holds the first frame black until the installed list has ended or 800 ms (KGUI_LIST_WAIT_MS) have passed, so the rail opens already laid out around the app. The reset cause is never read: only the fact that an app was launched.
When the app ended badly (a panic, a watchdog or a brownout) the Stopped card rises over Apps. A clean end shows no card.
The board's rest
When the board rests (CONFIG_KODE_LOWPOWER), AO_LowPower asks every owner to get ready, and AO_Gui answers by entering Gui_asleep:
- The tick stops, and a finger or a card in the middle of something is dropped.
- The glass takes the page the rest holds: the face with Kody standing on frame 0 of his scene, or a black page when the board is about to turn off.
- AO_Gui renders the figures of the clock once into PSRAM and hands them to AO_Display, which writes the time into the panel's memory once a minute while the link is down.
- It tells AO_LowPower it is ready.
A locked card of a running write refuses the round, so the glass stays lit during a write. On the wake, AO_Gui draws again on DISPLAY_REPAINT_SIG and returns to the face. The press of BOT that woke the board, and its release, do nothing else. If AO_LowPower refused a Turn off, the reason is said as a hint at the wake. The rest itself is on internals/power-and-sleep.
The face has a rest of its own, Rest, which dims the same face while the board stays awake (internals/gui/face). The two are different things: Rest is a look of the face, and Gui_asleep is the board parked.
The E5 test
A build with CONFIG_KODE_TEST_DOT makes AO_Gui the host of the E5 test's screens (sdk/kaos/test-dot). AO_TestDot publishes TEST_DOT_SCREEN_REQ_SIG with a screen or a pattern, and the first request that is not the end takes the glass from whichever screen is up. AO_Gui enters Gui_test_dot, a child of Gui_running, and logs door: <from> -> e5 test (the station).
On entry the hint and any card go, the armed press, the held bottom button and a finger that is down are forgotten, Rest's brightness steps stop so the glass keeps the level AO_TestDot set, and the black page is loaded. Each request is built with the kui_test_dot module on that page, drawn at once, flushed, and only then answered with test_dot_screen_done(kind, seq), echoing the number of the request. The answer is what lets the camera take its photo.
While the state stands:
- Touch, the buttons, HOME and the doors do nothing. The operator answers AO_TestDot's prompts on those very buttons, and the screens it publishes draw the prompts, so no guided test takes the glass from it.
- Nothing counts toward Rest, the auto-lock or the lock, and Kody does not play.
- A round of AO_LowPower is refused with
E5 test, and a Bluetooth pairing is refused. - The state ends on the request for no screen, which
test_dot_endpublishes. AO_Gui then clears the canvas and goes to the face with a cut, and logsdoor: e5 test -> face (test_dot_end). A FAIL stays on the glass until then, or until the next boot.
gui_info shows the state as test_dot.
Console doors
The console does not link to kgui. gui_info publishes GUI_DUMP_REQ_SIG and AO_Gui answers with a published state event, so a build without a GUI needs no stubs and gui_info times out. kgui adds six settings to config (lock, scene, clock, kody_sleep, haptic_ui, usb_awake), with AO_Gui as their owner, and seven assets_ functions for Kody's pack, registered in kgui_start() once the queue exists.
Specifications
The task
| Thing | Value |
|---|---|
| Task name | gui |
| Priority | 3, KODE_PRIO_USER_GUI |
| Queue | 64 events. A rescan can leave 24 + 24 + 2 = 50 events waiting unread, and touch samples and periodic publishes arrive on top |
| Stack | 24 KB (GUI_STACK_BYTES), static, internal RAM. The fewest bytes ever unused measured is 3,608 |
| Tick | One-shot, at most 10 ms; 1,000 ms at Rest |
| Flush wait | 500 ms at most (GUI_FLUSH_TIMEOUT_MS); on a timeout it logs an error and goes on |
| Lists | Pages of 24; 64 apps and 64 files at most (KODE_APP_LIST_MAX); 128 rows in the launcher; 10 folders at most (KGUI_GROUP_MAX) |
| A list round | Believed out for 10 s at most (GUI_LIST_ROUND_MS) |
| Hold and arc | A hold is 600 ms (KODE_BUTTON_HOLD_MS); the red arc of the face runs from 2,000 ms to 5,000 ms (GUI_OFF_ARC_FROM_MS, GUI_OFF_ARC_TO_MS). No hold locks |
| Auto-lock | 120 s unless set |
| Top band | The first 60 rows of the glass (KGUI_TOP_BAND_PX), where the hint stands |
The states
AO_Gui is a hierarchical state machine. The state that is current decides which screen is loaded. Only a state's entry action loads a screen.
| State | Screen | What it does |
|---|---|---|
Gui_initial | none | Subscribes, asks AO_Boot for the zone in force, reads the interface settings, calls display_state_request() |
Gui_waiting | none | Waits for the panel; on DISPLAY_STATE_SIG builds everything and enters the face or Gui_returning |
Gui_running | superstate | Arms the tick, feeds touch to LVGL, handles both lists, the manifest chain, the state subscriptions, the flash-write takeover and config |
Gui_returning | none | Holds the first frame black after an app, until Apps may light |
Gui_face | face | The lock screen and Kody. Its children Gui_awake and Gui_rest are two looks of the same screen |
Gui_browsing | launcher | Apps: the rail of folders and apps |
Gui_settings | settings screen | The control centre |
Gui_sheet_face, Gui_sheet_home, Gui_sheet_settings | the sheet | One shell per screen under the card. Each is a child of that screen, so closing the card reloads nothing. Gui_sheet_face is a child of Gui_awake |
Gui_btn_test | loading screen | The guided button, LED and display rows borrow it |
Gui_selftest | loading screen | A run of the Self-test page |
Gui_tp_test | touch test | The guided touch test's target |
Gui_asleep | the page the rest holds | The board parked by AO_LowPower |
Gui_test_dot | the black page, with the E5 test's canvas | The station has the glass (CONFIG_KODE_TEST_DOT). A child of Gui_running, entered from any screen on the first request and left for the face on the request for no screen |
A flash write is not a state. Gui_running handles APP_FLASH_PROGRESS_SIG, APP_FLASH_FAIL_SIG and APP_FLASH_DONE_SIG with no transition and raises, rewrites or removes the one sheet over whichever screen is up.
The doors
A door is a request to change screen, a private signal that whatever asks posts with gui_door_post(). The state that is current answers it, so one signal can open on one screen and close on another. Each taken door logs door: <from> -> <to> (<what asked>).
The face is home. Apps is beside it and Settings comes down over it.
| From | To | Asked by | Motion |
|---|---|---|---|
| face | Apps | A tap anywhere, TOP released. Rescans the lists unless a round is out | Apps grows out of the middle of the glass, in place |
| face | Apps | The pad's right, a swipe left or right. Rescans the lists unless a round is out | Sideways (GUI_PAGE_ANIM_MS): the face goes out to one side, Apps comes in from the other |
| Apps | face | BOT released, the pad's left, a swipe right, all at the root | The door Apps came by, reversed |
| face | Settings | The pad's up, TOP held, a drag down anywhere | The settings screen comes down; a finger holds it |
| Apps | Settings | TOP held, a drag down from the top 44 rows, or on a list at rest at its top | The settings screen comes down over Apps, and closing it goes back to Apps |
| Settings | face | BOT released, a swipe up | The settings screen goes up |
| Settings | Apps | BOT held 0.6 s (HOME_SIG). Apps opens at its root, no rescan | The settings screen goes up |
| Apps | face | The auto-lock | The face comes down |
| Settings | face | The auto-lock | The face comes down over Settings |
| face | Rest | BOT released before 2 s, kody_sleep with no input, a woken face left alone | None: a look of the same screen |
| Rest | face | TOP pressed twice; any button or touch on a cable | None: a look of the same screen |
| a sheet | its screen | BOT held 0.6 s, unless the sheet is busy | The screen's own |
A sheet swallows the settings door, the auto-lock, the unlock and the door to Rest, and answers only home. The boot, the end of a guided test and the return from an app are hard cuts with no motion.
While a curtain moves, the state is still the screen where the door started. The door is taken when the curtain lands.
The settings it keeps
AO_Gui keeps eleven keys in the NVS namespace kgui. The six that have a config key are described on internals/settings.
| Key | Type | Holds | When absent |
|---|---|---|---|
last | string | The name of the row launched last, so the cursor comes back where it was | The first row |
recents | string | The apps run last, by project name, one a line, latest first | None |
lock | 4-byte record | Auto-lock seconds; 0 is never | 60 s |
scene | string | The scene chosen, by name | The local hour picks |
clock | byte | 1 is the 12 h clock | 24 h |
kody_sleep | 4-byte record | Seconds before the face rests; 0 is never | The auto-lock's time |
haptic_ui | byte | How much of the interface buzzes: 0 off, 1 light, 2 normal | Light |
usb_awake | byte | 1 keeps the face out of Rest while a cable is in | Off |
scene_hr | record | The scene the hour drew, and for which hour | The hour picks again |
blename | string | The Bluetooth name typed on the Bluetooth page, after KODE | The radio's own name |
volvib | byte | The volume bar stands on its vibrate stop | Off |
A zone key left in kgui by an older build is copied to AO_Boot's kset/zone once and erased (gui_zone_migrate()). The keys pulls, quiet_kody and motion, which an older build kept, are erased once at boot.
The two interface settings, haptic_ui and usb_awake, are rows of the Settings page too, as Vibration and While charging. The console and the page go through the same code, so a value set in one shows in the other. A change is applied at once, stored, and logged as <key>: <word> (stored).
The Kconfig options
The component has two options.
| Option | Value | Meaning |
|---|---|---|
KGUI_ASSETS_URL | Default https://assets.kode.diy/kody/v1/kode.json, 127 characters at most | The manifest that the Kody page updates from |
KGUI_ASSETS_WAIT_MS | 0 to 5000, default 1000 | How long the face waits for the pack at boot before Kody wakes in the built-in scene |
CONFIG_KODEOS_GUI has no prompt: it is set whenever kgui is in the build, and it selects KODEOS_APPMGR and KODEOS_AUTOTEST.
The files
Everything is in components/kgui of the kodeOS repository. kgui_ao.c holds the state machine and decides. Each module below owns one thing and knows nothing of the Active Object; a module with a model keeps LVGL out of it, so its rules run in host tests.
| File | What it does |
|---|---|
include/kgui.h | The public header: AO_Gui, Gui_ctor() and kgui_start() |
kgui_ao.c | AO_Gui: the state machine, the doors, the auto-lock, every sheet, the lists, config and the gui_info dump |
kgui_assets.c, kgui_assets.h | Kody's pack on the board: reading, choosing the source, installing, and the seven assets_ functions |
screens/kgui_screens.c, screens/kgui_screen_*.c | The face, the settings screen, the launcher's window, the loading screen and the touch test |
screens/kgui_off.c | The numbers of the red arc |
kgui_list.c, kgui_motion.c | The Apps rail: the objects, and the model (the row, the focus line, Step, Glide, Rubber, Slide) |
include/kgui_group.h, kgui_group.c | The folders: apps filed by their first category, and the standard colours and glyphs |
kgui_details.c, kgui_dtext.c | The details of an app and the two cards of Remove, and their words |
kgui_keyboard.c | The keypad card |
kgui_curtain.c | The two pictures of a curtain |
kgui_rest.c | The numbers of Rest and the cycle of its burn-in shift |
kgui_ring.c, kgui_edge.c | The Ring (circle, line along the glass, edge of the system frame) and its curves |
kgui_sysscreen.c, kgui_rise.c, kgui_syscard.c | The system frame, the rise of a system card, and the words of the Stopped and Critical battery cards |
kgui_launch.c, kgui_grow.c, kgui_icon.c | The scene of a launch, the pop of its icon, and an icon as an object |
kgui_haptic.c, kgui_prefs.c, kgui_hint.c, kgui_frames.c | The buzzes, the five settings, the hint in the top band, and the timing of every motion |
kgui_meta.c, kgui_return.c | The wait for the apps' metadata, and the wait of the first frame after an app |
sim/kgui_sim.c | Builds the screens for the kit's desktop simulator |
Kconfig, CMakeLists.txt, idf_component.yml, README.md | The two options, the build, the pinned SDK components and LVGL 9.6.0, a short note |
The screens are built from kui.h and LVGL alone, never from kode.h, so the simulator builds them unchanged. Every callback is registered from C by ui_build(), and no screen declares one.
The parts
| Part | What it covers | Page |
|---|---|---|
| The face | The lock screen, the clock, Rest, the red arc, Kody's scenes | internals/gui/face |
| Apps | The rail, the folders, the details of an app, the empty state | internals/gui/launcher |
| The settings screen | The control centre, the Settings page and the pages it opens, the curtain | internals/gui/settings-screen |
| The sheets | The card, the system cards, and every page it shows | internals/gui/sheets |
| Kody's pack | The animation files, the manifest, the limits and the install | internals/gui/kody-pack |
| Buttons and gestures | What every input does on every screen, the haptics, the recognizer | internals/gui/buttons-and-gestures |
Gotchas
Example code
Walk the doors of the interface from the console of a Kode Dot. Opening the port restarts the board, so send everything in one session:
- Run
gui_info. The state readsface, because the board has started and did not come back from an app. - Send
tap x=205 y=300. Apps slides in from the right, andgui_inforeadsstate=browsingandlevel=root. - Send
btn id=bot state=press, thenbtn id=bot state=release. Apps goes back to the face, which slides in from the left. - Send
swipe x0=205 y0=30 x1=205 y1=320 ms=900. The finger pulls Settings down over the face and lets go past halfway. The settings screen opens, and the>|gesturerecord readscommits=1. - Send
swipe x0=205 y0=320 x1=205 y1=30 ms=900. The settings screen goes up and the face is back.
In the SDK
components/kgui: the whole component, in the kodeOS repository.kgui_ao.c: AO_Gui, with the contract of every function.components/kui: the kit that kgui draws with.