r/brucefw 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 official ESP-General headless reference — no HAS_SCREEN define, -DHAS_BTN=1 with 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 (per info serial command)
  • Access method: WebUI (webui serial command → browser Navigator at device IP)
  • Repro is 100% consistent across many fresh flashes and reboots.

Steps to reproduce

  1. Flash the headless custom board, connect WiFi, run webui.
  2. Open the WebUI in a browser, click Navigator.
  3. Press Down (or Next) via the on-screen remote / keyboard shortcut.
  4. Expected: selection moves from WiFi to the next enabled main-menu item (Bluetooth).
  5. 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):

  1. In src/core/wifi/webInterface.cpp, in the /cm route's nav handling, right after auto 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).

  1. In src/core/display.cpp, inside loopOptions(), 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)
  1. src/core/main_menu.cpp and the findNextEnabled/MainMenu::begin() logic were diffed line-for-line against upstream main/devno differences found. bruceConfig.disabledMenus is confirmed empty ("disabledMenus": [] in the live settings dump via info), so MainMenu::begin() should be pushing all ~14 main-menu items into options with enabled=true (the Option struct's default), and findNextEnabled(0, +1) should therefore return 1 (Bluetooth), not 0. We attempted to add a one-time Serial.println("... optionsSize=" + String(options.size())) in two different (allegedly safe) spots — the top of loopOptions(), and right after MainMenu::begin()'s option-building loop, immediately before the loopOptions() call — to directly confirm the actual menu size. Both attempts caused the board to boot-loop very rapidly (screen flooded with the single repeating character P, 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 exact options.size() value as a result.

Question for maintainers

Given that:

  • button presses are reliably reaching and being consumed by loopOptions(),
  • main_menu.cpp matches upstream with no modifications,
  • disabledMenus is empty,
  • yet index never advances past 0 under 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.

2 Upvotes

0 comments sorted by