From 81ef3ea3c5d0cfee9fc5d3bc0c5057d2e99d8d13 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:59:31 +1100 Subject: [PATCH] update hint text for nav hint for first-time flashers; fix spiffs failure for first-time flash boot --- examples/companion_radio/MyMesh.cpp | 1 + examples/companion_radio/NodePrefs.h | 1 + examples/companion_radio/main.cpp | 27 +++++ examples/companion_radio/ui-new/UITask.cpp | 110 ++++++++++++++++++++- examples/companion_radio/ui-new/UITask.h | 6 ++ merge_firmware.py | 98 +++++++++++++++++- 6 files changed, 239 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 77df6bd..e7a4fa6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1240,6 +1240,7 @@ void MyMesh::begin(bool has_display) { if (_prefs.autoadd_max_hops > 64) _prefs.autoadd_max_hops = 0; if (_prefs.dark_mode > 1) _prefs.dark_mode = 0; if (_prefs.portrait_mode > 1) _prefs.portrait_mode = 0; + if (_prefs.hint_shown > 1) _prefs.hint_shown = 0; #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index b4e339f..a985f43 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -38,4 +38,5 @@ struct NodePrefs { // persisted to file uint8_t dark_mode; // 0=off (white bg), 1=on (black bg) uint8_t portrait_mode; // 0=landscape, 1=portrait — T5S3 only uint8_t auto_lock_minutes; // 0=disabled, 2/5/10/15/30=auto-lock after idle + uint8_t hint_shown; // 0=show nav hint on boot, 1=already shown (dismiss permanently) }; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 1e352c6..73635a7 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -716,6 +716,12 @@ static void lastHeardToggleContact() { int vx, vy; touchToVirtual(x, y, vx, vy); + // Dismiss boot navigation hint on any tap + if (ui_task.isHintActive()) { + ui_task.dismissBootHint(); + return 0; + } + // --- Status bar tap (top ~18 virtual units) → go home from any non-home screen --- // Exception: text reader reading mode uses full screen for content (no header) if (vy < 18 && !ui_task.isOnHomeScreen()) { @@ -1731,6 +1737,21 @@ void setup() { if (strcmp(prefs->node_name, defaultName) == 0) { MESH_DEBUG_PRINTLN("setup() - Default node name detected, launching onboarding"); ui_task.gotoOnboarding(); + // Show hint immediately overlaid on the onboarding screen + if (!prefs->hint_shown) ui_task.showBootHint(true); + } else if (!prefs->hint_shown) { + // Not a first-time flash (has a name), but hint never dismissed yet + // Deferred — will activate after splash screen transitions to home + ui_task.showBootHint(false); + } + } + #endif + + #if defined(LilyGo_T5S3_EPaper_Pro) + { + NodePrefs* prefs = the_mesh.getNodePrefs(); + if (!prefs->hint_shown) { + ui_task.showBootHint(false); // Deferred — after splash } } #endif @@ -2487,6 +2508,12 @@ void handleKeyboardInput() { // Block all keyboard input while lock screen is active. // Still read the key above to clear the TCA8418 buffer. if (ui_task.isLocked()) return; + + // Dismiss boot navigation hint on any keypress + if (ui_task.isHintActive()) { + ui_task.dismissBootHint(); + return; // Consume the keypress (don't act on it) + } Serial.printf("handleKeyboardInput: key='%c' (0x%02X) composeMode=%d\n", key >= 32 ? key : '?', key, composeMode); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 1d2a6e0..78b3e6e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1238,6 +1238,34 @@ void UITask::showAlert(const char* text, int duration_millis) { _next_refresh = millis() + 100; // trigger re-render to show updated text } +void UITask::showBootHint(bool immediate) { + if (immediate) { + // Activate now — used when hint should overlay the current screen (e.g. onboarding) + _hintActive = true; + _hintExpiry = millis() + 8000; // 8 seconds auto-dismiss + _pendingBootHint = false; + _next_refresh = millis() + 100; + Serial.println("[UI] Boot hint activated (immediate)"); + } else { + // Defer until after splash screen — actual activation happens in gotoHomeScreen() + _pendingBootHint = true; + Serial.println("[UI] Boot hint pending (will show after splash)"); + } +} + +void UITask::dismissBootHint() { + if (!_hintActive) return; + _hintActive = false; + _hintExpiry = 0; + // Persist so hint never shows again + if (_node_prefs) { + _node_prefs->hint_shown = 1; + the_mesh.savePrefs(); + } + _next_refresh = millis() + 100; + Serial.println("[UI] Boot hint dismissed"); +} + void UITask::notify(UIEventType t) { #if defined(PIN_BUZZER) switch(t){ @@ -1426,6 +1454,7 @@ void UITask::setCurrScreen(UIScreen* c) { curr = c; _alert_expiry = 0; // Dismiss any active toast — prevents stale overlay from // triggering extra 644ms e-ink refreshes on the new screen + if (_hintActive) dismissBootHint(); // Dismiss hint when navigating away _next_refresh = 100; } @@ -1600,6 +1629,14 @@ void UITask::loop() { } #endif + if (c != 0 && curr) { + // Dismiss boot hint on any button input (boot button on T5S3) + if (_hintActive) { + dismissBootHint(); + c = 0; // Consume the press + } + } + if (c != 0 && curr) { curr->handleInput(c); _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer @@ -1721,7 +1758,41 @@ if (curr) curr->poll(); } #endif - if (millis() < _alert_expiry) { + if (_hintActive && millis() < _hintExpiry) { + // Boot navigation hint overlay — multi-line, larger box + _display->setTextSize(1); + int w = _display->width(); + int h = _display->height(); + int boxX = w / 8; + int boxY = h / 5; + int boxW = w - boxX * 2; + int boxH = h * 3 / 5; + _display->setColor(DisplayDriver::DARK); + _display->fillRect(boxX, boxY, boxW, boxH); + _display->setColor(DisplayDriver::LIGHT); + _display->drawRect(boxX, boxY, boxW, boxH); + int cx = w / 2; + int lineH = 11; + int startY = boxY + 6; +#if defined(LilyGo_T5S3_EPaper_Pro) + _display->drawTextCentered(cx, startY, "Swipe: Navigate"); + _display->drawTextCentered(cx, startY + lineH, "Tap: Select"); + _display->drawTextCentered(cx, startY + lineH * 2, "Long Press: Action"); + _display->drawTextCentered(cx, startY + lineH * 3, "Boot Btn: Home"); + _display->drawTextCentered(cx, startY + lineH * 4 + 4, "[Tap to dismiss hint]"); +#else + _display->drawTextCentered(cx, startY, "M:Msgs C:Contacts"); + _display->drawTextCentered(cx, startY + lineH, "S:Settings E:Reader"); + _display->drawTextCentered(cx, startY + lineH * 2, "N:Notes W/S:Scroll"); + _display->drawTextCentered(cx, startY + lineH * 3, "A/D:Cycle Left/Right"); + _display->drawTextCentered(cx, startY + lineH * 4 + 4, "[X to dismiss hint]"); +#endif + _next_refresh = _hintExpiry; + } else if (_hintActive) { + // Hint expired — auto-dismiss + dismissBootHint(); + _next_refresh = millis() + 200; + } else if (millis() < _alert_expiry) { _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; @@ -1737,7 +1808,33 @@ if (curr) curr->poll(); } #else int delay_millis = curr->render(*_display); - if (millis() < _alert_expiry) { // render alert popup + if (_hintActive && millis() < _hintExpiry) { + // Boot navigation hint overlay — multi-line, larger box + _display->setTextSize(1); + int w = _display->width(); + int h = _display->height(); + int boxX = w / 8; + int boxY = h / 5; + int boxW = w - boxX * 2; + int boxH = h * 3 / 5; + _display->setColor(DisplayDriver::DARK); + _display->fillRect(boxX, boxY, boxW, boxH); + _display->setColor(DisplayDriver::LIGHT); + _display->drawRect(boxX, boxY, boxW, boxH); + int cx = w / 2; + int lineH = 11; + int startY = boxY + 6; + _display->drawTextCentered(cx, startY, "M:Msgs C:Contacts"); + _display->drawTextCentered(cx, startY + lineH, "S:Settings E:Reader"); + _display->drawTextCentered(cx, startY + lineH * 2, "N:Notes W/S:Scroll"); + _display->drawTextCentered(cx, startY + lineH * 3, "A/D:Cycle Left/Right"); + _display->drawTextCentered(cx, startY + lineH * 4 + 4, "[X to dismiss]"); + _next_refresh = _hintExpiry; + } else if (_hintActive) { + // Hint expired — auto-dismiss + dismissBootHint(); + _next_refresh = millis() + 200; + } else if (millis() < _alert_expiry) { // render alert popup _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; @@ -2248,6 +2345,15 @@ void UITask::gotoHomeScreen() { } _auto_off = millis() + AUTO_OFF_MILLIS; _next_refresh = 100; + + // Activate deferred boot hint now that home screen is visible + if (_pendingBootHint) { + _pendingBootHint = false; + _hintActive = true; + _hintExpiry = millis() + 8000; // 8 seconds auto-dismiss + _next_refresh = millis() + 100; + Serial.println("[UI] Boot hint activated"); + } } bool UITask::isEditingHomeScreen() const { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 8d95cdb..1aa2466 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -56,6 +56,9 @@ class UITask : public AbstractUITask { NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; + bool _hintActive = false; // Boot navigation hint overlay + unsigned long _hintExpiry = 0; // Auto-dismiss time for hint + bool _pendingBootHint = false; // Deferred hint — show after splash screen int _msgcount; unsigned long ui_started_at, next_batt_chck; uint8_t _low_batt_count = 0; // Consecutive low-voltage readings for debounce @@ -186,6 +189,9 @@ public: #endif void showAlert(const char* text, int duration_millis) override; void forceRefresh() override { _next_refresh = 100; } + void showBootHint(bool immediate = false); // Show navigation hint overlay on first boot + void dismissBootHint(); // Dismiss hint and save preference + bool isHintActive() const { return _hintActive; } // Wake display and extend auto-off timer. Call this when handling keys // outside of injectKey() to prevent display auto-off during direct input. void keepAlive() { diff --git a/merge_firmware.py b/merge_firmware.py index 01cab2a..64ea17b 100644 --- a/merge_firmware.py +++ b/merge_firmware.py @@ -1,7 +1,10 @@ """ -PlatformIO post-build script: merge bootloader + partitions + firmware +PlatformIO post-build script: merge bootloader + partitions + firmware + SPIFFS into a single flashable binary. +Includes a pre-formatted empty SPIFFS image so first-boot doesn't need to +format the partition (which takes 1-2 minutes on 16MB flash). + Output: .pio/build//firmware_merged.bin Flash: esptool.py --chip esp32s3 write_flash 0x0 firmware_merged.bin @@ -12,6 +15,87 @@ Add to each environment (or the base section): Import("env") +def find_spiffs_partition(partitions_bin): + """Parse compiled partitions.bin to find SPIFFS partition offset and size. + + ESP32 partition entry format (32 bytes each): + 0xAA50 magic, type, subtype, offset(u32le), size(u32le), label(16), flags(u32le) + SPIFFS: type=0x01(data), subtype=0x82(spiffs) + """ + import struct + + with open(partitions_bin, "rb") as f: + data = f.read() + + for i in range(0, len(data) - 32, 32): + magic = struct.unpack_from("