Class: HIDAPI::Device

Inherits:
Object
  • Object
show all
Defined in:
lib/hidapi/device.rb

Overview

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.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(usb_device, interface = 0) ⇒ Device

Initializes an HID device.


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# 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

Instance Attribute Details

#blockingObject

Gets or sets the blocking nature for read.

Defaults to true. Set to false to have read be non-blocking.


60
61
62
# File 'lib/hidapi/device.rb', line 60

def blocking
  @blocking
end

#interfaceObject

Gets the interface this HID device uses on the USB device.


53
54
55
# File 'lib/hidapi/device.rb', line 53

def interface
  @interface
end

#pathObject

Gets the path for this device that can be used by HIDAPI::Engine#get_device_by_path


85
86
87
# File 'lib/hidapi/device.rb', line 85

def path
  @path
end

#usb_deviceObject

Gets the USB device this HID device uses.


24
25
26
# File 'lib/hidapi/device.rb', line 24

def usb_device
  @usb_device
end

Class Method Details

.make_path(usb_dev, interface = 0) ⇒ Object

Generates a path for a device.


416
417
418
419
420
421
422
423
424
425
# 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

.validate_path(path) ⇒ Object

Validates a device path.


429
430
431
432
433
434
435
436
437
438
439
# 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

Instance Method Details

#blocking?Boolean

Is this device in blocking mode (for reading)?


364
365
366
# File 'lib/hidapi/device.rb', line 364

def blocking?
  !!blocking
end

#closeObject

Closes the device (if open).

Returns the device.


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# 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

#get_feature_report(report_number, buffer_size = nil) ⇒ Object

Gets a feature report from the device.


389
390
391
392
393
394
395
396
397
398
399
400
401
# 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

#inspectObject

:nodoc:


404
405
406
# File 'lib/hidapi/device.rb', line 404

def inspect   # :nodoc:
  "#<#{self.class.name}:0x#{self.object_id.to_hex(16)} #{vendor_id.to_hex(4)}:#{product_id.to_hex(4)} #{manufacturer} #{product} #{serial_number} (#{open? ? 'OPEN' : 'CLOSED'})>"
end

#manufacturerObject

Gets the manufacturer of the device.


120
121
122
# File 'lib/hidapi/device.rb', line 120

def manufacturer
  @manufacturer ||= read_string(usb_device.iManufacturer, "VENDOR(0x#{vendor_id.to_hex(4)})").strip
end

#openObject

Opens the device.

Returns the device.


193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# 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

#open?Boolean

Is the device currently open?


150
151
152
# File 'lib/hidapi/device.rb', line 150

def open?
  !!handle
end

#productObject

Gets the product/model of the device.


126
127
128
# File 'lib/hidapi/device.rb', line 126

def product
  @product ||= read_string(usb_device.iProduct, "PRODUCT(0x#{product_id.to_hex(4)})").strip
end

#product_idObject

Gets the product ID.


144
145
146
# File 'lib/hidapi/device.rb', line 144

def product_id
  @product_id ||= usb_device.idProduct
end

#readObject

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.


358
359
360
# File 'lib/hidapi/device.rb', line 358

def read
  read_timeout blocking? ? -1 : 0
end

#read_string(index, on_failure = '') ⇒ Object

Reads a string descriptor from the USB device.


444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# 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

#read_timeout(milliseconds) ⇒ Object

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.

Raises:


297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# 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

#send_feature_report(data) ⇒ Object

Sends a feature report to the device.

Raises:

  • (ArgumentError)

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# 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

#serial_numberObject

Gets the serial number of the device.


132
133
134
# File 'lib/hidapi/device.rb', line 132

def serial_number
  @serial_number ||= read_string(usb_device.iSerialNumber, '?').strip
end

#to_sObject

:nodoc:


409
410
411
# File 'lib/hidapi/device.rb', line 409

def to_s      # :nodoc:
  "#{manufacturer} #{product} (#{serial_number})"
end

#vendor_idObject

Gets the vendor ID.


138
139
140
# File 'lib/hidapi/device.rb', line 138

def vendor_id
  @vendor_id ||= usb_device.idVendor
end

#write(*data) ⇒ Object

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.

Raises:

  • (ArgumentError)

262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# 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