Skip to content

Commit e8bd164

Browse files
committed
Actually add the changes methoned in the previous commit message, and add more documentation to msc.py
1 parent 5b5871c commit e8bd164

File tree

4 files changed

+142
-17
lines changed

4 files changed

+142
-17
lines changed

micropython/usbd/device.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# MIT license; Copyright (c) 2022 Angus Gratton
33
from micropython import const
44
import machine
5+
import micropython
56
import ustruct
67

78
from .utils import split_bmRequestType
@@ -72,6 +73,9 @@ def __init__(self):
7273
self.config_str = None
7374
self.max_power_ma = 50
7475

76+
# Workaround
77+
self._always_cb = set()
78+
7579
self._strs = self._get_device_strs()
7680

7781
usbd = self._usbd = machine.USBD()
@@ -84,12 +88,14 @@ def __init__(self):
8488
xfer_cb=self._xfer_cb,
8589
)
8690

87-
def add_interface(self, itf):
91+
def add_interface(self, itf, always_cb=False):
8892
# Add an instance of USBInterface to the USBDevice.
8993
#
9094
# The next time USB is reenumerated (by calling .reenumerate() or
9195
# otherwise), this interface will appear to the host.
9296
self._itfs.append(itf)
97+
if always_cb:
98+
self._always_cb.add(itf)
9399

94100
def remove_interface(self, itf):
95101
# Remove an instance of USBInterface from the USBDevice.
@@ -302,10 +308,21 @@ def _submit_xfer(self, ep_addr, data, done_cb=None):
302308
return True
303309
return False
304310

311+
def _retry_xfer_cb(self, args):
312+
# Workaround for when _xfer_cb is called before the callback can be set
313+
(ep_addr, result, xferred_bytes) = args
314+
self._xfer_cb(ep_addr, result, xferred_bytes)
315+
305316
def _xfer_cb(self, ep_addr, result, xferred_bytes):
306317
# Singleton callback from TinyUSB custom class driver when a transfer completes.
307318
try:
308319
itf, cb = self._eps[ep_addr]
320+
# Sometimes this part can be reached before the callback has been registered,
321+
# if this interface will *always* have callbacks then reschedule this function
322+
if cb is None and itf in self._always_cb:
323+
micropython.schedule(self._retry_xfer_cb, (ep_addr, result, xferred_bytes))
324+
return
325+
309326
self._eps[ep_addr] = (itf, None)
310327
except KeyError:
311328
cb = None

micropython/usbd/hid.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
from .utils import (
77
endpoint_descriptor,
88
split_bmRequestType,
9-
EP_OUT_FLAG,
109
STAGE_SETUP,
1110
REQ_TYPE_STANDARD,
1211
REQ_TYPE_CLASS,
1312
)
1413
from micropython import const
1514
import ustruct
1615

16+
EP_IN_FLAG = const(1 << 7)
17+
EP_OUT_FLAG = const(0x7F)
18+
1719
_DESC_HID_TYPE = const(0x21)
1820
_DESC_REPORT_TYPE = const(0x22)
1921
_DESC_PHYSICAL_TYPE = const(0x23)
@@ -45,6 +47,7 @@ def __init__(
4547
extra_descriptors=[],
4648
protocol=_INTERFACE_PROTOCOL_NONE,
4749
interface_str=None,
50+
use_out_ep=False,
4851
):
4952
# Construct a new HID interface.
5053
#
@@ -60,14 +63,23 @@ def __init__(
6063
# - protocol can be set to a specific value as per HID v1.11 section 4.3 Protocols, p9.
6164
#
6265
# - interface_str is an optional string descriptor to associate with the HID USB interface.
66+
#
67+
# - use_out_ep needs to be set to True if you're using the OUT endpoint, e.g. to get
68+
# keyboard LEDs
6369
super().__init__(_INTERFACE_CLASS, _INTERFACE_SUBCLASS_NONE, protocol, interface_str)
6470
self.extra_descriptors = extra_descriptors
6571
self.report_descriptor = report_descriptor
6672
self._int_ep = None # set during enumeration
73+
self._out_ep = None
74+
self.use_out_ep = use_out_ep
6775

6876
def get_report(self):
6977
return False
7078

79+
def set_report(self):
80+
# Override this if you are expecting reports from the host
81+
return False
82+
7183
def send_report(self, report_data):
7284
# Helper function to send a HID report in the typical USB interrupt
7385
# endpoint associated with a HID interface. return
@@ -80,12 +92,19 @@ def get_endpoint_descriptors(self, ep_addr, str_idx):
8092
# As per HID v1.11 section 7.1 Standard Requests, return the contents of
8193
# the standard HID descriptor before the associated endpoint descriptor.
8294
desc = self.get_hid_descriptor()
83-
ep_addr |= EP_OUT_FLAG
84-
desc += endpoint_descriptor(ep_addr, "interrupt", 8, 8)
95+
self._int_ep = ep_addr | EP_IN_FLAG
96+
ep_addrs = [self._int_ep]
97+
98+
desc += endpoint_descriptor(self._int_ep, "interrupt", 8, 8)
99+
100+
if self.use_out_ep:
101+
self._out_ep = (ep_addr + 1) & EP_OUT_FLAG
102+
desc += endpoint_descriptor(self._out_ep, "interrupt", 8, 8)
103+
ep_addrs.append(self._out_ep)
104+
85105
self.idle_rate = 0
86106
self.protocol = 0
87-
self._int_ep = ep_addr
88-
return (desc, [], [ep_addr])
107+
return (desc, [], ep_addrs)
89108

90109
def get_hid_descriptor(self):
91110
# Generate a full USB HID descriptor from the object's report descriptor
@@ -102,6 +121,7 @@ def get_hid_descriptor(self):
102121
0x22, # bDescriptorType, Report
103122
len(self.report_descriptor), # wDescriptorLength, Report
104123
)
124+
105125
# Fill in any additional descriptor type/length pairs
106126
#
107127
# TODO: unclear if this functionality is ever used, may be easier to not
@@ -115,7 +135,7 @@ def get_hid_descriptor(self):
115135

116136
def handle_interface_control_xfer(self, stage, request):
117137
# Handle standard and class-specific interface control transfers for HID devices.
118-
bmRequestType, bRequest, wValue, _, _ = request
138+
bmRequestType, bRequest, wValue, wIndex, wLength = request
119139

120140
recipient, req_type, _ = split_bmRequestType(bmRequestType)
121141

@@ -144,6 +164,9 @@ def handle_interface_control_xfer(self, stage, request):
144164
if bRequest == _REQ_CONTROL_SET_PROTOCOL:
145165
self.protocol = wValue
146166
return b""
167+
if bRequest == _REQ_CONTROL_SET_REPORT:
168+
return self.set_report()
169+
147170
return False # Unsupported
148171

149172

micropython/usbd/hidkeypad.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,91 @@
22

33
from .hid import HIDInterface
44
from .keycodes import KEYPAD_KEYS_TO_KEYCODES
5+
from .utils import STAGE_SETUP, split_bmRequestType
56
from micropython import const
7+
import micropython
8+
69
_INTERFACE_PROTOCOL_KEYBOARD = const(0x01)
10+
_REQ_CONTROL_SET_REPORT = const(0x09)
11+
_REQ_CONTROL_SET_IDLE = const(0x0A)
712

13+
# fmt: off
814
_KEYPAD_REPORT_DESC = bytes(
915
[
1016
0x05, 0x01, # Usage Page (Generic Desktop)
1117
0x09, 0x07, # Usage (Keypad)
1218
0xA1, 0x01, # Collection (Application)
1319
0x05, 0x07, # Usage Page (Keypad)
1420
0x19, 0x00, # Usage Minimum (00),
15-
0x29, 0xff, # Usage Maximum (ff),
21+
0x29, 0xFF, # Usage Maximum (ff),
1622
0x15, 0x00, # Logical Minimum (0),
17-
0x25, 0xff, # Logical Maximum (ff),
23+
0x25, 0xFF, # Logical Maximum (ff),
1824
0x95, 0x01, # Report Count (1),
1925
0x75, 0x08, # Report Size (8),
2026
0x81, 0x00, # Input (Data, Array, Absolute)
27+
0x05, 0x08, # Usage page (LEDs)
28+
0x19, 0x01, # Usage minimum (1)
29+
0x29, 0x05, # Usage Maximum (5),
30+
0x95, 0x05, # Report Count (5),
31+
0x75, 0x01, # Report Size (1),
32+
0x91, 0x02, # Output (Data, Variable, Absolute)
33+
0x95, 0x01, # Report Count (1),
34+
0x75, 0x03, # Report Size (3),
35+
0x91, 0x01, # Output (Constant)
2136
0xC0, # End Collection
2237
]
2338
)
39+
# fmt: on
2440

2541

2642
class KeypadInterface(HIDInterface):
2743
# Very basic synchronous USB keypad HID interface
2844

2945
def __init__(self):
46+
self.numlock = None
47+
self.capslock = None
48+
self.scrolllock = None
49+
self.compose = None
50+
self.kana = None
51+
self.set_report_initialised = False
3052
super().__init__(
3153
_KEYPAD_REPORT_DESC,
3254
protocol=_INTERFACE_PROTOCOL_KEYBOARD,
3355
interface_str="MicroPython Keypad!",
56+
use_out_ep=True,
3457
)
3558

36-
def send_report(self, key):
37-
super().send_report(KEYPAD_KEYS_TO_KEYCODES[key].to_bytes(1, "big"))
59+
def handle_interface_control_xfer(self, stage, request):
60+
if request[1] == _REQ_CONTROL_SET_IDLE and not self.set_report_initialised:
61+
# Hacky initialisation goes here
62+
self.set_report()
63+
self.set_report_initialised = True
64+
65+
if stage == STAGE_SETUP:
66+
return super().handle_interface_control_xfer(stage, request)
67+
68+
bmRequestType, bRequest, wValue, _, _ = request
69+
recipient, req_type, _ = split_bmRequestType(bmRequestType)
70+
71+
return True
72+
73+
def set_report(self, args=None):
74+
self.out_buffer = bytearray(1)
75+
self.submit_xfer(self._out_ep, self.out_buffer, self.set_report_cb)
76+
return True
77+
78+
def set_report_cb(self, ep_addr, result, xferred_bytes):
79+
buf_result = int(self.out_buffer[0])
80+
self.numlock = buf_result & 1
81+
self.capslock = (buf_result >> 1) & 1
82+
self.scrolllock = (buf_result >> 2) & 1
83+
self.compose = (buf_result >> 3) & 1
84+
self.kana = (buf_result >> 4) & 1
85+
86+
micropython.schedule(self.set_report, None)
87+
88+
def send_report(self, key=None):
89+
if key is None:
90+
super().send_report(bytes(1))
91+
else:
92+
super().send_report(KEYPAD_KEYS_TO_KEYCODES[key].to_bytes(1, "big"))

micropython/usbd/msc.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from micropython import const
1010
import micropython
1111
import ustruct
12-
import time
1312
from machine import Timer
1413

1514
_INTERFACE_CLASS_MSC = const(0x08)
@@ -26,7 +25,7 @@
2625

2726

2827
class CBW:
29-
"""Command Block Wrapper"""
28+
"""Command Block Wrapper - handles the incoming data from the host to the device"""
3029

3130
DIR_OUT = const(0)
3231
DIR_IN = const(1)
@@ -86,7 +85,7 @@ def from_binary(self, binary):
8685

8786

8887
class CSW:
89-
"""Command Status Wrapper"""
88+
"""Command Status Wrapper - handles status messages from the device to the host"""
9089

9190
STATUS_PASSED = const(0)
9291
STATUS_FAILED = const(1)
@@ -111,7 +110,13 @@ def __bytes__(self):
111110

112111

113112
class MSCInterface(USBInterface):
114-
"""Mass storage interface - contains the USB parts"""
113+
"""Mass storage interface - contains the USB parts
114+
115+
Properties:
116+
storage_device -- A StorageDevice object used by this instance, which handles all SCSI/filesystem-related operations
117+
cbw -- A CBW object to keep track of requests from the host to the device
118+
csw -- A CSW object to send status responses to the host
119+
lun -- The LUN of this device (currently only 0)"""
115120

116121
MSC_STAGE_CMD = const(0)
117122
MSC_STAGE_DATA = const(1)
@@ -130,6 +135,16 @@ def __init__(
130135
uart=None,
131136
print_logs=False,
132137
):
138+
"""Create a new MSCInterface object
139+
140+
Properties are all optional:
141+
subclass -- should always be _INTERFACE_SUBCLASS_SCSI
142+
protocol -- should likely always be _PROTOCOL_BBB
143+
filesystem -- can be left as None to have no currently mounted filesystem, or can be a bytes-like object containing a filesystem to use
144+
lcd -- an optional LCD object with a "putstr" method, used for logging
145+
uart -- an optional UART for serial logging
146+
print_logs -- set to True to log via print statements, useful if you have put the REPL on a UART
147+
"""
133148
super().__init__(_INTERFACE_CLASS_MSC, subclass, protocol)
134149
self.lcd = lcd
135150
self.uart = uart
@@ -173,6 +188,11 @@ def get_endpoint_descriptors(self, ep_addr, str_idx):
173188
return (desc, [], (self.ep_out, self.ep_in))
174189

175190
def try_to_prepare_cbw(self, args=None):
191+
"""Attempt to prepare a CBW, and if it fails, reschedule this.
192+
193+
This is mostly needed due to a bug where control callbacks aren't being received for interfaces other than the first
194+
that have been added. Otherwise calling prepare_cbw after the max LUN request has been received works fine.
195+
"""
176196
try:
177197
self.prepare_cbw()
178198
except KeyError:
@@ -204,7 +224,7 @@ def handle_interface_control_xfer(self, stage, request):
204224
return False
205225

206226
def reset(self):
207-
"""Theoretically reset, in reality just break things a bit"""
227+
"""Theoretically reset, in reality just break things a bit at the moment"""
208228
self.log("reset()")
209229
# This doesn't work properly at the moment, needs additional
210230
# functionality in the C side
@@ -437,7 +457,13 @@ def send_csw_callback(self, ep_addr, result, xferred_bytes):
437457

438458

439459
class StorageDevice:
440-
"""Storage Device - holds the SCSI parts"""
460+
"""Storage Device - holds the SCSI parts
461+
462+
Properties:
463+
filesystem -- a bytes-like thing representing the data this device is handling. If set to None, then the
464+
object will behave as if there is no medium inserted. This can be changed at runtime.
465+
block_size -- what size the blocks are for SCSI commands. This should probably be left as-is, at 512.
466+
"""
441467

442468
class StorageError(OSError):
443469
def __init__(self, message, status):
@@ -449,6 +475,10 @@ def __init__(self, message, status):
449475
INVALID_COMMAND = const(0x02)
450476

451477
def __init__(self, filesystem):
478+
"""Create a StorageDevice object
479+
480+
filesystem -- either None or a bytes-like object to represent the filesystem being presented
481+
"""
452482
self.filesystem = filesystem
453483
self.block_size = 512
454484
self.sense = None

0 commit comments

Comments
 (0)