class HIDAPI::Device
This class is the interface to a HID device.
Each instance can connect to a single interface on an HID device. If you have more than one interface, you will need to have more than one instance of this class to work with all of them.
When open, the device is polled continuously for incoming data. It will build up a cache of up to 32 packets. If you are not reading from the device, it will silently discard the oldest packets and continue storing the newest packets.
The read method can block. This is controlled by the blocking
attribute. The default value is true. If you want the read method to be non-blocking, set this attribute to false.
Attributes
Gets or sets the blocking nature for read
.
Defaults to true
. Set to false
to have read
be non-blocking.
Gets the device handle for I/O.
Gets the input endpoint.
Gets the maximum packet size for input packets.
Gets the interface this HID device uses on the USB device.
Gets the output endpoint.
Gets the path for this device that can be used by HIDAPI::Engine#get_device_by_path
Gets the USB device this HID device uses.
Public Class Methods
Generates a path for a device.
# File lib/hidapi/device.rb, line 416 def self.make_path(usb_dev, interface = 0) if usb_dev.is_a?(Hash) bus = usb_dev[:bus] || usb_dev['bus'] address = usb_dev[:device_address] || usb_dev['device_address'] else bus = usb_dev.bus_number address = usb_dev.device_address end "#{bus.to_hex(4)}:#{address.to_hex(4)}:#{interface.to_hex(2)}" end
Initializes an HID device.
# File lib/hidapi/device.rb, line 94 def initialize(usb_device, interface = 0) raise HIDAPI::InvalidDevice, "invalid object (#{usb_device.class.name})" unless usb_device.is_a?(LIBUSB::Device) self.usb_device = usb_device self.blocking = true self.mutex = Mutex.new self.interface = interface self.path = HIDAPI::Device.make_path(usb_device, interface) self.input_endpoint = self.output_endpoint = nil self.thread = nil self.thread_initialized = false self.input_reports = [] self.shutdown_thread = false self.transfer_cancelled = LIBUSB::Context::CompletionFlag.new self.open_count = 0 self.class.init_hook.each do |proc| proc.call self end end
Validates a device path.
# File lib/hidapi/device.rb, line 429 def self.validate_path(path) match = /(?<BUS>\d+):(?<ADDR>\d+):(?<IFACE>\d+)/.match(path) return nil unless match make_path( { bus: match['BUS'].to_i(16), device_address: match['ADDR'].to_i(16) }, match['IFACE'].to_i(16) ) end
Protected Class Methods
Defines a hook to execute when a device is initialized.
Yields the device instance.
# File lib/hidapi/device.rb, line 505 def self.init_hook(proc = nil, &block) @init_hook ||= [] proc = block if proc.nil? && block_given? if proc if proc.is_a?(Symbol) || proc.is_a?(String) proc_name = proc proc = Proc.new do |dev| dev.send(proc_name, dev) end end @init_hook << proc end @init_hook end
Defines a hook to execute when data is read from the device.
This can be provided as a proc, symbol, or simply as a block.
The proc should return a true value if it consumes the data. If it does not consume the data it must return false or nil.
If no read_hook
proc consumes the data, it will be cached for future calls to read
or read_timeout
.
The read hook is called from within the read thread. If it must access resources from another thread, you will want to use a mutex for locking.
read_hook do |device, input_report| ... true end
# File lib/hidapi/device.rb, line 484 def self.read_hook(proc = nil, &block) @read_hook ||= [] proc = block if proc.nil? && block_given? if proc if proc.is_a?(Symbol) || proc.is_a?(String) proc_name = proc proc = Proc.new do |dev, input_report| dev.send(proc_name, dev, input_report) end end @read_hook << proc end @read_hook end
Public Instance Methods
Is this device in blocking mode (for reading)?
# File lib/hidapi/device.rb, line 364 def blocking? !!blocking end
Closes the device (if open).
Returns the device.
# File lib/hidapi/device.rb, line 158 def close self.open_count = open_count - 1 if open_count <= 0 HIDAPI.debug("open_count for device #{path} is #{open_count}") if open_count < 0 if handle begin self.shutdown_thread = true transfer.cancel! rescue nil if transfer thread.join rescue =>e HIDAPI.debug "failed to kill read thread on device #{path}: #{e.inspect}" end begin handle.release_interface(interface) rescue =>e HIDAPI.debug "failed to release interface on device #{path}: #{e.inspect}" end begin handle.close rescue =>e HIDAPI.debug "failed to close device #{path}: #{e.inspect}" end HIDAPI.debug "closed device #{path}" end self.handle = nil mutex.synchronize { self.input_reports = [] } self.open_count = 0 end self end
Gets a feature report from the device.
# File lib/hidapi/device.rb, line 389 def get_feature_report(report_number, buffer_size = nil) buffer_size ||= input_ep_max_packet_size handle.control_transfer( bmRequestType: LIBUSB::REQUEST_TYPE_CLASS | LIBUSB::RECIPIENT_INTERFACE | LIBUSB::ENDPOINT_IN, bRequest: 0x01, # HID Get_Report wValue: (3 << 8) | report_number, wIndex: interface, dataIn: buffer_size ) end
Gets the manufacturer of the device.
# File lib/hidapi/device.rb, line 120 def manufacturer @manufacturer ||= read_string(usb_device.iManufacturer, "VENDOR(0x#{vendor_id.to_hex(4)})").strip end
Opens the device.
Returns the device.
# File lib/hidapi/device.rb, line 193 def open if open? self.open_count = open_count + 1 if open_count < 1 HIDAPI.debug "open_count for open device #{path} is #{open_count}" self.open_count = 1 end return self end self.open_count = 0 begin self.handle = usb_device.open raise 'no handle returned' unless handle begin if handle.kernel_driver_active?(interface) handle.detach_kernel_driver(interface) end rescue LIBUSB::ERROR_NOT_SUPPORTED HIDAPI.debug 'cannot determine kernel driver status, continuing to open device' end handle.claim_interface(interface) self.input_endpoint = self.output_endpoint = nil # now we need to find the endpoints. usb_device.settings .keep_if {|item| item.bInterfaceNumber == interface} .each do |intf_desc| intf_desc.endpoints.each do |ep| if ep.transfer_type == :interrupt if input_endpoint.nil? && ep.direction == :in self.input_endpoint = ep.bEndpointAddress self.input_ep_max_packet_size = ep.wMaxPacketSize end if output_endpoint.nil? && ep.direction == :out self.output_endpoint = ep.bEndpointAddress end end break if input_endpoint && output_endpoint end end # output_ep is optional, input_ep is required raise 'failed to locate input endpoint' unless input_endpoint # start the read thread self.input_reports = [] self.thread_initialized = false self.shutdown_thread = false self.thread = Thread.start(self) { |dev| dev.send(:execute_read_thread) } sleep 0 until thread_initialized rescue =>e handle.close rescue nil self.handle = nil HIDAPI.debug "failed to open device #{path}: #{e.inspect}" raise DeviceOpenFailed, e.inspect end HIDAPI.debug "opened device #{path}" self.open_count = 1 self end
Is the device currently open?
# File lib/hidapi/device.rb, line 150 def open? !!handle end
Gets the product/model of the device.
# File lib/hidapi/device.rb, line 126 def product @product ||= read_string(usb_device.iProduct, "PRODUCT(0x#{product_id.to_hex(4)})").strip end
Gets the product ID.
# File lib/hidapi/device.rb, line 144 def product_id @product_id ||= usb_device.idProduct end
Reads the next report from the device.
In blocking mode, it will wait for a report. In non-blocking mode, it will return immediately with an empty string if there is no report.
Returns nil on error.
# File lib/hidapi/device.rb, line 358 def read read_timeout blocking? ? -1 : 0 end
Reads a string descriptor from the USB device.
# File lib/hidapi/device.rb, line 444 def read_string(index, on_failure = '') begin # does not require an interface, so open from the usb_dev instead of using our open method. data = if open? handle.string_descriptor_ascii(index) else usb_device.open { |handle| handle.string_descriptor_ascii(index) } end HIDAPI.debug("read string at index #{index} for device #{path}: #{data.inspect}") data rescue =>e HIDAPI.debug("failed to read string at index #{index} for device #{path}: #{e.inspect}") on_failure || '' end end
Attempts to read from the device, waiting up to milliseconds
before returning.
If milliseconds is less than 1, it will wait forever. If milliseconds is 0, then it will return immediately.
Returns the next report on success. If no report is available and it is not waiting forever, it will return an empty string.
Returns nil on error.
# File lib/hidapi/device.rb, line 297 def read_timeout(milliseconds) raise DeviceNotOpen unless open? mutex.synchronize do if input_reports.count > 0 data = input_reports.delete_at(0) HIDAPI.debug "read data from device #{path}: #{data.inspect}" return data end if shutdown_thread HIDAPI.debug "read thread for device #{path} is not running" return nil end end # no data to return, do not block. return '' if milliseconds == 0 if milliseconds < 0 # wait forever (as long as the read thread doesn't die) until shutdown_thread mutex.synchronize do if input_reports.count > 0 data = input_reports.delete_at(0) HIDAPI.debug "read data from device #{path}: #{data.inspect}" return data end end sleep 0 end # error, return nil HIDAPI.debug "read thread ended while waiting on device #{path}" nil else # wait up to so many milliseconds for input. stop_at = Time.now + (milliseconds * 0.001) while Time.now < stop_at mutex.synchronize do if input_reports.count > 0 data = input_reports.delete_at(0) HIDAPI.debug "read data from device #{path}: #{data.inspect}" return data end end sleep 0 end # no input, return empty. '' end end
Sends a feature report to the device.
# File lib/hidapi/device.rb, line 370 def send_feature_report(data) raise ArgumentError, 'data must not be blank' if data.nil? || data.length < 1 raise HIDAPI::DeviceNotOpen unless open? data, report_number, skipped_report_id = clean_output_data(data) handle.control_transfer( bmRequestType: LIBUSB::REQUEST_TYPE_CLASS | LIBUSB::RECIPIENT_INTERFACE | LIBUSB::ENDPOINT_OUT, bRequest: 0x09, # HID Set_Report wValue: (3 << 8) | report_number, # HID feature = 3 wIndex: interface, dataOut: data ) data.length + (skipped_report_id ? 1 : 0) end
Gets the serial number of the device.
# File lib/hidapi/device.rb, line 132 def serial_number @serial_number ||= read_string(usb_device.iSerialNumber, '?').strip end
Gets the vendor ID.
# File lib/hidapi/device.rb, line 138 def vendor_id @vendor_id ||= usb_device.idVendor end
Writes data to the device.
The data to be written can be individual byte values, an array of byte values, or a string packed with data.
# File lib/hidapi/device.rb, line 262 def write(*data) raise ArgumentError, 'data must not be blank' if data.nil? || data.length < 1 raise HIDAPI::DeviceNotOpen unless open? data, report_number, skipped_report_id = clean_output_data(data) if output_endpoint.nil? # No interrupt out endpoint, use the control endpoint. handle.control_transfer( bmRequestType: LIBUSB::REQUEST_TYPE_CLASS | LIBUSB::RECIPIENT_INTERFACE | LIBUSB::ENDPOINT_OUT, bRequest: 0x09, # HID Set_Report wValue: (2 << 8) | report_number, # HID output = 2 wIndex: interface, dataOut: data ) data.length + (skipped_report_id ? 1 : 0) else # Use the interrupt out endpoint. handle.interrupt_transfer( endpoint: output_endpoint, dataOut: data ) end end
Private Instance Methods
# File lib/hidapi/device.rb, line 524 def clean_output_data(data) if data.length == 1 && data.first.is_a?(Array) data = data.first end if data.length == 1 && data.first.is_a?(String) data = data.first end data = data.pack('C*') unless data.is_a?(String) skipped_report_id = false report_number = data.getbyte(0) if report_number == 0x00 data = data[1..-1].to_s skipped_report_id = true end [ data, report_number, skipped_report_id ] end
# File lib/hidapi/device.rb, line 546 def execute_read_thread begin # make it available locally, prevent changes while we are running. length = input_ep_max_packet_size context = usb_device.context # Construct our transfer. self.transfer = LIBUSB::InterruptTransfer.new( dev_handle: handle, endpoint: input_endpoint, callback: method(:read_callback), timeout: 30000 ) transfer.alloc_buffer length # clear flag for transfer cancellation. transfer_cancelled.completed = false # perform the initial submission, the callback will resubmit. transfer.submit! rescue =>e HIDAPI.debug "failed to initialize read thread for device #{path}: #{e.inspect}" self.shutdown_thread = true raise e ensure # tell the main thread that we are running. self.thread_initialized = true end # wait for the main thread to kill this thread. until shutdown_thread begin context.handle_events 0 sleep 0 rescue LIBUSB::ERROR_BUSY, LIBUSB::ERROR_TIMEOUT, LIBUSB::ERROR_OVERFLOW, LIBUSB::ERROR_INTERRUPTED => e # non fatal errors. HIDAPI.debug "non-fatal error for read_thread on device #{path}: #{e.inspect}" rescue => e HIDAPI.debug "fatal error for read_thread on device #{path}: #{e.inspect}" self.shutdown_thread = true raise e end end # no longer running. self.thread_initialized = false # cancel any transfers that may be pending. transfer.cancel! rescue nil # wait for the cancellation to complete. until transfer_cancelled.completed? context.handle_events 0, transfer_cancelled end end
# File lib/hidapi/device.rb, line 604 def read_callback(tr) if tr.status == :TRANSFER_COMPLETED data = tr.actual_buffer consumed = false self.class.read_hook.each do |proc| consumed = begin proc.call(self, data) rescue =>e HIDAPI.debug "read_hook failed for device #{path}: #{e.inspect}" false end break if consumed end unless consumed mutex.synchronize do input_reports << tr.actual_buffer input_reports.delete_at(0) while input_reports.length > 32 end end elsif tr.status == :TRANSFER_CANCELLED mutex.synchronize do self.shutdown_thread = true transfer_cancelled.completed = true end HIDAPI.debug "read transfer cancelled for device #{path}" elsif tr.status == :TRANSFER_NO_DEVICE mutex.synchronize do self.shutdown_thread = true transfer_cancelled.completed = true end HIDAPI.debug "read transfer failed with no device for device #{path}" elsif tr.status == :TRANSFER_TIMED_OUT # ignore timeouts, they are normal else HIDAPI.debug "read transfer with unknown transfer code (#{tr.status}) for device #{path}" end # resubmit the transfer object. begin tr.submit! rescue =>e HIDAPI.debug "failed to resubmit transfer for device #{path}: #{e.inspect}" mutex.synchronize do self.shutdown_thread = true transfer_cancelled.completed = true end end end