kodeOS internals
Face and lock screen
The screen a Kode Dot boots to and returns to: the clock, the doors to Apps and Settings, Rest, the red arc, and Kody's scenes.
What it is
The face is the lock screen of kodeOS, and it is home. It shows the time and the date at the top left, the battery at the top right, and under them Kody, the Kode Dot's character, playing a scene. Apps is beside it and Settings comes down over it.
The board lands on the face after every boot except one: a boot that comes back from an app lights Apps, with that app in focus, and shows no face. Going back from Apps' root and the auto-lock bring you back to the face.
The face has two looks. Awake is the face as you use it. Rest is the same face, dimmed, with Kody standing still; the board goes there when nobody uses it.
Nothing on the face scrolls, so every drag is a gesture. A tap anywhere on the face, TOP, the pad's right or a swipe to either side opens Apps. The pad's up, TOP held or a drag down brings Settings over it, and BOT released sends the face to Rest. A card rises over the face only when the system needs one: a write, a failed write, a pairing code, an app that stopped, a critical battery or Turn off.
How it is connected
flowchart LR wall["khal wall clock<br/>UTC"] --> paint["gui_face_time_paint()"] zone["kset/zone<br/>POSIX TZ, AO_Boot"] --> paint paint --> face["screen_face<br/>time, date, battery, scene box"] power["AO_Power<br/>POWER_EVT_SIG"] --> pile["gui_face_pile_paint()"] pile --> face pack["Kody's pack<br/>SD, FAT or built-in"] --> assets["kgui_assets.c"] assets --> kody["kui_kody player<br/>410 x 300"] kody --> face class zone store class pack store
Gui_face in kgui_ao.c is the state that shows the screen, with two children: Gui_awake and Gui_rest. kgui_screen_face.c builds the screen from the kit and LVGL alone. It has five parts:
- The time (
face_time). - The date (
face_date). - The battery (
face_pile). - The scene box (
face_scene), whereui_build()puts Kody's player. - The red arc that the held BOT draws along the top of the glass.
The face has no handle: nothing painted says what a button or a swipe does. Nothing is anchored to a corner, because the glass has a radius of 114 px.
Local time
The board keeps UTC and the face shows local time. gui_local_tm() turns the wall clock into a struct tm with localtime_r() through a POSIX TZ string held in the environment. The zone is AO_Boot's setting zone (kset/zone), applied once at boot before an app runs (sdk/kaos/boot). A missing key, or one that is not a POSIX TZ string, means UTC.
You pick the zone from the Time zone row of More (internals/gui/settings-screen) or set it with config key="zone", which refuses a string that is not a POSIX TZ zone. A change applies at once, is stored, repaints the time and may move the hour into another scene's window.
The FAT driver stamps files in the same local time, since it calls localtime_r() on whichever task writes.
The time
The time repaints when the minute changes. The face has no timer of its own. Every tick of AO_Gui compares the local minute with the one on screen and paints only when they differ, or when the "approximate" mark changes.
The time is three labels: the hour, the colon and the minute. They start at x 40 and y 49 and each one begins where the one before ends, 3 px closer than the digits' advance. The clock font has no tabular figures, so the colon moves when the hour changes width. In the 12 h clock a one-figure hour is one character, and the colon follows it. A label is rewritten only when its text changes.
While the time is only approximate (the time put back after a power cut), the colon is Medium Grey.
The date and the unknown time
The date is built from two name tables in kgui, not strftime(), which follows a locale this board never sets. It reads the day, the date and the month, such as Thu 5 Nov, with no word for the zone or for an approximate time. Two cases change what the face reads:
- The wall clock is unknown. The time reads
--:--at 35 % and the date readsTime not set. - The time was restored from NVS after a power cut. The colon is Medium Grey until a time server answers or
time setruns.
Two things take the date's place. While the red arc is up, the words Turn off stand there. After the first press of TOP at Rest, Press ● again stands there: the two words in Outfit Regular 28, Light Grey, with a Kody Blue dot of 20 px between them (KGUI_FACE_KNOCK_DOT_PX), 10 px from each word. A press of BOT or the pad at Rest on the battery puts Press ● twice there.
The battery
The battery is a small pile in the top right corner, inside the glass curve. A bolt stands on it while the cable is in. It is hidden until AO_Power has published a cell voltage, and it does not show while the panel is asleep. Its colour follows the state:
| State | Colour |
|---|---|
| Charging, or charge done, on the cable | KUI_COLOR_CHARGE, green |
20 % or under (GUI_PILE_LOW_PCT), not charging | KUI_COLOR_WARN, Bold Yellow |
| The critical alert: 10 % or under, off the cable | KUI_COLOR_DANGER, red |
| Anything else | KUI_COLOR_TEXT_MUTED, Medium Grey |
What the inputs do
gui_face_navigate() is the map of the buttons of the awake face, and gui_gesture_zone() is the map of the touch. The full table is on internals/gui/buttons-and-gestures. In short:
| Input | What it does |
|---|---|
| A tap anywhere, Kody included | Opens Apps at its root |
| TOP released | Opens Apps at its root |
| The pad's right | Opens Apps at its root |
| BOT released before 2 s | Sends the face to Rest |
| The pad's up | Pulls Settings down |
| A drag down, anywhere | Settings follows the finger down |
| TOP held | Pulls Settings down |
| BOT held | Draws the red arc from 2 s, and raises "Turn off?" at 5 s. Let go after 2 s and nothing else happens |
| A swipe left or right | Opens Apps at its root, sliding in from the side the finger left |
| The pad's down, the pad's left, a swipe up | Nothing |
A side button arms on the press and acts on the release. Two side buttons down arm nothing, and the log says two side buttons down; nothing armed. A hold disarms the press it grew out of.
A tap on Kody is a tap anywhere: it opens Apps and does not make him look.
The red arc and Turn off
BOT held on the awake face draws a red arc along the top of the glass. It starts at the top left corner, crosses the straight and ends at the top right corner. The line is 24 px wide (KGUI_OFF_ARC_W) over a dark red track.
| Moment | What happens |
|---|---|
Before 2 s (GUI_OFF_ARC_FROM_MS) | Nothing. At 0.6 s the hold is home (HOME_SIG), which the face answers with nothing |
| 2 s to 5 s | The arc fills in a straight line and the words Turn off replace the date |
| Let go before 5 s | The arc empties in 160 ms (GUI_OFF_ARC_EMPTY_MS) and nothing else happens. The log says turn off: let go at <n> of 1000; the arc empties |
5 s (GUI_OFF_ARC_TO_MS) | The arc goes, the hold is spent, a mark buzz plays and the card "Turn off?" rises over the face (internals/gui/sheets) |
kgui_off.c holds the numbers of the arc with no LVGL in it, so a host test walks them. No hold locks the board on any screen: the face is the lock, and on the face the arc takes the hold.
The face and Apps
Apps opens over the face in one of two motions, picked by the input, and goes back to the face the same way reversed. A tap or TOP grows Apps out of the middle of the glass, in place: a fade, the curtain face grow, and the way back takes it into the middle again. The pad's right and a swipe slide the two past each other sideways, as kshell's pages do (the curtain face slide, GUI_PAGE_ANIM_MS), easing out.
The auto-lock is a different motion: the face comes down over Apps or over Settings like a curtain.
| Door | Asked by | What the face does |
|---|---|---|
| Back from Apps | BOT released, the pad's left or a swipe right, at the root | Slides in from the left as Apps goes out to the right |
| Into Apps | A tap, TOP released, the pad's right | Slides out to the left as Apps comes in from the right |
| The auto-lock | No input on Apps or Settings for the lock time | Comes down, aimed by a spring |
The two slides are one kind of curtain: the clock, not a spring, runs them, nothing dims and a finger is not followed. A press, a hold, home or a card that arrives during a motion lands the curtain at once, and the state it lands in handles the event.
Settings comes down over the face and closes back to it. Home over Settings goes to Apps' root.
Rest
Rest is the face dimmed. Time, date, battery and Kody's scene stay in colour; Kody stands still on frame 0 of his scene's loop, his player paused, and nothing rasterises. The board goes to Rest when nobody uses it. A sibling state of Gui_awake, Gui_rest is a look of the same screen: no curtain moves and no screen loads.
Two things send the awake face to Rest, both through the door GUI_REST_SIG:
- BOT pressed and let go before 2 s. The release posts the door. For a second after a wake (
GUI_REST_AFTER_WAKE_MS) it is ignored, so the press that woke the face cannot send it straight back. - No input for the
kody_sleeptime. The count starts when the face is entered and restarts on every button, hold and touch. It holds while a finger or a button is down, a card is up, a write runs, a curtain moves or a look of Kody waits or plays. With the time set toneverit posts nothing.
The awake face takes the door only when it is bare: the face is the active screen, no card is up, no finger is down, no button is held and no write runs. Otherwise it stays awake.
While usb_awake is on and a cable is in, the count never posts the door, but BOT still sends the face to Rest. The pad's down does nothing on the face, so a press in passing never dims it.
What Rest changes:
| Part | At Rest |
|---|---|
| Time, date, battery | Stay in colour and stay current |
| Kody | Stands still on frame 0 of his loop; the player is paused |
| The content | Moves one place along a cycle of nine each minute, 4 px at most from home (KGUI_REST_SHIFT_PX), so an AMOLED panel never keeps the same edges lit |
| Brightness | Steps down to 100 %, 60 % and 30 % of the stored level, 90 ms apart (KGUI_SETTLE_STEP_MS). The stored level does not change and a step is never below 1 % |
| The tick of AO_Gui | Waits 1,000 ms (KGUI_REST_TICK_MS) |
| A touch | Swallowed whole on the battery. On a cable its press wakes the face |
The cycle starts at home and visits the eight places round it in a ring, so the content creeps and never jumps. While the board has never been told the time, the cycle counts the board's own minutes.
On the battery only TOP, pressed twice, wakes the face. On a cable any button or touch wakes it at once. At Rest:
| Input | What happens |
|---|---|
| TOP pressed | The knock: the glass lights, Kody plays again, and "Press ● again" stands in the date's place with its dot under the hour's colon. The dot is Kody Blue, the colour of TOP, and beats once a second between full and 60 % (KGUI_KNOCK_BEAT_HALF_MS, KGUI_KNOCK_BEAT_LOW_OPA). Left alone for 5.94 s (KGUI_PEEK_MS), the face rests again |
| TOP pressed again during the knock | The face wakes on the press. Its hold and its release do nothing |
| BOT, the pad, on the battery | "Press ● twice" stands in the date's place for 5.94 s. Nothing wakes |
| A touch on the battery | Swallowed, with nothing shown. It counts as blocked in the >|gesture record |
| Any button, on a cable | The face wakes on the press. The release is swallowed. The log says door: rest -> face (button on a cable) |
| A touch on a cable | The face wakes on the press, and the touch is no tap on the face. The log says door: rest -> face (touch on a cable) |
| A system card (a pairing, a write, its failure, a critical battery) | The card rises over the face and its arrival lights the screen. When the card closes the face stays awake |
Waking puts everything back: the hint goes, the content returns home, the brightness ramps back to the stored level and Kody goes on from the frame he stands on. A face that was woken and left alone rests again after 60 s (KGUI_WOKEN_REST_MS). The first button, hold or touch ends that count, and from then on kody_sleep counts.
Kody on the face
Kody is one kui_kody player, 410 x 300 px, created once in the scene box and kept for the whole boot. The player takes a 492,000-byte buffer of PSRAM on its first play.
A scene has a wake-up and a loop: the last frame of the wake-up is frame 0 of the loop. A scene can also list files for five triggers: look (back on the face, once a minute at most), hour (on the hour, or when the time becomes known), notice (he reads a notice), plug (the cable) and joke (on waking, back on the face, and every 2 to 3 minutes). An anchor is a frame of the loop that shows the pose of frame 0, and a look starts and ends on an anchor.
Choosing a scene. The row Kody's scene of the Settings list and config key="scene" value="<name>" pick a scene by name; value="auto" gives the choice back to the hour. A swipe or the pad changes no scene. Both end in gui_face_scene_choose(), which stores the name in kgui/scene, logs scene: <name>, chosen (<what asked>) and, if the scene is not the one on the glass, wakes Kody in it with a cut.
The hour picks while nothing is chosen. Every hour kgui draws a scene at random from the list for that time of day, never the one on the glass, and keeps it until the hour turns. The draw is stored in kgui/scene_hr, so a reset inside the same hour wakes in the same scene, and the log says face: drew <scene> for the hour.
| Local time | Scenes |
|---|---|
| 07:00 to 09:59 | desayuno, tren, parque |
| 10:00 to 15:59 | tren, parque |
| 16:00 to 19:59 | taller, parque, tren |
| 20:00 to 21:59 | playa, azotea, camping |
| 22:00 to 06:59 | madrugada, camping, azotea |
| Time unknown | any of the nine scenes but reloj |
lluvia and reloj are never drawn by the hour; only a choice shows them. A pack with none of these scenes falls back to its own face key: its windows of local hours, then its default.
The change comes at the hour: on the first tick of each hour with the face seen, when the face is seen again, and when a new zone, time set or value="auto" moves the answer. It never interrupts a look, and a look delays the change by about 3 s at most. A name in kgui/scene that the active pack lacks leaves the hour in charge and keeps the key for a pack that has the scene.
The wake-up. gui_face_kody_rouse() plays the scene's wake-up once and queues its loop right after it. At boot the pass that plays the wake-up draws the first frame of the face, so the panel lights on it. The wake-up waits for the pack for KGUI_ASSETS_WAIT_MS at most, then Kody wakes in the built-in scene and moves to the pack's when it is read.
Looks. Kody looks at you when the face comes back from Apps or from Settings. Nothing on the face asks for a look. The tick answers with one file of the scene's look list, picked at random and read when the trigger fires. It plays at the next anchor and returns to the loop there. A scene with no anchors or no look file has no look. Every scene of the official pack has one.
Kody plays only when he can be seen. lv_lottie stays visible under a full-glass card, so the face pauses him when it leaves the glass and when a card covers it. He resumes from the same frame when LVGL reports the face loaded or the card goes. A curtain that lands on the face loads it before the state's entry, so the entry marks him as seen itself. A slide therefore moves a picture of Kody and runs no rasteriser.
Specifications
The screen
The parts of the face and where each one stands.
| Part | Name in code | Value |
|---|---|---|
| Time | face_time | Three labels (face_h, face_colon, face_m) from x 40 (KGUI_FACE_HOUR_X), digits starting at y 49 (KGUI_FACE_HOUR_Y). kui_font_clock: DM Sans Bold, 104 px. Light Grey |
| Date | face_date | kui_font_body, Outfit Regular 28 px, Medium Grey, from x 40, y 138 (KGUI_FACE_DATE_Y), cut with dots at 330 px. Example Thu 1 Oct |
| Battery | face_pile | At (338, 49): a body 31 x 20 px (KGUI_FACE_PILE_W, _H) with a 2 px stroke, a terminal 3 x 7 px and a fill up to 23 px wide |
| Scene box | face_scene | 410 x 300 px at y 202 to 502 (KGUI_FACE_SCENE_H) |
| Arc | face_off_words and the arc | 24 px line, corners of 160 px (KGUI_OFF_CORNER_PX), a straight of 182 px (KGUI_OFF_LINE_PX) |
| Unknown time | --:-- at 35 %, date Time not set |
The settings
The config keys the face reads.
| Setting | Type | Default | Stored in |
|---|---|---|---|
config key="zone" (AO_Boot's) | A POSIX TZ string, 32 characters at most | UTC0 | kset/zone |
config key="clock" | 24h or 12h. No AM or PM is shown | 24h | kgui/clock |
config key="scene" | A scene name of 1 to 15 characters a-z, 0-9, _, or auto | auto, the hour picks | kgui/scene |
config key="kody_sleep" | lock, 30s, 1m, 5m, 10m or never | lock, the auto-lock's time | kgui/kody_sleep |
config key="usb_awake" | on or off | off | kgui/usb_awake |
The Settings list shows kody_sleep as Sleep after, scene as Kody's scene, and usb_awake as While charging (internals/gui/sheets).
Refusals and edge cases:
zonerefuses a value that is not a POSIX TZ string with>err code=5 msg=value, and the console refuses more than 32 characters with>err code=3 msg=value.sceneanswers>err code=5 msg=valuefor a name the pack lacks or a name that cannot be one.- A stored zone that is not a POSIX TZ string, left by an older build, is ignored at boot and the face shows UTC.
- With
lock, the rest time is the auto-lock time chosen on the Settings list.
The scenes of the official pack
The ten scenes, in the pack's order. The first column is the name the Settings list shows (kgui_scene_label.c).
| Scene | Name | Reactions | Anchors |
|---|---|---|---|
| Train | tren | look, hour, notice, plug, joke | every 45 frames |
| Park | parque | look, hour, notice, plug, joke | every 135 frames |
| Camping | camping | look, hour, notice, plug, joke | every 135 frames |
| Rooftop | azotea | look, hour, notice, plug, joke | every 135 frames |
| Late night | madrugada | look, hour, notice, plug, joke | every 135 frames |
| Breakfast | desayuno | look, hour, notice, plug | every 135 frames |
| Workshop | taller | look, hour, notice, plug | every 135 frames |
| Rain | lluvia | look, hour, notice, plug | every 135 frames |
| Beach | playa | look, hour, notice, plug | every 135 frames |
| Clock | reloj | look, hour, notice, plug | every 45 frames |
When no pack is usable the face has one scene, the built-in reloj with its look, compiled into the image. With one scene there is nothing to choose between. The pack's format and limits are on internals/gui/kody-pack.
What a pass costs
With log_level tag="kgui" level=d, every 15 s of play logs face: the <scene> scene played 15.0 s: <n> passes drew, <a> ms average, <m> ms longest.
Measured on a Kode Dot with the dock console in the build, the passes of a window add up to more than 13 of its 15 s: AO_Gui draws nearly all the time the face is seen. What that costs the battery is TBD: a measurement that the bench cable cannot make. At Rest nothing rasterises.
Pass times per scene:
| Scene | Average pass | Longest, loop alone | Longest, with a switch |
|---|---|---|---|
tren | 74 to 79 ms | 79 to 88 ms | 153 to 160 ms |
reloj | 59 to 63 ms | 60 to 98 ms | 131 to 137 ms |
azotea | 72 to 77 ms | 74 to 107 ms | 147 to 148 ms |
camping | 67 to 73 ms | 69 to 93 ms | 141 to 145 ms |
madrugada | 56 to 57 ms | 58 to 59 ms | 116 to 117 ms |
parque, desayuno, taller, lluvia, playa | TBD: not measured |
A switch is a change of animation inside lv_timer_handler(), such as the hand-over from wake-up to loop or a look starting on an anchor.
The average varies by 3 to 4 ms between boots: the RISC-V port pins AO_Gui to the core of its first FPU use, and the boots that land slower leave 132 bytes less free on the stack.
Gotchas
Example code
Check the clock, the zone and Rest on a Kode Dot. Each step ends with something you see:
- Boot the Kode Dot. The first lit frame is the face: the time and date over Kody waking in a scene.
- Send
config key="zone" value="CET-1CEST,M3.5.0,M10.5.0/3". The console answers>|config key="zone" ... stored=yesand the time repaints in Madrid and Paris time. - Send
config key="clock" value=12h. The hour reads 1 to 12, and a one-figure hour moves the colon left. - Send
config key="scene" value="azotea". Kody wakes in the rooftop scene. Sendconfig key="scene" value="auto"to give the choice back to the hour. - Press BOT and let go. The face dims in three steps and Kody freezes.
gui_inforeportsstate=restandkody: asleep. - Press BOT. "Press ● twice" shows in the date's place. Press TOP: "Press ● again" shows and its blue dot beats. Press TOP again. The face wakes and the brightness ramps back.
- Press and hold BOT. Nothing happens for 2 s, then a red arc fills along the top. Let go before 5 s and the arc empties.
In the SDK
kgui_screen_face.c: the face's widgets, the three-label clock, the battery and the arc.kgui_ao.c:Gui_face,Gui_awakeandGui_rest, the zone, the scene choice, the hour's pick and Kody's player.kgui_rest.c: the numbers of Rest and the cycle of its shift.components/kui/kody: thekui_kodyplayer and the built-inrelojscene.