update hint text for nav hint for first-time flashers; fix spiffs failure for first-time flash boot

This commit is contained in:
pelgraine
2026-03-23 14:59:31 +11:00
parent 6f07b7a372
commit 81ef3ea3c5
6 changed files with 239 additions and 4 deletions

View File

@@ -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) {

View File

@@ -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)
};

View File

@@ -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
@@ -2488,6 +2509,12 @@ void handleKeyboardInput() {
// 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);

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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/<env>/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("<H", data, i)[0]
if magic != 0xAA50:
continue
ptype = data[i + 2]
subtype = data[i + 3]
offset = struct.unpack_from("<I", data, i + 4)[0]
size = struct.unpack_from("<I", data, i + 8)[0]
label = data[i + 12:i + 28].split(b'\x00')[0].decode("ascii", errors="ignore")
if ptype == 0x01 and subtype == 0x82: # data/spiffs
return offset, size, label
return None, None, None
def build_spiffs_image(env, size):
"""Generate an empty formatted SPIFFS image using mkspiffs."""
import subprocess, os, tempfile, glob
build_dir = env.subst("$BUILD_DIR")
spiffs_bin = os.path.join(build_dir, "spiffs_empty.bin")
# If already generated for this build, reuse it
if os.path.isfile(spiffs_bin) and os.path.getsize(spiffs_bin) == size:
return spiffs_bin
# Find mkspiffs in PlatformIO packages
pio_home = os.path.expanduser("~/.platformio")
mkspiffs_paths = glob.glob(os.path.join(pio_home, "packages", "tool-mkspiffs*", "mkspiffs*"))
if not mkspiffs_paths:
# Also check platform-specific tool paths
mkspiffs_paths = glob.glob(os.path.join(pio_home, "packages", "tool-mklittlefs*", "mkspiffs*"))
mkspiffs = None
for p in mkspiffs_paths:
if os.path.isfile(p) and os.access(p, os.X_OK):
mkspiffs = p
break
if not mkspiffs:
print("[merge] WARNING: mkspiffs not found, skipping SPIFFS image")
return None
# Create empty data directory for mkspiffs
data_dir = os.path.join(build_dir, "_empty_spiffs_data")
os.makedirs(data_dir, exist_ok=True)
# SPIFFS block/page sizes — ESP32 Arduino defaults
block_size = 4096
page_size = 256
cmd = [
mkspiffs,
"-c", data_dir,
"-b", str(block_size),
"-p", str(page_size),
"-s", str(size),
spiffs_bin,
]
print(f"[merge] Generating empty SPIFFS image ({size // 1024} KB)...")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0 and os.path.isfile(spiffs_bin):
print(f"[merge] SPIFFS image OK: {spiffs_bin}")
return spiffs_bin
else:
print(f"[merge] mkspiffs failed: {result.stderr}")
return None
def merge_bin(source, target, env):
import subprocess, os
@@ -52,8 +136,18 @@ def merge_bin(source, target, env):
"0x10000", firmware,
]
# Try to include a pre-formatted SPIFFS image (eliminates 1-2 min first-boot format)
spiffs_offset, spiffs_size, spiffs_label = find_spiffs_partition(partitions)
if spiffs_offset and spiffs_size:
spiffs_bin = build_spiffs_image(env, spiffs_size)
if spiffs_bin:
cmd.extend([f"0x{spiffs_offset:x}", spiffs_bin])
print(f"[merge] Including SPIFFS image at 0x{spiffs_offset:x} ({spiffs_size // 1024} KB)")
else:
print("[merge] No SPIFFS partition found in partition table, skipping SPIFFS image")
print(f"\n[merge] Creating merged firmware for {env_name}...")
print(f"[merge] {' '.join(cmd[-6:])}")
print(f"[merge] {' '.join(cmd[-8:])}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0: