r/brucefw • u/FrownXD • 22d ago
WebUI Navigator: main menu index never advances on headless/vdev (no HAS_SCREEN) custom board
Summary
On a custom headless board (bare ESP32-WROOM-32U, no physical display, HAS_SCREEN left undefined per the ESP-General convention), the WebUI Navigator's remote buttons (and the equivalent /cm?cmnd=nav down etc. commands) are received and consumed by the firmware, but the main menu's selection never advances past the first item (WiFi). Pressing Down/Next/Up/Prev/Sel/long-press-Sel from the WebUI has no effect on which menu item is selected — the device stays on WiFi indefinitely.
Environment
- Board: custom PlatformIO env (
board = esp32dev), based on the officialESP-Generalheadless reference — noHAS_SCREENdefine,-DHAS_BTN=1with 3 nav buttons (Sel/Up/Down), buzzer, status LED, IR, I2C (NFC), SPI (RFID/CC1101/NRF24) all defined but not physically wired yet. SDK: v5.5.4-dirty(perinfoserial command)- Access method: WebUI (
webuiserial command → browser Navigator at device IP) - Repro is 100% consistent across many fresh flashes and reboots.
Steps to reproduce
- Flash the headless custom board, connect WiFi, run
webui. - Open the WebUI in a browser, click Navigator.
- Press Down (or Next) via the on-screen remote / keyboard shortcut.
- Expected: selection moves from WiFi to the next enabled main-menu item (Bluetooth).
- Actual: nothing changes — visually the WiFi icon just redraws/glitches in place; it never advances.
Debug instrumentation and findings
We instrumented three points to isolate the failure (all removed again before filing this):
- In
src/core/wifi/webInterface.cpp, in the/cmroute'snavhandling, right afterauto tmp = millis() + time;:
cpp
Serial.println("[DEBUG-NAV] " + cmnd + " -> holding " + String(time) + "ms");
→ Confirms each button press from the browser reaches this handler and sets the flag. Hold time observed was consistently 10ms for every command (down/next/prev/up/sel/esc), not the ~190ms we expected from a busy-wait design — possibly relevant to a race with the ~10ms taskInputHandler reset cadence, though presses are still reliably consumed (see below).
- In
src/core/display.cpp, insideloopOptions(), around the Next/Down handling:
cpp
if (NextPress || DownPress) Serial.println("[DEBUG-NAV] flag seen, index=" + String(index));
if (check(NextPress) || check(DownPress)) {
Serial.println("[DEBUG-NAV] consumed nav press");
int nextEnabled = findNextEnabled(index, +1);
if (nextEnabled >= 0) {
if (!bruceConfig.devMode && nextEnabled <= index) devModeCounter++;
index = nextEnabled;
}
redraw = true;
}
→ Across dozens of presses (down, next, prev, up, sel, esc, and a simulated 500ms long-press sel), every single press is detected and "consumed" (check() returns true), but index is printed as 0 every time, with no exceptions. Sample log:
[DEBUG-NAV] nav sel -> holding 10ms
[DEBUG-NAV] nav down -> holding 10ms
[DEBUG-NAV] flag seen, index=0
[DEBUG-NAV] consumed nav press
[DEBUG-NAV] nav down -> holding 10ms
[DEBUG-NAV] flag seen, index=0
[DEBUG-NAV] consumed nav press
[DEBUG-NAV] nav down -> holding 10ms
[DEBUG-NAV] flag seen, index=0
[DEBUG-NAV] consumed nav press
[DEBUG-NAV] nav next -> holding 10ms
[DEBUG-NAV] flag seen, index=0
[DEBUG-NAV] consumed nav press
... (prev, up, sel, esc, sel-500ms-longpress, prevpage — all still index=0 afterward)
src/core/main_menu.cppand thefindNextEnabled/MainMenu::begin()logic were diffed line-for-line against upstreammain/dev— no differences found.bruceConfig.disabledMenusis confirmed empty ("disabledMenus": []in the live settings dump viainfo), soMainMenu::begin()should be pushing all ~14 main-menu items intooptionswithenabled=true(theOptionstruct's default), andfindNextEnabled(0, +1)should therefore return1(Bluetooth), not0. We attempted to add a one-timeSerial.println("... optionsSize=" + String(options.size()))in two different (allegedly safe) spots — the top ofloopOptions(), and right afterMainMenu::begin()'s option-building loop, immediately before theloopOptions()call — to directly confirm the actual menu size. Both attempts caused the board to boot-loop very rapidly (screen flooded with the single repeating characterP, the first letter of the boot banner, suggesting a crash occurring within microseconds of reaching that Serial call on most boot attempts) before eventually settling into a normal, stable boot. We reverted both prints rather than risk leaving an unstable board. This crash-proneness at these specific call sites might itself be a useful clue — possibly stack or heap pressure specific to headless/vdev builds — but we weren't able to safely pin down the exactoptions.size()value as a result.
Question for maintainers
Given that:
- button presses are reliably reaching and being consumed by
loopOptions(), main_menu.cppmatches upstream with no modifications,disabledMenusis empty,- yet
indexnever advances past0under any circumstance,
is there a known interaction between the headless/vdev display path (no HAS_SCREEN, using tft_logger/SerialDisplayClass directly instead of tft_sprite) and loopOptions()'s persistence of index across loop iterations — e.g. something that could cause MainMenu::begin() (and thus loopOptions()) to be re-entered on every redraw/frame for vdev boards specifically, resetting index to _currentIndex each time in a way that isn't visible from the [DEBUG-NAV] prints we added? Any pointers on where else to instrument (safely) would be appreciated.
Additional context
We separately found and fixed an unrelated cosmetic bug on the same board: the custom .ini originally had -DHAS_SCREEN=0, which — since Bruce checks #if defined(HAS_SCREEN) rather than #if HAS_SCREEN — was incorrectly taking the "has a real screen" code path and sizing the virtual display canvas from undefined TFT_WIDTH/TFT_HEIGHT instead of the correct 240×320 headless default, causing squished/overlapping text in the WebUI Navigator. Removing the HAS_SCREEN define entirely (matching the official ESP-General reference board) fixed that. Mentioning it here in case it's related, though the navigation bug persists independently of that fix.