Skip to content

Instantly share code, notes, and snippets.

@projectgus
Last active October 4, 2022 05:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save projectgus/f31f11282b6d914a0d89a1456abf5337 to your computer and use it in GitHub Desktop.
Save projectgus/f31f11282b6d914a0d89a1456abf5337 to your computer and use it in GitHub Desktop.
MicroPython report-only HID debug Proof Of Concept
import machine
from micropython import const
_CONTROL_STAGE_IDLE = const(0)
_CONTROL_STAGE_SETUP = const(1)
_CONTROL_STAGE_DATA = const(2)
_CONTROL_STAGE_ACK = const(3)
_PACKET_SIZE = const(64)
_REQ_GET_DESCRIPTOR = const(0x06)
_HID_DESC_TYPE_HID = const(0x21)
_HID_DESC_TYPE_REPORT = const(0x22)
_HID_DESC_TYPE_PHYSICAL = const(0x23)
_HID_REQ_CONTROL_GET_REPORT = const(0x01)
_HID_REQ_CONTROL_GET_IDLE = const(0x02)
_HID_REQ_CONTROL_GET_PROTOCOL = const(0x03)
_HID_REQ_CONTROL_SET_REPORT = const(0x09)
_HID_REQ_CONTROL_SET_IDLE = const(0x0A)
_HID_REQ_CONTROL_SET_PROTOCOL = const(0x0B)
_REQ_TYPE_STANDARD = const(0x00)
_REQ_TYPE_CLASS = const(0x01)
_REQ_TYPE_VENDOR = (const(0x02),)
EP_DIR_IN_MASK = const(0x80)
class SimpleHIDDebug:
def __init__(self):
self._epnum = None # set during enumeration
self._strings = None
usbd = self._usbd = machine.USBD()
usbd.init(
descriptor_device_cb=None, # TODO: fix usbd.c so this isn't needed
descriptor_config_cb=self.descriptor_config_cb,
descriptor_string_cb=self.descriptor_string_cb,
open_driver_cb=self.open_driver_cb,
control_xfer_cb=self.control_xfer_cb,
xfer_cb=self.xfer_cb,
)
self._protocol_mode = None # HID state
self._idle_rate = 1
def reenumerate(self): # hacky convenience method
self._usbd.reenumerate()
def send_hid_report(self, report_data=b'Debug data!'):
assert self._epnum is not None # need to have enumerated
return self._usbd.submit_xfer(self._epnum, report_data)
def descriptor_config_cb(self):
static = self._usbd.static
self._epnum = static.ep_max | EP_DIR_IN_MASK
self._strings = {static.str_max: "Simple HID"}
config = bytearray(static.desc_cfg) # start from the static configuration descriptor
# append HID interface descriptor
# this is obviously way too hacky for long term use, needs rewriting anyhow once there is a layer to handle multiple drivers
# and use ustruct instead of adding to bytearrays
config += b"\x09\x04" # bLength, bDescriptorType
config += bytes([static.itf_max]) # bInterfaceNumber
config += b"\x00\x01\x03\x00\x00" # bAlternateSetting, bNumEndpoints, bInterfaceClass, bInterfaceSubClass, bInterfaceProtocol
config += bytes([static.str_max]) # iInterface
config += self._get_hid_descriptor()
# append HID endpoint descriptor
config += b"\x07\x05" # bLength, bDescriptorType
config += bytes([self._epnum]) # bEndpointAddress
config += bytes(b"\x03") # bmAtttributes, interrupt endpoint
config += bytes([_PACKET_SIZE, 0]) # wMaxPacketSize
config += b"\x01" # bInterval
# go back and change the static config descriptor fields as needed
config[2:4] = bytes([len(config), len(config) >> 8]) # wTotalLength
config[4] += 1 # bNumInterfaces
return config
def _get_hid_descriptor(self):
desc = bytearray()
desc += b"\x09\x21\x11\x01\x00" # bLength, bDescriptorType, bcdHID, bCountryCode
desc += b"\x01\x22" # 1 descriptor, report type
rlen = len(self._get_hid_report_descriptor())
desc += bytes([rlen, rlen >> 8]) # descriptor total len
return desc
def _get_hid_report_descriptor(self):
desc = bytearray()
desc += b'\x06\x31\xFF' # Usage Page 0xFF31 (vendor)
desc += b'\x09\x74' # Usage 0x74
desc += b'\xa1\x53' # Collection 0x53
desc += b'\x75\x08' # report size 8 bit
desc += b'\x15\x00' # logical minimum 0
desc += b'\x26\xff\x00' # logical maximum 255
desc += bytes([0x95, _PACKET_SIZE]) # report count
desc += b'\x09\x75' # Usage 0x75
desc += b'\x81\x02' # Input (array)
desc += b'\xc0' # end collection
return desc
def descriptor_string_cb(self, index):
return self._strings.get(index, "Bad Index")
def open_driver_cb(self, interface_desc_view):
print(f"open_driver_cb {interface_desc_view}")
def xfer_cb(self, ep_addr, result, xferred_bytes):
print(f"xfer_cb EP {ep_addr} result {result} xferred {xferred_bytes}")
return True
def control_xfer_cb(self, stage, request):
bmRequestType, bRequest, wValue, wIndex, wLength = request
request_type = (bmRequestType >> 5) & 3
if request_type == _REQ_TYPE_STANDARD:
return self._handle_control_standard(stage, request)
elif request_type == _REQ_TYPE_CLASS:
return self._handle_control_class(stage, request)
print(f"unsupported request {request} stage {stage}")
return False
def _handle_control_standard(self, stage, request):
bmRequestType, bRequest, wValue, wIndex, wLength = request
if stage != _CONTROL_STAGE_SETUP:
return True # allow standard request DATA/ACK stages to complete normally
if bRequest == _REQ_GET_DESCRIPTOR:
desc_type = wValue >> 8
if desc_type == _HID_DESC_TYPE_HID:
return self._usbd.control_xfer(
request, self._get_hid_descriptor()
)
elif desc_type == _HID_DESC_TYPE_REPORT:
return self._usbd.control_xfer(
request, self._get_hid_report_descriptor()
)
print(f"unexpected standard SETUP bRequest {bRequest:#x} {request}")
return False # STALL
def _handle_control_class(self, stage, request):
bmRequestType, bRequest, wValue, wIndex, wLength = request
if bRequest == _HID_REQ_CONTROL_GET_REPORT:
print("HID_REQ_CONTROL_GET_REPORT unsupported")
return False
elif bRequest == _HID_REQ_CONTROL_GET_IDLE:
if stage == _CONTROL_STAGE_SETUP:
return self._usbd.control_xfer(request,
bytes([ self._idle_rate ]))
elif bRequest == _HID_REQ_CONTROL_GET_PROTOCOL:
if stage == _CONTROL_STAGE_SETUP:
return self._usbd.control_xfer(request,
bytes([ self._protocol_mode ]))
elif bRequest == _HID_REQ_CONTROL_SET_REPORT:
print("HID_REQ_CONTROL_SET_REPORT unsupported")
return False
elif bRequest == _HID_REQ_CONTROL_SET_IDLE:
if stage == _CONTROL_STAGE_SETUP:
self._idle_rate = wValue >> 8
return self._usbd.control_xfer(request, None)
elif bRequest == _HID_REQ_CONTROL_SET_PROTOCOL:
if stage == _CONTROL_STAGE_SETUP:
self._protocol_mode = wValue
return self._usbd.control_xfer(request, None)
else:
print(f"unsupported Class bRequest {bRequest:#x} {request}")
return False # STALL
return True # fallthrough for other supported handlers at different stages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment