Skip to content

How an app works

SDK 1.xWritten for core 1.53+ and gui 1.23+.

This page is the contract between your app and the CrossPad framework: when your code is called, on which task, with how much memory, and what it must clean up. Every example in the cookbook follows these rules. Follow them and your app behaves the same on every visit, in the simulator and on the board.

The code on this page comes from three examples: Hello screen, Pads and LEDs and UI kit.

An app is two plain functions and one registration line. You do not subclass anything.

lv_obj_t* MyApp_create(lv_obj_t* parent, App* app); // build the screen under parent, return its root
void MyApp_destroy(lv_obj_t* root); // free what create made

App is the platform’s app class. It has the same name on both targets, but each declares its own: the firmware’s comes from crosspad-gui (crosspad-gui/app/App.hpp), the simulator’s from pc_stubs/PcApp.hpp. Both derive from AppBase, which holds everything this page uses. The examples include whichever header the build has:

#if __has_include("pc_stubs/PcApp.hpp")
#include "pc_stubs/PcApp.hpp" // the simulator's App
#else
#include "crosspad-gui/app/App.hpp" // the firmware's App
#endif

The registration line names the two functions:

01-hello-screen/src/HelloScreenApp.cpp
REGISTER_APP(HelloScreen, nullptr, "example-hello-screen.png",
HelloScreen_create, HelloScreen_destroy,
nullptr, nullptr, nullptr, 0)

Source: examples/01-hello-screen/src/HelloScreenApp.cpp, lines 152–154 (repository opens soon)

REGISTER_APP takes nine arguments, four of which it ignores:

#ArgumentWhat to pass
1appNameA bare C++ identifier in CamelCase. It becomes the app’s name: the label in the simulator’s launcher, and the name mode_overlay_open_app() takes. Make it unique among all apps, ignoring case (names are compared without case, and the first match wins); two apps with exactly the same name do not even link, because each registration defines a function _register_<appName>_app().
2logicFuncnullptr. The macro drops it.
3iconPathThe icon’s file name in your app’s assets/ folder: a 48 × 48 px PNG with transparency (RGBA). Name it after your app id; every app’s icons share one folder (Limits).
4, 5createLVGL, destroyLVGLYour two functions.
6, 7, 8initSimple, refreshFunc, cleanupFuncnullptr each. The macro drops them.
9priority0. Higher values sort earlier in the app list; equal values keep registration order.
10 (optional)recolorIcontrue to tint a one-colour icon (black on transparent) with one flat colour.

An app that owns the pads uses REGISTER_APP_PL, which adds the name of its pad logic as the tenth argument; the framework then hands the pads to that logic while the app is open:

02-pads-leds/src/PadsLedsApp.cpp
REGISTER_APP_PL(PadsLeds, nullptr, "example-pads-leds.png",
PadsLeds_create, PadsLeds_destroy,
nullptr, nullptr, nullptr, 0, "PadsLeds")

Source: examples/02-pads-leds/src/PadsLedsApp.cpp, lines 158–160 (repository opens soon)

The tenth argument is a name, not a handler. Register the handler yourself with registerPadLogic, under exactly that string, once, and before create returns: the framework gives the pads to the logic right after create. Pads and LEDs does it at the top of create, on the first visit only:

02-pads-leds/src/PadsLedsApp.cpp
// Registered once and kept: REGISTER_APP_PL below makes the framework
// push it when the app opens and pop it when the app closes.
if (!s_logic) {
s_logic = std::make_shared<PadsLedsPadLogic>();
crosspad::getPadManager().registerPadLogic("PadsLeds", s_logic);
}

Source: examples/02-pads-leds/src/PadsLedsApp.cpp, lines 92–97 (repository opens soon)

With a name it cannot find, PadManager prints PadManager: App '<name>' not found, using default pad logic on the console, and your handler never sees a pad.

Keep the registration in a .cpp file under your app’s src/ folder: the build finds apps by scanning those files for the macro, and generates the list of apps from what it finds. On the firmware, the app is an ESP-IDF component whose CMakeLists.txt sets WHOLE_ARCHIVE ON, so the linker keeps the registration function that nothing else calls. The examples’ CMakeLists.txt is the one to copy.

The framework calls your code in this order:

  1. Boot. The generated registry calls every app’s registration function. The framework keeps the apps that have a create function, sorts them by priority and makes one App object per app. That object lives until the board powers off; your create has not run yet.
  2. Open. The user opens your app from the MODE map or the simulator’s launcher, or another app calls mode_overlay_open_app(). The framework:
    1. closes the app that was on screen (its destroy runs now: only one app is on screen at a time);
    2. remembers which group the encoder was driving;
    3. calls your create with the app area as parent, then the show handler if create set one with setOnShow;
    4. with REGISTER_APP_PL, gives the pads to the pad logic registered under the name in argument 10: its onActivate runs.
  3. Running. Your code runs from LVGL timers and widget events, from pad callbacks, and from event subscriptions made with subscribe. Each of those runs on its own task: see Threads.
  4. Back. A short click of the power button closes the most local thing first: an open dropdown, then the newest handler registered with ui_back_push (a dialog, an overlay), then the encoder’s edit mode (a slider being turned), then the transport bar if the encoder is on it, and only then asks your app. Your setOnBack handler returns true when it handled the click (it closed a panel of yours, the app stays) and false to let the framework close the app. With no handler, the click closes the app.
  5. Close. Your app closes itself with app->destroyAppAsync(), or the user leaves it (back, MODE, another app). The framework then:
    1. takes the pads back from your pad logic (its onDeactivate runs); the logic that had the pads before gets them again;
    2. calls your destroy with your root;
    3. gives the encoder back to the group it was driving before your app opened.
App lifecycle: what the framework calls in your app, and in which order, from open to close. Three columns: the framework, your app, and PadManager. Open: the framework closes the app on screen (its destroy runs), remembers which group the encoder drives, calls your create(parent, app), which builds the screen and returns its root, then calls your show handler if you set one with setOnShow, and, for an app registered with REGISTER_APP_PL, calls pushPadLogic(name): the Sampler's onDeactivate runs, then your pad logic's onActivate. Running: your code runs from lv_timer and widget callbacks, pad callbacks and event subscribers, each on its own task. Back, a short click of the power button: the first open item takes the click, in this order: an open dropdown, the newest ui_back_push handler, the encoder's edit mode, the transport bar while it has focus, and last your setOnBack handler. If it returns true your app stays open; if it returns false or there is no handler, the framework calls destroyAppAsync. Close, from destroyAppAsync or when another app opens: the framework runs destroyApp, calls popPadLogic (your pad logic's onDeactivate runs, then the Sampler's onActivate), calls your destroy(root), and gives the encoder back the group it drove before your app opened. The framework AppOrchestrator, AppBase Your app create, destroy, back handler PadManager who has the pads OPEN from the MODE map, the launcher, or mode_overlay_open_app() Close the app on screen its destroy runs now Remember the encoder's group to give it back at close create(parent, app) build the screen, return its root show handler only if set with setOnShow pushPadLogic(name) REGISTER_APP_PL only 1. Sampler: onDeactivate 2. your logic: onActivate RUNNING until the app closes Your code runs from lv_timer and widget callbacks, pad callbacks and event subscribers, each on its own task (see Threads). BACK a short click of the power button The first of these that is open takes the click; the rest are skipped. Open dropdown closes it ui_back_push newest first Edit mode of the encoder Transport bar while it has focus setOnBack your handler returns true: your app stays open returns false, or no handler: destroyAppAsync() CLOSE destroyAppAsync() from your code or the back click, or another app opens destroyApp() destroyAppAsync() defers it popPadLogic() REGISTER_APP_PL only 1. your logic: onDeactivate 2. Sampler: onActivate destroy(root) free everything, root last Restore the encoder's group the one remembered at open

One visit to an app, top to bottom. Blue boxes are your code; the dashed box is optional. The pad logic steps happen only for an app registered with REGISTER_APP_PL, and the Sampler is the base pad logic that has the pads whenever no app does.

Here is create, from Hello screen:

01-hello-screen/src/HelloScreenApp.cpp
lv_obj_t* HelloScreen_create(lv_obj_t* parent, App* app)
{
s_group = nullptr;
s_root = nullptr;
s_count = nullptr;
s_details = nullptr;
s_presses = 0;
// The encoder drives whichever group is on it. This app's buttons go in
// its own group; the framework gives the encoder back when the app closes.
if (lv_indev_t* enc = crosspad_gui::getGuiPlatform().getNavigationEncoder()) {
s_group = lv_group_create();
lv_indev_set_group(enc, s_group);
}
lv_obj_t* root = lv_obj_create(parent);
s_root = root;
lv_obj_set_size(root, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(root, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* title = lv_label_create(root);
lv_label_set_text(title, "Hello, CrossPad");
add_button(root, "Press me", on_press, nullptr);
s_count = lv_label_create(root);
lv_label_set_text(s_count, example_hello::pressedLabel(0).c_str());
add_button(root, "Details", on_details, nullptr);
add_button(root, LV_SYMBOL_CLOSE " Close", on_close, app);
s_details = lv_obj_create(parent); // on top of root, hidden until asked
lv_obj_set_size(s_details, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_opa(s_details, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(s_details, 16, 0);
lv_obj_t* note = lv_label_create(s_details);
lv_label_set_text(note, "A panel inside the app.\n\n"
"Click the power button:\nthis panel closes, the app stays.");
lv_obj_set_hidden(s_details, true);
app->setOnBack(on_back);
return root;
}

Source: examples/01-hello-screen/src/HelloScreenApp.cpp, lines 90–132 (repository opens soon)

Three things in it matter beyond this app. It resets every file-scope variable first, because they keep their values from the last visit (the App object and your statics outlive the screen). It puts its buttons in a group of its own and gives that group to the encoder from getNavigationEncoder(), which is null on a platform without one; it does not need to restore the old group, the framework does. And it sets its back handler with setOnBack inside create, where app is at hand.

The back handler closes the app’s own panel and keeps the app open; when there is no panel to close, it lets the framework close the app:

01-hello-screen/src/HelloScreenApp.cpp
// The power button's click. true = handled here (the panel closes);
// false = let the framework close the app.
bool on_back(lv_obj_t*)
{
if (s_details && !lv_obj_has_flag(s_details, LV_OBJ_FLAG_HIDDEN)) {
show_details(false);
return true;
}
return false;
}

Source: examples/01-hello-screen/src/HelloScreenApp.cpp, lines 62–71 (repository opens soon)

A button that closes the app calls destroyAppAsync(), never destroyApp(). The button’s click callback is still running, and destroyApp() would delete that button under it; destroyAppAsync() waits until the event is over:

01-hello-screen/src/HelloScreenApp.cpp
void on_close(lv_event_t* e)
{
// Never destroyApp() from inside an event: it tears the app down while LVGL
// is still delivering the click. destroyAppAsync() waits for the event to end.
auto* app = static_cast<App*>(lv_event_get_user_data(e));
app->destroyAppAsync();
}

Source: examples/01-hello-screen/src/HelloScreenApp.cpp, lines 52–58 (repository opens soon)

Destroy frees everything create made: timers, event subscriptions (unsubscribe), the encoder group (with lv_group_delete_deferred from crosspad-gui, because the encoder may be in the middle of reading it), and last the root, with lv_obj_delete_async. Hello screen has no timers or subscriptions, only its group and two panels:

01-hello-screen/src/HelloScreenApp.cpp
void HelloScreen_destroy(lv_obj_t* root)
{
// root is null when the app's screen was already wiped: free what is ours, delete nothing.
if (s_group) {
lv_group_delete_deferred(s_group); // the encoder may still be mid-read
s_group = nullptr;
}
if (root && s_details) lv_obj_delete_async(s_details);
s_details = nullptr;
s_root = nullptr;
s_count = nullptr;
if (root) lv_obj_delete_async(root);
}

Source: examples/01-hello-screen/src/HelloScreenApp.cpp, lines 136–148 (repository opens soon)

Write destroy so that it also works with a null root: free your resources and delete nothing, as the examples do.

Hooks to leave alone. Put your cleanup in destroy. These hooks are not part of the app lifecycle:

  • the IApp hooks onAppInit, onAppDeinit, onAppUpdate and onAppActivated;
  • pause(), resume() and the setOnHide handler.

The AppChanged event (postAppChanged) reports a kit change from the Sampler; it is not an app open or close notification.

Your code runs on several FreeRTOS tasks on the firmware. LVGL, which draws every screen, is not thread-safe: only the LVGL task may touch it.

TaskWhat of yours runs there
LVGL (esp_lvgl_port), 16 kB stackcreate, destroy, the back handler, lv_timer callbacks, widget event callbacks, and everything passed to postToGui
UART drv (pads from the STM32)your pad logic’s callbacks for a finger on a pad
seq_poll (loop playback)your pad logic’s callbacks for presses a running loop plays (pressSource() returns SequencerCallback)
crosspad_events, queue of 64your event bus subscribers
audio_rtan IAudioNode’s mix() and process()

Pad callbacks can also come from MIDI input, on its own task. Two pad callbacks can run at the same time.

Thread map: the firmware tasks your app's code runs on, and how work reaches the LVGL task. Five tasks on the ESP32-S3 firmware. The LVGL task (esp_lvgl_port, core 0, priority 4, 16 kB stack) is the only one that touches LVGL: it runs create, destroy, the back handler, lv_timer and widget event callbacks, and every function passed to postToGui. UART drv (core 0, priority 6) runs your pad logic's callbacks for a finger on a pad. seq_poll (core 0, priority 5) runs them for presses a running loop plays. crosspad_events (core 1, priority 4, a queue of 64) runs your event bus subscribers; an event's data is valid only during the call. audio_rt (core 1, priority 19) runs an IAudioNode's mix() and process(), which must not allocate, lock, log or touch files. Pad callbacks reach the screen by writing an atomic that an lv_timer reads; event subscribers call postToGui or use an atomic too; the audio task talks to others through atomics only. Pad callbacks can also come from MIDI input and the test tools, on their own tasks, and two can run at once. TASK WHAT OF YOURS RUNS THERE TO THE SCREEN LVGL esp_lvgl_port core 0 · priority 4 16 kB stack The only task that touches LVGL create, destroy, the back handler, lv_timer and widget event callbacks, and every function passed to postToGui UART drv core 0 · priority 6 Your pad logic's callbacks for a finger on a pad (from the STM32) seq_poll core 0 · priority 5 Your pad logic's callbacks for presses a running loop plays crosspad_events core 1 · priority 4 queue of 64 Your event bus subscribers copy the data: it is valid only in the call audio_rt core 1 · priority 19 An IAudioNode's mix() and process() no allocation, locks, logs or files write an atomic; an lv_timer reads it postToGui(cb) or an atomic atomics only Pad callbacks can also come from MIDI input and the test tools, on their own tasks, and two can run at once. Shown: the firmware on the ESP32-S3.

The same tasks as a map, with the ways work reaches the screen: a pad callback writes an atomic that an lv_timer reads, and an event subscriber calls postToGui (or uses an atomic too). The rules below say why.

The rules:

  • Touch LVGL only on the LVGL task. A pad callback that wants the screen to change writes an atomic that an lv_timer reads. An event subscriber or another task either does the same or hands a function to the LVGL task with crosspad_gui::getGuiPlatform().postToGui(cb, user_data). The pad logic in Pads and LEDs stores each pad’s level in an atomic; a timer on the LVGL task draws them:

    02-pads-leds/src/PadsLedsApp.cpp
    // Runs on the LVGL task every 33 ms: reads what the pad task recorded, draws it.
    void tick(lv_timer_t*)
    {
    for (uint8_t p = 0; p < 16; ++p) {
    const uint8_t lvl = s_logic->level(p);
    const auto c = example_pads::padColor(p, lvl ? lvl : example_pads::kRestLevel);
    lv_obj_set_style_bg_color(s_cells[p], lv_color_make(c.r, c.g, c.b), 0);
    }
    const int last = s_logic->lastPad();
    if (last >= 0) {
    lv_label_set_text_fmt(s_status, "Pad %d level %u", last + 1,
    static_cast<unsigned>(s_logic->level(static_cast<uint8_t>(last))));
    }
    }

    Source: examples/02-pads-leds/src/PadsLedsApp.cpp, lines 35–48 (repository opens soon)

  • Never call lv_async_call off the LVGL task. It is meant for the LVGL task only. postToGui is the version for every other task: on the firmware it queues the call and hands it to LVGL under the display lock. destroyAppAsync() uses lv_async_call too, so call it from the LVGL task; from another task, post a function that calls it.

  • Guard work you post. A function queued with postToGui before your app closed can run after your destroy, when its widgets are gone. Keep an atomic “alive” flag: set it in create, clear it first thing in destroy, and return early from the posted function when it is clear. UI kit’s show_pad (below) does exactly this with s_alive.

  • Keep pad logic state in atomics. Pad callbacks run concurrently with each other and with the LVGL task. Keep what your callbacks use alive for as long as the pad logic is registered; do not free it in onDeactivate.

  • No flash writes from a pad callback. The pad tasks run on stacks in PSRAM, and a flash write (saving settings, for example) turns off the cache that PSRAM sits behind. Hand the work to the LVGL task instead.

  • Copy event data in the callback. An event’s data is valid only while your subscriber runs (IEventBus). Copy what you need into atomics or post it.

  • Register your pad logic, do not switch it. With REGISTER_APP_PL, register the handler once under the name in argument 10, before create returns (What an app is); the framework pushes and pops it. Never call setActivePadLogic or pushPadLogic (PadManager) yourself, and never from inside onActivate or onDeactivate.

  • The audio task is real-time. mix() and process() must not allocate, lock, log or touch files. Allocate in onPrepare(), and talk to other tasks through atomics (IAudioNode).

  • Some GUI calls are LVGL-task only: every transportbar_* function (transport_bar), and every mode_overlay_* function (mode_overlay) except mode_overlay_select_pad() and mode_overlay_is_open().

Following the pads. To follow the pads the Sampler plays (by a finger or by a running loop), subscribe to SamplePlayback (EventTypes) and keep the events with source App, as the UI kit example does, from the event task, through postToGui:

10-ui-kit/src/UiKitApp.cpp
// On the LVGL task, when the queue reaches it. s_alive is false once the app
// is closing, so a hit that was queued before the close does nothing.
void show_pad(void* p)
{
if (!s_alive.load()) return;
ui_toast_show(example_ui::padToast(example_ui::unpackPad(p)).c_str(), 800, UI_TOAST_INFO);
++s_pads;
show_count();
}
// On the event task: no LVGL here. Hand the pad over to the LVGL task.
// These are the pads the Sampler plays (from a finger or a running sequence).
void on_pad(const void* data, crosspad::EventType, void*)
{
const auto* d = static_cast<const crosspad::SamplePlaybackData*>(data);
if (!d->isOn || d->note >= 16 || d->source != crosspad::EventSource::App) return;
crosspad_gui::getGuiPlatform().postToGui(show_pad, example_ui::packPad(d->note));
}

Source: examples/10-ui-kit/src/UiKitApp.cpp, lines 57–74 (repository opens soon)

When your app’s own pad logic has the pads, PadManager skips what it does by default for a press and a release: no note-on or note-off light, no MIDI note, no NoteOn, NoteOff, PadPressed or PadReleased event. Your logic does what it wants done. One path stays: a held pad’s pressure still reaches the PadLedController, which repaints the pad from it when the LED feedback mode is aftertouch and the pad is lit as pressed; your onPadPressure gets the same value.

The ESP32-S3 has two kinds of RAM: a little internal RAM and 8 MB of PSRAM. Internal RAM is the one to spare: keep big buffers in PSRAM. To measure it in code, heap_caps_get_free_size(MALLOC_CAP_INTERNAL) (ESP-IDF only) returns the free internal RAM in bytes.

  • LVGL objects live in PSRAM. The firmware routes every LVGL allocation to PSRAM, so widgets and timers cost no internal RAM.

  • new and malloc of more than 64 bytes take PSRAM first. Smaller blocks take internal RAM first, so many small allocations still add up there.

  • Big statics go in PSRAM with EXT_RAM_BSS_ATTR. A file-scope array or struct is placed in internal RAM unless you mark it. Mark anything bigger than a few dozen bytes. The attribute exists only on ESP-IDF; define a macro so the simulator builds too, and give the variable no initializer (the section is zero-filled at boot):

    #ifdef ESP_PLATFORM
    #include "esp_attr.h"
    #define APP_PSRAM_BSS EXT_RAM_BSS_ATTR
    #else
    #define APP_PSRAM_BSS
    #endif
    namespace {
    APP_PSRAM_BSS uint8_t s_history[16][128]; // 2 kB, zero at boot
    }
  • The SD card cannot move data straight into PSRAM. The card controller works by DMA, which cannot reach PSRAM, so a read into a PSRAM buffer (any malloc block over 64 bytes) goes one 512-byte block at a time through a small internal buffer. For the fastest reads, read into a small internal buffer, as the firmware’s own sample streamer does.

  • Decoded images take PSRAM. Keep images small and few.

A leak on the firmware is internal RAM that does not come back after your app closes. Checking your app shows how to measure it.

Closing an app deletes its screen and nothing else. These stay, some on purpose:

  • File-scope variables. They keep their values until power-off. Reset them at the top of create (Hello screen does) and set pointers to widgets back to null in destroy.
  • Your registered pad logic. Register it once, keep it registered, and let REGISTER_APP_PL push and pop it by name. Pads and LEDs keeps its handler in a static shared_ptr for this reason.
  • Event subscriptions you did not remove. A subscriber keeps running after the screen is gone, on the event task, and any widget pointer it uses now points at freed memory. Unsubscribe in destroy, unless the app should keep listening (the Sequencer keeps its tempo subscription on purpose).
  • A transport claim. When your app claims the transport with transportClaim, PLAY and STOP drive it until another target claims it, even after your screen closes. That is on purpose: the Sequencer keeps playing when you leave it. An app whose target means nothing without its screen stops and releases it in destroy, as Tempo and transport does.
  • The base pad logic. While no app owns the pads, the Sampler plays them. Nothing of yours is needed for that.
  • 24 apps. The registry holds 24 apps, built-in ones included. A firmware with every bundled app registers 15 (Settings, Firmware, KitSelector and a Power OFF entry among them), which leaves 9 for yours. An app past the limit is not registered, and the console says AppRegistry: no room for '<name>' at boot.

    To count yourself: the firmware build prints Generated app_registry_init.cpp with N apps: … while it configures (the Power OFF entry is added at run time, so the board has one more).

  • One folder for every icon. The build copies every app’s assets/*.png into one flat folder. Two files with the same name collide, one icon replaces the other, and CMake prints Asset collision. Name your icon after your app id, as the examples do (example-hello-screen.png).

  • A new app needs a clean configure. The build looks for apps when it configures, so after adding one run idf.py -B build_v2 fullclean on the firmware (build_v1 for a v1 board), or rerun cmake -B build for the simulator.

Three layers of checks, from fastest to closest to the real thing:

  1. Host tests. Keep the logic you can test (the numbers, the state) in plain C++ headers without LVGL, and test them with Catch2 on your computer. Every example has a tests/ folder that does this.
  2. The simulator. Open your app, use it, close it, and open it again. Most second-visit problems show here. What the simulator does differently: Simulator vs CrossPad.
  3. The board. Open your app, use it, close it, and open it again, several times. Then go deep into it and press the power button to come all the way back home. On the firmware, check with heap_caps_get_free_size(MALLOC_CAP_INTERNAL) that internal RAM comes back after each close (Memory).
SymptomLikely causeFix
Works on the first visit, not on the secondA file-scope pointer still points at a widget from the last visit, or a timer or subscription from the last visit is still runningReset your statics at the top of create; in destroy, delete every timer, unsubscribe, and set widget pointers back to null
LoadProhibited in an lv_timer callback after the app closedA timer of yours was not deleted and reaches deleted widgetslv_timer_delete every timer in destroy, before the root goes
LoadProhibited in lv_obj_get_state or lv_group_get_focused around a closeThe encoder group was deleted while the encoder was still using it, or a saved group was put back after it was deletedDelete groups with lv_group_delete_deferred; leave restoring the encoder to the framework, and if you restore one yourself, use lv_indev_restore_group, which skips a deleted group
LoadProhibited somewhere in LVGL, far from your code, now and thenLVGL touched from a pad callback or an event subscriber, or lv_async_call from another taskAtomics read by an lv_timer; from an event subscriber or another task, postToGui (Threads)
The encoder stops responding after closing with a dialog openA ui_modal_confirm dialog hands the encoder back to the group you gave it, which destroy has deletedDismiss open dialogs with ui_modal_confirm_dismiss in destroy, before deleting the group
The app is not in the simulator’s launcher or the build’s app listThe registry is full (console: AppRegistry: no room for '<name>'); the build was not reconfigured after the app was added; the registration is not in a .cpp under src/, or the component lacks WHOLE_ARCHIVE ONCount the apps (Limits); fullclean and build again; copy the example’s layout and CMakeLists.txt
The link fails with a multiple definition of _register_<Name>_app()Two apps registered the same nameRename one: the first argument of REGISTER_APP
A blank icon, or another app’s iconAnother app’s file has the same name (Asset collision in the configure log), or the assets on the board are older than the build (an OTA update does not rewrite them)Name the icon after your app id; send the assets as in Start from the template, step 5
The app does not react to the padsThe pad logic was not registered, or under another string than argument 10 of REGISTER_APP_PL (console: PadManager: App '<name>' not found, using default pad logic), or the app uses REGISTER_APPRegister the handler once, under the same string, before create returns (What an app is)
After the app closes, the pads keep its colours, or a note keeps soundingonDeactivate did not clear the colours the logic set, or did not stop a note it startedIn onDeactivate, call clearPadColor for every pad you coloured and stop your notes, as Pads and LEDs and Play a sample do