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
What puts pixels on the face

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 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:

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:

StateColour
Charging, or charge done, on the cableKUI_COLOR_CHARGE, green
20 % or under (GUI_PILE_LOW_PCT), not chargingKUI_COLOR_WARN, Bold Yellow
The critical alert: 10 % or under, off the cableKUI_COLOR_DANGER, red
Anything elseKUI_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:

InputWhat it does
A tap anywhere, Kody includedOpens Apps at its root
TOP releasedOpens Apps at its root
The pad's rightOpens Apps at its root
BOT released before 2 sSends the face to Rest
The pad's upPulls Settings down
A drag down, anywhereSettings follows the finger down
TOP heldPulls Settings down
BOT heldDraws 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 rightOpens Apps at its root, sliding in from the side the finger left
The pad's down, the pad's left, a swipe upNothing

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.

MomentWhat 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 sThe arc fills in a straight line and the words Turn off replace the date
Let go before 5 sThe 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.

DoorAsked byWhat the face does
Back from AppsBOT released, the pad's left or a swipe right, at the rootSlides in from the left as Apps goes out to the right
Into AppsA tap, TOP released, the pad's rightSlides out to the left as Apps comes in from the right
The auto-lockNo input on Apps or Settings for the lock timeComes 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:

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:

PartAt Rest
Time, date, batteryStay in colour and stay current
KodyStands still on frame 0 of his loop; the player is paused
The contentMoves 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
BrightnessSteps 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_GuiWaits 1,000 ms (KGUI_REST_TICK_MS)
A touchSwallowed 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:

InputWhat happens
TOP pressedThe 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 knockThe 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 batterySwallowed, with nothing shown. It counts as blocked in the >|gesture record
Any button, on a cableThe face wakes on the press. The release is swallowed. The log says door: rest -> face (button on a cable)
A touch on a cableThe 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 timeScenes
07:00 to 09:59desayuno, tren, parque
10:00 to 15:59tren, parque
16:00 to 19:59taller, parque, tren
20:00 to 21:59playa, azotea, camping
22:00 to 06:59madrugada, camping, azotea
Time unknownany 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.

PartName in codeValue
Timeface_timeThree 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
Dateface_datekui_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
Batteryface_pileAt (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 boxface_scene410 x 300 px at y 202 to 502 (KGUI_FACE_SCENE_H)
Arcface_off_words and the arc24 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.

SettingTypeDefaultStored in
config key="zone" (AO_Boot's)A POSIX TZ string, 32 characters at mostUTC0kset/zone
config key="clock"24h or 12h. No AM or PM is shown24hkgui/clock
config key="scene"A scene name of 1 to 15 characters a-z, 0-9, _, or autoauto, the hour pickskgui/scene
config key="kody_sleep"lock, 30s, 1m, 5m, 10m or neverlock, the auto-lock's timekgui/kody_sleep
config key="usb_awake"on or offoffkgui/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:

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).

SceneNameReactionsAnchors
Traintrenlook, hour, notice, plug, jokeevery 45 frames
Parkparquelook, hour, notice, plug, jokeevery 135 frames
Campingcampinglook, hour, notice, plug, jokeevery 135 frames
Rooftopazotealook, hour, notice, plug, jokeevery 135 frames
Late nightmadrugadalook, hour, notice, plug, jokeevery 135 frames
Breakfastdesayunolook, hour, notice, plugevery 135 frames
Workshoptallerlook, hour, notice, plugevery 135 frames
Rainlluvialook, hour, notice, plugevery 135 frames
Beachplayalook, hour, notice, plugevery 135 frames
Clockrelojlook, hour, notice, plugevery 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:

SceneAverage passLongest, loop aloneLongest, with a switch
tren74 to 79 ms79 to 88 ms153 to 160 ms
reloj59 to 63 ms60 to 98 ms131 to 137 ms
azotea72 to 77 ms74 to 107 ms147 to 148 ms
camping67 to 73 ms69 to 93 ms141 to 145 ms
madrugada56 to 57 ms58 to 59 ms116 to 117 ms
parque, desayuno, taller, lluvia, playaTBD: 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:

  1. Boot the Kode Dot. The first lit frame is the face: the time and date over Kody waking in a scene.
  2. Send config key="zone" value="CET-1CEST,M3.5.0,M10.5.0/3". The console answers >|config key="zone" ... stored=yes and the time repaints in Madrid and Paris time.
  3. Send config key="clock" value=12h. The hour reads 1 to 12, and a one-figure hour moves the colon left.
  4. Send config key="scene" value="azotea". Kody wakes in the rooftop scene. Send config key="scene" value="auto" to give the choice back to the hour.
  5. Press BOT and let go. The face dims in three steps and Kody freezes. gui_info reports state=rest and kody: asleep.
  6. 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.
  7. 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