Skip to content

USB REPL unavailable when GUI is running (VCP+MSC vs GUI event loop conflict) #28

@Amperstrand

Description

@Amperstrand

Summary

When the Specter-DIY GUI is running on the touchscreen, the USB VCP REPL becomes inaccessible. mpremote cannot enter raw REPL mode, and even raw serial Ctrl-C interrupts are silently consumed. The REPL only works during a brief ~1-2 second boot window before the GUI's asyncio event loop takes over.

This blocks automated GUI testing and developer REPL access during normal operation.

Environment

  • Board: STM32F469DISC (Specter-DIY Disco)
  • Firmware: make disco (GUI-enabled, no HIL)
  • Boot: boot/main/boot.py sets pyb.usb_mode("VCP+MSC")
  • USB: Device enumerates as 16c0:27dd (CDC-ACM), creates /dev/ttyACM1
  • MicroPython fork: f469-disco (custom)

Expected behavior

Per the MicroPython documentation and STM32 USB design, pyb.usb_mode("VCP+MSC") should provide:

  • USB VCP (virtual COM port) for REPL — always available
  • USB MSC (mass storage) for PYBFLASH — always available

These should coexist with the GUI (LVGL on LTDC/DSI touchscreen) since they use independent hardware peripherals.

Actual behavior

Boot sequence

  1. Device resets, boot/main/boot.py runs pyb.usb_mode("VCP+MSC")
  2. USB enumerates as CDC-ACM (/dev/ttyACM1 on Linux)
  3. Brief REPL window (~1-2s): mpremote connect works, print('ok') succeeds
  4. GUI starts (display.init()SpecterGUI()specter.start())
  5. REPL becomes permanently blocked: mpremote fails with TransportError: could not enter raw repl
  6. Raw serial Ctrl-C (\x03) is silently consumed — no response
  7. Raw serial Ctrl-D (\x04) causes USB re-enumeration, device disappears

After GUI starts

$ mpremote connect /dev/ttyACM1 exec "print('ok')"
mpremote.transport.TransportError: could not enter raw repl

$ python3 -c "
import serial
s = serial.Serial('/dev/ttyACM1', 115200)
s.write(b'\x03')  # Ctrl-C
" 
# No response, no error — silently consumed

What works

  • make hil (HIL mode, no GUI): REPL works indefinitely
  • make disco during boot window: REPL works for ~1-2s before GUI starts
  • make disco with main.py patched to skip GUI: REPL works indefinitely

Root cause analysis

The GUI's asyncio event loop (asyncio.run()) monopolizes the MicroPython VM. When mpremote tries to enter raw REPL mode, it sends a soft reset (Ctrl-B) which requires the VM to be in the REPL idle state. But the VM is running the asyncio event loop and never returns to the REPL prompt.

Specifically:

  1. boot/main/boot.py:53pyb.usb_mode("VCP+MSC") sets up USB VCP with dupterm slot 1 (REPL)
  2. src/main.py:69specter.start() calls asyncio.run(self.setup()) which never returns
  3. The asyncio loop polls USB VCP for Specter host protocol data but doesn't service REPL dupterm

The USBHost.enable() call (in platform.set_usb_mode()) explicitly kills REPL:

# src/platform.py:337-338
os.dupterm(None, 0)
os.dupterm(None, 1)

But this only happens when the user enables USB communication in settings — the REPL should survive until then.

What we tried

1. make hil + HIL commands over ST-Link UART

Approach: Build with HIL_ENABLED=1, use TEST_UI: commands over pyb.UART("YB", 9600) via ST-Link VCP.
Result: ST-Link V2.1 on this board doesn't bridge the UART "YB" signal to its USB VCP. The ST-Link VCP only provides SWD debug, not UART passthrough. platform.stlk is a physical UART pin that would need a separate USB-serial adapter.
Verdict: Requires hardware change (external USB-serial adapter wired to UART YB pins).

2. make hil with GUI enabled (patched main.py)

Approach: Removed if platform.hil_test_mode: pass guard so GUI runs in HIL mode.
Result: HIL mode auto-enables USBHost early (specter.py:564-567), which calls platform.enable_usb() and kills REPL via dupterm(None, 0) and dupterm(None, 1). REPL is gone before GUI even starts.
Verdict: USBHost + REPL are mutually exclusive on the same USB VCP.

3. make disco (no HIL, standard boot)

Approach: Standard production build with GUI. USB set to VCP+MSC.
Result: REPL works during ~1-2s boot window, then blocked by GUI event loop. Ctrl-C silently consumed.
Verdict: asyncio event loop prevents REPL from regaining control.

4. Raw serial interrupt during GUI

Approach: Send Ctrl-C (\x03), Ctrl-D (\x04), Ctrl-B (\x02) via pyserial to /dev/ttyACM1.
Result: All commands silently consumed. Ctrl-D causes USB re-enumeration and device disappears. No way to break into REPL.
Verdict: No interrupt mechanism works once GUI is running.

5. Boot window race condition

Approach: Script to detect REPL during boot, upload DGP files before GUI starts.
Result: Boot window is ~1-2s and unreliable. Script timed out.
Verdict: Too fragile for automation.

Potential workarounds

A. LVGL timer-based REPL yield (recommended)

Add a mechanism where the LVGL update timer periodically yields to the REPL. MicroPython's dupterm can coexist with asyncio if the event loop checks for REPL input. This could be:

  • A dupterm callback that queues REPL input for processing
  • A dedicated asyncio task that services dupterm alongside the GUI loop
  • LVGL v6 lv_task_handler() is already called from a timer — add dupterm servicing there

B. GPIO-based GUI suspend

Add a physical button or GPIO pin that, when pressed, suspends the GUI event loop and returns to the REPL. Similar to how some MicroPython boards use a BOOT button to enter safe mode.

C. USB serial adapter for UART YB

Connect an external USB-serial adapter (e.g., FT232H) to the STM32's UART "YB" pins. This gives a dedicated REPL port that's independent of USB VCP. The ST-Link SWD connector has UART TX/RX pins that could be used.
Pros: Guaranteed REPL access regardless of GUI state
Cons: Requires hardware modification or additional cable

D. Separate USB configurations

Use pyb.usb_mode("VCP") without MSC, or configure USB descriptors to expose two separate interfaces (VCP for REPL + another VCP for Specter protocol). This would require custom USB descriptor changes in the MicroPython port.
Pros: Clean separation
Cons: Significant MicroPython port changes

E. UART-based REPL only, no USB REPL

Keep USB exclusively for Specter host protocol and MSC. Use the physical UART (wired to ST-Link or external adapter) for REPL. This is closest to the existing HIL design intent (see the commented-out code in platform.py:339-351).
Pros: No conflict, clean separation of concerns
Cons: Requires physical serial connection for development

Related code

File Line(s) Relevance
boot/main/boot.py 53 pyb.usb_mode("VCP+MSC")
src/main.py 17-69 GUI setup and specter.start()
src/platform.py 320-356 set_usb_mode(), stlk UART, dupterm management
src/specter.py 555-621 HIL listener, USBHost auto-enable
src/hil.py 49-106 HIL command handler over UART
f469-disco/micropython/ports/stm32/usb.c 624-627 VCP dupterm slot assignment

Impact

  • Developer experience: Cannot use REPL while GUI is running. Must flash HIL firmware (no GUI) for any REPL-based development or testing.
  • Automated testing: Cannot automate GUI tests via USB. HIL UART requires physical serial adapter.
  • Debugging: Cannot inspect live GUI state via REPL. Must use debug traces or HIL commands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority: lowNice to have, not blockingupstreamIssues that should be submitted upstream

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions