SDK

kui, the UI kit

The Kode Dot's UI kit on LVGL: kshell's colours, type and line glyphs, the focus ring, six components, the glass, touch maths, a keypad, and Kody.

What it is

kui is how an app gets the look of the Kode Dot. It is written in plain C on top of LVGL 9.6, the graphics library that draws the interface. It holds everything the system's own screens are made of:

The kit also holds the maths a touch screen needs, in plain C with no LVGL: the shape of the glass, a recogniser that tells a tap from a swipe, a page that follows a finger and lands on a spring, and the text rules of a phone-style keypad. They are host-tested, and any tool can use them.

The look is kshell's, Luismi's shell for the Kode Dot: a black page, dark rows, one Bold Yellow ring for the cursor, Outfit for words and DM Sans for numbers. The tokens, the type and the line glyphs come from there.

kui depends on LVGL and on three SDK layers, kbsp, khal and klog, and on nothing above them. A firmware with no Active Objects draws with it exactly as kodeOS does, and an app that does not draw never takes LVGL into its image.

The display itself (LVGL on the panel, the flush, the touch feed, the render pass) belongs to sdk/kscreen. An app that names kscreen gets kui with it, and kscreen_open() calls kui_init().

How it is connected

flowchart LR
app["Your code<br/>or kscreen"] --> kui["kui"]
kui --> lvgl["LVGL 9.6"]
subgraph sdk["SDK layers"]
  kbsp["kbsp"]
  khal["khal"]
  klog["klog"]
end
kui -- "glass size" --> kbsp
kui -- "PSRAM heap,<br/>ROM inflate" --> khal
kui -- "refusals" --> klog
kui -. "kui_pack.c only" .-> cjson["cJSON"]
class app app
What kui uses

One thread, no lock

Everything in the kit runs on the one thread that drives LVGL, so it needs no lock and has none.

kui_init() once

kui_init(disp) puts LVGL's simple theme on the display and initialises the shared styles. Call it after lv_init() and after the display exists, before any style or component. A second call does nothing.

You pay flash for what you name

The kit has no table of names. Every font, every image and every animation is its own object file, and the linker keeps only what code names. The shared styles name seven of the eighteen faces (caption, body, button, name, title, heading, code), and six of those fall back on a digits face, so kui_init() links those seven and the four digits faces into any firmware that calls it. The clock face, each glyph, each animation and the player itself are linked only when something names them.

LVGL's heap is in PSRAM

The board file selects LVGL's custom allocator (CONFIG_LV_USE_CUSTOM_MALLOC=y), and kui_mem.c provides the hooks, so naming kui is all an app does. The hooks take PSRAM through khal and never fall back to internal RAM: a widget tree that does not fit returns NULL to LVGL and leaves the pool that DMA buffers and task stacks need alone. Blocks are aligned to 8 bytes.

An app that picks another allocator in its sdkconfig.defaults gets a kui_mem.c that compiles to nothing. CMake names one hook to the linker (-u lv_malloc_core), because libkui.a reaches the linker before liblvgl.a asks for it.

Build changes kui makes to LVGL

Three changes:

Faces and glyphs follow the brand

Words are Outfit and numbers are DM Sans. An Outfit face leaves out the digits 0 to 9 and falls back on the digits face of its own size, so LVGL draws each digit from DM Sans on the baseline of the line. The signs between digits (colon, point, percent, degree) stay Outfit.

The glass has round corners

The panel is 410 x 502 behind a cover glass with 114 px corners, so anything inside a corner square but outside its circle is under the silk screen and never seen.

kui_panel_check() walks an object tree and reports whatever draws under a corner.

Kody

The player draws Lottie animations with lv_lottie over ThorVG on the LVGL thread. The kit compiles one scene in, the clock scene, as the floor a board falls back on.

Every other animation belongs to Kody's pack, a flat folder /kode/ on the microSD card or the internal FAT that holds a manifest kode.json and the Lottie files it names. The pack reader in the kit parses and checks; the orchestration that finds the pack and loads it lives in kgui, above the SDK.

Specifications

The files

The kit's files, in five groups. This group is the core: the components, the styles, the tokens and the allocator.

File or folderWhat it does
core/include/kui.hThe one header an app includes: the components, the shared styles, the faces and the images. Includes the glass, panel and token headers
core/include/kui_glyphs.hThe declarations of the 43 line glyphs. Generated by tools/glyphgen.py; never edited by hand
core/include/kui_tokens.hThe tokens as KUI_* macros. Generated from core/assets/tokens.json by tools/gen_tokens.py; never edited by hand
core/src/kui.ckui_init()
core/src/kui_styles.cThe shared styles, 29 of them
core/src/kui_flex.cThe flex box every layout is built from
core/src/kui_pill.c, kui_cat_pill.c, kui_sheet.c, kui_level.c, kui_swatch.c, kui_lamp.cOne component each
core/src/kui_mem.cLVGL's allocator hooks on the PSRAM heap
core/src/kui_internal.hWhat kui's own files share; an app never sees it

The files: glass, panel and touch maths

The shape of the glass, the corner check and the three touch modules.

File or folderWhat it does
glass/include/kui_glass.h, glass/src/kui_glass.cThe shape of the glass as plain integers
panel/include/kui_panel.h, panel/src/kui_panel.cThe corner check and the desktop bezel
drag/include/kui_gesture.h, drag/src/kui_gesture.cThe recogniser: tap, claimed drag, or a named rejection
drag/include/kui_spring.h, drag/src/kui_spring.cA damped spring in closed form
drag/include/kui_drag.h, drag/src/kui_drag.cA page that follows a finger along one axis and lands on a spring
multitap/include/kui_multitap.h, multitap/src/kui_multitap.cThe phone keypad's text rules
test_dot/include/kui_test_dot.h, test_dot/src/The E5 test's operator screens and the full-glass patterns its camera photographs: kui_td_screen_build(), kui_td_pattern_build() and kui_td_grid_set(). Each call paints one 410 x 502 canvas on its parent, with no text. Compiled only with CONFIG_KODE_TEST_DOT (sdk/kaos/test-dot)

The files: Kody and his pack

The player, the built-in scene and the pack reader.

File or folderWhat it does
kody/include/kui_kody.h, kody/src/kui_kody.cThe Kody player on lv_lottie
kody/include/kui_kody_anchor.h, kody/src/kui_kody_anchor.cAnchors and crossings, as plain integers
kody/include/kui_kody_anims.hThe three built-in animations, one descriptor each. Generated
kody/src/kody_reloj.c, kody_despierta_reloj.c, kody_reloj_te_mira.cThe built-in clock scene, its wake-up and its look. Generated by tools/kodygen.py, compiled only with Lottie
kody/assets/The sources: 18 Lottie files and the manifest kody.json
kody/pack/The official pack: kode.json and 69 .json.z files. Generated
pack/include/kui_pack.h, pack/src/kui_pack.cThe reader of Kody's pack: the manifest, the limits and the checks. The one file that includes cJSON

The files: fonts and images

The generated faces and images.

File or folderWhat it does
fonts/src/The 18 faces, one generated kui_font_*.c each (eight Outfit, ten DM Sans); sources and licences in fonts/assets/
images/src/The 57 images, one generated kui_glyph_*.c or kui_wordmark.c each; PNG sources in images/assets/

The files: build, tools and simulator

The build configuration, the generators, the simulator and the component's own files.

File or folderWhat it does
thorvg/kui_tvg_config.hThorVG's build configuration for the firmware, forced into LVGL's C++ sources
tools/gen_tokens.py, fontgen.sh, glyphgen.py, imagegen.sh, kodygen.py, tools/tests/test_kodygen.pyThe generators, and the tests of the Kody one
sim/The desktop simulator: main.c, device.c, device.h, keys.c, keys.h, kui_sim.h, lv_conf.h, CMakeLists.txt, embed.cmake, README.md and the picture of the Kode Dot
README.md, CMakeLists.txt, idf_component.ymlThe component's guide, build and manifest (LVGL 9.6.0~1, cJSON ^1.7.19)

The components

Each call returns an LVGL object that already wears the kit's styles. You place it, fill it and listen to its events. Every object carries a name with its own counter, for example pill_3. The six components, and the flex box under them:

CallReturnsWhat it is
kui_flex_create(parent, flow, gap, grow, main_place, cross_place, track_place)A transparent flex boxThe one container every layout uses: no padding, no theme background or border, sized to its content until you set a size, scrollbar in kui_style_scrollbar
kui_pill_create(parent, title, subtitle)An lv_button named pill_#The row: a full-width capsule with a round slot, a name and a line under it. Children: fill (a level bar, hidden), slot (the disc, holding hidden slot_img and slot_text), name, sub. Pressed it wears kui_style_pill_pressed; with LV_STATE_USER_1, kui_style_pill_off; checked, kui_style_ring
kui_cat_pill_create(parent, title)An lv_button named cat_pill_#A category tab: an outlined capsule, filled when checked
kui_sheet_create(parent)A transparent root named sheet_#A card that covers the glass (410 x 502 at 0, 0, radius KUI_RADIUS_GLASS). Every child is created empty and, apart from the header, hidden. Children and places: see below
kui_level_create(parent, colour)An lv_slider named level_#A level the finger drags from 0 to KUI_LEVEL_MAX (100): a track, a fill in colour and a knob, a pill's width until you set another. Sends LV_EVENT_VALUE_CHANGED. kui_level_set_colour() repaints the fill
kui_swatch_create(parent, colour)An lv_obj named swatch_#A round colour to tap; LV_STATE_CHECKED rings it as it rings a pill
kui_lamp_create(parent, diameter)A holder named lamp_#A disc, child disc, in a holder padded by the glow's reach so the parent never cuts the glow. kui_lamp_set(lamp, colour, on) lights it or turns it off

A sheet carries at most one answer button. Its children, all of card:

The header disc is at (32, 56), the body at (32, 188) and the answer button at y 395.

The macros KUI_RADIUS_GLASS (the glass's corner radius, from kbsp), KUI_OPA_PCT(p) and KUI_LEVEL_MAX come with kui.h.

The shared styles

All are extern lv_style_t kui_style_*, initialised by kui_init(). They come in five groups:

GroupStyles
Pagescreen (the page, Light Grey type, the body face), scrollbar, scrollbar_hidden
Pillpill, pill_pressed, pill_off, pill_name, pill_sub, slot, slot_text, fill
Cursorring (on the page), ring_dark (on a light surface)
Sheetsheet_card, handle, handle_on_bg, hdr_disc, hdr_text, hdr_title, hdr_sub, hdr_title_on_bg, sheet_line, sheet_line_c, sheet_note, sheet_code
Action buttonaction, action_text, action_pressed, action_ring

The focus ring is the cursor of the pad. kui_style_ring is an outline KUI_RING_W (4 px) wide and KUI_RING_GAP (4 px) off the object, outside it, in KUI_COLOR_FOCUS (Bold Yellow). It rings a checked pill, a swatch, a circle or a row, and kgui leaves 8 px of room round a ringed object so its container never cuts the ring (gui_ring_room()). ring_dark is the same outline in Soft Black for an object on a light surface, and action_ring is the ring on the answer button of a sheet.

The faces

All are 4 bits per pixel. The brand sets words in Outfit and numbers in DM Sans, and the kit follows that rule in the faces themselves. Eight faces are Outfit, in three weights. Ten are DM Sans Bold: the clock, the codes, the two big figures and six faces that hold the digits 0 to 9 and nothing else. kui_font_caption and kui_font_code also hold the four arrows, for a hint that spells a code with them. TEXT is ASCII less 0-9; LATIN1 adds 0xA0-0xFF, so an accent in an app's name has a glyph. The eighteen faces:

FaceTypeface and sizeHoldsFalls back on
kui_font_captionOutfit Regular 22LATIN1, and ← ↑ → ↓kui_font_digits_22
kui_font_bodyOutfit Regular 28LATIN1kui_font_digits_28
kui_font_buttonOutfit Bold 26TEXTkui_font_digits_26
kui_font_nameOutfit Bold 28LATIN1kui_font_digits_28
kui_font_titleOutfit Bold 34LATIN1kui_font_digits_34
kui_font_headingOutfit ExtraBold 34, a screen's titleTEXTkui_font_digits_34
kui_font_display_40Outfit ExtraBold 40LATIN1kui_font_digits_40
kui_font_title_60Outfit ExtraBold 60TEXTkui_font_digits_60
kui_font_codeDM Sans Bold 64Space, 0-9 and ← ↑ → ↓ onlynone
kui_font_clockDM Sans Bold 1040-9, the colon and the hyphen only; no tabular figuresnone
kui_font_code_80DM Sans Bold 80Space, %, +, -, ., /, 0-9 and the colon onlynone
kui_font_hero_160DM Sans Bold 160Space, % and 0-9 onlynone
kui_font_digits_22, _26, _28, _34, _40, _60DM Sans Bold0-9 onlynone

Text on Kody Blue or Bold Yellow is Soft Black (KUI_COLOR_ON_ACCENT): Light Grey would read 2.7:1 on the blue and 1.3:1 on the yellow.

The faces are under the SIL Open Font License 1.1, which travels with any firmware that ships them.

The images

The kit has 57 images, each its own object file. All are extern const lv_image_dsc_t.

The thirteen glyphs are 40 x 40 ARGB8888, black on transparent, recoloured by C code:

ImageShows
kui_glyph_wifi, kui_glyph_bleWi-Fi, Bluetooth
kui_glyph_sun, kui_glyph_battery, kui_glyph_powerBrightness, battery, Turn off
kui_glyph_info, kui_glyph_check, kui_glyph_xAbout, a tick, a cross
kui_glyph_lock, kui_glyph_usb, kui_glyph_boltSecured, USB power, voltage
kui_glyph_sd, kui_glyph_radioThe microSD card, the radio

The 43 line glyphs are one stroke of 2.2 px with round ends on a 24 x 24 grid, drawn at the size where a screen sets it, in LV_COLOR_FORMAT_A8: the image holds only an alpha and the screen gives the colour with lv_obj_set_style_image_recolor() at LV_OPA_COVER. The name is kui_glyph_<name>_<size>, and core/include/kui_glyphs.h declares them all. The bodies of the drawings come from kshell and live in tools/glyphgen.py. The 96 px glyphs are drawn at their own size, because the 40 px ones come out soft when scaled up.

SizeNames
16sd
28cup
30clock, wifi, bt, moon, watch, scene, chip, info, lock, check, phone, x, power, apps, share, chev_l, chev_r
40wifi, bt, torch, share, cup, power, more, spk0 to spk3, vib, sun0 to sun3, games
64torch, apps
96bt, wifi, chip, power, share

The games glyph is a gamepad. kgui draws it on the Games folder of Apps, beside more (a cog) for Tools, wifi for Radios, chip for Sensors, spk3 for Medias and apps for every other folder and for an app with no icon. A firmware keeps only the glyphs a screen names.

The wordmark, kui_wordmark, is 300 x 90 RGB565A8.

The tokens

core/assets/tokens.json is the source. A role is what a style asks for and a palette entry is what it gets, so re-pointing a role repaints every style that names it. tools/gen_tokens.py --check exits non-zero when the header does not match the source.

The palette is a black page, dark rows, and one yellow for what you press and where the cursor is. The first six entries are the page and the brand manual's five colours; the rest are the greys and greens of kshell's look. Each entry with the role it serves:

PaletteValueRole it serves
KUI_PALETTE_PAGE0x000000The AMOLED page and a sheet's card: black costs nothing
KUI_PALETTE_SOFT_BLACK0x191919Dark ink: text on a lit fill, never a ground
KUI_PALETTE_LIGHT_GREY0xDED7CDType on the page or on a row
KUI_PALETTE_MEDIUM_GREY0xC2BAB3Secondary text, borders, tracks, scrollbars
KUI_PALETTE_BOLD_YELLOW0xFFAC00A level, the answer button, the focus ring, and a warning: the page has no orange
KUI_PALETTE_KODY_BLUE0x028AC2Kody and connections: a radio that is on, the chosen thing
KUI_PALETTE_ROW_DARK0x212121A row on the page, a shade up from Soft Black, so it reads as a surface and not as a hole
KUI_PALETTE_OFF_GREY0x2A2A2AA control at rest, lighter than a row: a switch that is off
KUI_PALETTE_REST_GREY0x5C5A56The track a level fills over, a pill held down, a disabled thing
KUI_PALETTE_OK_GREEN0x31C45ADone and healthy, on a dark ground
KUI_PALETTE_CHARGE_GREEN0x67EA94Charging and paired: lighter than the OK green, so the two are never read as one
KUI_PALETTE_DANGER_RED0xC9303DAn ending, and what cannot be undone: Turn off, Remove, a critical battery

The roles are KUI_COLOR_BG, _TEXT_ON_BG, _TEXT_MUTED, _SURFACE, _SHEET, _TEXT, _ON_ACCENT, _OFF, _REST, _ACCENT, _LEVEL, _ACTION, _TRACK, _FOCUS, _OK, _CHARGE, _WARN and _DANGER, each an lv_color_hex() of a palette entry. _SURFACE is Row Dark, _SHEET is the page, _LEVEL, _ACTION, _FOCUS and _WARN are Bold Yellow, and _ACCENT is Kody Blue. Include kui.h, which brings LVGL first.

The sizes, in pixels:

Size tokenPixelsMeaning
KUI_SPACE_SM, _MD, _LG4, 8, 12Spacing units; _LG is the gap between two pills, anywhere
KUI_BORDER_WIDTH1Default border
KUI_SCREEN_SIDE, _TOP, _BOTTOM32, 40, 40Side margin; where the first row begins; where the last row ends
KUI_PILL_H, KUI_PILL_W80, 346A pill at rest; a full-width pill (410 less two side margins)
KUI_ICON_SLOT56The round slot on a pill's left
KUI_CAT_H44A category tab's height
KUI_HDR_DISC112The disc in a sheet's header
KUI_PROGRESS_H138A progress pill
KUI_ACTION_H, KUI_ACTION_W60, 160An action button
KUI_RING_W, KUI_RING_GAP4, 4The cursor ring's width, and the gap between an object and its ring, outside it
KUI_LEVEL_H, KUI_LEVEL_KNOB_PAD12, 8A level's track; how far the knob stands out of it
KUI_SWATCH48A colour swatch's diameter
KUI_LAMP_GLOW, KUI_LAMP_GLOW_SPREAD60, 12How far a lamp's glow reaches; where it starts
KUI_OPA_MUTED114 (45 %)Dimmed content

The glass and the panel

The calls that know the shape of the glass.

CallWhat it does
kui_glass_kode_dot()The glass from kbsp: 410 x 502, corner radius 114
kui_glass_inset(glass, depth)How far in from the side the glass starts, depth pixels from the top or bottom edge, rounded up. At depth 0 it is the whole radius, from the radius on it is 0. With radius 114: depth 4 gives 85, 18 gives 53, 53 gives 18, 80 gives 6
kui_glass_rect_inside(glass, x1, y1, x2, y2)Whether a rectangle (inclusive) lies entirely on the visible glass
kui_panel_check(glass, root, cb, user)Walks an LVGL tree and calls cb once per object that paints under a corner, with its worst corner and how far it enters. Returns the count. A background that spans the panel and runs off an edge is exempt, and so is a surface whose name begins bleed_; what such a surface carries is still checked
kui_panel_draw_bezel(glass, layer, colour, edge_line)Paints the four corners over on layer (an object named kui_bezel), so a desktop render shows what the glass shows
kui_panel_corner_name(corner)top-left, top-right, bottom-left, bottom-right, or ?

Touch maths

All three modules are plain C with no LVGL, integer or float arithmetic only, and times are a free-running 32-bit millisecond counter that may wrap (kui_age_ms() is the one rule for the age of a stamp).

The calls of each module:

ModuleCalls
kui_gesturekui_gesture_press(g, x, y, allowed, blocked), kui_gesture_move(g, x, y), kui_gesture_release(g, x, y), kui_gesture_name(state)
kui_springkui_spring_start(s, pos, vel, target, period_ms, zeta, t_ms), kui_spring_at(s, t_ms, &pos, &vel), kui_spring_rested(s, t_ms, pos_eps, vel_eps), kui_spring_retarget(s, target, t_ms), kui_age_ms(stamp, now)
kui_dragkui_drag_init, _set, _grab, _catch, _follow, _release, _aim, _step, _finish, _offset, _speed_px_s, _would_open, _phase, _is_open

How each module behaves:

kui_gesture

kui_spring

kui_drag

The caller owes a follower one answer to every catch: kui_drag_release(), kui_drag_grab() then its release, kui_drag_finish(), kui_drag_aim() or kui_drag_set(). A page left HELD is not stepped and stays frozen. Positions are projected onto the axis and signed toward open, and the point where the finger lifted is fed through kui_drag_follow() before kui_drag_release(), which takes no position.

The multitap keypad

The text rules of the phone keypad: ten character keys, a mode key and Delete. The module logs and prints nothing, because the text may be a secret, and kui_multitap_clear() zeroes every byte.

How the keypad behaves:

ThingValue
Callskui_multitap_init(m, cap), _press(m, key, t_ms), _tick(m, t_ms), _text, _len, _pending, _show_plain(m, t_ms), _mode, _chars(key, mode), _clear
CycleA press of the same key less than KUI_MULTITAP_MS (800) after the last cycles the pending character; another key, the mode key, a tick or a late press commits it. A key with one character in the current mode commits at once
Modeslower case, upper case, digits, symbols, then lower again; together they reach every printable ASCII character
ResultsTYPED, CYCLED, DELETED, STEPPED, FULL (the cap would be exceeded; cycling at the cap is allowed), EMPTY, IGNORED
LimitsText up to KUI_MULTITAP_TEXT_MAX (63, the longest Wi-Fi passphrase), a network name KUI_MULTITAP_SSID_MAX (32)
DisplayThe last character shows plain while pending and for KUI_MULTITAP_SHOW_MS (1000) after its press; every other character shows as a dot
Key 4, lower caseghi4

Kody's player

kui_kody.h plays animations one at a time on one draw buffer of its own, w x h ARGB8888, taken through LVGL's allocator on the first play and freed with the object.

It exists only with Lottie on, so a firmware without these three options has none of the functions, and a call fails at link time:

CONFIG_LV_USE_THORVG=y
CONFIG_LV_USE_THORVG_INTERNAL=y
CONFIG_LV_USE_LOTTIE=y

The calls of the player:

CallWhat it does
kui_kody_create(parent, w, h)A player for one canvas size, named kody_#, w x h at (0, 0), transparent, neither clickable nor scrollable. Nothing is drawn and no buffer is taken until the first play. NULL when a size is 0
kui_kody_play(k, anim, loop)Plays from frame 0, dropping whatever played, queued or stopped. The JSON is inflated, parsed and the first frame drawn before it returns. A one-shot holds its last frame. False for a different canvas, an allocation failure or data that does not inflate
kui_kody_then(k, next, loop)Queues what plays when the current pass ends. The hand-over is on the last frame, so a wake-up whose last frame is its scene's frame 0 shows no jump. A second call replaces the first
kui_kody_react(k, reaction)Waits for the loop's next anchor, plays a one-shot there and picks the loop up again at that anchor. Needs a loop playing; one reaction at a time
kui_kody_freeze(k)Stops on the next anchor, at once when on one or when there are none
kui_kody_pause(k), kui_kody_resume(k)Stop now, on the frame on the glass; go on from there
kui_kody_stop(k)Lets go of every animation the player was given and deletes the widget, so the caller may free their data
kui_kody_is_still(k)True when nothing rasterises: frozen, paused, holding a one-shot's end, or empty. A player only out of sight is not still
kui_kody_is_reacting(k)True while a reaction waits or plays

A player that is not visible (hidden, on a screen not on the glass, scrolled out of view) stands on the last frame it drew and goes on when seen.

An animation is its Lottie JSON, minified and deflated into one zlib stream (kui_kody_anim_t: zlib, zlib_len, json_len, canvas w and h, frames, fps 30, anchor, deflated), or plain JSON as a user's .json arrives. Each start inflates the JSON into a PSRAM block of exactly that size with the ROM inflater (khal_zlib_decompress()), hands it to lv_lottie, and frees it before the start is over.

An anchor is a frame of a loop with the same pose as frame 0, every anchor frames; reactions start and end on one, and the player switches only on an anchor or the last frame of a pass. kui_kody_next_anchor() and kui_kody_crossed() are the plain-integer answers behind that.

The three animations built into the kit:

Built-in animationCanvasFrames at 30 fpsAnchorJSON inflated
kody_reloj, the clock scene, loops410 x 300540every 4540,124 B
kody_despierta_reloj, his wake-up; its last frame is reloj's frame 0410 x 30057none26,765 B
kody_reloj_te_mira, he looks at you, anchor to anchor410 x 30045none26,003 B

A player plays only animations drawn for its own canvas. Scenes, wake-ups and reactions are 410 x 300; duda is 410 x 369; portatil and despide are 410 x 160; cargando is 264 x 264. All sit on the bottom edge of the glass except cargando, which is centred.

What a player costs:

Cost of KodyValue
FlashThe ThorVG engine, lv_lottie, the player, and the stream of each built-in animation you name: 5,735, 4,449 and 4,400 bytes. Animations of a pack take no flash
PSRAMOne buffer per player: 492,000 B at 410 x 300. At each start, 11 KB for the inflater's state and a block the size of the JSON, both freed before the start ends
Internal RAMNone from the model. ThorVG builds it from many small objects (22 to 34 KB a live model) with new, and the board's malloc() takes PSRAM first, so define no operator new
StackThe rasteriser takes about 18 KB of stack in one rleRender call on the task that runs lv_timer_handler(), plus LVGL's own
TimeA play, hand-over or reaction inflates, parses and draws the first frame in the caller's step; the inflate is 1.9 ms for the largest animation, measured on a Kode Dot

Kody's pack

A pack is a flat folder: kode.json and the Lottie files it names, on the microSD card (/sdcard/kode/, wins) or the internal FAT (/storage/kode/), else the built-in scene (kui_pack_src_t).

The official pack in kody/pack/ has ten scenes, tren, parque, camping, azotea, madrugada, desayuno, taller, lluvia, playa and reloj. Its face has no windows and picks tren by default. Every scene has a wake-up, a loop and reactions for look, hour, notice and plug, and the first five also a joke. It also names the four roles outside the face.

The manifest has:

The canvas each role plays on:

RoleCanvasPlays
scene410 x 300 (KUI_PACK_SCENE_W, _H)The face: a wake-up, a loop and reactions
empty410 x 369The launcher with no apps
writing410 x 160The Opening card while an image writes
goodbye410 x 160The wave before a run
loading264 x 264 (KUI_PACK_LOADING_SIDE)The loading screen

The reader checks before anything plays. Unknown keys are ignored at every level, so a pack made for a newer firmware reads here. A window naming no scene is dropped, and a default naming none leaves the first scene the default, each with a warning.

The limits it enforces:

LimitValue
Manifest256 KB (KUI_PACK_MANIFEST_MAX)
One file as stored64 KB (KUI_PACK_FILE_MAX)
A Lottie's JSON inflated96 KB (KUI_PACK_JSON_MAX)
JSON nesting32 (KUI_PACK_MAX_DEPTH)
Precomp chain2 (KUI_PACK_MAX_PRECOMP)
Frames in one pass1 to 1800, a multiple of a non-zero anchor
Frame rateA whole 1 to 60
Scene name, file name, pack name and version15, 40, 31 characters
Scenes, reactions32, 128
File name1 to 40 of [A-Za-z0-9_.-], not starting with ., ending .json or .json.z

A refusal is one of size, depth, json, format, schema, name, file, hash, canvas, frames, fps, precomp or memory (kui_pack_err_word()).

The calls are:

Parser arrays live in PSRAM, and cJSON appears in kui_pack.c and nowhere else.

The generators and the simulator

Five scripts regenerate the generated files. Run each from the kit's directory.

The five scripts:

ScriptWritesNotes
tools/gen_tokens.pycore/include/kui_tokens.h from tokens.json--check exits non-zero on a mismatch. Building needs no Python
tools/fontgen.shThe 18 faces in fonts/src/Fetches lv_font_conv at a pinned commit through npx; the first run builds it and takes minutes. FONT_CONV overrides it. The npm releases up to 1.5.3 do not write the cap_height, x_height and static_bitmap fields these faces carry
tools/glyphgen.pyimages/assets/glyph_<name>_<size>.png and core/include/kui_glyphs.h for the line glyphsDraws each glyph from its SVG body. --check exits non-zero when a file on disk is not what the script draws. Needs cairosvg and Pillow; on macOS with Homebrew run it with DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib. The PNGs are checked in, so building never needs it
tools/imagegen.shimages/src/kui_*.c from the PNGsRuns LVGL's LVGLImage.py (path in LVGL_IMAGE); needs pypng and lz4. The thirteen glyphs come out ARGB8888 and the line glyphs A8. The pixel data reproduces byte for byte
tools/kodygen.pykody/pack/ and the built-in kody/src/kody_*.c and kui_kody_anims.h from kody/assets/Deflates each Lottie with zlib, drops the keys meta, mn and nm, checks the pack block with the firmware's rules, and lints each source. Plain Python 3. Options and lint rules: see below

The lint of kodygen.py refuses pucker and bloat, zig-zag, an effect that is not a blur, an expression, an image layer, a text layer or a 3D layer, naming the animation, layer and property. --check compares every stored stream with a fresh one and fails a stream over 5 % longer. --lint prints one line per animation and writes nothing.

python3 -m unittest discover -s components/kui/tools/tests runs the generator's tests.

The simulator in sim/ draws a consumer's screens on a desktop with LVGL 9.6 over SDL2: kui_sim <screen> [--snapshot OUT.png] [--set SUBJECT=VALUE]... [--device] [--bezel]. A consumer hands it kui_sim_screens[], kui_sim_setup() and kui_sim_set() (kui_sim.h) and builds with kui_sim_add().

It does not simulate an Active Object: no selection, scrolling, sheets or live values, and no Lottie, because its LVGL is built in C alone, so kui_kody.c compiles to nothing there. Timing on the desktop says nothing about the board.

Gotchas

Example code

The part of kui that needs no screen: the keypad's text rules. Press key 4 twice for "h", wait a second, then three times for "i". The console prints text: hi. The visual half of the kit has its example in sdk/kscreen.

/*
 * multitap_hi.c - 2026-10-28 - from Kode with love
 *
 * Types "hi" on the UI kit's phone keypad with no screen. Key 4 twice is
 * "h"; a second later, key 4 three times is "i". The console prints hi.
 */
#include "kode.h"
#include "kui_multitap.h"
 
/* Press key 4 count times, 100 ms apart, starting at start_ms. */
static void press_four(kui_multitap_t * const keypad, uint32_t start_ms, uint32_t count)
{
    for (uint32_t i = 0U; i < count; ++i) {
        (void)kui_multitap_press(keypad, KUI_MULTITAP_KEY_4, start_ms + (i * 100U));
    }
}
 
/* Bring the system up, type the text and log it. */
void app_main(void)
{
    kui_multitap_t keypad;
 
    kode_init();
    kui_multitap_init(&keypad, KUI_MULTITAP_SSID_MAX);
    press_four(&keypad, 0U, 2U);
    press_four(&keypad, 1000U, 3U);
    KLOG_I("multitap", "text: %s", kui_multitap_text(&keypad));
    kui_multitap_clear(&keypad);
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK