Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,35 @@ mpy-cross sub-make, which then plants its own qstr fragments in your build
directory and breaks the link. Use `idf.py -B` for an out-of-tree build
instead, which does not inherit the variable.

## Examples

| File | Role | What it shows |
|---|---|---|
| [`soundcard.py`](examples/soundcard.py) | device | Class-compliant UAC sound card (C pump). Pair with `usb_speaker.py` |
| [`uac_pump.py`](examples/uac_pump.py) | device | Python FIFO pump -- inspectable path, not the shipping card |
| [`hid_keyboard.py`](examples/hid_keyboard.py) | device | Board types into the host |
| [`hid_mouse.py`](examples/hid_mouse.py) | device | Board moves the host cursor |
| [`ram_drive.py`](examples/ram_drive.py) | device | RAM disk via `msc_attach` |
| [`sd_drive.py`](examples/sd_drive.py) | device | SD card as a USB drive via `msc_attach_blockdev` |
| [`usbif_webcam.py`](examples/usbif_webcam.py) | device | Board is a UVC webcam (`cameraif` when present) |
| [`midi_harmonizer.py`](examples/midi_harmonizer.py) | device | MIDI effect: melody in, triads out |
| [`midi_harmonizer_ui.py`](examples/midi_harmonizer_ui.py) | device | Harmonizer with a touchscreen chord picker |
| [`midi_device_in.py`](examples/midi_device_in.py) | device | Prove the board receives MIDI from a host |
| [`midi_latency.py`](examples/midi_latency.py) | device | MIDI-to-audio round-trip timing |
| [`host_enum.py`](examples/host_enum.py) | host | Attach/detach via the portable API (board or desktop) |
| [`hid_host.py`](examples/hid_host.py) | host | USB keyboard → PyDevices key events (M1) |
| [`usb_serial.py`](examples/usb_serial.py) | host | CDC read/write to a USB-serial device |
| [`usb_speaker.py`](examples/usb_speaker.py) | host | Play through a hosted USB speaker / `soundcard.py` |
| [`usb_mic.py`](examples/usb_mic.py) | host | Capture from a hosted USB microphone |
| [`usb_drive_mount.py`](examples/usb_drive_mount.py) | host | Mount a flash drive and list files |
| [`usb_drive_log.py`](examples/usb_drive_log.py) | host | Append sensor lines to a hosted stick |
| [`midi_host.py`](examples/midi_host.py) | host | Host a MIDI keyboard; send a chord back |
| [`uvc_display.py`](examples/uvc_display.py) | host | Hosted webcam on the board's panel (MJPEG via `jpegio` when present) |
| [`costume_selftest.py`](examples/costume_selftest.py) | — | Validate every costume's descriptors without a host |

Board-to-board pairings, VBUS warnings, and firmware holes (by issue number)
live in [`examples/README.md`](examples/README.md).

## Tests

The ring buffer is tested on the host, where a failure is a two-second answer
Expand Down
69 changes: 69 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# usbif examples

One script per use of the shipping API. Each role is class-compliant on its
own: a PC or a commercial peripheral is a valid other end. Board-to-board
pairing is the same scripts on two boards, not a special `pair_*` file.

## Class × role

| Class | Device (board presents as…) | Host (board drives…) |
|---|---|---|
| **UAC** | [`soundcard.py`](soundcard.py) (C pump); [`uac_pump.py`](uac_pump.py) (Python FIFO) | [`usb_speaker.py`](usb_speaker.py), [`usb_mic.py`](usb_mic.py) |
| **UVC** | [`usbif_webcam.py`](usbif_webcam.py) | [`uvc_display.py`](uvc_display.py) (MJPEG via `jpegio` when present) |
| **MIDI** | [`midi_harmonizer.py`](midi_harmonizer.py), [`midi_harmonizer_ui.py`](midi_harmonizer_ui.py), [`midi_device_in.py`](midi_device_in.py), [`midi_latency.py`](midi_latency.py) | [`midi_host.py`](midi_host.py) |
| **HID** | [`hid_keyboard.py`](hid_keyboard.py), [`hid_mouse.py`](hid_mouse.py) | [`hid_host.py`](hid_host.py) |
| **MSC** | [`sd_drive.py`](sd_drive.py), [`ram_drive.py`](ram_drive.py) | [`usb_drive_mount.py`](usb_drive_mount.py), [`usb_drive_log.py`](usb_drive_log.py) |
| **CDC** | built-in MicroPython console / costume bit | [`usb_serial.py`](usb_serial.py) |
| **enum** | — | [`host_enum.py`](host_enum.py) (portable API; board or desktop) |
| **self-test** | [`costume_selftest.py`](costume_selftest.py) | — |

## Board-to-board pairings

Roles follow the hardware each board has, not their size. **P4 is always the
device** in these pairings: its high-speed host detects no device
([usbif#3](https://github.com/PyDevices/usbif/issues/3)).

| Device board | Script | Host board | Script | What is lent |
|---|---|---|---|---|
| **P4** | `soundcard.py` | **S3** | `usb_speaker.py` | Sound output -- the headline offload |
| **P4** | `usbif_webcam.py` | **S3** | `uvc_display.py` | Camera → panel |
| **P4** | `sd_drive.py` | **S3** | `usb_drive_mount.py` | Shared storage |
| **P4** / **S3** | `midi_harmonizer.py` | **S3** | `midi_host.py` | MIDI effect / instrument |
| **S3** | `hid_keyboard.py` | **S3** | `hid_host.py` | Control surface (needs two S3s, or S3-device + PC) |

A PC can replace either end. That is the point of standard classes.

### Headline: P4 as a USB sound card for an S3

1. Console on each board's **UART bridge**, not on the OTG port under test.
2. P4: `mpftp run examples/soundcard.py` -- enumerates as Speakers, C pump
into the ES8311.
3. S3: OTG adapter or powered hub on the host port (the Waveshare S3 touch
boards do not switch VBUS; see also [usbif#5](https://github.com/PyDevices/usbif/issues/5)).
4. S3: `mpftp run examples/usb_speaker.py` -- finds the P4, plays a 440 Hz tone.
5. Hear it on the P4's speaker.

### Power

Connecting two self-powered boards can **back-feed** VBUS and leave a board
silent and unflashable. Default link: a cable that omits VBUS, or a powered
hub between them. Unplug both USB cables and use UART alone to recover a
wedged board.

## Firmware holes (do not invent an example)

| Hole | Issue | Consequence for examples |
|---|---|---|
| Device UAC is speaker-only (no mic endpoint) | [usbif#7](https://github.com/PyDevices/usbif/issues/7) | `usb_mic.py` hosts a commercial mic; no device-side capture script |
| Portable `Device.functions()` omits video | [usbif#8](https://github.com/PyDevices/usbif/issues/8) | `usbif_webcam.py` pokes `_usbif.FN_VIDEO` directly |
| P4 high-speed host detects nothing | [usbif#3](https://github.com/PyDevices/usbif/issues/3) | Every pairing puts the P4 in device role |
| Host FIFO bias is build-time | [usbif#2](https://github.com/PyDevices/usbif/issues/2) | Stereo UAC host and UVC host compete; Bias-IN may cost stereo |

## Conventions

- Headers name the use, the board role, the other end, and whether UART must
hold the REPL (any costume that drops CDC cuts a native-USB session).
- Prefer `usbif.auto` / portable names where they cover the class; fall back
to `_usbif` for streaming surfaces the portable API does not yet wrap.
- No measured fps or latency numbers are claimed in headers for scripts that
have not been re-run in this pass -- cite prior evidence or stay quiet.
72 changes: 72 additions & 0 deletions examples/hid_host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""A USB keyboard on the host port drives ordinary PyDevices key events.

Milestone M1: the same application code, producing the same ``events.Key``
records, as an SDL keyboard on the desktop. The host stack delivers raw boot
reports; ``usbif.hid_keyboard.KeyboardDecoder`` turns them into presses and
releases by diffing successive reports.

mpftp run -d COM49 examples/hid_host.py

**Pairing.** A commercial keyboard, or a PyDevices board running
``hid_keyboard.py``. Needs an S3 (or any board whose host mode works); P4
host is blocked (usbif#3).

**Rollover.** When more keys are held than the report can carry, the decoder
ignores the ErrorRollOver report rather than emitting garbage -- see the
module docstring in ``lib/usbif/hid_keyboard.py``.
"""

import time

import _usbif
import events
from usbif.hid_keyboard import KeyboardDecoder


def find_keyboard(timeout_ms=15000):
_usbif.host_start(("hid",))
deadline = time.ticks_add(time.ticks_ms(), timeout_ms)
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
for dev in _usbif.host_devices():
if "hid" in dev[5]:
return dev[0]
time.sleep_ms(250)
return None


def main(seconds=30):
dev_id = find_keyboard()
if dev_id is None:
print("no HID device found")
_usbif.host_stop()
return

print("HID device", dev_id, "-- type on it for %d s" % seconds)
_usbif.host_hid_open(dev_id)
decoder = KeyboardDecoder()
buf = bytearray(8)
t0 = time.ticks_ms()
try:
while time.ticks_diff(time.ticks_ms(), t0) < seconds * 1000:
n = _usbif.host_hid_read(buf)
if n >= 3:
for ev in decoder.feed(buf):
kind = "DOWN" if ev.type == events.KEYDOWN else "UP"
# Field names follow events.Key; fall back to repr if a
# firmware builds the record differently.
name = getattr(ev, "name", "?")
key = getattr(ev, "key", None) or 0
mod = getattr(ev, "mod", 0)
scan = getattr(ev, "scancode", None)
print(" %s %-12s key=0x%02x mod=0x%02x scancode=%s"
% (kind, name, key, mod, scan))
else:
time.sleep_ms(5)
finally:
_usbif.host_hid_close()
_usbif.host_stop()
print("done")


if __name__ == "__main__":
main()
83 changes: 83 additions & 0 deletions examples/hid_keyboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""The board is a USB keyboard: type a short string into the host.

Presents as a boot keyboard (report ID 1) and sends HID reports that any
ordinary OS keyboard stack accepts -- Notepad, a browser address bar, a
terminal. No driver to install.

mpftp run -d COM49 examples/hid_keyboard.py

**Console.** Costume is CDC+HID so the REPL stays on the same cable. Run the
REPL on the UART bridge if you change the costume to HID alone.

**LEDs.** After typing, the script prints ``hid_leds()`` -- the lock-key state
the host last set (caps / num / scroll). A control surface that wants to show
them reads the same call.

**Pairing.** An S3 hosting HID (``hid_host.py``) can take this board as its
keyboard. A PC works the same way. P4 cannot be the host (usbif#3).
"""

import time

import _usbif

# HID usage ids for a-z (0x04..) and a few extras. Boot protocol only.
_ALPHA = {c: 0x04 + i for i, c in enumerate("abcdefghijklmnopqrstuvwxyz")}
_EXTRA = {
" ": 0x2C,
"\n": 0x28,
".": 0x37,
",": 0x36,
"-": 0x2D,
}
MESSAGE = "hello from usbif\n"


def _send(report, retries=50):
"""Submit one keyboard report; retry while the host has not polled."""
for _ in range(retries):
if _usbif.hid_send(_usbif.HID_KEYBOARD, report):
return True
time.sleep_ms(2)
return False


def _tap(usage, modifier=0):
"""Press then release one key."""
down = bytes([modifier, 0, usage, 0, 0, 0, 0, 0])
up = bytes(8)
if not _send(down):
return False
time.sleep_ms(30)
return _send(up)


def type_string(text):
for ch in text:
lower = ch.lower()
usage = _ALPHA.get(lower) or _EXTRA.get(ch)
if usage is None:
print("skip unsupported char %r" % ch)
continue
mod = 0x02 if ch.isalpha() and ch.isupper() else 0 # left shift
if not _tap(usage, mod):
print("host did not accept a report; is the keyboard mounted?")
return False
time.sleep_ms(20)
return True


def main():
_usbif.dev_functions(_usbif.FN_CDC | _usbif.FN_HID)
print("costume: cdc+hid -- focus a text field on the host")
# Give the host time to configure us before the first report.
time.sleep_ms(1500)

ok = type_string(MESSAGE)
leds = _usbif.hid_leds()
print("typed %r: %s" % (MESSAGE.strip(), "ok" if ok else "failed"))
print("hid_leds: 0x%02x (bit0=num bit1=caps bit2=scroll)" % leds)


if __name__ == "__main__":
main()
62 changes: 62 additions & 0 deletions examples/hid_mouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""The board is a USB mouse: nudge the host cursor, then put it back.

Presents as a boot mouse (report ID 2) and sends relative motion reports.
Verified originally by watching Windows' cursor move 82 pixels and return --
the host is the instrument, not a counter in this script.

mpftp run -d COM49 examples/hid_mouse.py

**Report layout.** Boot mouse behind a report ID: buttons, dx, dy, wheel --
four signed bytes after the ID the C side prepends via ``hid_send``.

**Pairing.** Same as ``hid_keyboard.py``: a PC, or an S3 running a HID host.
"""

import time

import _usbif

STEPS = 20
DELTA = 4 # pixels per report


def _send(report, retries=50):
for _ in range(retries):
if _usbif.hid_send(_usbif.HID_MOUSE, report):
return True
time.sleep_ms(2)
return False


def move(dx, dy):
# buttons=0, dx, dy, wheel=0. Values are signed 8-bit.
def s8(n):
return n & 0xFF

return _send(bytes([0, s8(dx), s8(dy), 0]))


def main():
_usbif.dev_functions(_usbif.FN_CDC | _usbif.FN_HID)
print("costume: cdc+hid -- watch the host cursor")
time.sleep_ms(1500)

print("moving +x")
for _ in range(STEPS):
if not move(DELTA, 0):
print("host did not accept a report")
return
time.sleep_ms(20)

print("moving -x (return)")
for _ in range(STEPS):
if not move(-DELTA, 0):
print("host did not accept a report")
return
time.sleep_ms(20)

print("done; cursor should be back where it started")


if __name__ == "__main__":
main()
62 changes: 62 additions & 0 deletions examples/host_enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Enumerate USB devices and watch attach / detach.

Uses the portable host API (``usbif.auto.host``), so the same script runs on
a board with the native module and on a desktop where the OS owns the bus.
Capabilities are discovered, never assumed -- an empty set is a valid answer.

mpftp run -d COM49 examples/host_enum.py
python examples/host_enum.py # desktop

Plug and unplug devices while it runs; each attach and detach is printed.
On an S3 host that means a powered hub or OTG adapter (no VBUS switching on
the Waveshare touch boards). P4 high-speed host is blocked (usbif#3).
"""

import sys
import time

import events
import usbif
from usbif import auto


def main(seconds=60):
host = auto.host()
caps = host.capabilities()
print("backend capabilities:", sorted(caps) if caps else "(none)")
host.start()

print("currently attached:")
for info in host.devices():
print(" ", usbif.describe(info))
if not host.devices():
print(" (none yet -- plug something in)")

print("watching attach/detach for %d s ..." % seconds)
deadline = time.ticks_add(time.ticks_ms(), seconds * 1000) if hasattr(time, "ticks_ms") \
else None
end = time.time() + seconds
try:
while True:
if deadline is not None:
if time.ticks_diff(deadline, time.ticks_ms()) <= 0:
break
elif time.time() >= end:
break
for event in host.poll():
kind = "attach" if event.type == events.USBATTACH else "detach"
if event.type not in (events.USBATTACH, events.USBDETACH):
kind = str(event.type)
print("%s: %s" % (kind, usbif.describe(event.device)))
if host.overflowed:
print("warning: event buffer overflowed -- poll more often")
time.sleep(0.1)
except KeyboardInterrupt:
print("stopped")
finally:
host.stop()


if __name__ == "__main__":
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 60
main(secs)
Loading
Loading