kodeOS internals

Buttons and gestures

What every button and every touch gesture does on every kodeOS screen, the buzzes the interface plays, and the recognizer that reads a touch.

What it is

The Kode Dot has six buttons. Four form the pad: up, down, left and right. Two round buttons sit to the right of it on the front: the blue TOP button and, lower, the red BOT button. Together they are the side buttons. This page lists what each button and each touch gesture does on each screen. Every row is an action or nothing (by design), which means the system chooses to do nothing for that input.

The short version:

How it is connected

flowchart LR
btn["AO_Buttons"] -- "BUTTON_EVT_SIG<br/>BUTTON_HOLD_SIG" --> gui["AO_Gui"]
boot["AO_Boot"] -- "HOME_SIG at 0.6 s" --> gui
touch["AO_Touch"] -- "TOUCH_EVT_SIG" --> rec["recognizer<br/>kui_gesture"]
rec -- "tap, claim, rejection" --> gui
console["console: btn, tap, touch, swipe"] -. "injected" .-> gui
gui -- "kode_haptic_play()" --> hap["AO_Haptic"]
class btn ao
class boot ao
class touch ao
class gui ao
class hap ao
class console ext
From a finger or a button to an action

Where inputs come from

An injected press starts no hold, so btn id=bot state=press on the face begins the count of the red arc but never produces HOME_SIG, and btn id=bot state=hold is home alone.

A gesture only records an intent

LVGL calls a click callback inside its input processing, and a repaint there would delete objects the input device is still walking. A callback first ends its press with lv_indev_wait_release() where it must, then records an intent: a back (pending_back), a door, or a tap. The next tick applies it.

The arc counts edges, never the hold

On the face AO_Gui times BOT itself, from its press (bot_hold_ms, on khal_time_ms(), read by gui_off_arc_tick()). BUTTON_HOLD_SIG is not an edge and leaves the count running, although it disarms every armed press. From 2,000 ms (GUI_OFF_ARC_FROM_MS) the red arc fills, and at 5,000 ms (GUI_OFF_ARC_TO_MS) with BOT still down the card "Turn off?" rises. No hold on any screen locks the board.

5 s sits far past the 0.6 s home hold and 11 s below the shortest full reset the charger can make. BOT is the BQ25896's QON pin, and holding it for tQON_RST, 16 to 23 s, with no USB cuts and closes the battery path.

Specifications

Words

WordMeaning
Press, releaseThe edges of a button going down and coming up
HoldA side button down for 0.6 s (KODE_BUTTON_HOLD_MS); BUTTON_HOLD_SIG
ArmedA side button whose press AO_Gui has seen: the action waits for the release
SwallowReceive an input and do nothing with it
HomeApps' root, asked for by HOME_SIG
RestThe dimmed look of the face (internals/gui/face)
TapA short touch of one finger that never goes farther than 16 px from where it landed
Drag, swipeThe finger moves and an object follows it, or the recognizer reads a quick move in one direction
FlickA quick drag; the list goes on after the finger lifts
Settings pullA drag down on Apps from the top of the glass, or on the list when it rests at its top; Settings comes down over Apps with the finger
FolderA group of apps on the rail, filed by their first category
DetailsThe page of one app, opened by TOP held or a finger held on its row
Top bandThe first 60 rows of the glass (KGUI_TOP_BAND_PX), where the hint stands
HintA few words in the top band, for 2 s, that say why an input did nothing
ClaimThe moment the recognizer decides a press is a drag
CurtainA panel that comes down over a screen as a picture on a screen of its own
Busy sheetThe write screen and the Removing card, which answer no input. The log calls them locked
System cardA card the system raises. It ignores every input for 700 ms and takes no drag, except Turn off
DoorA request to change screen (internals/gui/overview)

The face

gui_face_navigate() reads the buttons in Gui_awake, the awake look of Gui_face.

InputWhat happens
A tap anywhere, Kody and the top band includedApps opens at its root
TOP, pressed and releasedApps opens at its root
The pad's right, pressedApps opens at its root
BOT, pressed and released before 2 sThe face goes to Rest
The pad's up, pressedSettings comes down over the face
A drag down, from anywhereSettings follows the finger down as a curtain
The pad's down, the pad's leftNothing (by design)
TOP heldSettings comes down over the face. The release that follows does nothing
BOT heldNothing at 0.6 s (HOME_SIG). From 2 s a red arc fills along the top
BOT held for 5 sThe arc is full and goes, and the card "Turn off?" rises
BOT let go between 2 s and 5 sThe arc empties in 160 ms. Nothing else happens
TOP and BOT togetherNothing. Two side buttons down arm neither
A swipe leftApps opens at its root, sliding in from the right
A swipe rightApps opens at its root, sliding in from the left
A swipe upNothing (by design). It is no tap either
No input for the kody_sleep timeThe face goes to Rest

The face at Rest

At Rest the face is dimmed. The state is Gui_rest. Nothing a person does reaches a screen from it. On the battery only TOP, pressed twice, wakes it. On a cable any button or touch wakes it at once.

InputWhat happens
TOP, pressedThe knock: the glass lights and "Press ● again" stands in the date's place. Its dot is Kody Blue, the colour of TOP, and beats once a second (KGUI_KNOCK_BEAT_HALF_MS, 495 ms each way). Left alone for 5.94 s (KGUI_PEEK_MS), the face rests again
TOP, pressed again during the knockThe face wakes on the press. The log says door: rest -> face (top pressed twice)
BOT or a pad button, pressed, on the battery"Press ● twice" in the date's place, for 5.94 s. Nothing wakes
A touch on the battery: a tap, a swipe or a dragNothing, and nothing shows. The touch counts as blocked
Any button, on a cableThe face wakes on the press, and its release is swallowed. The log says door: rest -> face (button on a cable)
A touch on a cableThe face wakes on the press. The log says door: rest -> face (touch on a cable)
A system card: a pairing, a write, its failure, a critical batteryThe card rises over the face and its arrival lights the screen
No input after a wakeThe face rests again after 60 s (KGUI_WOKEN_REST_MS)

Gui_rest records the waking button in wake_btn, and Gui_awake swallows the rest of that press (gui_wake_swallow()). Gui_rest takes every touch sample, so LVGL and the recognizer never see it, and the log writes no gesture: line. A touch that is down when the face wakes stays swallowed until the finger lifts.

Apps

The launcher is the rail of folders and apps (internals/gui/launcher). gui_navigate() reads the buttons, in Gui_browsing.

InputWhat happens
The pad's up or downThe selection moves one row and the rows roll under the focus line. No wrap; at an end the list gives a little and comes back. On an empty list, nothing
The pad's rightGoes in, as TOP does
The pad's leftGoes back: out of the open folder to the root, and from the root to the face
TOP, pressed and releasedGoes in on the selected row: a folder opens, an installed app launches, a file is written and then launched. On an empty list, Kody's door works
TOP heldThe details of the selected app open. A folder's row has none. The release that follows launches nothing
BOT, pressed and releasedGoes back, as the pad's left does
BOT heldHome at 0.6 s: an open folder closes and Apps stays at its root. Nothing fills and nothing locks
A tap on a rowGoes in on the row, as TOP does. A tap on a row whose icon is not wholly inside the rail only moves the focus to it
A finger held on a row for 0.6 sThe details of an app open. A folder's row has none, and its lift is no tap either
A tap on Kody, on the empty launcherKody's door
A vertical drag or flickThe list follows the finger one to one and glides to the row the finger's speed reaches, at most five rows on. The row on the focus line is the selection
A press on a list that movesThe list stops where it is. The click of that press acts on nothing
A drag down from the top 44 rows of the glass, or on a list at rest at its top (the Settings pull)Settings comes down over Apps with the finger. Letting go past halfway opens it, and closing it goes back to Apps
A swipe rightGoes back, as BOT does
A swipe left at the rootGoes to the face, which comes in from the right. In a folder, nothing (by design)
20 s with no input, with the QR code showingKody comes back
No input for the auto-lock time (1 min by default)The auto-lock: the face comes down over Apps

A launch is one gesture. gui_launch_selection() has two callers: gui_level_enter() on an app row, and Run again on the Stopped card. TOP and a tap on an app row launch it at once, with no card between. Remove sits behind the details and behind a question of its own, so the one act that cannot be undone asks first.

The settings screen

The control centre. gui_settings_navigate() reads the buttons, in Gui_settings.

InputWhat happens
The pad's left or rightThe ring walks the top row and stops at its ends, and on the second row goes between the battery and Turn off. On a level, it steps the brightness or the volume by 5 %
The pad's up or downThe ring walks the controls: down from Wi-Fi to the battery, from Settings to Turn off, from Bluetooth, the battery or Turn off to the brightness level, and from the brightness to the volume. Up is the same walk back. Nothing wraps
TOP, pressed and releasedThe control with the ring acts: Wi-Fi and Bluetooth switch the radio, Settings opens the Settings page, the battery opens the Battery page, Turn off asks "Turn off?". A level does nothing
TOP held, on Wi-Fi or BluetoothThe page of the radio opens. Elsewhere, nothing
BOT, pressed and releasedBack to the face. The screen goes up as a curtain
BOT heldHome: Apps' root, with no rescan
A tap on a circleThe ring moves there and the circle acts as TOP does
A finger held on the Wi-Fi or Bluetooth circleThe page of the radio opens. The lift does not switch the radio
A press or a drag on a levelThe level follows the finger and is stored when the finger lifts
A swipe up, anywhere but on a levelBack to the face. The screen follows the finger up
A drag or a flick in any other directionNothing (by design)
No input for the auto-lock timeThe auto-lock: the face comes down over Settings

A page

Wi-Fi, Bluetooth, Settings, Screen, Battery, About, Kody, Self-test and the two lists of choices. gui_sheet_navigate() and gui_sheet_body() read them.

InputWhat happens
The pad's up or downThe ring moves one row, wrapping
The pad's left or rightThe value of the row under the ring changes by one step. A row with no value does nothing
TOP, pressed and releasedThe row under the ring acts
TOP heldOn the Wi-Fi page: forgets the network under the ring, when the board remembers it. Elsewhere nothing
BOT, pressed and releasedThe page goes back one level: a page to Settings, Settings, Wi-Fi, Bluetooth and the battery's Battery page to the control centre, a list of choices to its page
BOT heldHome. The settings screen goes back to Apps' root and the page goes with it. A busy sheet ignores the hold
A tap on a rowThe ring moves to the row and the row acts
A press or a drag on a value rowThe ring moves to the row and the level follows the finger
A swipe right on a pageThe page goes back one level. A swipe that starts on the brightness bar is the bar's
A drag or a flick on the bodyA page longer than the card scrolls and settles on a whole row. On Wi-Fi and Bluetooth the switch stays at the top
A touch on the blocker while the card slidesNothing (by design)

Rows with no value, where left and right do nothing: every row of the Wi-Fi, Bluetooth, Battery and About pages, the Auto-lock row, and every choice in a list. On the Settings list, Sleep after, Kody's scene and Vibration step through their values and wrap, and the switches While charging, Header 3.3 V and Header 5 V have no step. TOP or a tap changes any of them where it stands.

The keypad

The pad moves the focus over the controls of the card, and TOP presses the control with the focus. The ring marks the focus.

InputWhat happens
The pad's up or downThe focus moves one row: the yellow pill, the Show password row, and four rows of keys. It wraps
The pad's left or rightThe focus moves one key along a row, wrapping. On the pill and the Show password row, nothing
TOP, pressed and releasedThe control with the focus is pressed
TOP heldNothing (by design)
BOT, pressed and releasedThe keypad closes back to the Wi-Fi page. What was typed is wiped
BOT heldHome, as for any card. What was typed is wiped
A tap on a controlThe focus moves to the control and the control is pressed
A drag down on the header, the top 92 rows of the cardThe card follows the finger and closes. What was typed is wiped
A drag on the field or on the keysNothing (by design)

Down from the pill or from the Show password row lands on the column where the focus last stood. The Show password row is skipped for a hidden network's name. While the join is out, the focus still moves, the pill is dimmed and no control acts.

A system card

The pad does nothing on a system card. One table serves them all, and the last column says where a card differs.

InputWhat happens
Any button or touch in the first 700 ms after the card began to riseNothing (by design). A press that began under the gate answers nothing even if its lift comes later
TOP released, on Didn't open or StoppedThe answer button acts: Try again or Run again
TOP released, or a tap, on Turn offThe red button "Turn off" confirms. On USB the button is grey and the board does not stop
BOT, pressed and releasedThe card closes. On Turn off it answers no. On the pairing code it refuses the passkey. On the write screen it does nothing
BOT heldHome, as for any card. Home closes a card that can be closed, over the face too, and the face stays the face. It is refused inside the gate, for a press that began before the card, and over the write screen
A tap on the button of Didn't open or StoppedThe answer button acts
A swipe down on Turn offAnswers no, at the lift
A drag, a swipe or a tap anywhere elseNothing (by design). The recognizer has no zone on a system card

A hold that reached its end acts once; the Ring stays full and takes no second hold until the card is closed.

The details

The details of an app open over Apps. gui_details_navigate() reads the buttons while they are up.

InputWhat happens
The padNothing (by design)
TOP releasedThe Remove pill acts, for an installed app: it asks its question. On the question, the red Remove erases the app
TOP heldNothing (by design). It opens no Settings
BOT releasedThe card closes. On the question of Remove it answers no and closes both cards
BOT heldHome: the card closes and Apps stays at its root
A tap on the pillThe pill acts, as TOP does
A drag down from anywhere on the cardThe card follows the finger down and closes. It closes when the finger lifts past halfway, or faster than 800 px/s
Any touch on the Removing cardNothing (by design). It is locked

The hold that opened the details disarmed the press it grew out of, so its release is no answer. The question takes no answer for 400 ms after it is written.

The guided tests and the self-test

During a guided factory test the operator presses what a script asks for. Gui_btn_test and Gui_tp_test swallow BUTTON_EVT_SIG, BUTTON_HOLD_SIG, HOME_SIG and every door, because AO_Buttons, AO_Touch, AO_LedMgr and AO_Display score those very presses. Every input does nothing (by design).

While a run of the Self-test page lasts, AO_Gui is in Gui_selftest on the loading screen.

InputWhat happens
TOP at the holdAfter "Now we need you", the run goes on to the rows that need hands
BOT before the hold or at itThe run is asked to stop after the check in flight and the line reads "Stopping after this check"
BOT after the holdNothing from AO_Gui. The row in flight reads the press as its answer
Any other press, a hold of either button, HOME, a doorNothing (by design)

Haptics

The interface buzzes at the moments that act, refuse and arm. The buzz is the LRA motor on the antenna board, which AO_Haptic drives. kgui_haptic_play(effect) in kgui_haptic.c is the one call every moment makes. It applies the level, keeps silent while a recording would pick the buzz up, spaces the step, and asks AO_Haptic by event with kode_haptic_play().

The setting haptic_ui sets how much of the interface buzzes: off, light or normal, default light. The Vibration row of the Settings list sets it too.

LevelWhat plays
offNothing
lightclick, thud, tick and mark
normalLight, and a step each time the focus moves, at most 14 a second (KGUI_HAPTIC_STEPS_PER_S)
EffectLengthStrengthMeans
click12 ms60 %Something acted
thud25 ms80 %A limit was reached, or an input was refused
tick6 ms40 %A pull passed the point where letting go commits, or a hold began to fill its Ring
mark18 ms70 %A hold reached its end and did something
step6 ms40 %The focus moved one place (level normal only, at least 72 ms apart)

The moments:

A buzz that is not played is counted and said at debug level with the reason: haptic: <effect> held back (<reason>). The reasons are level (the level does not play this effect), recording (the microphone or the IMU records), pace (a step came less than 72 ms after the last) and AO_Haptic is busy, no event in the pool or AO_Haptic refused it. The silence for a recording follows the rows of AO_Autotest alone: AUTOTEST_ROW_SIG raises it at the start of the audio, IMU or haptic row and the verdict lowers it. A recording nobody started through a row is not seen.

config key="haptic" is a percentage AO_Haptic applies to every gain it is asked for, the interface's included, so haptic=0 silences the interface as well. haptic_ui=off is the setting meant for the interface alone.

gui_info ends its interface line with ui: haptics <level> (silenced by <nothing|the microphone|the IMU|both>), motion <word>, awake on USB <on|off>; <n> buzzes asked, <m> held back. The bench board 6c:0a has no AW86233, so its evidence is the count. While a secret is typed, kgui writes no haptic: line for a buzz, but the counts still count.

The four rules

The rules behind every table above.

RuleMeaning
1. The pad acts at the press, the side buttons at the releaseSeeing a side button's press arms it. A hold disarms the press it grew from. A side button acts only when it has seen both edges, so a stray edge, such as electrical noise on GPIO35 that the 30 ms debounce confirmed, never acts alone. AO_Gui times no press; AO_Buttons declares the hold
2. A swallowed input disarms the press behind itPressing the other button, a finger landing, a hold, a screen leaving the glass, a curtain starting, a sheet opening, a rescan and a busy sheet's refusal all call gui_press_disarm(). On the face and Apps a pad button and the other side button disarm; on Settings only the other side button; on a sheet no button
3. A cursor that cannot move changes nothingApps refuses at its ends and stops the repeat. A ring that would wrap onto itself stays
4. Only a tap acts, and a press that travels is not a tapEvery click handler that acts asks gui_click_is_tap(): an Apps row, a settings circle, a page row, a keypad control, the answer button of a card, and Kody on the empty launcher. A press is a tap when the finger never went farther than 16 px from where it landed and the recognizer did not claim it. LVGL's own distance plays no part

A press that is not a tap acts on nothing. It logs not a tap: the press on <what> was <outcome>; nothing acts at debug level, with the recognizer's word: claim, diagonal, axis, zone or blocked.

A held button counts as input for as long as it is down. Nothing publishes while a hand rests on one, so the face, Apps, Settings and any card over them track which buttons are down (btn_held, gui_btn_track()), and the auto-lock and the rest hold their counts meanwhile.

The recognizer

Every touch sample goes through the recognizer of the kit (kui_gesture, sdk/kui) before LVGL reads it. The recognizer decides what a press is, from where the finger has gone, and it owns every door and every tap: the curtains a finger pulls or catches, the card a finger takes down by its header, the way back a swipe right makes, the Settings pull over Apps, and the tap on the face and on every row, circle and button. LVGL keeps one thing: the scroll of the list page of a card. The rail of Apps is the interface's own, so no LVGL scroll is involved.

Each sample is measured from the point where the finger landed, never from the sample before, so the answer does not depend on how often the touch sensor reports.

OutcomeRule
TapThe finger never went farther than 16 px (KUI_GESTURE_TAP_PX) from the landing, on either axis, and then lifted
ClaimThe first sample 12 px or more from the landing (KUI_GESTURE_CLAIM_PX) decides the press. The farther axis must lead the nearer one by at least 1.5:1, and the zone must take that direction. The claim keeps its axis
diagonalThe axes are under 1.5:1
axisThe zone takes other directions only
zoneThe press landed where no drag is born
blockedInput was shut when the finger landed. A touch at Rest on the battery counts here too

A rejection is noted at first and becomes the state only when the finger goes farther than 16 px, so a sloppy tap still works where nothing drags. The input device of AO_Gui starts a scroll one pixel after the claim distance, at 13 px, so the recognizer has decided before a scroll can begin.

gui_gesture_zone() says what a press may become. It reads the screen, the card over it and the landing point on the press edge, once.

Where the press landsDirections it takesWhat a claim is for
A guided test, or the locked card of a running writeNone: blocked-
A curtain, while its spring settlesUp and downA catch: close, settings or unlock
A curtain a finger holds, or one that has landed and waitsNone: blocked-
The Turn off cardDowndismiss: a swipe down answers no, at the lift
Any other system cardNone-
A card that a finger or a spring has, or that slidesNone: blocked-
A settings pageRightback: one level. A press on the brightness bar takes no drag
A card's header (top of the card to the foot of the title disc; the keypad's 92 rows; all of the details)Downsheet: the card follows the finger
A card, anywhere elseNoneA list page scrolls under LVGL
The faceDownsettings: the curtain. A swipe up or sideways is zone
The face at RestNone: Gui_rest swallows the touch firstThe press counts as blocked
Apps, with rowsUp and down; right, and left at the rootscroll, the Settings pull for down from the top 44 rows or when its conditions hold, and level sideways: back
Apps, while the rows slide, or with no rowsNone; right onlyBack to the face
The settings screenUpclose

The top band. The first 60 rows of the glass are the top band. The corners are cut away, so at each row the band starts where the glass does. The band draws nothing of its own and takes no press: the content may stand in it, and a tap there is the content's tap. The hint stands in it. The face's drag down starts anywhere on it, and over a card a drag down from the band is the card's.

At the end of every press AO_Gui logs one line at debug level (gui_gesture_end()), such as gesture: claim up scroll at 205,400, 0,-180 px or gesture: tap at 205,300, 0,0 px. While a secret is typed, every gesture line is written without coordinates. The list logs its own lines at INFO level: gesture: caught the list at <p>, gesture: list released at <v> px/s -> row <r>. A curtain logs gesture: caught the curtain at <p>% and gesture: release at <p>% <v> px/s -> open|back, and a card logs gesture: card release at <p>% <v> px/s -> stay|close.

The counters. gui_info reports >|gesture. The counters count presses since boot and stop at 65535:

Gotchas

Example code

Walk the screens with the buttons:

  1. Boot the Kode Dot. The face shows. Press TOP and let go. Apps slides in from the right.
  2. Press BOT and let go. The face slides back in from the left. Press the pad's up button. Settings comes down over it. Press BOT to close it.
  3. Press the pad's right button. Apps opens. Move the pad's down button until an app is in focus, then hold TOP for a moment. The details of the app open. Press BOT to close them.
  4. On the face, press BOT and let go. The face dims. Press BOT: "Press ● twice" shows. Press TOP, then TOP again, to wake the face.
  5. On the face, press and hold BOT. From 2 s a red arc fills along the top, and at 5 s "Turn off?" rises. Press BOT to answer no.

In the SDK