# 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

```mermaid
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 time (`face_time`).
- The date (`face_date`).
- The battery (`face_pile`).
- The scene box (`face_scene`), where `ui_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 reads `Time not set`.
- **The time was restored from NVS after a power cut.** The colon is Medium Grey until a time server answers or `time set` runs.

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_sleep` time.** 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 to `never` it 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:

- `zone` refuses 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`.
- `scene` answers `>err code=5 msg=value` for 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

> **NOTE**
>
> **The time is an hour or more off.** The zone is UTC, the default when nothing is stored or when the stored zone is not a valid one. Pick a city in More, under Time zone, or run `config key="zone" value="CET-1CEST,M3.5.0,M10.5.0/3"`.

> **NOTE**
>
> **BOT and the pad do not wake a dim face.** At Rest only TOP, pressed twice, wakes the face, and the other buttons show "Press ● twice". On the battery a touch is swallowed and shows nothing. Set `kody_sleep` to `never`, or turn on While charging while a cable is in, to keep the face awake.

> **NOTE**
>
> **A swipe on the face does not change Kody's scene.** A swipe sideways opens Apps, and a swipe up and the pad's left do nothing. Pick the scene on the Settings list, under Kody's scene, or with `config key="scene"`.

> **WARNING**
>
> **A Lottie keeps drawing under a card.** `lv_lottie` asks whether its widget is visible, and a card on `lv_layer_top()` hides nothing from it. The face pauses Kody when a card rises and when the face leaves the glass. Any other task that drives a Kody player needs the same pauses and the same stack room: ThorVG's `rleRender` alone takes 18,368 bytes in one frame.

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

- [`kgui_screen_face.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/screens/kgui_screen_face.c): the face's widgets, the three-label clock, the battery and the arc.
- [`kgui_ao.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/kgui_ao.c): `Gui_face`, `Gui_awake` and `Gui_rest`, the zone, the scene choice, the hour's pick and Kody's player.
- [`kgui_rest.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/kgui_rest.c): the numbers of Rest and the cycle of its shift.
- [`components/kui/kody`](https://github.com/kodediy/kode-sdk/tree/main/components/kui/kody): the `kui_kody` player and the built-in `reloj` scene.
