App Kit reference

New screens start with #include "app_framework.h" and APP_HELPER(). Up, Down, Left, Right, Select, and Escape are already bound. A tool fills .on_view and sleeps until a key or .every_ms. While the header band is showing, that sleep is at most one second and the body is redrawn with the clock. Pass .keys, .live, .game or .fullscreen, .on_tick, .on_load, .on_ready, .on_draw, or .state / .state_size when a screen needs a different map, a frame every tick, no chrome, a timed step, RAM-heavy setup at boot, setup after the UI exists, a canvas instead of a scene, or a session restored on start and resume and stored on quit. Pixels are fw/ui.h. Files and pins are fw/io.h. Include those only when the screen uses them. Title and help come from app.json. The launcher icon is <symbol>_icon. .type defaults to APP_TYPE_TOOL. .fps applies only with .live or .game, and then it defaults to 30. The short example is in docs/apps.md.

#include "app_kit.h" is the lower-level umbrella (APP_DEFINE, open/exit) plus the UI components under apps/ui/components/. fw/events.h wraps the kernel bus (os_event_subscribe / os_event_publish). A screen that calls those links kernel/event.c. Key presses do not publish on that bus.

Short how-to for adding a builtin: docs/apps.md.

Layout

apps/
  app_framework.h                # the one include for an app
  fw/ui.h                        # pixels, text, frame timing
  fw/io.h                        # keys, settings store, files
  fw/layout.h                    # canvas, screen, menu, catalog, status
  fw/events.h                    # os_event_subscribe / os_event_publish
  app_kit.h / app_kit.c          # runtime: ctx, APP_DEFINE, focus, open/exit
  stdapps/<name>/                # one folder per builtin app
flowchart TB
  app[stdapps] --> kit[app_kit]
  kit --> canvas
  kit --> screen
  kit --> menu
  kit --> catalog
  kit --> status
  screen --> canvas
  screen --> status
  menu --> canvas
  menu --> catalog
  menu --> display
  menu --> status
  catalog --> display

Lifecycle

  1. Install manifests with stdapps_install(), which also builds the launcher catalog once.
  2. os_boot_load() paints the splash (ArdubotOS, the last 8 characters of the git commit, a progress bar, and the step name). It mounts storage, starts clock, notify, and sensors, then calls each installed app's on_load once.
  3. app_start(stdapps_start_name()). On a full image that name is sensors. Escape from any other app resumes the launcher.
  4. Kit runs on_init → loop on_frame + sleep → on_cleanup on hard exit.
  5. Only the foreground app receives keys and may flush the display.
  6. app_open(from, name) starts or resumes the child and suspends the caller.
  7. app_request_exit / app_bind_back soft-leaves a non-home app (suspends it, keeps workers/state) and resumes launcher.

on_load is void (*)(void). It runs once during the splash, before the app task and UI exist. Put tables, caches, and other RAM-heavy setup there. on_ready still runs later, when the app actually starts and the UI exists. A second app_kit_load does not call the hook again. An app with no hook is still named on the splash.

APP_DEFINE

APP_DEFINE(symbol, "install_name",
    .type = APP_TYPE_USER,   /* 0 means "keep the default", which is USER */
    .icon = &my_app_icon,    /* optional 16×16 launcher bitmap (app-owned) */
    .fps = 30,
    .on_init = on_init,
    .on_frame = on_frame,
    .on_cleanup = on_cleanup   /* optional */
);

Name, version, author, and description come from the app's app.json at compile time. Pass them in APP_DEFINE only to override that file. APP_HELPER is the screen path: it reads title and help from the same file, registers <symbol>_icon, and uses APP_TYPE_TOOL when .type is left out.

Apps own their icons (const app_icon_t in a *_icon.c). APP_DEFINE registers .icon via app_kit_set_icon; the catalog/launcher never hardcodes glyphs by name.

Runtime API (app_kit.h)

Call Purpose
app_bind_key(app, SIM_KEY_*, fn, user) Map a key; fn(app, user) on press
app_bind_back(app) Escape → app_request_exit
app_open(from, "name") Start or resume another app; suspend caller
app_request_exit(app) Soft-leave non-home app (suspend + resume launcher)
app_kit_is_foreground(app) Focus check (canvas uses this)
app_kit_set_demand(app, POWER_DEMAND_*) Compute ceiling for the governor. See Power
app_mark_dirty / app_is_dirty Redraw scheduling (also via canvas)

Components

Display — display.h

Compile-time panel size. CMake sets APP_DISPLAY_WIDTH / APP_DISPLAY_HEIGHT from ARDUBOT_DISPLAY_WIDTH / ARDUBOT_DISPLAY_HEIGHT. The NodeMCU profile is 128×32. A simulator build (ARDUBOT_BUILD_SIM) forces 320×172.

Canvas — canvas.h

Call Purpose
app_mark_dirty / app_is_dirty / app_clear_dirty Dirty flag
app_clear Clear framebuffer (foreground only)
app_text / app_textf Draw text
app_flush Present + clear dirty

Screen — screen.h

Titled frames without hand-rolling dirty/clear/flush:

static void on_frame(app_ctx_t* app) {
    if (!app_screen_begin(app, "Counter")) {
        return;
    }
    app_textf(app, 0, 16, "Count: %d", g_count);
    app_screen_end(app);
}
Call Purpose
app_menu_init / app_menu_add / app_menu_load_catalog Build list
app_menu_move(app, menu, delta, user) Move; consumes user, marks dirty
APP_MENU_ONE_UP / APP_MENU_ONE_DOWN Deltas
app_menu_bind_nav(app, menu) Bind Up/Down for you
app_menu_selected Current item
app_menu_draw(app, menu, title, help) Full redraw

Header — header_app.c

The status band (battery, uptime clock, separator). Every standard app gets it on a panel at least 280×150, including the launcher. app_display_flush reads the paint from header_app.c after the app draws, so the band does not scroll with content. .game and .fullscreen skip it. app_header_height() is 25 on those panels and 0 on a smaller one. The focused app wakes at least once a second while the band is up and redraws its body in that same frame, so the clock is not the only thing moving.

Status — status.h

Top-right corner icons (Wi-Fi glyph, 4-bar signal, battery). Drawn automatically from app_screen_end and app_menu_draw. Wi-Fi credentials come from device_secrets.yaml (see README.md). Set the pack level once at boot:

app_status_set_battery_percent(100);  /* USB-powered boards */
app_status_set_link(APP_STATUS_LINK_UP, -50);  /* associated, RSSI dBm */
Call Purpose
app_status_set_battery_percent / _battery_percent 0–100 level
app_status_set_link / _link / _rssi Association + RSSI for the icons
app_status_signal_bars 0–4 bars from link + RSSI
app_status_redraw_due At most 10 full-panel paints per second
app_status_stable_bars Ignore signal-bar changes shorter than 400 ms
app_status_draw(app) Blit icons (foreground only)
app_status_blit(...) Same glyphs via a set_pixel callback (board shell)

Catalog — catalog.h

Built once after installs:

app_kit_catalog_build("launcher");  /* exclude home from the list */
Call Purpose
app_kit_catalog_build / _clear / _count / _at Snapshot
app_type_tag(type) "[SYS]" / "[USR]" / "[GME]" / "[TOL]"
app_menu_load_catalog(menu) Fill a menu from the snapshot

Minimal app sketch

#include "app_kit.h"

static int32_t g_count;

static void on_inc(app_ctx_t* app, void* user) {
    (void)user;
    g_count++;
    app_mark_dirty(app);
}

static void on_init(app_ctx_t* app) {
    app_bind_key(app, SIM_KEY_1, on_inc, NULL);
    app_bind_back(app);
}

static void on_frame(app_ctx_t* app) {
    if (!app_screen_begin(app, "Demo")) {
        return;
    }
    app_textf(app, 0, 16, "%d", g_count);
    app_screen_end(app);
}

APP_DEFINE(demo_app, "demo", .on_init = on_init, .on_frame = on_frame);

Tests

Binary Covers
test_app_helper chrome from app.json, keys, frames, layout math
test_app_manifest name, version, author, description, title, help
test_app_kit manifest, bind, back, exit, foreground
test_app_ui rows, bars, key tables
test_stdapps_register install set and the sensors boot name
test_event_bus kernel bus and os_event_publish
test_components_canvas dirty / draw safety
test_components_screen begin/end
test_components_menu move, nav bind, display macros
test_components_catalog boot catalog
test_status battery clamp + blit pixels
test_sensors_app / test_sensor_service sensor lines and the config registry
test_clock / test_clock_service wall clock and /flash/clock.dat

Logging

APP_INFO, APP_WARN, APP_ERROR, APP_DEBUG via app_kit.h / app_framework.h.