Lua logo

Preface

This is the reference manual of MoonUSB, which is a Lua binding library for libusb. [1]

It is assumed that the reader is familiar with both libusb and the Lua programming language.

For convenience of reference, this document contains external (deep) links to the Lua Reference Manual and the libusb-1.0 API Reference.

Getting and installing

For installation intructions, refer to the README file in the MoonUSB official repository on GitHub.

Module organization

The MoonUSB module is loaded using Lua’s require() and returns a table containing the functions it provides (as usual with Lua modules). This manual assumes that such table is named usb, i.e. that it is loaded with:

 usb = require("moonusb")

but nothing forbids the use of a different name.

Examples

Complete examples can be found in the examples/ directory of the release package.

License

MoonUSB is released under the MIT/X11 license (same as Lua, and with the same only requirement to give proper credits to the original author). The copyright notice is in the LICENSE file in the base directory of the official repository on GitHub.

See also

MoonUSB is part of MoonLibs, a collection of Lua libraries for graphics and audio programming.

Introduction

MoonUSB is an almost one-to-one Lua binding library to libusb. This means that by and large, it is intended to be used as described in the libusb documentation.

MoonUSB binds libusb structs (contexts, devices, device handles, etc) to Lua userdata, exposing an object oriented interface to the Lua programmer. Most libusb functions are exposed by MoonUSB as methods of the relevant objects they act on. For example, the libusb_open( ) and libusb_close( ) functions are exposed as the device:open( ) and devhandle:close( ) methods, respectively.

Lua object types in MoonUSB are listed in the tree below, together with the corresponding structs in libusb, if any:

context (libusb_context)
├─ hotplug (libusb_hotplug_callback_handle)
├─ device (libusb_device)
└─ └─ devhandle (libusb_device_handle)
        ├─ interface (libusb_device_handle + interface number)
        ├─ transfer (libusb_transfer)
        └─ hostmem (none)
hostmem (none)

The hostmem object is a MoonUSB-specific object that encapsulates a memory area, to be (optionally) used as memory buffer in transfer functions. It is listed twice because it may be either standalone, or tied to a devhandle (possibly encapsulating DMA memory for that specific device).

Objects are garbage collected at exit (which includes on error), and automatically deleted at the libusb level with graceful release whenever possible, so there is no need to explicitly invoke their deletion methods at exit for cleanup.

Apart from at exit, however, objects are not automatically garbage collected [2] and one must release them explicitly when needed, e.g. to release resources when the application is not exiting and some objects are no longer needed.

Releasing an object causes the automatic (pre) destruction of all its children objects (as per the above tree), and the invalidation of any reference to the object and to its children.[3]

Unless otherwise stated, on error all MoonUSB functions and methods raise a Lua error. If needed, this behaviour can be overridden by wrapping function calls in the standard Lua pcall( ).

MoonUSB supports multiple concurrent libusb contexts (that is, sessions), but it does not support multithreading.

Contexts

A context represents a libusb session. There is no default context in MoonUSB, so an application must begin by creating at least one context object using the init( ) function. It will typically create just one context, but multiple concurrent contexts are allowed too.

When a context exits, all the corresponding objects are deleted too, with graceful release of underlying resources whenever possible. Contexts exit automatically at program termination.

  • context = init( )
    context:exit( )
    Initialize/deinitialize a libusb session, creating/deleting a context.
    Rfr: libusb_init( ), libusb_exit( ).

  • set_option([context], option, […​])
    context:set_option(option, […​])
    Set an option in the library. The currently available options are:
    - set_option(context, 'log level', loglevel)
    - set_option(context, 'use usbdk')
    - set_option(nil, 'weak authority')
    Rfr: libusb_set_option( ).

  • set_log_cb([context], func)
    context:set_log_cb(func)
    Set a log callback for context-related log messages (if context is given) or for global log messages (if context is not given).
    The func callback, a function, is executed as func(context, loglevel, message), where message is a string, and context is nil if the callback is global.
    Rfr: libusb_set_log_cb( ).

Devices and device handles

Devices are represented with two types of objects: a device object that represents a device that has been detected, and a devhandle object that represents a device that has been opened by the application. The former is sufficient to inspect the USB descriptors of a device, but to perform I/O and control operations on it, the latter is needed.

To obtain device objects one can either use the context:get_device_list( ) method, or the Hotplug API. Any device of interest can then be opened with device:open( ), that creates the corresponding devhandle object. Depending on the system, this operation may require access permissions.

  • {device} = context:get_device_list( )
    device:free( )
    Returns a list of USB devices currently attached to the system.
    An application should delete any device objects it obtains but it is not interested in, using the device:free() method.
    Rfr: libusb_get_device_list( ), libusb_unref_device( ).

  • devhandle = device:open( )
    devhandle:close( )
    Open/close a device, creating/deleting a devhandle object.
    A devhandle object is automatically closed when its device is deleted.
    Rfr: libusb_open( ), libusb_close( ).

  • device, devhandle = context:open_device(vendor_id, product_id)
    Shortcut to locate and open a device with a particular vendor_id + product_id combination.
    Creates and returns both the device and the devhandle objects, or raises an error if no such device is found.
    Rfr: libusb_open_device_with_vid_pid( ).

  • lock_on_close(boolean)
    If true, lock events when closing a device (defaults to not locking). See issues #1 and #2.

Device properties

The methods described here can be used to query information about devices without the need to open them. The relevant information, if available, has already been retrieved during enumeration and cached by the lower levels (i.e. by libusb and the OS).

  • bus_number = device:get_bus_number( )
    port_number = device:get_port_number( )
    {port_number} = device:get_port_numbers( )
    Get the number of the bus the device is connected to, the number of the port, and the list of all port numbers from root for the device.
    Rfr: libusb_get_bus_number( ), libusb_get_port_number( ), libusb_get_port_numbers( ).

  • parent_device = device:get_parent( )
    Get the parent device of the specified device.
    Rfr: libusb_get_parent( ).

  • address = device:get_address( )
    Get the address of the device on the bus it is connected to.
    Rfr: libusb_get_device_address( ).

  • speed = device:get_speed( )
    Get the negotiated connection speed for the device.
    Rfr: libusb_get_device_speed( ).

  • size = device:get_max_packet_size(endpoint)
    size = device:get_max_iso_packet_size(endpoint)
    Get/calculate the wMaxPacketSize for the given endpoint in the active device configuration.
    endpoint: endpoint address.
    Rfr: libusb_get_max_packet_size( ), libusb_get_max_iso_packet_size( ).

USB descriptors

  • devicedescriptor = device:get_device_descriptor( )
    configdescriptor = device:get_config_descriptor(index)
    configdescriptor = device:get_config_descriptor_by_value(value)
    configdescriptor = device:get_active_config_descriptor( )
    Get a descriptor for device.
    These methods return immediately since the descriptors are cached during enumeration.
    index: configuration index (0, .., bNumConfigurations-1),
    value: configuration value (bConfigurationValue).
    Rfr: libusb_get_device_descriptor( ), libusb_get_config_descriptor( ), libusb_get_config_descriptor_by_value( ), libusb_get_active_config_descriptor( ).

  • string = devhandle:get_string_descriptor(index)
    length = devhandle:get_descriptor(descriptor_type, descriptor_index, ptr, length)
    bosdecriptor = devhandle:get_bos_descriptor( )
    Retrieve a descriptor.
    These methods are blocking since they involve transfers.
    Rfr: libusb_get_string_descriptor( ), libusb_get_descriptor( ), libusb_get_bos_descriptor( ).

Devhandle operations

Beware that the methods described hereafter perform operations that may involve control transfers, causing the calls to block until the transfers either complete or fail. For more details, refer to the libusb manual.

  • value = devhandle:get_configuration( )
    devhandle:set_configuration(value)
    Determine / set the bConfigurationValue for the currently active configuration. May block.
    Rfr: libusb_get_configuration( ), libusb_set_configuration( ).

  • devhandle:clear_halt(endpoint)
    Clear the halt/stall condition for the given endpoint. Blocking.
    Rfr: libusb_clear_halt( ).

  • devhandle:reset_device( )
    Perform a USB reset to reinitialize the device. Blocking.
    Rfr: libusb_clear_halt( ).

  • n = devhandle:alloc_streams(num_streams, {endpoint})
    devhandle:free_streams({endpoint})
    Allocate up to num_streams USB bulk streams on the specified endpoints.
    {endpoints}: list of endpoint addresses (must belong to the same interface).
    Returns the number n of actually allocated streams, with stream identifiers 1 to n.
    The streams are automatically released with the interface.
    Rfr: libusb_alloc_streams( ), libusb_free_streams( ).

Claiming/releasing interfaces

  • interface = devhandle:claim_interface(interface_number)
    Claim an interface and create the corresponding interface object.
    This also detaches the interface’s kernel driver (where applicable).
    Rfr: libusb_claim_interface( ).

  • interface:release( )
    Release the interface and delete the corresponding object.
    This also reattaches the interface’s kernel driver (where applicable).
    The interface is automatically released when its devhandle gets closed.
    Rfr: libusb_release_interface( ).

  • interface:set_alt_setting(alternate_setting)
    Activate an alternate setting for the interface.
    Rfr: libusb_set_interface_alt_setting( ).

Hotplug

This API needs an event loop. See the polling section.

  • hotplug = context:hotplug_register(event, func, [enumerate], [vendor_id], [product_id], [device_class] )
    Registers a hotplug callback function and returns the corresponding hotplug object.
    event: hotplugevent, the event of interest,
    func: the callback function,
    enumerate: boolean, if true, the callback is executed also for already discovered devices,
    vendor_id: integer (nil means 'any'),
    product_id: integer (nil means 'any'),
    device_class: integer (nil means 'any'),
    The func callback is executed as boolean = func(context, device, event). By returning true it causes the hotplug to be deregistered.
    The callback should free any device it is not interested in, by calling device:free( ).
    Rfr: libusb_hotplug_register_callback( ).

  • hotplug:deregister( )
    Deregisters the callback and deletes the hotplug object.
    Rfr: libusb_hotplug_deregister_callback( ).

Asynchronous I/O

This API needs an event loop. See the polling section.

Transfer submit methods

Asynchronous transfers are submitted using the devhandle:submit_xxx( ) methods, that create and return a new transfer object. Each of these methods requires a function func as one of its arguments. This is the callback that will be executed, as boolean=func(transfer, status), when the transfer either completes, fails, or is canceled. As the callback returns, if its return value is nil or false, then the transfer object is automatically deleted, otherwise it is automatically resubmitted.

Rfr: libusb_alloc_transfer( ), libusb_fill_xxx_transfer( ), libusb_submit_transfer( ).

  • transfer = devhandle:submit_control_transfer(ptr, size, timeout, func)
    Submit a USB control transfer.
    ptr: lightuserdata containing a pointer to at least size bytes of contiguous memory,
    timeout: timeout in milliseconds (=0 for unlimited timeout).
    func: the callback function.
    The first 8 bytes pointed to by ptr must contain an encoded control setup packet, and the size of the memory area must be at least wLength+8.
    The setup packet must be followed by the wLength bytes of data to be transferred to the device ('out' transfers), or of space for the data to be received from the device ('in' transfers).
    For host to device transfers ('out'), the setup packet must be followed by the wLength bytes of data to be transferred.
    For device to host transfers ('in'), up the setup packet must be followed by at least wLength bytes of space where to receive data (the received data thus will start at ptr+8).
    Rfr: libusb_fill_control_transfer( ).

  • transfer = devhandle:submit_interrupt_transfer(endpoint, ptr, length, timeout, func)
    transfer = devhandle:submit_bulk_transfer(endpoint, ptr, length, timeout, func)
    transfer = devhandle:submit_bulk_stream_transfer(endpoint, stream_id, ptr, length, timeout, func)
    Submit a USB interrupt or bulk transfer.
    endpoint: endpoint address,
    stream_id: stream identifier, see devhandle:alloc_streams( ),
    ptr: lightuserdata containing a pointer to at least length bytes of contiguous memory,
    timeout: timeout in milliseconds (=0 for unlimited timeout).
    func: the callback function.
    For host to device transfers ('out'), the memory pointed to by ptr must contain the length bytes of data to be transferred.
    For device to host transfers ('in'), up to length bytes of data will be received and store there, provided the transfer succeeds.
    Rfr: libusb_fill_interrupt_transfer( ), libusb_fill_bulk_transfer( ), libusb_fill_bulk_stream_transfer( ).

  • transfer = devhandle:submit_iso_transfer(endpoint, ptr, length, num_iso_packets, iso_packet_length, timeout, func)
    Submit a USB isochronous transfer.
    endpoint: endpoint address,
    ptr: lightuserdata containing a pointer to at least length bytes of contiguous memory,
    num_iso_packets: the number of isochronous packets to be transferred.
    iso_packet_length: the length of each isochronous packet.
    timeout: timeout in milliseconds (=0 for unlimited timeout).
    func: the callback function.
    For host to device transfers ('out'), the memory pointed to by ptr must contain the length bytes of data to be transferred, consisting of num_iso_packets concatenated packets of iso_packet_length bytes each.
    For device to host transfers ('in'), up to length bytes of data will be received and store there, provided the transfer succeeds. To locate the actually received packets within the memory, use the transfer:get_iso_packet_descriptors( ) method.
    Rfr: libusb_fill_iso_transfer( ).

  • transfer:cancel( )
    Cancels the transfer and deletes the transfer object.
    Note that transfer objects are automatically deleted at the end of the execution of their callback, so calling this method is usually not needed.
    Rfr: libusb_cancel_transfer( ).

  • status = transfer:get_status( )
    Returns the current status of the transfer.

  • endpoint = transfer:get_endpoint( )
    Returns the endpoint address for the endpoint involved in this transfer.

  • length = transfer:get_actual_length( )
    Returns the no. of bytes of data actually transferred.
    This method is meant to be called only within a transfer callback.

  • id = transfer:get_stream_id( )
    Returns the stream id used in the transfer.
    This method is meant to be called only within a bulk stream transfer callback.

  • descr = transfer:get_iso_packet_descriptors( )
    Returns a list of descriptors for the packets of a completed isochronous transfer.
    This method is meant to be called only within an isochronous transfer callback.
    Returns nil if the transfer’s status is not 'completed', otherwise returns a table containing num_endpoints tables, each describing a packet as follows:
    - descr[i].status: transferstatus, status of the i-th packet,
    - descr[i].offset: integer, offset of the i-th packet in the buffer (pointed to by ptr),
    - descr[i].length: integer, actual length of the i-th packet.

Polling

The functions described in this section are to be used in the main event loop of the application.

An event loop is required when using the asynchronous I/O API for transfers, and/or the hotplug API for detecting devices, since callbacks registered via those APIs are executed within calls of the context:handle_events( ) method described below.

Note that MoonUSB does not support multithreading, so the related libusb functions are not exposed.

  • context:handle_events([timeout])
    Handle any pending events.
    timeout: number of seconds to block waiting for events to handle.
    Passing timeout=0 makes the function handle any pending event and return immediately.
    Passing timeout=nil (or not passing it at all) makes it block indefinitely.
    Rfr: libusb_handle_events( ), libusb_handle_events_timeout( ).

  • next_timeout = context:get_next_timeout( )
    Returns the next internal timeout (in number of seconds) that libusb needs to handle, or nil if none.
    Rfr: libusb_get_next_timeout( ).

Synchronous I/O

The functions listed in this section are blocking and return only when the requested transfer either completes or fails. This means that they do not require an event loop (see polling), but also that they may block the application for long times, which may or may not be acceptable. If it is not, then the asynchronous I/O API should be used instead.

  • transferred = devhandle:control_transfer(ptr, length, timeout)
    Perform a USB control transfer and returns the number of bytes of data actually transferred.
    ptr: lightuserdata containing a pointer to at least length bytes of contiguous memory,
    timeout: timeout in milliseconds (=0 for unlimited timeout).
    The first 8 bytes pointed to by ptr must contain an encoded control setup packet, and the length of the memory area must be at least 8 + wLength.
    The setup packet must be followed by the wLength bytes of data to be transferred to the device, or of space for the data to be received from the device.
    Rfr: libusb_control_transfer( ).

  • transferred = devhandle:bulk_transfer(endpoint, ptr, length, timeout)
    transferred = devhandle:interrupt_transfer(endpoint, ptr, length, timeout)
    Perform a USB bulk or interrupt transfer and returns the number of bytes of data actually transferred.
    endpoint: endpoint address,
    ptr: lightuserdata containing a pointer to at least length bytes of contiguous memory,
    timeout: timeout in milliseconds (=0 for unlimited timeout).
    Rfr: libusb_bulk_transfer( ), libusb_interrupt_transfer( ).

Host memory

An hostmem object encapsulates (a pointer to) host accessible memory, with methods to access it from Lua and to retrieve pointers to any of its locations in form of lightuserdata.

(Note that hostmem objets are specific to MoonUSB, i.e. they do not correspond to libusb objects).

The memory encapsulated by an hostmem object may be either host memory allocated via the usb.malloc( ) or the usb.aligned_alloc( ) functions, or memory obtained by other means and passed to the usb.hostmem( ) constructor.

Hostmem objects are automatically deleted at exit, but they may also be deleted manually via the usb.free( ) function or the corresponding method. Hostmem objects that are tied to a devhandle object are automatically deleted when the devhandle is closed.

Hostmem objects come in handy, for example, when issuing transfer commands either via the asynchronous API or vie the synchronous API.

  • hostmem = malloc([devhandle], size)
    hostmem = malloc([devhandle], data)
    hostmem = malloc([devhandle], type, {value1, …​, valueN})
    hostmem = malloc([devhandle], type, value1, …​, valueN)
    Allocates host memory and creates an hostmem object to encapsulate it.
    malloc(devhandle, size), where size is an integer, allocates size bytes of contiguous memory and initializes them to 0;
    malloc(devhandle, data), where data is a binary string, allocates #data bytes of contiguous memory and initializes them with the contents of data;
    malloc(devhandle, type, …​) is functionally equivalent to malloc(usb.pack(type, …​)).
    If the devhandle argument is not nil, an attempt is made to allocate DMA memory for the given device (rfr: libusb_dev_mem_alloc( )). If the attempt fails, normal heap memory is allocated instead. In both cases the hostmem is automatically deleted (and its memory released) when the devhandle is closed.

  • hostmem = aligned_alloc(alignment, size)
    hostmem = aligned_alloc(alignment, data)
    hostmem = aligned_alloc(alignment, type, {value1, …​, valueN})
    hostmem = aligned_alloc(alignment, type, value1, …​, valueN)
    Same as malloc( ), with the additional alignment parameter to control memory address alignment (rfr. aligned_alloc(3)).

  • hostmem = hostmem(size, ptr)
    hostmem = hostmem(data)
    Creates a hostmem object encapsulating user provided memory.
    Such memory may be provided in form of a lightuserdata (ptr) containing a valid pointer to size bytes of contiguous memory, or as a binary string (data).
    In both cases, care must be taken that the memory area remains valid until the hostmem object is deleted, or at least until it is accessed via its methods. In the data case, this means ensuring that data is not garbage collected during the hostmem object lifetime.
    (Note that malloc(data) and hostmem(data) differ in that the former allocates memory and copies data in it, while the latter just stores a pointer to data).

  • free(hostmem)
    hostmem:free( )
    Deletes the hostmem object. If hostmem was created with usb.malloc( ) or usb.aligned_alloc( ), this function also releases the encapsulated memory.

  • ptr = hostmem:ptr([offset=0], [nbytes=0])
    Returns a pointer (lightuserdata) to the location at offset bytes from the beginning of the encapsulated memory.
    Raises an error if the requested location is beyond the boundaries of the memory area, or if there are not at least nbytes of memory after it.

  • nbytes = hostmem:size([offset=0])
    nbytes = hostmem:size(ptr)
    Returns the number of bytes of memory available after offset bytes from the beginning of the encapsulated memory area, or after ptr (a lightuserdata obtained with hostmem:ptr( )).

  • data = hostmem:read([offset], [nbytes])
    {val1, …​, valN} = hostmem:read([offset], [nbytes], type)
    Reads nbytes of data starting from offset, and returns it as a binary string or as a table of primitive values.
    The offset parameter defaults to 0, and nbytes defaults to the memory size minus offset.
    hostmem:read(offset, nbytes, type) is functionally equivalent to cl.unpack(type, hostmem:read(offset, nbytes)).

  • hostmem:write(offset, nil, data)
    hostmem:write(offset, type, val1, …​, valN)
    hostmem:write(offset, type, {value1, …​, valueN})
    Writes to the encapsulated memory area, starting from the byte at offset.
    write(offset, nil, data) writes the contents of data (a binary string);
    write(offset, type, …​) is equivalent to write(offset, nil, usb.pack(type, …​)).

  • hostmem:copy(offset, size, srcptr)
    hostmem:copy(offset, size, srchostmem, srcoffset)
    Copies size bytes to the encapsulated memory area, starting from the byte at offset.
    copy(offset, size, srcptr), copies the size bytes pointed to by srcptr (a lightuserdata).
    copy(offset, size, srchostmem, srcoffset), copies size bytes from the memory encapsulated by srchostmem (a hostmem object), starting from the location at srcoffset.

  • hostmem:clear(offset, nbytes, [val=0])
    Clears nbytes of memory starting from offset. If the val parameter is given, the bytes are set to its value instead of 0 (val may be an integer or a character, i.e. a string of length 1).

Data handling

This section describes additional utilities that can be used to encode data from Lua variables to binary strings and viceversa.

  • val1, …​, valN = flatten(table)
    Flattens out the given table and returns the terminal elements in the order they are found.
    Similar to Lua’s table.unpack( ), but it also unpacks any nested table. Only the array part of the table and of nested tables is considered.

  • {val1, …​, valN} = flatten_table(table)
    Same as flatten( ), but returns the values in a flat table. Unlike flatten( ), this function can be used also with very large tables.

  • size = sizeof(type)
    Returns the size in bytes of the given type.

  • data = pack(type, val1, …​, valN)
    data = pack(type, table)
    Packs the numbers val1, …​, valN, encoding them according to the given type, and returns the resulting binary string.
    The values may also be passed in a (possibly nested) table. Only the array part of the table (and of nested tables) is considered.

  • {val1, …​, valN} = unpack(type, data)
    Unpacks the binary string data, interpreting it as a sequence of values of the given type, and returns the extracted values in a flat table.
    The length of data must be a multiple of sizeof(type).

  • encode_control_setup(ptr, setup)
    bstring = encode_control_setup(nil, setup)
    setup = decode_control_setup(ptr)
    setup = decode_control_setup(bstring)
    Encode/decode a Setup packet for control transfers.
    The packet is encoded to / decoded from the first 8 bytes pointed by ptr, or of the binary string bstring.
    ptr: a lightuserdata containing a pointer to at least 8 bytes of memory,
    bstring: a binary string with length greater or equal to 8.

Miscellanea

  • major, minor, micro, nano, rc = get_version( )
    Returns libusb version information.
    Also available are the usb._VERSION and usb._LIBUSB_VERSION variables, that contain the string versions for MoonUSB and libusb, respectively.
    Rfr: libusb_get_version( ).

  • set_locale(locale)
    locale: string.
    Rfr: libusb_set_locale( ).

  • has_capability(capability)
    Rfr: libusb_has_capability( ).

  • trace_objects(boolean)
    Enable/disable tracing of objects creation and destruction (which by default is disabled).
    If enabled, a printf is generated whenever an object is created or deleted, indicating the object type and the value of its raw handle.

  • t = now( )
    Returns the current time in seconds (a Lua number).
    This is implemented with monotonic clock_gettime(3), if available, or with gettimeofday(3) otherwise.

  • dt = since(t)
    Returns the time in seconds (a Lua number) elapsed since the time t, previously obtained with the now( ) function.

Structs

  • devicedescriptor = {
    usb_version: string (e.g. '02.00', bcdUSB),
    class: class (bDeviceClass),
    subclass: integer (bDeviceSubClass),
    protocol: integer (bDeviceProtocol),
    max_packet_size_0: integer (bMaxPacketSize0),
    vendor_id: integer (idVendor),
    product_id: integer (idProduct),
    release_number: string (e.g. '01.02', bcdDevice),
    manufacturer_index: integer (iManufacturer),
    product_index: integer (iProduct),
    serial_number_index: integer (iSerialNumber),
    num_configurations: integer (bNumConfigurations),
    configuration: {configdescriptor},
    } (rfr: libusb_device_descriptor)

  • configdescriptor = {
    value: integer (bConfigurationValue),
    index: integer (iConfiguration),
    self_powered: boolean (bmAttributes),
    remote_wakeup: boolean (bmAttributes),
    max_power: integer (MaxPower),
    num_interfaces: integer (bNumInterfaces),
    interface: {{interfacedescriptor}},
    extra: binary string,
    } (rfr: libusb_config_descriptor)

  • interfacedescriptor = {
    number: integer (bInterfaceNumber),
    alt_setting: integer (bAlternateSetting),
    class: class (bInterfaceClass),
    subclass: integer (bInterfaceSubClass),
    protocol: integer (bInterfaceProtocol),
    index: integer (iInterface),
    num_endpoints: integer (bNumEndpoints),
    endpoint: {endpointdescriptor},
    extra: binary string,
    } (rfr: libusb_interface_descriptor)

  • endpointdescriptor = {
    address: integer (bEndpointAddress),
    number: integer (bEndpointAddress),
    direction: direction (bEndpointAddress),
    transfer_type: transfertype (bmAttributes),
    iso_sync_type: isosynctype (bmAttributes, isochronous transfers only),
    iso_usage_type: isousagetype (bmAttributes, isochronous transfers only),
    max_packet_size: integer (wMaxPacketSize),
    interval: integer (bInterval),
    refresh: integer (bRefresh),
    synch_address: integer (bSynchAddress),
    ss_endpoint_companion_descriptor: ssendpointcompaniondescriptor or nil,
    extra: binary string,
    } (rfr: libusb_endpoint_descriptor)

  • ssendpointcompaniondescriptor = {
    max_burst: integer (bMaxBurst),
    attributes: integer (bmAttributes),
    bytes_per_interval: integer (wBytesPerInterval),
    } (rfr: libusb_ss_endpoint_companion_descriptor)



Enums

Libusb enums are mapped in MoonUSB to sets of string literals (as is customary in Lua).

If needed, the following function can be used to obtain the list of literals admitted by a particular enum type.

  • {literal} = usb.enum(enumtype)
    Returns a table listing the literals admitted by enumtype (given as a string, e.g. 'blendop', 'format', etc).

Below is the list of the enum types and the literals they admit.

capability: libusb_capability
Values: 'hotplug', 'hid access', 'detach kernel driver'.

class: USB class codes (see: https://www.usb.org/defined-class-codes).
Values: 'per interface', 'audio', 'cdc', 'hid', 'physical', 'image', 'printer', 'mass storage', 'hub', 'cdc data', 'smart card', 'content security', 'video', 'personal healthcare', 'audio video', 'billboard', 'type c bridge', 'diagnostic', 'wireless', 'miscellaneous', 'application specific', 'vendor specific'.

direction: libusb_endpoint_direction
Values: 'out', 'in'.

errcode: libusb_error, libusb_transfer_status
Values: 'success', 'io error', 'invalid param', 'access', 'no device', 'not found', 'busy', 'timeout', 'overflow', 'pipe', 'interrupted', 'no mem', 'not supported', 'other', 'success', 'error', 'timeout', 'cancelled', 'stall', 'no device', 'overflow'.

hotplugevent: libusb_hotplug_event + libusb_hotplug_flags
Values: 'attached', 'detached'.

isosynctype: libusb_iso_sync_type
Values: 'none', 'async', 'adaptive', 'sync'.

isousagetype: libusb_iso_usage_type
Values: 'data', 'feedback', 'implicit'.

loglevel: libusb_log_level
Values: 'none', 'error', 'warning', 'info', 'debug'.

option: libusb_option
Values: 'log level', 'use usbdk', 'weak authority'.

requestrecipient: libusb_request_recipient
Values: 'device', 'interface', 'endpoint', 'other'.

requesttype: libusb_request_type
Values: 'standard', 'class', 'vendor', 'reserved'.

speed: libusb_speed
Values: 'unknown', 'low', 'full', 'high', 'super', 'super plus'.

standardrequest: libusb_standard_request
Values: 'get status', 'clear feature', 'set feature', 'set address', 'get descriptor', 'set descriptor', 'get configuration', 'set configuration', 'get interface', 'set interface', 'synch frame', 'set sel', 'set isoch delay'.

transfertype: libusb_endpoint_transfer_type
Values: 'control', 'isochronous', 'bulk', 'interrupt'.

transferstatus: libusb_transfer_status
Values: 'completed', 'error', 'timeout', 'cancelled', 'stall', 'no device', 'overflow'.

type: primitive type (char=8 bit, short=16 bit, int=32 bit, long=64 bit, float=32 bit, double=64 bit)
Values: 'char', 'uchar', 'short', 'ushort', 'int', 'uint', 'long', 'ulong', 'float', 'double'.

Emulating USB devices

This section describes the optional moonusb.emulator module, that can be loaded with:

 local emulator = require("moonusb.emulator")

The module uses Diego Nehab’s LuaSocket (required) and the MoonTimers module from the MoonLibs collection (optional).

Overview

The purpose of libusb, and hence of MoonUSB, is to implement USB user applications. That is, applications that use USB devices. MoonUSB however provides an additonal - and experimental - submodule named moonusb.emulator to emulate USB devices. The emulation leverages the USB/IP framework, which should be available by default on most GNU/Linux systems.

The original goal of USB/IP is to allow applications to access USB devices that are plugged into a remote host, as if they were plugged into the local host. This is achieved as follows:

  • on the remote host, a TCP server implements the USB/IP protocol (server-side) to make USB devices plugged into that host available for export to clients that request them;

  • on the local host, a USB/IP client - usually the usbip tool - can then connect to the remote server, list the available devices, possibly import one or more of them and send them USB requests over the connection. The server relays the client’s requests to the remote devices and their responses back to the client over the TCP connection. If the usbip tool is used, once it has imported a device it handles the connection to the VHCI driver, which in turns makes the device available to user applications in the same way that real USB devices plugged in locally are made available by their drivers.

Even if it is not its original goal, USB/IP can be used also to emulate a USB device. This is what the moonusb.emulator module does: it implements a USB/IP server that runs locally and exports a fake USB device, as opposed to running remotely to export real devices (the emulation server could also run remotely, but I’m not sure USB/IP is safe enough to advise it).

Since there are gazillions of USB device types out there, in order to fake a particular device the emulator module has to be customized by a user script. The emulator module implements the core, device-independent parts of the server: it manages the TCP connection, the reception and delivery of data over it, and encodes/decodes USB/IP protocol messages. The user script, on the other hand, is responsible for configuring the module and for implementing the device-specific parts of the emulation. Loosely speaking, the emulator module implements the USB/IP protocol (server-side), while the user script implements the USB protocol (device-side) on top of the emulator API described in a later section.

The code snippet below shows the skeleton of a user script. More complete (and working) examples can be found in the examples/ directory.

 -- Script: myfakedevice.lua
 local emulator = require("moonusb.emulator")
 -- local timers = require("moontimers") -- optional

 local function attached()
    -- callback executed when the device has been attached
 end

 local function detached()
    -- callback executed when the device has been detached
 end

 local function receive_submit(submit)
    -- callback executed when a 'submit' command has been received
    -- ... process the command and prepare the response ...
    emulator.send_submit_response(submit, status, error_count, data)
 end

 local function receive_unlink(unlink)
    -- callback executed when a 'unlink' command has been received
    -- ... process the command and prepare the response ...
    emulator.send_unlink_response(unlink, status)
 end

 -- Configure the emulator and start the emulation
 emulator.start({
    -- ... --
    busnum = 4,
    devnum = 5,
    vendor_id = 0x1234,
    product_id = 0x5678,
    -- ... --
    attached = attached,
    detached = detached,
    receive_submit = receive_submit,
    receive_unlink = receive_unlink,
 })

Emulator API

This section describes the API exposed by the moonusb.emulator module to the user scripts that implement emulators for particular devices.

  • emulator.start(cfg)
    Configure the emulator and start the emulation.
    cfg: emulatorconfig.

  • attached( ) callback
    detached( ) callback
    Signature for the cfg.attached and cfg.detached callbacks.
    These callbacks are executed respectively when the fake device is attached (that is, imported by a client), and when it is detached (that is, the connection is closed either intentionally or due to an error).

  • receive_submit(submit) callback
    receive_unlink(unlink) callback
    Signatures for the cfg.receive_submit and cfg.receive_unlink callbacks.
    These callbacks are executed respectively when a USBIP_CMD_SUBMIT or a USBIP_CMD_UNLINK message is received on the connection in attached state. The user is expected to process the message, and possibly send a response using the send_xxx_response( ) functions described below.
    Rfr: USBIP_CMD_SUBMIT, USBIP_CMD_UNLINK.

  • emulator.send_submit_response(submit, status, error_count, [data])
    emulator.send_unlink_response(unlink, status)
    Respectively send a USBIP_RET_SUBMIT or a USBIP_RET_UNLINK response.
    The first argument is the table received in the corresponding receive_xxx( ) callback, unchanged.
    status: signed integer, 0 for success, otherwise an error code.
    error_count: integer.
    data: binary string containing the data to be transferred, or nil if none.
    (I presume that the error codes are from <errno.h>, but I am not sure. Neither am I sure about the meaning of the error count. The USB/IP specification is vague, to say the least.)
    Rfr: USBIP_RET_SUBMIT, USBIP_RET_UNLINK.


Structs

  • emulatorconfig = {
    usbip_ver: string (opt. USB/P bcd version, defaults to '01.11'),
    ip: string (opt. IP address, defaults to 'localhost'),
    port: integer (opt. TCP port, defaults to 3240),
    busnum: integer (opt., defaults to 1),
    devnum: integer (opt., defaults to 1),
    path: string (opt., defaults to 'moonusb emulated device'),
    vendor_id: integer (opt., defaults to 0x0000),
    product_id: integer (opt., defaults to 0x0000),
    release_number: string (opt. bcd device version, defaults to '00.00'),
    speed: speed (opt., defaults to 'high'),
    device_class: class (opt., defaults to 'per interface'),
    device_subclass: integer (opt., defaults to 0),
    device_protocol: integer (opt., defaults to 0),
    configuration_value: integer (opt., defaults to 1),
    num_configurations: integer (opt., defaults to 1),
    interfaces: {{ class=class, subclass=integer, protocol=integer }},
    attached: function (opt. callback, see signature above),
    detached: function (opt. callback, see signature above),
    receive_submit: function (callback, see signature above),
    receive_unlink: function (callback, see signature above),
    } (rfr: OP_REQ_DEVLIST)

  • submit = {
    seqnum: integer,
    devid: integer,
    direction: direction,
    ep: integer,
    transfer_flags: integer,
    transfer_buffer_length: integer,
    start_frame: integer,
    number_of_packets: integer,
    interval: integer,
    setup: binary strings (8 bytes long),
    data: binary string or nil,
    } (rfr: USBIP_CMD_SUBMIT)

  • unlink = {
    seqnum: integer,
    devid: integer,
    direction: direction,
    ep: integer,
    victim_seqnum: integer,
    } (rfr: USBIP_CMD_UNLINK)

Emulator usage

This section shows how to run a device emulation. It is assumed that the emulator is implemented in a user script named myfakedevice.lua, and a client application (using that particular USB device) is implemented in a script named myclient.lua.

First of all, ensure that the needed kernel modules are loaded:

 $ sudo modprobe usbip-core
 $ sudo modprobe vhci-hcd

Then, launch the emulator:

 $ lua myfakedevice.lua

The emulator will open a TCP socket on the specified port and wait for clients to connect.

From another shell, list the devices exported by the script, using the usbip tool:

 $ usbip list -r 127.0.0.1

This command should list a single device, and indicate "4-5" as its busid (assuming busnum=4 and devnum=5). Import the device, again with the usbip tool:

 $ sudo usbip attach -r 127.0.0.1 -b 4-5

This should start the configuration of the device, with the vhci driver issuing commands (get descriptor, etc) and the fake device responding. At the end of this phase, the fake device should appear in the list of USB devices available on the system, which you can see using the lsusb tool:

 $ lsusb

Finally, launch the client application from yet another shell:

 $ lua myclient.lua

If everything goes as expected, the client should now be able to detect and use the fake device as if it were a real one.

To detach the fake device, first see its 'port' number (likely 00), then issue the detach command:

 $ sudo usbip port                # list the imported devices
 $ sudo usbip detach -p 00        # detach Port 00

As a final note, the traffic between the emulator and the vhci driver can be captured and analized using Wireshark, which has a built-in dissector for the USB/IP protocol. (The traffic travels on the loopback interface, assuming the server uses the 127.0.0.1 ip address.)


1. This manual is written in AsciiDoc, rendered with AsciiDoctor and a CSS from the AsciiDoctor Stylesheet Factory.
2. Objects are anchored to the Lua registry at their creation, so even if the script does not have references to an object, a reference always exists on the registry and this prevents the GC to collect it.
3. It is good practice to not leave invalid references to objects around, because they prevent the GC to collect the memory associated with the userdata.