| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Linux gadgetfs glue. |
| |
| Exposes a USB gadget using a USB peripheral controller on Linux. The userspace |
| ABI is documented here: |
| |
| https://github.com/torvalds/linux/blob/master/drivers/usb/gadget/inode.c |
| """ |
| |
| import errno |
| import multiprocessing |
| import os |
| import struct |
| |
| from tornado import ioloop |
| |
| import usb_constants |
| import usb_descriptors |
| |
| GADGETFS_NOP = 0 |
| GADGETFS_CONNECT = 1 |
| GADGETFS_DISCONNECT = 2 |
| GADGETFS_SETUP = 3 |
| GADGETFS_SUSPEND = 4 |
| |
| BULK = 0x01 |
| INTERRUPT = 0x02 |
| ISOCHRONOUS = 0x04 |
| |
| USB_TRANSFER_TYPE_TO_MASK = { |
| usb_constants.TransferType.BULK: BULK, |
| usb_constants.TransferType.INTERRUPT: INTERRUPT, |
| usb_constants.TransferType.ISOCHRONOUS: ISOCHRONOUS |
| } |
| |
| IN = 0x01 |
| OUT = 0x02 |
| |
| HARDWARE = { |
| 'beaglebone-black': ( |
| 'musb-hdrc', # Gadget controller name, |
| { |
| 0x01: ('ep1out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x81: ('ep1in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x02: ('ep2out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x82: ('ep2in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x03: ('ep3out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x83: ('ep3in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x04: ('ep4out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x84: ('ep4in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x05: ('ep5out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x85: ('ep5in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x06: ('ep6out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x86: ('ep6in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x07: ('ep7out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x87: ('ep7in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x08: ('ep8out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x88: ('ep8in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x09: ('ep9out', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x89: ('ep9in', BULK | INTERRUPT | ISOCHRONOUS, 512), |
| 0x0A: ('ep10out', BULK | INTERRUPT | ISOCHRONOUS, 64), |
| 0x8A: ('ep10in', BULK | INTERRUPT | ISOCHRONOUS, 256), |
| 0x0B: ('ep11out', BULK | INTERRUPT | ISOCHRONOUS, 64), |
| 0x8B: ('ep11in', BULK | INTERRUPT | ISOCHRONOUS, 256), |
| 0x0C: ('ep12out', BULK | INTERRUPT | ISOCHRONOUS, 64), |
| 0x8C: ('ep12in', BULK | INTERRUPT | ISOCHRONOUS, 256), |
| 0x0D: ('ep13', BULK | INTERRUPT | ISOCHRONOUS, 4096), |
| 0x8D: ('ep13', BULK | INTERRUPT | ISOCHRONOUS, 4096), |
| 0x0E: ('ep14', BULK | INTERRUPT | ISOCHRONOUS, 1024), |
| 0x8E: ('ep14', BULK | INTERRUPT | ISOCHRONOUS, 1024), |
| 0x0F: ('ep15', BULK | INTERRUPT | ISOCHRONOUS, 1024), |
| 0x8F: ('ep15', BULK | INTERRUPT | ISOCHRONOUS, 1024), |
| } |
| ) |
| } |
| |
| |
| class LinuxGadgetfs(object): |
| """Linux gadgetfs-based gadget driver. |
| """ |
| |
| def __init__(self, hardware, mountpoint='/dev/gadget'): |
| """Initialize bindings to the Linux gadgetfs interface. |
| |
| Args: |
| hardware: Hardware type. |
| mountpoint: Gadget filesystem mount point. |
| """ |
| self._chip, self._hw_eps = HARDWARE[hardware] |
| self._ep_dir = mountpoint |
| self._gadget = None |
| self._fd = None |
| # map from bEndpointAddress to hardware ep name and open file descriptor |
| self._ep_fds = {} |
| self._io_loop = ioloop.IOLoop.current() |
| |
| def Create(self, gadget): |
| """Bind a gadget to the USB peripheral controller.""" |
| self._gadget = gadget |
| self._fd = os.open(os.path.join(self._ep_dir, self._chip), os.O_RDWR) |
| buf = ''.join([struct.pack('=I', 0), |
| gadget.GetFullSpeedConfigurationDescriptor().Encode(), |
| gadget.GetHighSpeedConfigurationDescriptor().Encode(), |
| gadget.GetDeviceDescriptor().Encode()]) |
| os.write(self._fd, buf) |
| self._io_loop.add_handler(self._fd, self.HandleEvent, self._io_loop.READ) |
| |
| def Destroy(self): |
| """Unbind the gadget from the USB peripheral controller.""" |
| self.Disconnected() |
| self._io_loop.remove_handler(self._fd) |
| os.close(self._fd) |
| self._gadget = None |
| self._fd = None |
| |
| def IsConfigured(self): |
| return self._gadget is not None |
| |
| def HandleEvent(self, unused_fd, unused_events): |
| buf = os.read(self._fd, 12) |
| event_type, = struct.unpack_from('=I', buf, 8) |
| |
| if event_type == GADGETFS_NOP: |
| print 'NOP' |
| elif event_type == GADGETFS_CONNECT: |
| speed, = struct.unpack('=Ixxxxxxxx', buf) |
| self.Connected(speed) |
| elif event_type == GADGETFS_DISCONNECT: |
| self.Disconnected() |
| elif event_type == GADGETFS_SETUP: |
| request_type, request, value, index, length = struct.unpack( |
| '<BBHHHxxxx', buf) |
| self.HandleSetup(request_type, request, value, index, length) |
| elif event_type == GADGETFS_SUSPEND: |
| print 'SUSPEND' |
| else: |
| print 'Unknown gadgetfs event type:', event_type |
| |
| def Connected(self, speed): |
| print 'CONNECT speed={}'.format(speed) |
| self._gadget.Connected(self, speed) |
| |
| def Disconnected(self): |
| print 'DISCONNECT' |
| for endpoint_addr in self._ep_fds.keys(): |
| self.StopEndpoint(endpoint_addr) |
| self._ep_fds.clear() |
| self._gadget.Disconnected() |
| |
| def HandleSetup(self, request_type, request, value, index, length): |
| print ('SETUP bmRequestType=0x{:02X} bRequest=0x{:02X} wValue=0x{:04X} ' |
| 'wIndex=0x{:04X} wLength={}' |
| .format(request_type, request, value, index, length)) |
| |
| if request_type & usb_constants.Dir.IN: |
| data = self._gadget.ControlRead( |
| request_type, request, value, index, length) |
| if data is None: |
| print 'SETUP STALL' |
| try: |
| os.read(self._fd, 0) # Backwards I/O stalls the pipe. |
| except OSError, e: |
| # gadgetfs always returns EL2HLT which we should ignore. |
| if e.errno != errno.EL2HLT: |
| raise |
| else: |
| os.write(self._fd, data) |
| else: |
| data = '' |
| if length: |
| data = os.read(self._fd, length) |
| result = self._gadget.ControlWrite( |
| request_type, request, value, index, data) |
| if result is None: |
| print 'SETUP STALL' |
| try: |
| os.write(self._fd, '') # Backwards I/O stalls the pipe. |
| except OSError, e: |
| # gadgetfs always returns EL2HLT which we should ignore. |
| if e.errno != errno.EL2HLT: |
| raise |
| elif not length: |
| # Only empty OUT transfers can be ACKed. |
| os.read(self._fd, 0) |
| |
| def StartEndpoint(self, endpoint_desc): |
| """Activate an endpoint. |
| |
| To enable a hardware endpoint the appropriate endpoint file must be opened |
| and the endpoint descriptors written to it. Linux requires both full- and |
| high-speed descriptors to be written for a high-speed device but since the |
| endpoint is always reinitialized after disconnect only the high-speed |
| endpoint will be valid in this case. |
| |
| Args: |
| endpoint_desc: Endpoint descriptor. |
| |
| Raises: |
| RuntimeError: If the hardware endpoint is in use or the configuration |
| is not supported by the hardware. |
| """ |
| endpoint_addr = endpoint_desc.bEndpointAddress |
| name, hw_ep_type, hw_ep_size = self._hw_eps[endpoint_addr] |
| |
| if name in self._ep_fds: |
| raise RuntimeError('Hardware endpoint {} already in use.'.format(name)) |
| |
| ep_type = USB_TRANSFER_TYPE_TO_MASK[ |
| endpoint_desc.bmAttributes & usb_constants.TransferType.MASK] |
| ep_size = endpoint_desc.wMaxPacketSize |
| |
| if not hw_ep_type & ep_type: |
| raise RuntimeError('Hardware endpoint {} does not support this transfer ' |
| 'type.'.format(name)) |
| elif hw_ep_size < ep_size: |
| raise RuntimeError('Hardware endpoint {} only supports a maximum packet ' |
| 'size of {}, {} requested.' |
| .format(name, hw_ep_size, ep_size)) |
| |
| fd = os.open(os.path.join(self._ep_dir, name), os.O_RDWR) |
| |
| buf = struct.pack('=I', 1) |
| if self._gadget.GetSpeed() == usb_constants.Speed.HIGH: |
| # The full speed endpoint descriptor will not be used but Linux requires |
| # one to be provided. |
| full_speed_endpoint = usb_descriptors.EndpointDescriptor( |
| bEndpointAddress=endpoint_desc.bEndpointAddress, |
| bmAttributes=0, |
| wMaxPacketSize=0, |
| bInterval=0) |
| buf = ''.join([buf, full_speed_endpoint.Encode(), endpoint_desc.Encode()]) |
| else: |
| buf = ''.join([buf, endpoint_desc.Encode()]) |
| os.write(fd, buf) |
| |
| pipe_r, pipe_w = multiprocessing.Pipe(False) |
| child = None |
| |
| # gadgetfs doesn't support polling on the endpoint file descriptors (why?) |
| # so we have to start background threads for each. |
| if endpoint_addr & usb_constants.Dir.IN: |
| def WriterProcess(): |
| while True: |
| data = pipe_r.recv() |
| written = os.write(fd, data) |
| print('IN bEndpointAddress=0x{:02X} length={}' |
| .format(endpoint_addr, written)) |
| |
| child = multiprocessing.Process(target=WriterProcess) |
| self._ep_fds[endpoint_addr] = fd, child, pipe_w |
| else: |
| def ReceivePacket(unused_fd, unused_events): |
| data = pipe_r.recv() |
| print('OUT bEndpointAddress=0x{:02X} length={}' |
| .format(endpoint_addr, len(data))) |
| self._gadget.ReceivePacket(endpoint_addr, data) |
| |
| def ReaderProcess(): |
| while True: |
| data = os.read(fd, ep_size) |
| pipe_w.send(data) |
| |
| child = multiprocessing.Process(target=ReaderProcess) |
| pipe_fd = pipe_r.fileno() |
| self._io_loop.add_handler(pipe_fd, ReceivePacket, self._io_loop.READ) |
| self._ep_fds[endpoint_addr] = fd, child, pipe_r |
| |
| child.start() |
| print 'Started endpoint 0x{:02X}.'.format(endpoint_addr) |
| |
| def StopEndpoint(self, endpoint_addr): |
| """Deactivate the given endpoint.""" |
| fd, child, pipe = self._ep_fds.pop(endpoint_addr) |
| pipe_fd = pipe.fileno() |
| child.terminate() |
| child.join() |
| if not endpoint_addr & usb_constants.Dir.IN: |
| self._io_loop.remove_handler(pipe_fd) |
| os.close(fd) |
| print 'Stopped endpoint 0x{:02X}.'.format(endpoint_addr) |
| |
| def SendPacket(self, endpoint_addr, data): |
| """Send a packet on the given endpoint.""" |
| _, _, pipe = self._ep_fds[endpoint_addr] |
| pipe.send(data) |
| |
| def HaltEndpoint(self, endpoint_addr): |
| """Signal a stall condition on the given endpoint.""" |
| fd, _ = self._ep_fds[endpoint_addr] |
| # Reverse I/O direction sets the halt condition on the pipe. |
| try: |
| if endpoint_addr & usb_constants.Dir.IN: |
| os.read(fd, 0) |
| else: |
| os.write(fd, '') |
| except OSError, e: |
| # gadgetfs always returns EBADMSG which we should ignore. |
| if e.errno != errno.EBADMSG: |
| raise |