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:

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
What AO_Gui talks to

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:

  1. Applies a back that a swipe recorded.
  2. Repeats a held pad button on the rail.
  3. Feeds LVGL one input read while a finger rests.
  4. Steps whatever moves: a curtain, a card, the rail, the launch scene, a system card's rise, the face's red arc.
  5. Calls lv_timer_handler().
  6. 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:

What it wants done it asks for with a request function that posts to the owner:

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:

  1. Starts LVGL.
  2. Builds the screens, the sheet, the hint and the players of Kody.
  3. Reads its stored settings.
  4. Asks both owners for their lists.
  5. 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:

  1. The tick stops, and a finger or a card in the middle of something is dropped.
  2. 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.
  3. 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.
  4. 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:

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

ThingValue
Task namegui
Priority3, KODE_PRIO_USER_GUI
Queue64 events. A rescan can leave 24 + 24 + 2 = 50 events waiting unread, and touch samples and periodic publishes arrive on top
Stack24 KB (GUI_STACK_BYTES), static, internal RAM. The fewest bytes ever unused measured is 3,608
TickOne-shot, at most 10 ms; 1,000 ms at Rest
Flush wait500 ms at most (GUI_FLUSH_TIMEOUT_MS); on a timeout it logs an error and goes on
ListsPages 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 roundBelieved out for 10 s at most (GUI_LIST_ROUND_MS)
Hold and arcA 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-lock120 s unless set
Top bandThe 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.

StateScreenWhat it does
Gui_initialnoneSubscribes, asks AO_Boot for the zone in force, reads the interface settings, calls display_state_request()
Gui_waitingnoneWaits for the panel; on DISPLAY_STATE_SIG builds everything and enters the face or Gui_returning
Gui_runningsuperstateArms the tick, feeds touch to LVGL, handles both lists, the manifest chain, the state subscriptions, the flash-write takeover and config
Gui_returningnoneHolds the first frame black after an app, until Apps may light
Gui_facefaceThe lock screen and Kody. Its children Gui_awake and Gui_rest are two looks of the same screen
Gui_browsinglauncherApps: the rail of folders and apps
Gui_settingssettings screenThe control centre
Gui_sheet_face, Gui_sheet_home, Gui_sheet_settingsthe sheetOne 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_testloading screenThe guided button, LED and display rows borrow it
Gui_selftestloading screenA run of the Self-test page
Gui_tp_testtouch testThe guided touch test's target
Gui_asleepthe page the rest holdsThe board parked by AO_LowPower
Gui_test_dotthe black page, with the E5 test's canvasThe 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.

FromToAsked byMotion
faceAppsA tap anywhere, TOP released. Rescans the lists unless a round is outApps grows out of the middle of the glass, in place
faceAppsThe pad's right, a swipe left or right. Rescans the lists unless a round is outSideways (GUI_PAGE_ANIM_MS): the face goes out to one side, Apps comes in from the other
AppsfaceBOT released, the pad's left, a swipe right, all at the rootThe door Apps came by, reversed
faceSettingsThe pad's up, TOP held, a drag down anywhereThe settings screen comes down; a finger holds it
AppsSettingsTOP held, a drag down from the top 44 rows, or on a list at rest at its topThe settings screen comes down over Apps, and closing it goes back to Apps
SettingsfaceBOT released, a swipe upThe settings screen goes up
SettingsAppsBOT held 0.6 s (HOME_SIG). Apps opens at its root, no rescanThe settings screen goes up
AppsfaceThe auto-lockThe face comes down
SettingsfaceThe auto-lockThe face comes down over Settings
faceRestBOT released before 2 s, kody_sleep with no input, a woken face left aloneNone: a look of the same screen
RestfaceTOP pressed twice; any button or touch on a cableNone: a look of the same screen
a sheetits screenBOT held 0.6 s, unless the sheet is busyThe 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.

KeyTypeHoldsWhen absent
laststringThe name of the row launched last, so the cursor comes back where it wasThe first row
recentsstringThe apps run last, by project name, one a line, latest firstNone
lock4-byte recordAuto-lock seconds; 0 is never60 s
scenestringThe scene chosen, by nameThe local hour picks
clockbyte1 is the 12 h clock24 h
kody_sleep4-byte recordSeconds before the face rests; 0 is neverThe auto-lock's time
haptic_uibyteHow much of the interface buzzes: 0 off, 1 light, 2 normalLight
usb_awakebyte1 keeps the face out of Rest while a cable is inOff
scene_hrrecordThe scene the hour drew, and for which hourThe hour picks again
blenamestringThe Bluetooth name typed on the Bluetooth page, after KODE The radio's own name
volvibbyteThe volume bar stands on its vibrate stopOff

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.

OptionValueMeaning
KGUI_ASSETS_URLDefault https://assets.kode.diy/kody/v1/kode.json, 127 characters at mostThe manifest that the Kody page updates from
KGUI_ASSETS_WAIT_MS0 to 5000, default 1000How 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.

FileWhat it does
include/kgui.hThe public header: AO_Gui, Gui_ctor() and kgui_start()
kgui_ao.cAO_Gui: the state machine, the doors, the auto-lock, every sheet, the lists, config and the gui_info dump
kgui_assets.c, kgui_assets.hKody's pack on the board: reading, choosing the source, installing, and the seven assets_ functions
screens/kgui_screens.c, screens/kgui_screen_*.cThe face, the settings screen, the launcher's window, the loading screen and the touch test
screens/kgui_off.cThe numbers of the red arc
kgui_list.c, kgui_motion.cThe Apps rail: the objects, and the model (the row, the focus line, Step, Glide, Rubber, Slide)
include/kgui_group.h, kgui_group.cThe folders: apps filed by their first category, and the standard colours and glyphs
kgui_details.c, kgui_dtext.cThe details of an app and the two cards of Remove, and their words
kgui_keyboard.cThe keypad card
kgui_curtain.cThe two pictures of a curtain
kgui_rest.cThe numbers of Rest and the cycle of its burn-in shift
kgui_ring.c, kgui_edge.cThe Ring (circle, line along the glass, edge of the system frame) and its curves
kgui_sysscreen.c, kgui_rise.c, kgui_syscard.cThe 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.cThe 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.cThe buzzes, the five settings, the hint in the top band, and the timing of every motion
kgui_meta.c, kgui_return.cThe wait for the apps' metadata, and the wait of the first frame after an app
sim/kgui_sim.cBuilds the screens for the kit's desktop simulator
Kconfig, CMakeLists.txt, idf_component.yml, README.mdThe 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

PartWhat it coversPage
The faceThe lock screen, the clock, Rest, the red arc, Kody's scenesinternals/gui/face
AppsThe rail, the folders, the details of an app, the empty stateinternals/gui/launcher
The settings screenThe control centre, the Settings page and the pages it opens, the curtaininternals/gui/settings-screen
The sheetsThe card, the system cards, and every page it showsinternals/gui/sheets
Kody's packThe animation files, the manifest, the limits and the installinternals/gui/kody-pack
Buttons and gesturesWhat every input does on every screen, the haptics, the recognizerinternals/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:

  1. Run gui_info. The state reads face, because the board has started and did not come back from an app.
  2. Send tap x=205 y=300. Apps slides in from the right, and gui_info reads state=browsing and level=root.
  3. Send btn id=bot state=press, then btn id=bot state=release. Apps goes back to the face, which slides in from the left.
  4. 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 >|gesture record reads commits=1.
  5. 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