Skip to content

Commit bb92f5d

Browse files
usb: add USBManager framework doc
1 parent 2138998 commit bb92f5d

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

docs/frameworks/usb-manager.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# USBManager
2+
3+
USBManager is a singleton framework that provides a unified API for USB host support on ESP32: DisplayLink display adapters plus boot-protocol HID mice and keyboards. It owns the host lifecycle, display/panel switching, HID arming, and the 1-second poll timer that drives the whole subsystem.
4+
5+
## Overview
6+
7+
USB host support is opt-in and ESP32-S3-only (needs USB OTG):
8+
9+
```
10+
./scripts/build_mpos.sh esp32s3 --usb
11+
```
12+
13+
This compiles in the `usb` C module (`c_mpos/usb/`) and disables MicroPython's TinyUSB *device* mode (USB-serial REPL goes away; console remains over UART REPL / USB-Serial-JTAG). On stock builds `USBManager.is_available()` is `False` and every arm call is a harmless no-op.
14+
15+
USBManager centralizes all USB-host operations in a single class with class methods:
16+
17+
- **Unified API** - Single class for display + HID management
18+
- **Clean Namespace** - No scattered functions cluttering imports
19+
- **Testable** - USBManager can be tested independently with a fake `usb` module
20+
- **Hotplug** - The poll timer auto-switches to a ready display and auto-reverts on unplug
21+
- **HID** - Mice and keyboards arm together, enabled per-kind from live device state
22+
23+
```python
24+
from mpos import USBManager
25+
26+
if USBManager.is_available():
27+
USBManager.arm_display()
28+
USBManager.arm_hid()
29+
```
30+
31+
## Architecture
32+
33+
USBManager is implemented as a singleton using class variables and class methods. No instance creation is needed:
34+
35+
```python
36+
class USBManager:
37+
_usb_dev = None # usb.Display handle (the adapter)
38+
_usb_mouse = None # USBMouse indev, enabled only while a mouse streams
39+
_usb_keyboard = None # USBHIDKeyboard indev, enabled only while a keyboard streams
40+
_hid_hub = None # HIDHub demux shared by both indevs
41+
42+
@classmethod
43+
def arm_display(cls, width=640, height=480):
44+
...
45+
```
46+
47+
The layers, bottom to top:
48+
49+
- **C module `usb`** (`c_mpos/usb/`): `Display` class (start/poll/ready, frame upload) plus module-level host functions (`bus_devices`, `lsusb`, `hub_ports`, `reset_port`, watchdog toggles) and HID transport (`hid_start/poll/drain/state/...`). Upstream Pico_USB_Disp is vendored under `c_mpos/usb/upstream/` with a few clearly-commented MPOS adaptations (hub-port watchdog, `lsusb`, held-handle hook).
50+
- **Drivers**: `drivers/display/usb_display.py` (`USBDisplayDriver`, a `DisplayDriver` behind a shim bus) and `drivers/indev/usb_hid.py` (parser registry + `HIDHub` demux + `USBMouse` + `USBHIDKeyboard`). Report parsing is Python-side, so new device kinds never need C changes.
51+
- **USBManager** (`mpos/usb/`): boot arming (`arm_display`, `arm_hid`), the 1 s LVGL poll timer (HID pump + display state machine + auto-switch), panel/USB swapping, touch remapping, and HID/watchdog coexistence (idle-reset suppression while HID is claimed or parked).
52+
53+
Only DisplayLink DL-1xx adapters work on ESP32-S3 (Full-Speed OTG); DL-165/DL-195 recommended. T6/MS91xx need High-Speed, i.e. ESP32-P4 only (protocol code is vendored but untested on hardware).
54+
55+
### HCD channel budget
56+
57+
The S3 DWC_OTG core has 8 host channels (~7 usable): one channel per USB *pipe*, held for the pipe's lifetime. Rule of thumb: **max 1 hub + 2 downstream devices** on S3. ESP32-P4/S31 have 16 channels, so **1 hub + 4 devices** fits there. Claim priority is display > mouse > keyboard; a claim that fails with channel exhaustion parks silently with backoff until a topology change or `usb.hid_retry()`.
58+
59+
### Display switching
60+
61+
`mpos.main` arms the display at boot without waiting; the poll timer switches the UI over automatically when a monitor becomes ready and reverts on disconnect. The swap suspends the LVGL pump, tears down activities, repoints indevs to the new display (panel touch is remapped through the panel's own proven mapping — no per-board tables), moves the topmenu, and restarts the launcher. Resolution floor is 640x480: smaller modes need a sub-25 MHz pixel clock that real monitors cannot sync to.
62+
63+
## Usage
64+
65+
### Checking availability and arming
66+
67+
```python
68+
from mpos import USBManager
69+
70+
if USBManager.is_available():
71+
dev = USBManager.arm_display() # construct + start host + poll timer
72+
USBManager.arm_hid() # HID client + mouse/keyboard indevs
73+
```
74+
75+
Both are idempotent and safe to retry. On stock builds (no `usb` module) they return `None`.
76+
77+
### Manual display switching
78+
79+
```python
80+
from mpos import USBManager
81+
82+
USBManager.switch_to_usb(timeout_s=5) # init adapter, swap UI over
83+
USBManager.switch_to_panel() # swap back, free USB buffers
84+
```
85+
86+
### REPL inspection (on `--usb` builds)
87+
88+
```python
89+
import usb
90+
91+
print(usb.lsusb()) # Linux-style bus listing with VID:PID + product strings
92+
usb.bus_devices() # [addr, ...] currently sensed
93+
usb.hub_ports() # [(hub_addr, port, connected, enabled, high_speed), ...]
94+
usb.reset_port(2, 3) # re-enumerate one hub port (never a high-speed uplink)
95+
usb.hid_state() # [(addr, kind, vid, pid, speed), ...] streaming HIDs
96+
usb.hid_parked() # [(vid, pid, kind, fails), ...] parked (fails=255) or cooling down
97+
usb.hid_retry() # clear parked/cooldown and rescan now
98+
usb.hid_poll_stats() # [(addr, kind, polls, ch_fails), ...] sample twice, diff polls
99+
usb.hid_loop_lag() # ms since the HID task pumped events (~100 healthy)
100+
```
101+
102+
## API Reference
103+
104+
### Class Methods
105+
106+
- `is_available()` - `True` when the `usb` C module is importable (`--usb` build).
107+
- `arm_display(width=640, height=480)` - Construct (once) and start the adapter handle, ensure the poll timer. Returns the `usb.Display` or `None`.
108+
- `arm_hid()` - Start the HID client, create the shared `HIDHub` plus `USBMouse`/`USBHIDKeyboard` indevs (disabled until their kind streams). Returns the mouse or `None`.
109+
- `switch_to_usb(width=640, height=480, timeout_s=0)` - Blocking-init the adapter and swap the UI to it.
110+
- `switch_to_panel()` - Swap back to the panel display.
111+
- `try_init_usb_display(width=640, height=480, timeout_s=10, buf_lines=16)` - Wait for READY and build the `USBDisplayDriver`. Raises `RuntimeError` on timeout.
112+
113+
### The `usb` C module
114+
115+
- `usb.Display(port=0, width=0, height=0, ignore_edid=False)` - Adapter handle. `start()`, `poll()` (True on READY/disconnect/mode change), `ready()`, `width()`, `height()`, `chip_name()`, `update_565(x, y, w, h, buf)`, `fill(x, y, w, h, color)`, `flush(timeout_ms=100)`, `set_mode(w, h)`, `force_reenum()` (root-port power cycle).
116+
- Host inspection: `bus_devices()`, `lsusb()`, `hub_ports()`, `reset_port(hub_addr, port[, power_cycle[, force]])`, `set_watchdog(on)`, `auto_reset_idle([on])` (bare call reads back), `set_log(on)`.
117+
- HID transport: `hid_start()`, `hid_poll()`, `hid_drain()`, `hid_state()`, `hid_claimed_addrs()`, `hid_parked()`, `hid_retry()`, `hid_poll_stats()`, `hid_loop_lag()`, `hid_verbose([on])`, `hid_set_kbd_transient([on])` (keyboards stay persistent by default; the toggle is a live A/B switch).
118+
119+
## Limitations
120+
121+
- ESP32-S3 only; DisplayLink DL-1xx only (tested on DL-165/DL-195).
122+
- 640x480 minimum mode; rotation fixed to `_0`; RGB565 only.
123+
- Full display + mouse + keyboard combo cannot fit the S3 channel budget persistently (the keyboard parks until something unplugs).
124+
- A wedged adapter behind an externally powered hub is not recoverable by port resets — power-cut the adapter itself.
125+
- Field upgrades across this rename need `--erase-all` or a lib re-sync: the frozen C module changed name (`usb_disp` to `usb`), so stale flash shadows will not fall back.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ nav:
7979
- SettingsActivity: frameworks/settings-activity.md
8080
- TaskManager: frameworks/task-manager.md
8181
- TimeZone: frameworks/time-zone.md
82+
- USBManager: frameworks/usb-manager.md
8283
- WebServer: frameworks/webserver.md
8384
- WidgetAnimator: frameworks/widget-animator.md
8485
- WifiService: frameworks/wifi-service.md

0 commit comments

Comments
 (0)