# This file is part of Libusb for Ruby. # # Libusb for Ruby is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Libusb for Ruby is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with Libusb for Ruby. If not, see . require 'rubygems' require 'ffi' module LIBUSB VERSION = "0.1.2" module Call extend FFI::Library if RUBY_PLATFORM=~/mingw|mswin/i bundled_dll = File.join(File.dirname(__FILE__), 'libusb-1.0.dll') ffi_lib(['libusb-1.0', bundled_dll]) else ffi_lib 'libusb-1.0' end ClassCodes = enum :libusb_class_code, [ :CLASS_PER_INTERFACE, 0, :CLASS_AUDIO, 1, :CLASS_COMM, 2, :CLASS_HID, 3, :CLASS_PRINTER, 7, :CLASS_PTP, 6, :CLASS_MASS_STORAGE, 8, :CLASS_HUB, 9, :CLASS_DATA, 10, :CLASS_WIRELESS, 0xe0, :CLASS_APPLICATION, 0xfe, :CLASS_VENDOR_SPEC, 0xff ] Errors = enum :libusb_error, [ :SUCCESS, 0, :ERROR_IO, -1, :ERROR_INVALID_PARAM, -2, :ERROR_ACCESS, -3, :ERROR_NO_DEVICE, -4, :ERROR_NOT_FOUND, -5, :ERROR_BUSY, -6, :ERROR_TIMEOUT, -7, :ERROR_OVERFLOW, -8, :ERROR_PIPE, -9, :ERROR_INTERRUPTED, -10, :ERROR_NO_MEM, -11, :ERROR_NOT_SUPPORTED, -12, :ERROR_OTHER, -99, ] # Transfer status codes TransferStatus = enum :libusb_transfer_status, [ :TRANSFER_COMPLETED, :TRANSFER_ERROR, :TRANSFER_TIMED_OUT, :TRANSFER_CANCELLED, :TRANSFER_STALL, :TRANSFER_NO_DEVICE, :TRANSFER_OVERFLOW, ] # libusb_transfer.flags values TransferFlags = enum :libusb_transfer_flags, [ :TRANSFER_SHORT_NOT_OK, 1 << 0, :TRANSFER_FREE_BUFFER, 1 << 1, :TRANSFER_FREE_TRANSFER, 1 << 2, ] TransferTypes = enum :libusb_transfer_type, [ :TRANSFER_TYPE_CONTROL, 0, :TRANSFER_TYPE_ISOCHRONOUS, 1, :TRANSFER_TYPE_BULK, 2, :TRANSFER_TYPE_INTERRUPT, 3, ] StandardRequests = enum :libusb_standard_request, [ :REQUEST_GET_STATUS, 0x00, :REQUEST_CLEAR_FEATURE, 0x01, :REQUEST_SET_FEATURE, 0x03, :REQUEST_SET_ADDRESS, 0x05, :REQUEST_GET_DESCRIPTOR, 0x06, :REQUEST_SET_DESCRIPTOR, 0x07, :REQUEST_GET_CONFIGURATION, 0x08, :REQUEST_SET_CONFIGURATION, 0x09, :REQUEST_GET_INTERFACE, 0x0A, :REQUEST_SET_INTERFACE, 0x0B, :REQUEST_SYNCH_FRAME, 0x0C, ] EndpointDirections = enum :libusb_endpoint_direction, [ :ENDPOINT_IN, 0x80, :ENDPOINT_OUT, 0x00, ] DescriptorTypes = enum :libusb_descriptor_type, [ :DT_DEVICE, 0x01, :DT_CONFIG, 0x02, :DT_STRING, 0x03, :DT_INTERFACE, 0x04, :DT_ENDPOINT, 0x05, :DT_HID, 0x21, :DT_REPORT, 0x22, :DT_PHYSICAL, 0x23, :DT_HUB, 0x29, ] RequestTypes = enum :libusb_request_type, [ :REQUEST_TYPE_STANDARD, (0x00 << 5), :REQUEST_TYPE_CLASS, (0x01 << 5), :REQUEST_TYPE_VENDOR, (0x02 << 5), :REQUEST_TYPE_RESERVED, (0x03 << 5), ] RequestRecipients = enum :libusb_request_recipient, [ :RECIPIENT_DEVICE, 0x00, :RECIPIENT_INTERFACE, 0x01, :RECIPIENT_ENDPOINT, 0x02, :RECIPIENT_OTHER, 0x03, ] IsoSyncTypes = enum :libusb_iso_sync_type, [ :ISO_SYNC_TYPE_NONE, 0, :ISO_SYNC_TYPE_ASYNC, 1, :ISO_SYNC_TYPE_ADAPTIVE, 2, :ISO_SYNC_TYPE_SYNC, 3, ] typedef :pointer, :libusb_context typedef :pointer, :libusb_device_handle attach_function 'libusb_init', [ :pointer ], :int attach_function 'libusb_exit', [ :pointer ], :void attach_function 'libusb_set_debug', [:pointer, :int], :void attach_function 'libusb_get_device_list', [:pointer, :pointer], :size_t attach_function 'libusb_free_device_list', [:pointer, :int], :void attach_function 'libusb_ref_device', [:pointer], :pointer attach_function 'libusb_unref_device', [:pointer], :void attach_function 'libusb_get_device_descriptor', [:pointer, :pointer], :int attach_function 'libusb_get_active_config_descriptor', [:pointer, :pointer], :int attach_function 'libusb_get_config_descriptor', [:pointer, :uint8, :pointer], :int attach_function 'libusb_get_config_descriptor_by_value', [:pointer, :uint8, :pointer], :int attach_function 'libusb_free_config_descriptor', [:pointer], :void attach_function 'libusb_get_bus_number', [:pointer], :uint8 attach_function 'libusb_get_device_address', [:pointer], :uint8 attach_function 'libusb_get_max_packet_size', [:pointer, :uint8], :int attach_function 'libusb_get_max_iso_packet_size', [:pointer, :uint8], :int attach_function 'libusb_open', [:pointer, :pointer], :int attach_function 'libusb_close', [:pointer], :void attach_function 'libusb_get_device', [:libusb_device_handle], :pointer attach_function 'libusb_set_configuration', [:libusb_device_handle, :int], :int, :blocking=>true attach_function 'libusb_claim_interface', [:libusb_device_handle, :int], :int attach_function 'libusb_release_interface', [:libusb_device_handle, :int], :int, :blocking=>true attach_function 'libusb_open_device_with_vid_pid', [:pointer, :int, :int], :pointer attach_function 'libusb_set_interface_alt_setting', [:libusb_device_handle, :int, :int], :int, :blocking=>true attach_function 'libusb_clear_halt', [:libusb_device_handle, :int], :int, :blocking=>true attach_function 'libusb_reset_device', [:libusb_device_handle], :int, :blocking=>true attach_function 'libusb_kernel_driver_active', [:libusb_device_handle, :int], :int attach_function 'libusb_detach_kernel_driver', [:libusb_device_handle, :int], :int attach_function 'libusb_attach_kernel_driver', [:libusb_device_handle, :int], :int attach_function 'libusb_get_string_descriptor_ascii', [:pointer, :uint8, :pointer, :int], :int attach_function 'libusb_alloc_transfer', [:int], :pointer attach_function 'libusb_submit_transfer', [:pointer], :int attach_function 'libusb_cancel_transfer', [:pointer], :int attach_function 'libusb_free_transfer', [:pointer], :void attach_function 'libusb_handle_events', [:libusb_context], :int, :blocking=>true callback :libusb_transfer_cb_fn, [:pointer], :void class IsoPacketDescriptor < FFI::Struct layout :length, :uint, :actual_length, :uint, :status, :libusb_transfer_status end # Setup packet for control transfers. class ControlSetup < FFI::Struct layout :bmRequestType, :uint8, :bRequest, :uint8, :wValue, :uint16, :wIndex, :uint16, :wLength, :uint16 end class Transfer < FFI::ManagedStruct layout :dev_handle, :libusb_device_handle, :flags, :uint8, :endpoint, :uchar, :type, :uchar, :timeout, :uint, :status, :libusb_transfer_status, :length, :int, :actual_length, :int, :callback, :libusb_transfer_cb_fn, :user_data, :pointer, :buffer, :pointer, :num_iso_packets, :int def self.release(ptr) Call.libusb_free_transfer(ptr) end end end Call::ClassCodes.to_h.each{|k,v| const_set(k,v) } Call::TransferTypes.to_h.each{|k,v| const_set(k,v) } Call::StandardRequests.to_h.each{|k,v| const_set(k,v) } Call::RequestTypes.to_h.each{|k,v| const_set(k,v) } Call::DescriptorTypes.to_h.each{|k,v| const_set(k,v) } Call::EndpointDirections.to_h.each{|k,v| const_set(k,v) } Call::RequestRecipients.to_h.each{|k,v| const_set(k,v) } Call::IsoSyncTypes.to_h.each{|k,v| const_set(k,v) } class Error < RuntimeError end ErrorClassForResult = {} # define an exception class for each error code Call::Errors.to_h.each do |k,v| klass = Class.new(Error) klass.send(:define_method, :code){ v } const_set(k, klass) ErrorClassForResult[v] = klass end def self.raise_error(res, text) klass = ErrorClassForResult[res] raise klass, "#{klass} #{text}" end CONTROL_SETUP_SIZE = 8 DT_DEVICE_SIZE = 18 DT_CONFIG_SIZE = 9 DT_INTERFACE_SIZE = 9 DT_ENDPOINT_SIZE = 7 DT_ENDPOINT_AUDIO_SIZE = 9 # Audio extension DT_HUB_NONVAR_SIZE = 7 ENDPOINT_ADDRESS_MASK = 0x0f # in bEndpointAddress ENDPOINT_DIR_MASK = 0x80 TRANSFER_TYPE_MASK = 0x03 # in bmAttributes ISO_SYNC_TYPE_MASK = 0x0C ISO_USAGE_TYPE_MASK = 0x30 # :stopdoc: # http://www.usb.org/developers/defined_class CLASS_CODES = [ [0x01, nil, nil, "Audio"], [0x02, nil, nil, "Comm"], [0x03, nil, nil, "HID"], [0x05, nil, nil, "Physical"], [0x06, 0x01, 0x01, "StillImaging"], [0x06, nil, nil, "Image"], [0x07, nil, nil, "Printer"], [0x08, 0x01, nil, "MassStorage RBC Bluk-Only"], [0x08, 0x02, 0x50, "MassStorage ATAPI Bluk-Only"], [0x08, 0x03, 0x50, "MassStorage QIC-157 Bluk-Only"], [0x08, 0x04, nil, "MassStorage UFI"], [0x08, 0x05, 0x50, "MassStorage SFF-8070i Bluk-Only"], [0x08, 0x06, 0x50, "MassStorage SCSI Bluk-Only"], [0x08, nil, nil, "MassStorage"], [0x09, 0x00, 0x00, "Full speed Hub"], [0x09, 0x00, 0x01, "Hi-speed Hub with single TT"], [0x09, 0x00, 0x02, "Hi-speed Hub with multiple TTs"], [0x09, nil, nil, "Hub"], [0x0a, nil, nil, "CDC"], [0x0b, nil, nil, "SmartCard"], [0x0d, 0x00, 0x00, "ContentSecurity"], [0x0e, nil, nil, "Video"], [0xdc, 0x01, 0x01, "Diagnostic USB2"], [0xdc, nil, nil, "Diagnostic"], [0xe0, 0x01, 0x01, "Bluetooth"], [0xe0, 0x01, 0x02, "UWB"], [0xe0, 0x01, 0x03, "RemoteNDIS"], [0xe0, 0x02, 0x01, "Host Wire Adapter Control/Data"], [0xe0, 0x02, 0x02, "Device Wire Adapter Control/Data"], [0xe0, 0x02, 0x03, "Device Wire Adapter Isochronous"], [0xe0, nil, nil, "Wireless Controller"], [0xef, 0x01, 0x01, "Active Sync"], [0xef, 0x01, 0x02, "Palm Sync"], [0xef, 0x02, 0x01, "Interface Association Descriptor"], [0xef, 0x02, 0x02, "Wire Adapter Multifunction Peripheral"], [0xef, 0x03, 0x01, "Cable Based Association Framework"], [0xef, nil, nil, "Miscellaneous"], [0xfe, 0x01, 0x01, "Device Firmware Upgrade"], [0xfe, 0x02, 0x00, "IRDA Bridge"], [0xfe, 0x03, 0x00, "USB Test and Measurement"], [0xfe, 0x03, 0x01, "USB Test and Measurement (USBTMC USB488)"], [0xfe, nil, nil, "Application Specific"], [0xff, nil, nil, "Vendor specific"], ] CLASS_CODES_HASH1 = {} CLASS_CODES_HASH2 = {} CLASS_CODES_HASH3 = {} CLASS_CODES.each {|base_class, sub_class, protocol, desc| if protocol CLASS_CODES_HASH3[[base_class, sub_class, protocol]] = desc elsif sub_class CLASS_CODES_HASH2[[base_class, sub_class]] = desc else CLASS_CODES_HASH1[base_class] = desc end } def self.dev_string(base_class, sub_class, protocol) if desc = CLASS_CODES_HASH3[[base_class, sub_class, protocol]] desc elsif desc = CLASS_CODES_HASH2[[base_class, sub_class]] desc + " (%02x)" % [protocol] elsif desc = CLASS_CODES_HASH1[base_class] desc + " (%02x,%02x)" % [sub_class, protocol] else "Unkonwn(%02x,%02x,%02x)" % [base_class, sub_class, protocol] end end # :startdoc: # Abstract base class for USB transfers. Use # {ControlTransfer}, {BulkTransfer}, {InterruptTransfer}, {IsochronousTransfer} # to do transfers. class Transfer def initialize(args={}) args.each{|k,v| send("#{k}=", v) } @buffer = nil end private :initialize # Set the handle for the device to communicate with. def dev_handle=(dev) @dev_handle = dev @transfer[:dev_handle] = @dev_handle.pHandle end # Timeout for this transfer in millseconds. # # A value of 0 indicates no timeout. def timeout=(value) @transfer[:timeout] = value end # Set the address of a valid endpoint to communicate with. def endpoint=(endpoint) endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress @transfer[:endpoint] = endpoint end # Set output data that should be sent. def buffer=(data) if !@buffer || data.bytesize>@buffer.size free_buffer @buffer = FFI::MemoryPointer.new(data.bytesize, 1, false) end @buffer.put_bytes(0, data) @transfer[:buffer] = @buffer @transfer[:length] = data.bytesize end # Retrieve the current data buffer. def buffer @transfer[:buffer].read_string(@transfer[:length]) end # Clear the current data buffer. def free_buffer if @buffer @buffer.free @buffer = nil @transfer[:buffer] = nil @transfer[:length] = 0 end end # Allocate +len+ bytes of data buffer for input transfer. # # @param [Fixnum] len Number of bytes to allocate # @param [String, nil] data some data to initialize the buffer with def alloc_buffer(len, data=nil) if !@buffer || len>@buffer.size free_buffer @buffer = FFI::MemoryPointer.new(len, 1, false) end @buffer.put_bytes(0, data) if data @transfer[:buffer] = @buffer @transfer[:length] = len end # The number of bytes actually transferred. def actual_length @transfer[:actual_length] end # Retrieve the data actually transferred. # # @param [Fixnum] offset optional offset of the retrieved data in the buffer. def actual_buffer(offset=0) @transfer[:buffer].get_bytes(offset, @transfer[:actual_length]) end # Set the block that will be invoked when the transfer completes, # fails, or is cancelled. # # @param [Proc] proc The block that should be called def callback=(proc) # Save proc to instance variable so that GC doesn't free # the proc object before the transfer. @callback_proc = proc do |pTrans| proc.call(self) end @transfer[:callback] = @callback_proc end # The status of the transfer. # # Only for use within transfer callback function or after the callback was called. # # If this is an isochronous transfer, this field may read :TRANSFER_COMPLETED even if there # were errors in the frames. Use the status field in each packet to determine if # errors occurred. def status @transfer[:status] end # Submit a transfer. # # This function will fire off the USB transfer and then return immediately. # This method can be called with block. It is called when the transfer completes, # fails, or is cancelled. def submit!(&block) self.callback = block if block_given? # puts "submit transfer #{@transfer.inspect} buffer: #{@transfer[:buffer].inspect} length: #{@transfer[:length].inspect} status: #{@transfer[:status].inspect} callback: #{@transfer[:callback].inspect} dev_handle: #{@transfer[:dev_handle].inspect}" res = Call.libusb_submit_transfer( @transfer ) LIBUSB.raise_error res, "in libusb_submit_transfer" if res!=0 end # Asynchronously cancel a previously submitted transfer. # # This function returns immediately, but this does not indicate cancellation is # complete. Your callback function will be invoked at some later time with a # transfer status of :TRANSFER_CANCELLED. def cancel! res = Call.libusb_cancel_transfer( @transfer ) LIBUSB.raise_error res, "in libusb_cancel_transfer" if res!=0 end TransferStatusToError = { :TRANSFER_ERROR => LIBUSB::ERROR_IO, :TRANSFER_TIMED_OUT => LIBUSB::ERROR_TIMEOUT, :TRANSFER_CANCELLED => LIBUSB::ERROR_INTERRUPTED, :TRANSFER_STALL => LIBUSB::ERROR_PIPE, :TRANSFER_NO_DEVICE => LIBUSB::ERROR_NO_DEVICE, :TRANSFER_OVERFLOW => LIBUSB::ERROR_OVERFLOW, } # Submit the transfer and wait until the transfer completes or fails. # # A proper {LIBUSB::Error} is raised, in case the transfer did not complete. def submit_and_wait! completed = false submit! do |tr2| completed = true end until completed begin @dev_handle.device.context.handle_events rescue ERROR_INTERRUPTED next rescue LIBUSB::Error cancel! until completed @dev_handle.device.context.handle_events end raise end end raise( TransferStatusToError[status] || ERROR_OTHER, "error #{status}") unless status==:TRANSFER_COMPLETED end end class BulkTransfer < Transfer def initialize(args={}) @transfer = Call::Transfer.new Call.libusb_alloc_transfer(0) @transfer[:type] = TRANSFER_TYPE_BULK @transfer[:timeout] = 1000 super end end class ControlTransfer < Transfer def initialize(args={}) @transfer = Call::Transfer.new Call.libusb_alloc_transfer(0) @transfer[:type] = TRANSFER_TYPE_CONTROL @transfer[:timeout] = 1000 super end end class InterruptTransfer < Transfer def initialize(args={}) @transfer = Call::Transfer.new Call.libusb_alloc_transfer(0) @transfer[:type] = TRANSFER_TYPE_INTERRUPT @transfer[:timeout] = 1000 super end end class IsoPacket def initialize(ptr, pkg_nr) @packet = Call::IsoPacketDescriptor.new ptr @pkg_nr = pkg_nr end def status @packet[:status] end def length @packet[:length] end def length=(len) @packet[:length] = len end def actual_length @packet[:actual_length] end end class IsochronousTransfer < Transfer def initialize(num_packets, args={}) @ptr = Call.libusb_alloc_transfer(num_packets) @transfer = Call::Transfer.new @ptr @transfer[:type] = TRANSFER_TYPE_ISOCHRONOUS @transfer[:timeout] = 1000 @transfer[:num_iso_packets] = num_packets super(args) end def num_packets @transfer[:num_iso_packets] end def num_packets=(number) @transfer[:num_iso_packets] = number end def [](nr) IsoPacket.new( @ptr + Call::Transfer.size + nr*Call::IsoPacketDescriptor.size, nr) end # Convenience function to set the length of all packets in an # isochronous transfer, based on {IsochronousTransfer#num_packets}. def packet_lengths=(len) ptr = @ptr + Call::Transfer.size num_packets.times do ptr.write_uint(len) ptr += Call::IsoPacketDescriptor.size end end # The actual_length field of the transfer is meaningless and should not # be examined; instead you must refer to the actual_length field of # each individual packet. private :actual_length, :actual_buffer end class DeviceDescriptor < FFI::Struct include Comparable layout :bLength, :uint8, :bDescriptorType, :uint8, :bcdUSB, :uint16, :bDeviceClass, :uint8, :bDeviceSubClass, :uint8, :bDeviceProtocol, :uint8, :bMaxPacketSize0, :uint8, :idVendor, :uint16, :idProduct, :uint16, :bcdDevice, :uint16, :iManufacturer, :uint8, :iProduct, :uint8, :iSerialNumber, :uint8, :bNumConfigurations, :uint8 end class Configuration < FFI::ManagedStruct include Comparable layout :bLength, :uint8, :bDescriptorType, :uint8, :wTotalLength, :uint16, :bNumInterfaces, :uint8, :bConfigurationValue, :uint8, :iConfiguration, :uint8, :bmAttributes, :uint8, :maxPower, :uint8, :interface, :pointer, :extra, :pointer, :extra_length, :int members.each do |member| define_method(member) do self[member] end end def initialize(device, *args) @device = device super(*args) end def self.release(ptr) Call.libusb_free_config_descriptor(ptr) end # @return [Device] the device this configuration belongs to. attr_reader :device def interfaces ifs = [] self[:bNumInterfaces].times do |i| ifs << Interface.new(self, self[:interface] + i*Interface.size) end return ifs end def inspect attrs = [] attrs << self.bConfigurationValue.to_s bits = self.bmAttributes attrs << "SelfPowered" if (bits & 0b1000000) != 0 attrs << "RemoteWakeup" if (bits & 0b100000) != 0 desc = self.description attrs << desc if desc != '?' "\#<#{self.class} #{attrs.join(' ')}>" end # Return name of the configuration as String. def description return @description if defined? @description @description = device.try_string_descriptor_ascii(self.iConfiguration) end # Return all interface decriptions of the configuration as Array of InterfaceDescriptor s. def settings() self.interfaces.map {|d| d.settings }.flatten end # Return all endpoints of all interfaces of the configuration as Array of EndpointDescriptor s. def endpoints() self.settings.map {|d| d.endpoints }.flatten end def <=>(o) t = device<=>o.device t = bConfigurationValue<=>o.bConfigurationValue if t==0 t end end class Interface < FFI::Struct include Comparable layout :altsetting, :pointer, :num_altsetting, :int def initialize(configuration, *args) @configuration = configuration super(*args) end # @return [Configuration] the configuration this interface belongs to. attr_reader :configuration def alt_settings ifs = [] self[:num_altsetting].times do |i| ifs << Setting.new(self, self[:altsetting] + i*Setting.size) end return ifs end alias settings alt_settings # The Device the Interface belongs to. def device() self.configuration.device end # Return all endpoints of all alternative settings as Array of EndpointDescriptor s. def endpoints() self.alt_settings.map {|d| d.endpoints }.flatten end def <=>(o) configuration<=>o.configuration end end class Setting < FFI::Struct include Comparable layout :bLength, :uint8, :bDescriptorType, :uint8, :bInterfaceNumber, :uint8, :bAlternateSetting, :uint8, :bNumEndpoints, :uint8, :bInterfaceClass, :uint8, :bInterfaceSubClass, :uint8, :bInterfaceProtocol, :uint8, :iInterface, :uint8, :endpoint, :pointer, :extra, :pointer, :extra_length, :int members.each do |member| define_method(member) do self[member] end end def initialize(interface, *args) @interface = interface super(*args) end # @return [Interface] the interface this setting belongs to. attr_reader :interface def endpoints ifs = [] self[:bNumEndpoints].times do |i| ifs << Endpoint.new(self, self[:endpoint] + i*Endpoint.size) end return ifs end def inspect attrs = [] attrs << self.bAlternateSetting.to_s devclass = LIBUSB.dev_string(self.bInterfaceClass, self.bInterfaceSubClass, self.bInterfaceProtocol) attrs << devclass desc = self.description attrs << desc if desc != '?' "\#<#{self.class} #{attrs.join(' ')}>" end # Return name of the Interface as String. def description return @description if defined? @description @description = device.try_string_descriptor_ascii(self.iInterface) end # The Device the InterfaceDescriptor belongs to. def device() self.interface.configuration.device end # The ConfigDescriptor the InterfaceDescriptor belongs to. def configuration() self.interface.configuration end def <=>(o) t = interface<=>o.interface t = bInterfaceNumber<=>o.bInterfaceNumber if t==0 t = bAlternateSetting<=>o.bAlternateSetting if t==0 t end end class Endpoint < FFI::Struct include Comparable layout :bLength, :uint8, :bDescriptorType, :uint8, :bEndpointAddress, :uint8, :bmAttributes, :uint8, :wMaxPacketSize, :uint16, :bInterval, :uint8, :bRefresh, :uint8, :bSynchAddress, :uint8, :extra, :pointer, :extra_length, :int members.each do |member| define_method(member) do self[member] end end def initialize(setting, *args) @setting = setting super(*args) end # @return [Setting] the setting this endpoint belongs to. attr_reader :setting def inspect endpoint_address = self.bEndpointAddress num = endpoint_address & 0b00001111 inout = (endpoint_address & 0b10000000) == 0 ? "OUT" : "IN " bits = self.bmAttributes transfer_type = %w[Control Isochronous Bulk Interrupt][0b11 & bits] type = [transfer_type] if transfer_type == 'Isochronous' synchronization_type = %w[NoSynchronization Asynchronous Adaptive Synchronous][(0b1100 & bits) >> 2] usage_type = %w[Data Feedback ImplicitFeedback ?][(0b110000 & bits) >> 4] type << synchronization_type << usage_type end "\#<#{self.class} #{num} #{inout} #{type.join(" ")}>" end # The Device the EndpointDescriptor belongs to. def device() self.setting.interface.configuration.device end # The ConfigDescriptor the EndpointDescriptor belongs to. def configuration() self.setting.interface.configuration end # The Interface the EndpointDescriptor belongs to. def interface() self.setting.interface end def <=>(o) t = setting<=>o.setting t = bEndpointAddress<=>o.bEndpointAddress if t==0 t end end # Class representing a libusb session. class Context # Initialize libusb context. def initialize m = FFI::MemoryPointer.new :pointer Call.libusb_init(m) @ctx = m.read_pointer end # Deinitialize libusb. # # Should be called after closing all open devices and before your application terminates. def exit Call.libusb_exit(@ctx) end # Set message verbosity. # # * Level 0: no messages ever printed by the library (default) # * Level 1: error messages are printed to stderr # * Level 2: warning and error messages are printed to stderr # * Level 3: informational messages are printed to stdout, warning and # error messages are printed to stderr # # The default level is 0, which means no messages are ever printed. If you # choose to increase the message verbosity level, ensure that your # application does not close the stdout/stderr file descriptors. # # You are advised to set level 3. libusb is conservative with its message # logging and most of the time, will only log messages that explain error # conditions and other oddities. This will help you debug your software. # # If the LIBUSB_DEBUG environment variable was set when libusb was # initialized, this method does nothing: the message verbosity is # fixed to the value in the environment variable. # # If libusb was compiled without any message logging, this method # does nothing: you'll never get any messages. # # If libusb was compiled with verbose debug message logging, this # method does nothing: you'll always get messages from all levels. # # @param [Fixnum] level debug level to set def debug=(level) Call.libusb_set_debug(@ctx, level) end def device_list pppDevs = FFI::MemoryPointer.new :pointer size = Call.libusb_get_device_list(@ctx, pppDevs) ppDevs = pppDevs.read_pointer pDevs = [] size.times do |devi| pDev = ppDevs.get_pointer(devi*FFI.type_size(:pointer)) pDevs << Device.new(self, pDev) end Call.libusb_free_device_list(ppDevs, 1) pDevs end private :device_list # Handle any pending events in blocking mode. # # This method must be called when libusb is running asynchronous transfers. # This gives libusb the opportunity to reap pending transfers, # invoke callbacks, etc. def handle_events res = Call.libusb_handle_events(@ctx) LIBUSB.raise_error res, "in libusb_handle_events" if res<0 end # Obtain a list of devices currently attached to the USB system, optionally matching certain criteria. # # @param [Hash] filter_hash A number of criteria can be defined in key-value pairs. # Only devices that equal all given criterions will be returned. If a criterion is # not specified or its value is +nil+, any device will match that criterion. # The following criteria can be filtered: # * :idVendor, :idProduct (+FixNum+) for matching vendor/product ID, # * :bClass, :bSubClass, :bProtocol (+FixNum+) for the device type - # Devices using CLASS_PER_INTERFACE will match, if any of the interfaces match. # * :bcdUSB, :bcdDevice, :bMaxPacketSize0 (+FixNum+) for the # USB and device release numbers. # Criteria can also specified as Array of several alternative values. # # @example # # Return all devices of vendor 0x0ab1 where idProduct is 3 or 4: # context.device :idVendor=>0x0ab1, :idProduct=>[0x0003, 0x0004] # # @return [Array] def devices(filter_hash={}) device_list.select do |dev| ( !filter_hash[:bClass] || (dev.bDeviceClass==CLASS_PER_INTERFACE ? dev.settings.map(&:bInterfaceClass).&([filter_hash[:bClass]].flatten).any? : [filter_hash[:bClass]].flatten.include?(dev.bDeviceClass))) && ( !filter_hash[:bSubClass] || (dev.bDeviceClass==CLASS_PER_INTERFACE ? dev.settings.map(&:bInterfaceSubClass).&([filter_hash[:bSubClass]].flatten).any? : [filter_hash[:bSubClass]].flatten.include?(dev.bDeviceSubClass))) && ( !filter_hash[:bProtocol] || (dev.bDeviceClass==CLASS_PER_INTERFACE ? dev.settings.map(&:bInterfaceProtocol).&([filter_hash[:bProtocol]].flatten).any? : [filter_hash[:bProtocol]].flatten.include?(dev.bDeviceProtocol))) && ( !filter_hash[:bMaxPacketSize0] || [filter_hash[:bMaxPacketSize0]].flatten.include?(dev.bMaxPacketSize0) ) && ( !filter_hash[:idVendor] || [filter_hash[:idVendor]].flatten.include?(dev.idVendor) ) && ( !filter_hash[:idProduct] || [filter_hash[:idProduct]].flatten.include?(dev.idProduct) ) && ( !filter_hash[:bcdUSB] || [filter_hash[:bcdUSB]].flatten.include?(dev.bcdUSB) ) && ( !filter_hash[:bcdDevice] || [filter_hash[:bcdDevice]].flatten.include?(dev.bcdDevice) ) end end end # Class representing a USB device detected on the system. # # Devices of the system can be obtained with {Context#devices} . class Device include Comparable # @return [Context] the context this device belongs to. attr_reader :context def initialize context, pDev @context = context def pDev.unref_device(id) Call.libusb_unref_device(self) end ObjectSpace.define_finalizer(self, pDev.method(:unref_device)) Call.libusb_ref_device(pDev) @pDev = pDev @pDevDesc = DeviceDescriptor.new res = Call.libusb_get_device_descriptor(@pDev, @pDevDesc) LIBUSB.raise_error res, "in libusb_get_device_descriptor" if res!=0 end # Open the device and obtain a device handle. # # A handle allows you to perform I/O on the device in question. # This is a non-blocking function; no requests are sent over the bus. # # If called with a block, the handle is passed to the block # and is closed when the block has finished. # # You need proper access permissions on: # * Linux: /dev/bus/usb// # # @return [DevHandle] Handle to the device. def open ppHandle = FFI::MemoryPointer.new :pointer res = Call.libusb_open(@pDev, ppHandle) LIBUSB.raise_error res, "in libusb_open" if res!=0 handle = DevHandle.new self, ppHandle.read_pointer return handle unless block_given? begin yield handle ensure handle.close end end # Open the device and claim an interface. # # This is a convenience method to {Device#open} and {DevHandle#claim_interface}. # Must be called with a block. When the block has finished, the interface # will be released and the device will be closed. # # @param [Interface, Fixnum] interface the interface or it's bInterfaceNumber you wish to claim def open_interface(interface) open do |dev| dev.claim_interface(interface) do yield dev end end end # Get the number of the bus that a device is connected to. def bus_number Call.libusb_get_bus_number(@pDev) end # Get the address of the device on the bus it is connected to. def device_address Call.libusb_get_device_address(@pDev) end # Convenience function to retrieve the wMaxPacketSize value for a # particular endpoint in the active device configuration. # # @param [Endpoint, Fixnum] endpoint (address of) the endpoint in question # @return [Fixnum] the wMaxPacketSize value def max_packet_size(endpoint) endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress res = Call.libusb_get_max_packet_size(@pDev, endpoint) LIBUSB.raise_error res, "in libusb_get_max_packet_size" unless res>=0 res end # Calculate the maximum packet size which a specific endpoint is capable is # sending or receiving in the duration of 1 microframe. # # Only the active configution is examined. The calculation is based on the # wMaxPacketSize field in the endpoint descriptor as described in section 9.6.6 # in the USB 2.0 specifications. # # If acting on an isochronous or interrupt endpoint, this function will # multiply the value found in bits 0:10 by the number of transactions per # microframe (determined by bits 11:12). Otherwise, this function just returns # the numeric value found in bits 0:10. # # This function is useful for setting up isochronous transfers, for example # you might use the return value from this function to call # IsoPacket#alloc_buffer in order to set the length field # of an isochronous packet in a transfer. # # @param [Endpoint, Fixnum] endpoint (address of) the endpoint in question # @return [Fixnum] the maximum packet size which can be sent/received on this endpoint def max_iso_packet_size(endpoint) endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress res = Call.libusb_get_max_iso_packet_size(@pDev, endpoint) LIBUSB.raise_error res, "in libusb_get_max_iso_packet_size" unless res>=0 res end # Obtain a config descriptor of the device. # # @param [Fixnum] index number of the config descriptor # @return Configuration def config_descriptor(index) ppConfig = FFI::MemoryPointer.new :pointer res = Call.libusb_get_config_descriptor(@pDev, index, ppConfig) LIBUSB.raise_error res, "in libusb_get_config_descriptor" if res!=0 pConfig = ppConfig.read_pointer config = Configuration.new(self, pConfig) config end # allow access to Descriptor members on Device DeviceDescriptor.members.each do |member| define_method(member) do @pDevDesc[member] end end def inspect attrs = [] attrs << "#{self.bus_number}/#{self.device_address}" attrs << ("%04x:%04x" % [self.idVendor, self.idProduct]) attrs << self.manufacturer attrs << self.product attrs << self.serial_number if self.bDeviceClass == LIBUSB::CLASS_PER_INTERFACE devclass = self.settings.map {|i| LIBUSB.dev_string(i.bInterfaceClass, i.bInterfaceSubClass, i.bInterfaceProtocol) }.join(", ") else devclass = LIBUSB.dev_string(self.bDeviceClass, self.bDeviceSubClass, self.bDeviceProtocol) end attrs << "(#{devclass})" attrs.compact! "\#<#{self.class} #{attrs.join(' ')}>" end def try_string_descriptor_ascii(i) begin open{|h| h.string_descriptor_ascii(i) } rescue "?" end end # Return manufacturer of the device # @return String def manufacturer return @manufacturer if defined? @manufacturer @manufacturer = try_string_descriptor_ascii(self.iManufacturer) @manufacturer.strip! if @manufacturer @manufacturer end # Return product name of the device. # @return String def product return @product if defined? @product @product = try_string_descriptor_ascii(self.iProduct) @product.strip! if @product @product end # Return serial number of the device. # @return String def serial_number return @serial_number if defined? @serial_number @serial_number = try_string_descriptor_ascii(self.iSerialNumber) @serial_number.strip! if @serial_number @serial_number end # Return configurations of the device. # @return [Array] def configurations configs = [] bNumConfigurations.times do |config_index| begin configs << config_descriptor(config_index) rescue RuntimeError # On Windows some devices don't return it's configuration. end end configs end # Return all interfaces of the device. # @return [Array] def interfaces() self.configurations.map {|d| d.interfaces }.flatten end # Return all interface decriptions of the device. # @return [Array] def settings() self.interfaces.map {|d| d.settings }.flatten end # Return all endpoints of all interfaces of the device. # @return [Array] def endpoints() self.settings.map {|d| d.endpoints }.flatten end def <=>(o) t = bus_number<=>o.bus_number t = device_address<=>o.device_address if t==0 t end end # Class representing a handle on a USB device. # # A device handle is used to perform I/O and other operations. When finished # with a device handle, you should call DevHandle#close . class DevHandle # @private attr_reader :pHandle # @return [Device] the device this handle belongs to. attr_reader :device def initialize device, pHandle @device = device @pHandle = pHandle @bulk_transfer = @control_transfer = @interrupt_transfer = nil end # Close a device handle. # # Should be called on all open handles before your application exits. # # Internally, this function destroys the reference that was added by {Device#open} # on the given device. # # This is a non-blocking function; no requests are sent over the bus. def close Call.libusb_close(@pHandle) end def string_descriptor_ascii(index) pString = FFI::MemoryPointer.new 0x100 res = Call.libusb_get_string_descriptor_ascii(@pHandle, index, pString, pString.size) LIBUSB.raise_error res, "in libusb_get_string_descriptor_ascii" unless res>=0 pString.read_string(res) end # Claim an interface on a given device handle. # # You must claim the interface you wish to use before you can perform I/O on any # of its endpoints. # # It is legal to attempt to claim an already-claimed interface, in which case # libusb just returns without doing anything. # # Claiming of interfaces is a purely logical operation; it does not cause any # requests to be sent over the bus. Interface claiming is used to instruct the # underlying operating system that your application wishes to take ownership of # the interface. # # This is a non-blocking function. # # If called with a block, the device handle is passed through to the block # and the interface is released when the block has finished. # # @param [Interface, Fixnum] interface the interface or it's bInterfaceNumber you wish to claim def claim_interface(interface) interface = interface.bInterfaceNumber if interface.respond_to? :bInterfaceNumber res = Call.libusb_claim_interface(@pHandle, interface) LIBUSB.raise_error res, "in libusb_claim_interface" if res!=0 return self unless block_given? begin yield self ensure release_interface(interface) end end # Release an interface previously claimed with {DevHandle#claim_interface}. # # You should release all claimed interfaces before closing a device handle. # # This is a blocking function. A SET_INTERFACE control request will be sent to the # device, resetting interface state to the first alternate setting. # # @param [Interface, Fixnum] interface the interface or it's bInterfaceNumber you # claimed previously def release_interface(interface) interface = interface.bInterfaceNumber if interface.respond_to? :bInterfaceNumber res = Call.libusb_release_interface(@pHandle, interface) LIBUSB.raise_error res, "in libusb_release_interface" if res!=0 end # Set the active configuration for a device. # # The operating system may or may not have already set an active configuration on # the device. It is up to your application to ensure the correct configuration is # selected before you attempt to claim interfaces and perform other operations. # # If you call this function on a device already configured with the selected # configuration, then this function will act as a lightweight device reset: it # will issue a SET_CONFIGURATION request using the current configuration, causing # most USB-related device state to be reset (altsetting reset to zero, endpoint # halts cleared, toggles reset). # # You cannot change/reset configuration if your application has claimed interfaces - # you should free them with {DevHandle#release_interface} first. You cannot # change/reset configuration if other applications or drivers have claimed # interfaces. # # A configuration value of +nil+ will put the device in unconfigured state. The USB # specifications state that a configuration value of 0 does this, however buggy # devices exist which actually have a configuration 0. # # You should always use this function rather than formulating your own # SET_CONFIGURATION control request. This is because the underlying operating # system needs to know when such changes happen. # # This is a blocking function. # # @param [Configuration, Fixnum] configuration the configuration or it's # bConfigurationValue you wish to activate, or +nil+ if you wish to put # the device in unconfigured state def set_configuration(configuration) configuration = configuration.bConfigurationValue if configuration.respond_to? :bConfigurationValue res = Call.libusb_set_configuration(@pHandle, configuration || -1) LIBUSB.raise_error res, "in libusb_set_configuration" if res!=0 end alias configuration= set_configuration # Activate an alternate setting for an interface. # # The interface must have been previously claimed with {DevHandle#claim_interface}. # # You should always use this function rather than formulating your own # SET_INTERFACE control request. This is because the underlying operating system # needs to know when such changes happen. # # This is a blocking function. # # @param [Setting, Fixnum] setting_or_interface_number the alternate setting # to activate or the bInterfaceNumber of the previously-claimed interface # @param [Fixnum, nil] alternate_setting the bAlternateSetting of the alternate setting to activate # (only if first param is a Fixnum) def set_interface_alt_setting(setting_or_interface_number, alternate_setting=nil) alternate_setting ||= setting_or_interface_number.bAlternateSetting if setting_or_interface_number.respond_to? :bAlternateSetting setting_or_interface_number = setting_or_interface_number.bInterfaceNumber if setting_or_interface_number.respond_to? :bInterfaceNumber res = Call.libusb_set_interface_alt_setting(@pHandle, setting_or_interface_number, alternate_setting) LIBUSB.raise_error res, "in libusb_set_interface_alt_setting" if res!=0 end # Clear the halt/stall condition for an endpoint. # # Endpoints with halt status are unable to receive or transmit # data until the halt condition is stalled. # # You should cancel all pending transfers before attempting to # clear the halt condition. # # This is a blocking function. # # @param [Endpoint, Fixnum] endpoint the endpoint in question or it's bEndpointAddress def clear_halt(endpoint) endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress res = Call.libusb_clear_halt(@pHandle, endpoint) LIBUSB.raise_error res, "in libusb_clear_halt" if res!=0 end # Perform a USB port reset to reinitialize a device. # # The system will attempt to restore the previous configuration and # alternate settings after the reset has completed. # # If the reset fails, the descriptors change, or the previous # state cannot be restored, the device will appear to be disconnected # and reconnected. This means that the device handle is no longer # valid (you should close it) and rediscover the device. A Exception # of LIBUSB::ERROR_NOT_FOUND indicates when this is the case. # # This is a blocking function which usually incurs a noticeable delay. def reset_device res = Call.libusb_reset_device(@pHandle) LIBUSB.raise_error res, "in libusb_reset_device" if res!=0 end # Determine if a kernel driver is active on an interface. # # If a kernel driver is active, you cannot claim the interface, # and libusb will be unable to perform I/O. # # @param [Interface, Fixnum] interface the interface to check or it's bInterfaceNumber # @return [Boolean] def kernel_driver_active?(interface) interface = interface.bInterfaceNumber if interface.respond_to? :bInterfaceNumber res = Call.libusb_kernel_driver_active(@pHandle, interface) LIBUSB.raise_error res, "in libusb_kernel_driver_active" unless res>=0 return res==1 end # Detach a kernel driver from an interface. # # If successful, you will then be able to claim the interface and perform I/O. # # @param [Interface, Fixnum] interface the interface to detach the driver # from or it's bInterfaceNumber def detach_kernel_driver(interface) interface = interface.bInterfaceNumber if interface.respond_to? :bInterfaceNumber res = Call.libusb_detach_kernel_driver(@pHandle, interface) LIBUSB.raise_error res, "in libusb_detach_kernel_driver" if res!=0 end # Re-attach an interface's kernel driver, which was previously detached # using {DevHandle#detach_kernel_driver}. # # @param [Interface, Fixnum] interface the interface to attach the driver to def attach_kernel_driver(interface) interface = interface.bInterfaceNumber if interface.respond_to? :bInterfaceNumber res = Call.libusb_attach_kernel_driver(@pHandle, interface) LIBUSB.raise_error res, "in libusb_attach_kernel_driver" if res!=0 end # Perform a USB bulk transfer. # # The direction of the transfer is inferred from the direction bits of the # endpoint address. # # For bulk reads, the +:dataIn+ param indicates the maximum length of data you are # expecting to receive. If less data arrives than expected, this function will # return that data. # # You should also check the returned number of bytes for bulk writes. Not all of the # data may have been written. # # Also check transferred bytes when dealing with a timeout error code. libusb may have # to split your transfer into a number of chunks to satisfy underlying O/S # requirements, meaning that the timeout may expire after the first few chunks # have completed. libusb is careful not to lose any data that may have been # transferred; do not assume that timeout conditions indicate a complete lack of # I/O. # # @param [Endpoint, Fixnum] :endpoint the (address of a) valid endpoint to communicate with # @param [String] :dataOut the data to send with an outgoing transfer # @param [Fixnum] :dataIn the number of bytes expected to receive with an ingoing transfer # @param [Fixnum] :timeout timeout (in millseconds) that this function should wait before giving # up due to no response being received. For an unlimited timeout, use value 0. Defaults to 1000 ms. # # @return [Fixnum] Number of bytes sent for an outgoing transfer # @return [String] Received data for an ingoing transfer def bulk_transfer(args={}) timeout = args.delete(:timeout) || 1000 endpoint = args.delete(:endpoint) || raise(ArgumentError, "no endpoint given") endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress if endpoint&ENDPOINT_IN != 0 dataIn = args.delete(:dataIn) || raise(ArgumentError, "no :dataIn given for bulk read") else dataOut = args.delete(:dataOut) || raise(ArgumentError, "no :dataOut given for bulk write") end raise ArgumentError, "invalid params #{args.inspect}" unless args.empty? # reuse transfer struct to speed up transfer @bulk_transfer ||= BulkTransfer.new :dev_handle => self tr = @bulk_transfer tr.endpoint = endpoint tr.timeout = timeout if dataOut tr.buffer = dataOut else tr.alloc_buffer(dataIn) end tr.submit_and_wait! if dataOut tr.actual_length else tr.actual_buffer end end # Perform a USB interrupt transfer. # # The direction of the transfer is inferred from the direction bits of the # endpoint address. # # For interrupt reads, the +:dataIn+ param indicates the maximum length of data you # are expecting to receive. If less data arrives than expected, this function will # return that data. # # You should also check the returned number of bytes for interrupt writes. Not all of # the data may have been written. # # Also check transferred when dealing with a timeout error code. libusb may have # to split your transfer into a number of chunks to satisfy underlying O/S # requirements, meaning that the timeout may expire after the first few chunks # have completed. libusb is careful not to lose any data that may have been # transferred; do not assume that timeout conditions indicate a complete lack of # I/O. # # The default endpoint bInterval value is used as the polling interval. # # @param [Endpoint, Fixnum] :endpoint the (address of a) valid endpoint to communicate with # @param [String] :dataOut the data to send with an outgoing transfer # @param [Fixnum] :dataIn the number of bytes expected to receive with an ingoing transfer # @param [Fixnum] :timeout timeout (in millseconds) that this function should wait before giving # up due to no response being received. For an unlimited timeout, use value 0. Defaults to 1000 ms. # # @return [Fixnum] Number of bytes sent for an outgoing transfer # @return [String] Received data for an ingoing transfer def interrupt_transfer(args={}) timeout = args.delete(:timeout) || 1000 endpoint = args.delete(:endpoint) || raise(ArgumentError, "no endpoint given") endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress if endpoint&ENDPOINT_IN != 0 dataIn = args.delete(:dataIn) || raise(ArgumentError, "no :dataIn given for interrupt read") else dataOut = args.delete(:dataOut) || raise(ArgumentError, "no :dataOut given for interrupt write") end raise ArgumentError, "invalid params #{args.inspect}" unless args.empty? # reuse transfer struct to speed up transfer @interrupt_transfer ||= InterruptTransfer.new :dev_handle => self tr = @interrupt_transfer tr.endpoint = endpoint tr.timeout = timeout if dataOut tr.buffer = dataOut else tr.alloc_buffer(dataIn) end tr.submit_and_wait! if dataOut tr.actual_length else tr.actual_buffer end end # Perform a USB control transfer. # # The direction of the transfer is inferred from the +:bmRequestType+ field of the # setup packet. # # @param [Fixnum] :bmRequestType the request type field for the setup packet # @param [Fixnum] :bRequest the request field for the setup packet # @param [Fixnum] :wValue the value field for the setup packet # @param [Fixnum] :wIndex the index field for the setup packet # @param [String] :dataOut the data to send with an outgoing transfer, it # is appended to the setup packet # @param [Fixnum] :dataIn the number of bytes expected to receive with an ingoing transfer # (excluding setup packet) # @param [Fixnum] :timeout timeout (in millseconds) that this function should wait before giving # up due to no response being received. For an unlimited timeout, use value 0. Defaults to 1000 ms. # # @return [Fixnum] Number of bytes sent (excluding setup packet) for outgoing transfer # @return [String] Received data (without setup packet) for ingoing transfer def control_transfer(args={}) bmRequestType = args.delete(:bmRequestType) || raise(ArgumentError, "param :bmRequestType not given") bRequest = args.delete(:bRequest) || raise(ArgumentError, "param :bRequest not given") wValue = args.delete(:wValue) || raise(ArgumentError, "param :wValue not given") wIndex = args.delete(:wIndex) || raise(ArgumentError, "param :wIndex not given") timeout = args.delete(:timeout) || 1000 if bmRequestType&ENDPOINT_IN != 0 dataIn = args.delete(:dataIn) || 0 dataOut = '' else dataOut = args.delete(:dataOut) || '' end raise ArgumentError, "invalid params #{args.inspect}" unless args.empty? # reuse transfer struct to speed up transfer @control_transfer ||= ControlTransfer.new :dev_handle => self tr = @control_transfer tr.timeout = timeout if dataIn setup_data = [bmRequestType, bRequest, wValue, wIndex, dataIn].pack('CCvvv') tr.alloc_buffer( dataIn + CONTROL_SETUP_SIZE, setup_data ) else tr.buffer = [bmRequestType, bRequest, wValue, wIndex, dataOut.bytesize, dataOut].pack('CCvvva*') end tr.submit_and_wait! if dataIn tr.actual_buffer(CONTROL_SETUP_SIZE) else tr.actual_length end end end end