mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
update hint text for nav hint for first-time flashers; fix spiffs failure for first-time flash boot
This commit is contained in:
@@ -1240,6 +1240,7 @@ void MyMesh::begin(bool has_display) {
|
|||||||
if (_prefs.autoadd_max_hops > 64) _prefs.autoadd_max_hops = 0;
|
if (_prefs.autoadd_max_hops > 64) _prefs.autoadd_max_hops = 0;
|
||||||
if (_prefs.dark_mode > 1) _prefs.dark_mode = 0;
|
if (_prefs.dark_mode > 1) _prefs.dark_mode = 0;
|
||||||
if (_prefs.portrait_mode > 1) _prefs.portrait_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
|
#ifdef BLE_PIN_CODE // 123456 by default
|
||||||
if (_prefs.ble_pin == 0) {
|
if (_prefs.ble_pin == 0) {
|
||||||
|
|||||||
@@ -38,4 +38,5 @@ struct NodePrefs { // persisted to file
|
|||||||
uint8_t dark_mode; // 0=off (white bg), 1=on (black bg)
|
uint8_t dark_mode; // 0=off (white bg), 1=on (black bg)
|
||||||
uint8_t portrait_mode; // 0=landscape, 1=portrait — T5S3 only
|
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 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)
|
||||||
};
|
};
|
||||||
@@ -716,6 +716,12 @@ static void lastHeardToggleContact() {
|
|||||||
int vx, vy;
|
int vx, vy;
|
||||||
touchToVirtual(x, y, 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 ---
|
// --- 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)
|
// Exception: text reader reading mode uses full screen for content (no header)
|
||||||
if (vy < 18 && !ui_task.isOnHomeScreen()) {
|
if (vy < 18 && !ui_task.isOnHomeScreen()) {
|
||||||
@@ -1731,6 +1737,21 @@ void setup() {
|
|||||||
if (strcmp(prefs->node_name, defaultName) == 0) {
|
if (strcmp(prefs->node_name, defaultName) == 0) {
|
||||||
MESH_DEBUG_PRINTLN("setup() - Default node name detected, launching onboarding");
|
MESH_DEBUG_PRINTLN("setup() - Default node name detected, launching onboarding");
|
||||||
ui_task.gotoOnboarding();
|
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
|
#endif
|
||||||
@@ -2488,6 +2509,12 @@ void handleKeyboardInput() {
|
|||||||
// Still read the key above to clear the TCA8418 buffer.
|
// Still read the key above to clear the TCA8418 buffer.
|
||||||
if (ui_task.isLocked()) return;
|
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",
|
Serial.printf("handleKeyboardInput: key='%c' (0x%02X) composeMode=%d\n",
|
||||||
key >= 32 ? key : '?', key, composeMode);
|
key >= 32 ? key : '?', key, composeMode);
|
||||||
|
|
||||||
|
|||||||
@@ -1238,6 +1238,34 @@ void UITask::showAlert(const char* text, int duration_millis) {
|
|||||||
_next_refresh = millis() + 100; // trigger re-render to show updated text
|
_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) {
|
void UITask::notify(UIEventType t) {
|
||||||
#if defined(PIN_BUZZER)
|
#if defined(PIN_BUZZER)
|
||||||
switch(t){
|
switch(t){
|
||||||
@@ -1426,6 +1454,7 @@ void UITask::setCurrScreen(UIScreen* c) {
|
|||||||
curr = c;
|
curr = c;
|
||||||
_alert_expiry = 0; // Dismiss any active toast — prevents stale overlay from
|
_alert_expiry = 0; // Dismiss any active toast — prevents stale overlay from
|
||||||
// triggering extra 644ms e-ink refreshes on the new screen
|
// triggering extra 644ms e-ink refreshes on the new screen
|
||||||
|
if (_hintActive) dismissBootHint(); // Dismiss hint when navigating away
|
||||||
_next_refresh = 100;
|
_next_refresh = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1600,6 +1629,14 @@ void UITask::loop() {
|
|||||||
}
|
}
|
||||||
#endif
|
#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) {
|
if (c != 0 && curr) {
|
||||||
curr->handleInput(c);
|
curr->handleInput(c);
|
||||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||||
@@ -1721,7 +1758,41 @@ if (curr) curr->poll();
|
|||||||
}
|
}
|
||||||
#endif
|
#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);
|
_display->setTextSize(1);
|
||||||
int y = _display->height() / 3;
|
int y = _display->height() / 3;
|
||||||
int p = _display->height() / 32;
|
int p = _display->height() / 32;
|
||||||
@@ -1737,7 +1808,33 @@ if (curr) curr->poll();
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
int delay_millis = curr->render(*_display);
|
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);
|
_display->setTextSize(1);
|
||||||
int y = _display->height() / 3;
|
int y = _display->height() / 3;
|
||||||
int p = _display->height() / 32;
|
int p = _display->height() / 32;
|
||||||
@@ -2248,6 +2345,15 @@ void UITask::gotoHomeScreen() {
|
|||||||
}
|
}
|
||||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||||
_next_refresh = 100;
|
_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 {
|
bool UITask::isEditingHomeScreen() const {
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class UITask : public AbstractUITask {
|
|||||||
NodePrefs* _node_prefs;
|
NodePrefs* _node_prefs;
|
||||||
char _alert[80];
|
char _alert[80];
|
||||||
unsigned long _alert_expiry;
|
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;
|
int _msgcount;
|
||||||
unsigned long ui_started_at, next_batt_chck;
|
unsigned long ui_started_at, next_batt_chck;
|
||||||
uint8_t _low_batt_count = 0; // Consecutive low-voltage readings for debounce
|
uint8_t _low_batt_count = 0; // Consecutive low-voltage readings for debounce
|
||||||
@@ -186,6 +189,9 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
void showAlert(const char* text, int duration_millis) override;
|
void showAlert(const char* text, int duration_millis) override;
|
||||||
void forceRefresh() override { _next_refresh = 100; }
|
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
|
// Wake display and extend auto-off timer. Call this when handling keys
|
||||||
// outside of injectKey() to prevent display auto-off during direct input.
|
// outside of injectKey() to prevent display auto-off during direct input.
|
||||||
void keepAlive() {
|
void keepAlive() {
|
||||||
|
|||||||
@@ -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.
|
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
|
Output: .pio/build/<env>/firmware_merged.bin
|
||||||
Flash: esptool.py --chip esp32s3 write_flash 0x0 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")
|
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):
|
def merge_bin(source, target, env):
|
||||||
import subprocess, os
|
import subprocess, os
|
||||||
|
|
||||||
@@ -52,8 +136,18 @@ def merge_bin(source, target, env):
|
|||||||
"0x10000", firmware,
|
"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"\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)
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
|
|||||||
Reference in New Issue
Block a user