From ad96725da6d336ccb33d1bd76b267ab2f4dcf351 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Thu, 8 Oct 2026 22:26:22 +0200 Subject: [PATCH 1/2] Crash diagnostics app rewrite --- Documentation/ideas.md | 6 - .../Private/Tactility/app/boot/BootScreen.h | 16 ++ .../app/crashdiagnostics/CrashDiagnostics.h | 11 +- .../Private/Tactility/app/launcher/Launcher.h | 7 - Tactility/Source/InitApps.cpp | 2 - Tactility/Source/app/boot/BootInit.cpp | 13 +- Tactility/Source/app/boot/BootScreen.cpp | 57 ++++- .../app/crashdiagnostics/CrashDiagnostics.cpp | 237 ++++-------------- Tactility/Source/app/launcher/Launcher.cpp | 9 - 9 files changed, 130 insertions(+), 228 deletions(-) delete mode 100644 Tactility/Private/Tactility/app/launcher/Launcher.h diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 2942c9153..64d4a7740 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -7,13 +7,8 @@ ## Higher Priority -- Wi-Fi and Bluetooth settings app: scanning should start and gather results, then it should stop refreshing the scan results automatically. When scanning is finished, a paginated view should be shown with the results (as many as fit on the screen, but not more). There should be an icon on the toolbar to refresh scanning (looking glass? something else?) and this icon is only visible when the radio is enabled. When radio is enabled by the user, automatically start a scan. If the radio was already enabled and the app starts, start scanning. - Update to LVGL 9.6.x (ESP-IDF and POSIX) -- Terminal app: on devices with pointer device: LVGL rendering mode should hide the toolbar by default, but tapping the terminal should toggle the visibility of the toolbar. -- Terminal app: should always run in non-LVGL mode if no PSRAM is present. -- Crash app should work without LVGL, like Boot app. It should wait for any input (keyboard or pointer) to continue. - NimBLE looses pairing key after reboot -- CrashDiagnostics shouldn't show a QR when there's no callstack - Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp - Add bold fonts for e-ink readability improvement - Improve Setup: Add keyboard/keypad/encoder navigation explanation. @@ -49,7 +44,6 @@ - Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html - Make USB host driver disabled by default, so it doesn't consume memory - TactilityTool: Make API compatibility table (and check for compatibility in the tool itself) -- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device) - Refactor HttpServer into C code and move implementation to http-module - Use GPS time to set/update the current time - Make a URL handler. Use it for handling local files. Match file types with apps. diff --git a/Tactility/Private/Tactility/app/boot/BootScreen.h b/Tactility/Private/Tactility/app/boot/BootScreen.h index bb5c78131..52bc97e82 100644 --- a/Tactility/Private/Tactility/app/boot/BootScreen.h +++ b/Tactility/Private/Tactility/app/boot/BootScreen.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -30,6 +32,12 @@ class BootScreen { int logicalWidth() const { return (rotation % 2 == 0) ? panelWidth : panelHeight; } int logicalHeight() const { return (rotation % 2 == 0) ? panelHeight : panelWidth; } + /** @return true when the image has a pixel at (x, y), with its colour as RGB565 */ + using ImagePixel = std::function; + + /** Draws an image with text lines below it, centered on a black background */ + void draw(int imageWidth, int imageHeight, const ImagePixel& imagePixel, const std::vector& lines); + public: ~BootScreen(); @@ -49,6 +57,14 @@ class BootScreen { * @param[in] lines text lines, wrapped when they're too long for the display */ void show(const std::string& logoPath, const std::vector& lines); + + /** + * Shows a QR code with text lines below it, centered on a black background. The set modules are white. + * @param[in] moduleCount the width and height of the QR code in modules + * @param[in] isModuleSet whether the module at (x, y) is set + * @param[in] lines text lines, wrapped when they're too long for the display + */ + void showQrCode(int moduleCount, const std::function& isModuleSet, const std::vector& lines); }; /** diff --git a/Tactility/Private/Tactility/app/crashdiagnostics/CrashDiagnostics.h b/Tactility/Private/Tactility/app/crashdiagnostics/CrashDiagnostics.h index 9ffa03b02..ffb799262 100644 --- a/Tactility/Private/Tactility/app/crashdiagnostics/CrashDiagnostics.h +++ b/Tactility/Private/Tactility/app/crashdiagnostics/CrashDiagnostics.h @@ -2,9 +2,18 @@ #ifdef ESP_PLATFORM +namespace tt::app::boot { +class BootScreen; +} + namespace tt::app::crashdiagnostics { -void start(); +/** + * Writes the crash log and shows the crash with a QR code for reporting it, after a panic reboot. + * Returns when the user continues with any input, and blocks forever when there's no input device to continue with. + * @param[in] screen the boot screen, which may have no display + */ +void showCrashScreen(boot::BootScreen& screen); } diff --git a/Tactility/Private/Tactility/app/launcher/Launcher.h b/Tactility/Private/Tactility/app/launcher/Launcher.h deleted file mode 100644 index f5e13716a..000000000 --- a/Tactility/Private/Tactility/app/launcher/Launcher.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -namespace tt::app::launcher { - -uint32_t start(); - -} diff --git a/Tactility/Source/InitApps.cpp b/Tactility/Source/InitApps.cpp index b4235889c..1d5bc4362 100644 --- a/Tactility/Source/InitApps.cpp +++ b/Tactility/Source/InitApps.cpp @@ -81,7 +81,6 @@ namespace app { #ifdef ESP_PLATFORM namespace apwebserver { extern const ::AppManifest manifest; } namespace camera { extern const ::AppManifest manifest; } - namespace crashdiagnostics { extern const ::AppManifest manifest; } #if CONFIG_TT_TDECK_WORKAROUND == 1 namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now #endif @@ -152,7 +151,6 @@ static void registerInternalApps() { if (device_exists_of_type(&CAMERA_TYPE)) { app_manager_add(&app::camera::manifest); } - app_manager_add(&app::crashdiagnostics::manifest); #if defined(CONFIG_TT_TDECK_WORKAROUND) app_manager_add(&app::keyboardsettings::manifest); #endif diff --git a/Tactility/Source/app/boot/BootInit.cpp b/Tactility/Source/app/boot/BootInit.cpp index ab92b153e..96ffd0859 100644 --- a/Tactility/Source/app/boot/BootInit.cpp +++ b/Tactility/Source/app/boot/BootInit.cpp @@ -187,13 +187,6 @@ void waitForInputAndReboot() { } void startNextApp() { -#ifdef ESP_PLATFORM - if (esp_reset_reason() == ESP_RST_PANIC) { - crashdiagnostics::start(); // fire-and-forget; no result expected back - return; - } -#endif - auto launcher_app_id = getLauncherAppId(); uint32_t launcher_instance_id = 0; AppStartContext context; @@ -271,6 +264,12 @@ bool bootInit(TickType_t startTime) { return false; } +#ifdef ESP_PLATFORM + if (esp_reset_reason() == ESP_RST_PANIC) { + crashdiagnostics::showCrashScreen(screen); + } +#endif + LOG_I(TAG, "Loading fonts"); lvgl::loadFonts(lvgl::loadFontConfiguration()); lvgl::configureTheme(settings::appearance::loadOrGetDefault()); diff --git a/Tactility/Source/app/boot/BootScreen.cpp b/Tactility/Source/app/boot/BootScreen.cpp index d04859156..a008ecaaa 100644 --- a/Tactility/Source/app/boot/BootScreen.cpp +++ b/Tactility/Source/app/boot/BootScreen.cpp @@ -154,7 +154,41 @@ void BootScreen::show(const std::string& logoPath, const std::vector(logo.width), static_cast(logo.height), [&logo](int x, int y, uint16_t& color) { + const unsigned char* pixel = logo.rgba + (static_cast(y) * logo.width + x) * 4; + const uint32_t alpha = pixel[3]; + if (alpha == 0) { + return false; + } + // Blended onto the black background + color = toRgb565(pixel[0] * alpha / 255, pixel[1] * alpha / 255, pixel[2] * alpha / 255); + return true; + }, lines); +} + +void BootScreen::showQrCode(int moduleCount, const std::function& isModuleSet, const std::vector& lines) { + if (target == nullptr || moduleCount <= 0) { + return; + } + + // About 60% of the display, which leaves room for the text + const int module_size = std::max(1, getSmallestDimension() * 6 / 10 / moduleCount); + const int size = module_size * moduleCount; + draw(size, size, [&isModuleSet, module_size](int x, int y, uint16_t& color) { + if (!isModuleSet(x / module_size, y / module_size)) { + return false; + } + color = 0xFFFF; + return true; + }, lines); +} + +void BootScreen::draw(int imageWidth, int imageHeight, const ImagePixel& imagePixel, const std::vector& lines) { + if (target == nullptr) { + return; + } + + // Layout in logical (rotated) coordinates: the image with text below it, centered as a whole if (!lines.empty() && textFont == nullptr) { textFont = lvgl::loadMonoFont(); if (textFont == nullptr) { @@ -179,10 +213,10 @@ void BootScreen::show(const std::string& logoPath, const std::vector(wrapped_lines.size()) * line_height; - const int gap = (logo.height > 0 && !wrapped_lines.empty()) ? line_height : 0; - const int content_top = (logical_height - static_cast(logo.height) - gap - text_height) / 2; - const int logo_left = (logical_width - static_cast(logo.width)) / 2; - const int text_top = content_top + static_cast(logo.height) + gap; + const int gap = (imageHeight > 0 && !wrapped_lines.empty()) ? line_height : 0; + const int content_top = (logical_height - imageHeight - gap - text_height) / 2; + const int image_left = (logical_width - imageWidth) / 2; + const int text_top = content_top + imageHeight + gap; std::vector glyph_bitmap; for (int band_top = 0; band_top < panelHeight; band_top += bandHeight) { @@ -204,14 +238,11 @@ void BootScreen::show(const std::string& logoPath, const std::vector(y) * logo.width + x) * 4; - const uint32_t alpha = pixel[3]; - if (alpha != 0) { - // Blended onto the black background - const uint16_t color = toRgb565(pixel[0] * alpha / 255, pixel[1] * alpha / 255, pixel[2] * alpha / 255); - plot(logo_left + static_cast(x), content_top + static_cast(y), color, PIXEL_BUFFER_CONVERSION_LUMA_THRESHOLD); + for (int y = 0; y < imageHeight; y++) { + for (int x = 0; x < imageWidth; x++) { + uint16_t color; + if (imagePixel(x, y, color)) { + plot(image_left + x, content_top + y, color, PIXEL_BUFFER_CONVERSION_LUMA_THRESHOLD); } } } diff --git a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp index a59d21e69..179319756 100644 --- a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp +++ b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp @@ -1,26 +1,15 @@ #ifdef ESP_PLATFORM -#include "Tactility/PanicHandler.h" - +#include +#include +#include #include #include -#include #include -#include - -#include -#include -#include -#include -#include -#include - -#include #include -#include -#include +#include #include #include @@ -32,32 +21,18 @@ #include -#include #include #include #include +#include +#include namespace tt::app::crashdiagnostics { constexpr auto* TAG = "CrashDiagnostics"; -extern const ::AppManifest manifest; - namespace { -struct Context { - uint32_t appInstanceId; - // Set when widget creation hit an unrecoverable error (e.g. the QR code doesn't fit on - // screen) - appMain() skips the event loop and closes immediately without ever starting - // the launcher, matching the old model's stop()-without-launcher-start() error paths. - bool hasFatalError = false; - // Set by onContinuePressed() right before it emits APP_EVENT_CLOSE - read by appMain() - // after its own thread finishes cleanup, to decide whether to start the launcher - // afterwards (matches the old model's onContinuePressed(): stop() then launcher::start()). - bool continuePressed = false; -}; - - const char* crashCauseToString(CrashCause cause) { switch (cause) { case CrashCause::Debug: return "Debug"; @@ -117,175 +92,71 @@ void writeCrashLogFile(const CrashData& crashData) { } } -void onContinuePressed(lv_event_t* event) { - auto* ctx = static_cast(lv_event_get_user_data(event)); - ctx->continuePressed = true; - app_event_emit_close(ctx->appInstanceId); -} - -void createWidgets(lv_obj_t* parent, void* userData) { - auto* ctx = static_cast(userData); - - auto* display = lv_obj_get_display(parent); - int32_t parent_height = lv_display_get_vertical_resolution(display) - lvgl::statusbar_get_height(); - - lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_SHORT_CLICKED, ctx); - auto* top_label = lv_label_create(parent); - lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages - lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2); - - auto* bottom_label = lv_label_create(parent); - if (device_has_active_by_type(&POINTER_TYPE)) { - lv_label_set_text(bottom_label, "Tap screen to continue"); +void waitForInputOrForever(bool hasInput) { + if (hasInput) { + boot::waitForInput(); } else { - lv_label_set_text(bottom_label, "Reboot device to continue"); - } - lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2); - - const auto& crash_data = getRtcCrashData(); - - std::string url = getUrlFromCrashData(crash_data); - LOG_I(TAG, "%s", url.c_str()); - size_t url_length = url.length(); - - int qr_version; - if (!getQrVersionForBinaryDataLength(url_length, qr_version)) { - LOG_E(TAG, "QR is too large"); - ctx->hasFatalError = true; - return; - } - - LOG_I(TAG, "QR version %d (length: %d)", qr_version, (int)url_length); - auto qrcodeData = std::make_shared(qrcode_getBufferSize(qr_version)); - if (qrcodeData == nullptr) { - LOG_E(TAG, "Failed to allocate QR buffer"); - ctx->hasFatalError = true; - return; - } - - QRCode qrcode; - LOG_I(TAG, "QR init text"); - if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) { - LOG_E(TAG, "QR init text failed"); - ctx->hasFatalError = true; - return; + // Without input to continue with, the device has to be restarted + while (true) { + delay_millis(1000); + } } +} - LOG_I(TAG, "QR size: %d", qrcode.size); - - // Calculate QR dot size - int32_t top_label_height = lv_obj_get_height(top_label) + 2; - int32_t bottom_label_height = lv_obj_get_height(bottom_label) + 2; - LOG_I(TAG, "Create canvas"); - int32_t available_height = parent_height - top_label_height - bottom_label_height; - int32_t available_width = lv_display_get_horizontal_resolution(display); - int32_t smallest_size = std::min(available_height, available_width); - // Target ~60% of the available space so the code scales with screen size but keeps a margin - // from the labels/screen edges. - int32_t target_size = smallest_size * 6 / 10; - int32_t pixel_size = std::max(1, target_size / qrcode.size); - if (pixel_size * qrcode.size > smallest_size) { - LOG_E(TAG, "QR code won't fit screen"); - ctx->hasFatalError = true; - return; - } +} // namespace - auto* canvas = lv_canvas_create(parent); - lv_obj_set_size(canvas, pixel_size * qrcode.size, pixel_size * qrcode.size); - lv_obj_align(canvas, LV_ALIGN_CENTER, 0, 0); - lv_canvas_fill_bg(canvas, lv_color_black(), LV_OPA_COVER); - lv_obj_set_content_height(canvas, qrcode.size * pixel_size); - lv_obj_set_content_width(canvas, qrcode.size * pixel_size); +void showCrashScreen(boot::BootScreen& screen) { + const auto& crash_data = getRtcCrashData(); + writeCrashLogFile(crash_data); - LOG_I(TAG, "Create draw buffer"); - auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO); - if (draw_buf == nullptr) { - LOG_E(TAG, "Failed to allocate draw buffer"); - ctx->hasFatalError = true; - return; + std::string prompt = boot::getInputPrompt("continue"); + const bool has_input = !prompt.empty(); + if (!has_input) { + prompt = "Restart device"; } - - lv_canvas_set_draw_buf(canvas, draw_buf); - - for (uint8_t y = 0; y < qrcode.size; y++) { - for (uint8_t x = 0; x < qrcode.size; x++) { - bool colored = qrcode_getModule(&qrcode, x, y); - auto color = colored ? lv_color_white() : lv_color_black(); - int32_t pos_x = x * pixel_size; - int32_t pos_y = y * pixel_size; - for (int px = 0; px < pixel_size; px++) { - for (int py = 0; py < pixel_size; py++) { - lv_canvas_set_px(canvas, pos_x + px, pos_y + py, color, LV_OPA_COVER); - } - } + // Without a callstack, there's nothing worth reporting through the QR code + if (crash_data.callstackLength == 0) { + std::vector lines = { "Oops! We've crashed ..."}; + if (crash_data.cause != CrashCause::Unknown) { + lines.push_back(std::string("Only the cause is known: ") + crashCauseToString(crash_data.cause)); } + lines.push_back(prompt); + screen.show("", lines); + waitForInputOrForever(has_input); + return; } -} - -int32_t appMain(int argc, char* argv[]) { - uint32_t appInstanceId = app_scheduler_current_app_id(); - Context ctx {}; - ctx.appInstanceId = appInstanceId; - - writeCrashLogFile(getRtcCrashData()); - - TaskEventGroup event_group {}; - task_event_group_construct(&event_group); - - AppEventSubscription sub {}; - check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); + const std::vector lines = { "Oops! We've crashed ...", prompt }; - if (!ctx.hasFatalError) { - bool shouldClose = false; - while (!shouldClose) { - task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); - - AppEvent event {}; - while (app_event_poll(&sub, &event) == ERROR_NONE) { - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; - } - if (shouldClose) break; - } + // The QR code links to a page that shows the crash details + const std::string url = getUrlFromCrashData(crash_data); + LOG_I(TAG, "%s", url.c_str()); + int qr_version; + std::unique_ptr qr_buffer; + QRCode qr_code; + bool has_qr_code = false; + if (!getQrVersionForBinaryDataLength(url.length(), qr_version)) { + LOG_E(TAG, "QR is too large"); + } else { + qr_buffer = std::make_unique(qrcode_getBufferSize(qr_version)); + if (qrcode_initText(&qr_code, qr_buffer.get(), qr_version, ECC_LOW, url.c_str()) != 0) { + LOG_E(TAG, "QR init text failed"); + } else { + has_qr_code = true; } } - window_manager_remove(window); - check(app_event_unsubscribe(&sub) == ERROR_NONE); - task_event_group_destruct(&event_group); - - bool continuePressed = ctx.continuePressed; - - if (continuePressed) { - launcher::start(); + if (has_qr_code) { + screen.showQrCode(qr_code.size, [&qr_code](int x, int y) { + return qrcode_getModule(&qr_code, static_cast(x), static_cast(y)); + }, lines); + } else { + screen.show("", lines); } - return 0; + waitForInputOrForever(has_input); } -} // namespace - -void start() { - uint32_t instanceId = 0; - AppStartContext context = app_start_context_for_manifest(&manifest); - app_start_with_context(&context, &instanceId); } -extern const ::AppManifest manifest = { - .id = "tactility.crashdiagnostics", - .name = "Crash Diagnostics", - .category = APP_CATEGORY_SYSTEM, - .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast(appMain) }, - .flags = APP_MANIFEST_FLAG_HIDDEN, - .stack = {} -}; - -} // namespace - #endif diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 687136437..2923c1d3d 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -221,13 +221,4 @@ extern const ::AppManifest manifest = { .stack = { .depth = 5120, .desired_memory_capability = 0 } }; -// Kept for Tactility/Private/Tactility/app/launcher/Launcher.h's existing declaration (still -// used by the old, unconverted CrashDiagnostics app to return to the launcher after a crash). -uint32_t start() { - uint32_t instance_id = 0; - AppStartContext context = app_start_context_for_manifest(&manifest); - app_start_with_context(&context, &instance_id); - return instance_id; -} - } // namespace From 52b133eed9e8170a32bb3e4fdf7522d662bc66be Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Thu, 8 Oct 2026 23:31:15 +0200 Subject: [PATCH 2/2] Fix --- Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp index 179319756..17ffff167 100644 --- a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp +++ b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -138,8 +139,10 @@ void showCrashScreen(boot::BootScreen& screen) { if (!getQrVersionForBinaryDataLength(url.length(), qr_version)) { LOG_E(TAG, "QR is too large"); } else { - qr_buffer = std::make_unique(qrcode_getBufferSize(qr_version)); - if (qrcode_initText(&qr_code, qr_buffer.get(), qr_version, ECC_LOW, url.c_str()) != 0) { + qr_buffer.reset(new (std::nothrow) uint8_t[qrcode_getBufferSize(qr_version)]); + if (qr_buffer == nullptr) { + LOG_E(TAG, "Failed to allocate QR buffer"); + } else if (qrcode_initText(&qr_code, qr_buffer.get(), qr_version, ECC_LOW, url.c_str()) != 0) { LOG_E(TAG, "QR init text failed"); } else { has_qr_code = true;