-
Notifications
You must be signed in to change notification settings - Fork 977
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
usb: Add USB device support packages.
These packages build on top of machine.USBDevice() to provide high level and flexible support for implementing USB devices in Python code. Additional credits, as per included copyright notices: - CDC support based on initial implementation by @hoihu with fixes by @linted. - MIDI support based on initial implementation by @paulhamsh. - HID keypad example based on work by @turmoni. - Everyone who tested and provided feedback on early versions of these packages. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
- Loading branch information
1 parent
45ead11
commit 81962f1
Showing
20 changed files
with
2,733 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Dynamic USB packages | ||
|
||
These packages allow implementing USB functionality on a MicroPython device using pure Python code. | ||
|
||
Currently only USB device is implemented, not USB host. | ||
|
||
## USB Device support | ||
|
||
### Support | ||
|
||
USB Device support depends on the low-level [machine.USBDevice](https://docs.micropython.org/en/latest/library/machine.USBDevice.html) class. This class is new and not supported on all ports, so please check the documentation for your MicroPython version. It is possible to implement a USB device using only the low-level USBDevice class. These packages are higher level and easier to use. | ||
|
||
For more information about how to install packages, or "freeze" them into a | ||
firmware image, consult the [MicroPython documentation on "Package | ||
management"](https://docs.micropython.org/en/latest/reference/packages.html). | ||
|
||
### Examples | ||
|
||
The [examples/device](examples/device) directory in this repo has a range of examples. After installing necessary packages, you can download an example and run it with `mpremote run EXAMPLE_FILENAME.py` ([mpremote docs](https://docs.micropython.org/en/latest/reference/mpremote.html#mpremote-command-run)). | ||
|
||
#### Unexpected serial disconnects | ||
|
||
If you normally connect to your MicroPython device over a USB serial port ("USB CDC"), then running a USB example will disconnect mpremote when the new USB device configuration activates and the serial port has to temporarily disconnect. It is likely that mpremote will print an error. The example should still start running, if necessary then you can reconnect with mpremote and type Ctrl-B to restore the MicroPython REPL and/or Ctrl-C to stop the running example. | ||
|
||
If you use `mpremote run` again while a different runtime USB configuration is already active, then the USB serial port may disconnect immediately before the example runs. This is because mpremote has to soft-reset MicroPython, and when the existing USB device is reset then the entire USB port needs to reset. If this happens, run the same `mpremote run` command again. | ||
|
||
We plan to add features to `mpremote` so that this limitation is less disruptive. Other tools that communicate with MicroPython over the serial port will encounter similar issues when runtime USB is in use. | ||
|
||
### Initialising runtime USB | ||
|
||
The overall pattern for enabling USB devices at runtime is: | ||
|
||
1. Instantiate the Interface objects for your desired USB device. | ||
2. Call `usb.device.get()` to get the singleton object for the high-level USB device. | ||
3. Call `init(...)` to pass the desired interfaces as arguments, plus any custom | ||
keyword arguments to configure the overall device. | ||
|
||
An example, similar to [mouse_example.py](examples/device/mouse_example.py): | ||
|
||
```py | ||
m = usb.device.mouse.MouseInterface() | ||
usb.device.get().init(m, builtin_driver=True) | ||
``` | ||
|
||
Setting `builtin_driver=True` means that any built-in USB serial port will still | ||
be available. Otherwise, you may permanently lose access to MicroPython until | ||
the next time the device resets. | ||
|
||
See [Unexpected serial disconnects](#Unexpected-serial-disconnects), above, for | ||
an explanation of possible errors or disconnects when the runtime USB device | ||
initialises. | ||
|
||
Placing the call to `usb.device.get().init()` into the `boot.py` of the MicroPython file system allows the runtime USB device to initialise immediately on boot, before any built-in USB. However, note that calling this function on boot without `builtin_driver=True` will make the MicroPython USB serial interface permanently inaccessible until you "safe mode boot" (on supported boards) or completely erase the flash of your device. | ||
|
||
### Package usb-device-keyboard | ||
|
||
This package provides the `usb.device.keyboard` module. See [keyboard_example.py](examples/device/keyboard_example.py) for an example program. | ||
|
||
### Package usb-device-mouse | ||
|
||
This package provides the `usb.device.mouse` module. See [mouse_example.py](examples/device/mouse_example.py) for an example program. | ||
|
||
### Package usb-device-hid | ||
|
||
This package provides the `usb.device.hid` module. USB HID (Human Interface Device) class allows creating a wide variety of device types. The most common are mouse and keyboard, which have their own packages in micropython-lib. However, using the usb-device-hid package directly allows creation of any kind of HID device. | ||
|
||
See [hid_custom_keypad_example.py](examples/device/hid_custom_keypad_example.py) for an example of a Keypad HID device with a custom HID descriptor. | ||
|
||
### Package usb-device-cdc | ||
|
||
This package provides the `usb.device.cdc` module. USB-CDC (Communications Device Class) is most commonly used for virtual serial port USB interfaces, and that is what is supported here. | ||
|
||
The example [cdc_repl_example.py](examples/device/cdc_repl_example.py) demonstrates how to add a second USB serial interface and duplicate the MicroPython REPL between the two. | ||
|
||
### Package usb-device-midi | ||
|
||
This package provides the `usb.device.midi` module. This allows implementing MIDI devices in MicroPython. | ||
|
||
The example [midi_example.py](examples/device/midi_example.py) demonstrates how to create a simple MIDI device to send MIDI data to the USB host. | ||
|
||
### Package usb-device | ||
|
||
This package contains the common implementation components for the other packages, and can be used to create new and different USB device types. All of the other packages depend on this package. | ||
|
||
It provides the `usb.device.get()` function for accessing the Device singleton object, and the `usb.device.core` module which contains the low-level classes and utility functions for implementing new USB interface drivers in Python. The best examples of how to use the core classes is the source code of the other USB device packages. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# MicroPython USB CDC REPL example | ||
# | ||
# Example demonstrating how to use os.dupterm() to provide the | ||
# MicroPython REPL on a dynamic CDCInterface() serial port. | ||
# | ||
# Note that if you run this example on the built-in USB CDC port via 'mpremote | ||
# run' then you'll have to reconnect after it re-enumerates, and it may be | ||
# necessary afterward to type Ctrl-B to exit the Raw REPL mode and resume the | ||
# interactive REPL back. | ||
# | ||
# This example uses the usb-device-cdc package for the CDCInterface class. | ||
# This can be installed with: | ||
# | ||
# mpremote mip install usb-device-cdc | ||
# | ||
# MIT license; Copyright (c) 2023-2024 Angus Gratton | ||
import os | ||
import time | ||
import usb.device | ||
from usb.device.cdc import CDCInterface | ||
|
||
cdc = CDCInterface() | ||
cdc.init(timeout=0) # zero timeout makes this non-blocking, suitable for os.dupterm() | ||
|
||
# pass builtin_driver=True so that we get the built-in USB-CDC alongside, | ||
# if it's available. | ||
usb.device.get().init(cdc, builtin_driver=True) | ||
|
||
print("Waiting for USB host to configure the interface...") | ||
|
||
# wait for host enumerate as a CDC device... | ||
while not cdc.is_open(): | ||
time.sleep_ms(100) | ||
|
||
# Note: This example doesn't wait for the host to access the new CDC port, | ||
# which could be done by polling cdc.dtr, as this will block the REPL | ||
# from resuming while this code is still executing. | ||
|
||
print("CDC port enumerated, duplicating REPL...") | ||
|
||
old_term = os.dupterm(cdc) |
137 changes: 137 additions & 0 deletions
137
micropython/usb/examples/device/hid_custom_keypad_example.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
# MicroPython USB HID custom Keypad example | ||
# | ||
# This example demonstrates creating a custom HID device with its own | ||
# HID descriptor, in this case for a USB number keypad. | ||
# | ||
# For higher level examples that require less code to use, see mouse_example.py | ||
# and keyboard_example.py | ||
# | ||
# This example uses the usb-device-hid package for the HIDInterface class. | ||
# This can be installed with: | ||
# | ||
# mpremote mip install usb-device-hid | ||
# | ||
# MIT license; Copyright (c) 2023 Dave Wickham, 2023-2024 Angus Gratton | ||
from micropython import const | ||
import time | ||
import usb.device | ||
from usb.device.hid import HIDInterface | ||
|
||
_INTERFACE_PROTOCOL_KEYBOARD = const(0x01) | ||
|
||
|
||
def keypad_example(): | ||
k = KeypadInterface() | ||
|
||
usb.device.get().init(k, builtin_driver=True) | ||
|
||
while not k.is_open(): | ||
time.sleep_ms(100) | ||
|
||
while True: | ||
time.sleep(2) | ||
print("Press NumLock...") | ||
k.send_key("<NumLock>") | ||
time.sleep_ms(100) | ||
k.send_key() | ||
time.sleep(1) | ||
# continue | ||
print("Press ...") | ||
for _ in range(3): | ||
time.sleep(0.1) | ||
k.send_key(".") | ||
time.sleep(0.1) | ||
k.send_key() | ||
print("Starting again...") | ||
|
||
|
||
class KeypadInterface(HIDInterface): | ||
# Very basic synchronous USB keypad HID interface | ||
|
||
def __init__(self): | ||
super().__init__( | ||
_KEYPAD_REPORT_DESC, | ||
set_report_buf=bytearray(1), | ||
protocol=_INTERFACE_PROTOCOL_KEYBOARD, | ||
interface_str="MicroPython Keypad", | ||
) | ||
self.numlock = False | ||
|
||
def on_set_report(self, report_data, _report_id, _report_type): | ||
report = report_data[0] | ||
b = bool(report & 1) | ||
if b != self.numlock: | ||
print("Numlock: ", b) | ||
self.numlock = b | ||
|
||
def send_key(self, key=None): | ||
if key is None: | ||
self.send_report(b"\x00") | ||
else: | ||
self.send_report(_key_to_id(key).to_bytes(1, "big")) | ||
|
||
|
||
# See HID Usages and Descriptions 1.4, section 10 Keyboard/Keypad Page (0x07) | ||
# | ||
# This keypad example has a contiguous series of keys (KEYPAD_KEY_IDS) starting | ||
# from the NumLock/Clear keypad key (0x53), but you can send any Key IDs from | ||
# the table in the HID Usages specification. | ||
_KEYPAD_KEY_OFFS = const(0x53) | ||
|
||
_KEYPAD_KEY_IDS = [ | ||
"<NumLock>", | ||
"/", | ||
"*", | ||
"-", | ||
"+", | ||
"<Enter>", | ||
"1", | ||
"2", | ||
"3", | ||
"4", | ||
"5", | ||
"6", | ||
"7", | ||
"8", | ||
"9", | ||
"0", | ||
".", | ||
] | ||
|
||
|
||
def _key_to_id(key): | ||
# This is a little slower than making a dict for lookup, but uses | ||
# less memory and O(n) can be fast enough when n is small. | ||
return _KEYPAD_KEY_IDS.index(key) + _KEYPAD_KEY_OFFS | ||
|
||
|
||
# HID Report descriptor for a numeric keypad | ||
# | ||
# fmt: off | ||
_KEYPAD_REPORT_DESC = ( | ||
b'\x05\x01' # Usage Page (Generic Desktop) | ||
b'\x09\x07' # Usage (Keypad) | ||
b'\xA1\x01' # Collection (Application) | ||
b'\x05\x07' # Usage Page (Keypad) | ||
b'\x19\x00' # Usage Minimum (0) | ||
b'\x29\xFF' # Usage Maximum (ff) | ||
b'\x15\x00' # Logical Minimum (0) | ||
b'\x25\xFF' # Logical Maximum (ff) | ||
b'\x95\x01' # Report Count (1), | ||
b'\x75\x08' # Report Size (8), | ||
b'\x81\x00' # Input (Data, Array, Absolute) | ||
b'\x05\x08' # Usage page (LEDs) | ||
b'\x19\x01' # Usage Minimum (1) | ||
b'\x29\x01' # Usage Maximum (1), | ||
b'\x95\x01' # Report Count (1), | ||
b'\x75\x01' # Report Size (1), | ||
b'\x91\x02' # Output (Data, Variable, Absolute) | ||
b'\x95\x01' # Report Count (1), | ||
b'\x75\x07' # Report Size (7), | ||
b'\x91\x01' # Output (Constant) - padding bits | ||
b'\xC0' # End Collection | ||
) | ||
# fmt: on | ||
|
||
|
||
keypad_example() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# MicroPython USB Keyboard example | ||
# | ||
# This example uses the usb-device-keyboard package for the KeyboardInterface class. | ||
# This can be installed with: | ||
# | ||
# mpremote mip install usb-device-keyboard | ||
# | ||
# To implement a keyboard with different USB HID characteristics, copy the | ||
# usb-device-keyboard/usb/device/keyboard.py file into your own project and modify | ||
# KeyboardInterface. | ||
# | ||
# MIT license; Copyright (c) 2024 Angus Gratton | ||
import usb.device | ||
from usb.device.keyboard import KeyboardInterface, KeyCode, LEDCode | ||
from machine import Pin | ||
import time | ||
|
||
# Tuples mapping Pin inputs to the KeyCode each input generates | ||
# | ||
# (Big keyboards usually multiplex multiple keys per input with a scan matrix, | ||
# but this is a simple example.) | ||
KEYS = ( | ||
(Pin.cpu.GPIO10, KeyCode.CAPS_LOCK), | ||
(Pin.cpu.GPIO11, KeyCode.LEFT_SHIFT), | ||
(Pin.cpu.GPIO12, KeyCode.M), | ||
(Pin.cpu.GPIO13, KeyCode.P), | ||
# ... add more pin to KeyCode mappings here if needed | ||
) | ||
|
||
# Tuples mapping Pin outputs to the LEDCode that turns the output on | ||
LEDS = ( | ||
(Pin.board.LED, LEDCode.CAPS_LOCK), | ||
# ... add more pin to LEDCode mappings here if needed | ||
) | ||
|
||
|
||
class ExampleKeyboard(KeyboardInterface): | ||
def on_led_update(self, led_mask): | ||
# print(hex(led_mask)) | ||
for pin, code in LEDS: | ||
# Set the pin high if 'code' bit is set in led_mask | ||
pin(code & led_mask) | ||
|
||
|
||
def keyboard_example(): | ||
# Initialise all the pins as active-low inputs with pullup resistors | ||
for pin, _ in KEYS: | ||
pin.init(Pin.IN, Pin.PULL_UP) | ||
|
||
# Initialise all the LEDs as active-high outputs | ||
for pin, _ in LEDS: | ||
pin.init(Pin.OUT, value=0) | ||
|
||
# Register the keyboard interface and re-enumerate | ||
k = ExampleKeyboard() | ||
usb.device.get().init(k, builtin_driver=True) | ||
|
||
print("Entering keyboard loop...") | ||
|
||
keys = [] # Keys held down, reuse the same list object | ||
prev_keys = [None] # Previous keys, starts with a dummy value so first | ||
# iteration will always send | ||
while True: | ||
if k.is_open(): | ||
keys.clear() | ||
for pin, code in KEYS: | ||
if not pin(): # active-low | ||
keys.append(code) | ||
if keys != prev_keys: | ||
# print(keys) | ||
k.send_keys(keys) | ||
prev_keys.clear() | ||
prev_keys.extend(keys) | ||
|
||
# This simple example scans each input in an infinite loop, but a more | ||
# complex implementation would probably use a timer or similar. | ||
time.sleep_ms(1) | ||
|
||
|
||
keyboard_example() |
Oops, something went wrong.