hidapi 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 75e7069e09a362e591f7edba9bfa9a7763f5f31a
4
+ data.tar.gz: 981e48c8d73d8add81d2728e461a060761ce7bae
5
+ SHA512:
6
+ metadata.gz: 2f20a27445bf2f1052572a72857f74b0d575c486e2c5018dffd9a5b6547a89f3b47adfc93e00c323af92b66fdd01a266f164315cdee1203a3c4c1b0da75d7ca5
7
+ data.tar.gz: 829d91b1b8c1f9a7402bd50a8ab82c167a4738542872a91904659ea936767b056f81dc0d27d676e0b1157f03b3973f828016d7fab9ac108dbc261c18d88e2ee8
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/*
10
+ !/tmp/.keep
11
+ /.idea/
12
+ **/.DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hidapi.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Beau Barker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # HIDAPI
2
+
3
+ This is a Ruby port of the [HID API from Signal 11](http://www.signal11.us/oss/hidapi).
4
+
5
+ __I am not associated with Signal 11.__
6
+
7
+ More specifically, it is a
8
+ port of the "libusb" version of the HID API. I took creative liberty where I needed to and basically just sought to
9
+ make it work uniformly. The gem relies on the [libusb](https://rubygems.org/gems/libusb).
10
+
11
+
12
+ I know there are at least two other projects that were meant to bring an HID API to the Ruby world. However, one of
13
+ them is a C plugin (no real problem, just not Ruby) and the other is an FFI wrapper around the original HID API with
14
+ a few missing components. I didn't see any reason to bring FFI into it when the end result is something fairly simple.
15
+
16
+ The entire library basically consists of the HIDAPI::Engine and the HIDAPI::Device classes. The HIDAPI module maintains
17
+ an instance of the HIDAPI::Engine and maps missing methods to the engine. So basically `HIDAPI.enumerate` is the same
18
+ as `HIDAPI.engine.enumerate` where the `engine` method creates an HIDAPI::Engine on the first call. The HIDAPI::Engine
19
+ class is used to enumerate and retrieve devices, while the HIDAPI::Device class is used for everything else.
20
+
21
+ The original source included internationalization. I have not included that (yet), but the HIDAPI::Language class has
22
+ been defined and the [i18n](https://rubygems.org/gems/i18n) is required, even though we aren't using it yet.
23
+
24
+
25
+ ## Installation
26
+
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem 'hidapi'
31
+ ```
32
+
33
+ And then execute:
34
+
35
+ $ bundle
36
+
37
+ Or install it yourself as:
38
+
39
+ $ gem install hidapi
40
+
41
+
42
+ ## Usage
43
+
44
+ Basic usage would be as follows.
45
+ ```ruby
46
+ my_dev = HIDAPI::open(0x4d4d, 0xc0c0)
47
+ my_dev.write 0x01, 0x02, 0x03, 0x04, 0x05
48
+ my_dev.write [ 0x01, 0x02, 0x03, 0x04, 0x05 ]
49
+ my_dev.write "\x01\x02\x03\x04\x05"
50
+ input = my_dev.read
51
+ my_dev.close
52
+ ```
53
+
54
+ The `write` method takes data in any of the 3 forms shown above. Individual arguments, an array of arguments, or a string of arguments.
55
+ Internally the first two are converted into the 3rd form using `pack("C*")`. If you have a custom data set your are sending,
56
+ such as 16 or 32 bit values, then you will likely want to pack the string yourself to prevent issues.
57
+
58
+ The `read` method returns a packed string from the device. For instance it may return "\x10\x01\x00". Your application
59
+ needs to know how to handle the values returned.
60
+
61
+ In order to use a USB device in Linux, udev needs to grant access to the user running the application. If run as root,
62
+ then it should just work. However, you'd be running it as root. A better option is to have udev grant the appropriate permissions.
63
+
64
+ In order to use a USB device in OS X, the system needs a kernel extension telling the OS not to map the device to its own
65
+ HID drivers.
66
+
67
+ The `HIDAPI::SetupTaskHelper` handles both of these situations. The gem includes a rake task `setup_hid_device` that
68
+ calls this class. You can also execute the `lib/hidapi/setup_task_helper.rb` file directly. However, in your application,
69
+ both of these may be too cumbersome. You can create an instance of the SetupTaskHelper class with the appropriate arguments
70
+ and just run it yourself.
71
+
72
+ ```ruby
73
+ require "hidapi"
74
+ HIDAPI::SetupTaskHelper.new(
75
+ 0x04d8, # vendor_id
76
+ 0xc002, # product_id
77
+ "pico-lcd-graphic", # simple_name
78
+ 0 # interface
79
+ ).run
80
+ ```
81
+
82
+ This will take the appropriate action on your OS to make the USB device available for use. On linux, it will also add
83
+ convenient symlinks to the /dev filesystem. For instance, the above setup could give you something like `/dev/hidapi/pico-lcd-graphic@1-4`
84
+ that points to the correct USB device. The library doesn't use them, but the presence of the links in the`/dev/hidapi`
85
+ directory would be a clear indicator that the device has been recognizes and configured.
86
+
87
+
88
+ ## Contributing
89
+
90
+ Bug reports and pull requests are welcome on GitHub at https://github.com/barkerest/hidapi.
91
+
92
+
93
+ ## License
94
+
95
+ Copyright (c) 2016 [Beau Barker](mailto:beau@barkerest.com)
96
+
97
+ As said before, this is a port of the [HID API from Signal 11](http://www.signal11.us/oss/hidapi) so it has significant
98
+ code in common with that library, although the very fact that it was ported means that there is no code that was copied
99
+ from that library. That library can be licensed under the GPL, BSD, or a custom license very similar to the MIT license.
100
+ This gem is not that library.
101
+
102
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
103
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'hidapi/setup_task_helper'
3
+
4
+ desc 'Setup an HID device for use with the library'
5
+ task :setup_hid_device, :vendor_id, :product_id, :simple_name, :interface do |t,args|
6
+ args ||= {}
7
+ helper = HIDAPI::SetupTaskHelper.new(args[:vendor_id], args[:product_id], args[:simple_name], args[:interface])
8
+ helper.run
9
+ end
10
+
11
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENV['ENABLE_DEBUG'] = '1'
4
+
5
+ require "bundler/setup"
6
+ require "hidapi"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+
16
+ require "irb"
17
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/hidapi.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hidapi/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'hidapi'
8
+ spec.version = HIDAPI::VERSION
9
+ spec.authors = ['Beau Barker']
10
+ spec.email = ['beau@barkerest.com']
11
+
12
+ spec.summary = 'A Ruby port of the HID API from Signal 11 Software (http://www.signal11.us/)'
13
+ spec.homepage = 'https://github.com/barkerest/hidapi'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.required_ruby_version = '>= 2.2.0'
22
+ spec.add_dependency 'libusb', '~>0.5.1'
23
+ spec.add_dependency 'i18n'
24
+
25
+ spec.add_development_dependency 'bundler', '~>1.12'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ end
data/lib/hidapi.rb ADDED
@@ -0,0 +1,92 @@
1
+ require 'libusb'
2
+ require 'hidapi/version'
3
+
4
+ ##
5
+ # A Ruby implementation of the HID API library from Signal 11 software.
6
+ #
7
+ # I am not associated with Signal 11 software.
8
+ #
9
+ # This library was written out of a need to get better debugging information.
10
+ # By writing it, I learned quite a bit about HID devices and how to get them
11
+ # working with multiple operating systems from one Ruby gem. To do this, I use LIBUSB.
12
+ #
13
+ # This module contains the library and wraps around an instance of the HIDAPI::Engine
14
+ # class to simplify calls. For instance, HIDAPI.engine.enumerate can also be used as
15
+ # just HIDAPI.enumerate.
16
+ #
17
+ module HIDAPI
18
+
19
+ raise 'LIBUSB version must be at least 1.0' unless LIBUSB.version.major >= 1
20
+
21
+ ##
22
+ # Gets the engine used by the API.
23
+ #
24
+ # All engine methods can be passed through the HIDAPI module.
25
+ def self.engine
26
+ @engine ||= HIDAPI::Engine.new
27
+ end
28
+
29
+
30
+ def self.method_missing(m,*a,&b) # :nodoc:
31
+ if engine.respond_to?(m)
32
+ engine.send(m,*a,&b)
33
+ else
34
+ # no super available for modules.
35
+ raise NoMethodError, "undefined method `#{m}` for HIDAPI:Module"
36
+ end
37
+ end
38
+
39
+
40
+ def self.respond_to_missing?(m) # :nodoc:
41
+ engine.respond_to?(m)
42
+ end
43
+
44
+
45
+ ##
46
+ # Processes a debug message.
47
+ #
48
+ # You can either provide a debug message directly or via a block.
49
+ # If a block is provided, it will not be executed unless a debugger has been set and the message is left nil.
50
+ def self.debug(msg = nil, &block)
51
+ dbg = @debugger
52
+ if dbg
53
+ mutex.synchronize do
54
+ msg = block.call if block_given? && msg.nil?
55
+ dbg.call(msg)
56
+ end
57
+ end
58
+ end
59
+
60
+ ##
61
+ # Sets the debugger to use.
62
+ #
63
+ # :yields: the message to debug
64
+ def self.set_debugger(&block)
65
+ mutex.synchronize do
66
+ @debugger = block_given? ? block : nil
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def self.mutex
73
+ @mutex ||= Mutex.new
74
+ end
75
+
76
+
77
+ if ENV['ENABLE_DEBUG'].to_s.to_i != 0
78
+ set_debugger do |msg|
79
+ msg = msg.to_s.strip
80
+ if msg.length > 0
81
+ @debug_file ||= File.open(File.expand_path('../../tmp/debug.log', __FILE__), 'w')
82
+ @debug_file.write "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{msg.gsub("\n", "\n" + (' ' * 22))}\n"
83
+ @debug_file.flush
84
+ STDOUT.print "(debug) #{msg.gsub("\n", "\n" + (' ' * 8))}\n"
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ # load all of the library components.
92
+ Dir.glob(File.expand_path('../hidapi/*.rb', __FILE__)) { |file| require file }
@@ -0,0 +1,634 @@
1
+
2
+
3
+ module HIDAPI
4
+
5
+ ##
6
+ # This class is the interface to a HID device.
7
+ #
8
+ # Each instance can connect to a single interface on an HID device.
9
+ # If you have more than one interface, you will need to have more
10
+ # than one instance of this class to work with all of them.
11
+ #
12
+ # When open, the device is polled continuously for incoming data.
13
+ # It will build up a cache of up to 32 packets. If you are not
14
+ # reading from the device, it will silently discard the oldest
15
+ # packets and continue storing the newest packets.
16
+ #
17
+ # The read method can block. This is controlled by the +blocking+
18
+ # attribute. The default value is true. If you want the read method
19
+ # to be non-blocking, set this attribute to false.
20
+ class Device
21
+
22
+ ##
23
+ # Gets the USB device this HID device uses.
24
+ attr_accessor :usb_device
25
+ private :usb_device=
26
+
27
+ ##
28
+ # Gets the device handle for I/O.
29
+ attr_accessor :handle
30
+ private :handle=
31
+ protected :handle
32
+
33
+ ##
34
+ # Gets the input endpoint.
35
+ attr_accessor :input_endpoint
36
+ private :input_endpoint=
37
+ protected :input_endpoint
38
+
39
+ ##
40
+ # Gets the output endpoint.
41
+ attr_accessor :output_endpoint
42
+ private :output_endpoint=
43
+ protected :output_endpoint
44
+
45
+ ##
46
+ # Gets the maximum packet size for input packets.
47
+ attr_accessor :input_ep_max_packet_size
48
+ private :input_ep_max_packet_size=
49
+ protected :input_ep_max_packet_size
50
+
51
+ ##
52
+ # Gets the interface this HID device uses on the USB device.
53
+ attr_accessor :interface
54
+ private :interface=
55
+
56
+ ##
57
+ # Gets or sets the blocking nature for +read+.
58
+ #
59
+ # Defaults to +true+. Set to +false+ to have +read+ be non-blocking.
60
+ attr_accessor :blocking
61
+
62
+ attr_accessor :thread
63
+ private :thread, :thread=
64
+
65
+ attr_accessor :mutex
66
+ private :mutex, :mutex=
67
+
68
+ attr_accessor :thread_initialized
69
+ private :thread_initialized, :thread_initialized=
70
+
71
+ attr_accessor :shutdown_thread
72
+ private :shutdown_thread, :shutdown_thread=
73
+
74
+ attr_accessor :transfer_cancelled
75
+ private :transfer_cancelled, :transfer_cancelled=
76
+
77
+ attr_accessor :transfer
78
+ private :transfer, :transfer=
79
+
80
+ attr_accessor :input_reports
81
+ private :input_reports, :input_reports=
82
+
83
+ ##
84
+ # Gets the path for this device that can be used by HIDAPI::Engine#get_device_by_path
85
+ attr_accessor :path
86
+ private :path=
87
+
88
+ attr_accessor :open_count
89
+ private :open_count, :open_count=
90
+
91
+
92
+ ##
93
+ # Initializes an HID device.
94
+ def initialize(usb_device, interface = 0)
95
+ raise HIDAPI::InvalidDevice, "invalid object (#{usb_device.class.name})" unless usb_device.is_a?(LIBUSB::Device)
96
+
97
+ self.usb_device = usb_device
98
+ self.blocking = true
99
+ self.mutex = Mutex.new
100
+ self.interface = interface
101
+ self.path = HIDAPI::Device.make_path(usb_device, interface)
102
+
103
+ self.input_endpoint = self.output_endpoint = nil
104
+ self.thread = nil
105
+ self.thread_initialized = false
106
+ self.input_reports = []
107
+ self.shutdown_thread = false
108
+ self.transfer_cancelled = LIBUSB::Context::CompletionFlag.new
109
+ self.open_count = 0
110
+
111
+ self.class.init_hook.each do |proc|
112
+ proc.call self
113
+ end
114
+ end
115
+
116
+
117
+
118
+ ##
119
+ # Gets the manufacturer of the device.
120
+ def manufacturer
121
+ @manufacturer ||= read_string(usb_device.iManufacturer, "VENDOR(0x#{vendor_id.to_hex(4)})").strip
122
+ end
123
+
124
+ ##
125
+ # Gets the product/model of the device.
126
+ def product
127
+ @product ||= read_string(usb_device.iProduct, "PRODUCT(0x#{product_id.to_hex(4)})").strip
128
+ end
129
+
130
+ ##
131
+ # Gets the serial number of the device.
132
+ def serial_number
133
+ @serial_number ||= read_string(usb_device.iSerialNumber, '?').strip
134
+ end
135
+
136
+ ##
137
+ # Gets the vendor ID.
138
+ def vendor_id
139
+ @vendor_id ||= usb_device.idVendor
140
+ end
141
+
142
+ ##
143
+ # Gets the product ID.
144
+ def product_id
145
+ @product_id ||= usb_device.idProduct
146
+ end
147
+
148
+ ##
149
+ # Is the device currently open?
150
+ def open?
151
+ !!handle
152
+ end
153
+
154
+ ##
155
+ # Closes the device (if open).
156
+ #
157
+ # Returns the device.
158
+ def close
159
+ self.open_count = open_count - 1
160
+ if open_count <= 0
161
+ HIDAPI.debug("open_count for device #{path} is #{open_count}") if open_count < 0
162
+ if handle
163
+ begin
164
+ self.shutdown_thread = true
165
+ transfer.cancel! rescue nil if transfer
166
+ thread.join
167
+ rescue =>e
168
+ HIDAPI.debug "failed to kill read thread on device #{path}: #{e.inspect}"
169
+ end
170
+ begin
171
+ handle.release_interface(interface)
172
+ rescue =>e
173
+ HIDAPI.debug "failed to release interface on device #{path}: #{e.inspect}"
174
+ end
175
+ begin
176
+ handle.close
177
+ rescue =>e
178
+ HIDAPI.debug "failed to close device #{path}: #{e.inspect}"
179
+ end
180
+ HIDAPI.debug "closed device #{path}"
181
+ end
182
+ self.handle = nil
183
+ mutex.synchronize { self.input_reports = [] }
184
+ self.open_count = 0
185
+ end
186
+ self
187
+ end
188
+
189
+ ##
190
+ # Opens the device.
191
+ #
192
+ # Returns the device.
193
+ def open
194
+ if open?
195
+ self.open_count = open_count + 1
196
+ if open_count < 1
197
+ HIDAPI.debug "open_count for open device #{path} is #{open_count}"
198
+ self.open_count = 1
199
+ end
200
+ return self
201
+ end
202
+ self.open_count = 0
203
+ begin
204
+ self.handle = usb_device.open
205
+ raise 'no handle returned' unless handle
206
+
207
+ begin
208
+ if handle.kernel_driver_active?(interface)
209
+ handle.detach_kernel_driver(interface)
210
+ end
211
+ rescue LIBUSB::ERROR_NOT_SUPPORTED
212
+ HIDAPI.debug 'cannot determine kernel driver status, continuing to open device'
213
+ end
214
+
215
+ handle.claim_interface(interface)
216
+
217
+ self.input_endpoint = self.output_endpoint = nil
218
+
219
+ # now we need to find the endpoints.
220
+ usb_device.settings
221
+ .keep_if {|item| item.bInterfaceNumber == interface}
222
+ .each do |intf_desc|
223
+ intf_desc.endpoints.each do |ep|
224
+ if ep.transfer_type == :interrupt
225
+ if input_endpoint.nil? && ep.direction == :in
226
+ self.input_endpoint = ep.bEndpointAddress
227
+ self.input_ep_max_packet_size = ep.wMaxPacketSize
228
+ end
229
+ if output_endpoint.nil? && ep.direction == :out
230
+ self.output_endpoint = ep.bEndpointAddress
231
+ end
232
+ end
233
+ break if input_endpoint && output_endpoint
234
+ end
235
+ end
236
+
237
+ # output_ep is optional, input_ep is required
238
+ raise 'failed to locate input endpoint' unless input_endpoint
239
+
240
+ # start the read thread
241
+ self.input_reports = []
242
+ self.thread_initialized = false
243
+ self.shutdown_thread = false
244
+ self.thread = Thread.start(self) { |dev| dev.send(:execute_read_thread) }
245
+ sleep 0 until thread_initialized
246
+
247
+ rescue =>e
248
+ handle.close rescue nil
249
+ self.handle = nil
250
+ HIDAPI.debug "failed to open device #{path}: #{e.inspect}"
251
+ raise DeviceOpenFailed, e.inspect
252
+ end
253
+ HIDAPI.debug "opened device #{path}"
254
+ self.open_count = 1
255
+ self
256
+ end
257
+
258
+ ##
259
+ # Writes data to the device.
260
+ #
261
+ # The data to be written can be individual byte values, an array of byte values, or a string packed with data.
262
+ def write(*data)
263
+ raise ArgumentError, 'data must not be blank' if data.nil? || data.length < 1
264
+ raise HIDAPI::DeviceNotOpen unless open?
265
+
266
+ data, report_number, skipped_report_id = clean_output_data(data)
267
+
268
+ if output_endpoint.nil?
269
+ # No interrupt out endpoint, use the control endpoint.
270
+ handle.control_transfer(
271
+ bmRequestType: LIBUSB::REQUEST_TYPE_CLASS | LIBUSB::RECIPIENT_INTERFACE | LIBUSB::ENDPOINT_OUT,
272
+ bRequest: 0x09, # HID Set_Report
273
+ wValue: (2 << 8) | report_number, # HID output = 2
274
+ wIndex: interface,
275
+ dataOut: data
276
+ )
277
+ data.length + (skipped_report_id ? 1 : 0)
278
+ else
279
+ # Use the interrupt out endpoint.
280
+ handle.interrupt_transfer(
281
+ endpoint: output_endpoint,
282
+ dataOut: data
283
+ )
284
+ end
285
+ end
286
+
287
+ ##
288
+ # Attempts to read from the device, waiting up to +milliseconds+ before returning.
289
+ #
290
+ # If milliseconds is less than 1, it will wait forever.
291
+ # If milliseconds is 0, then it will return immediately.
292
+ #
293
+ # Returns the next report on success. If no report is available and it is not waiting
294
+ # forever, it will return an empty string.
295
+ #
296
+ # Returns nil on error.
297
+ def read_timeout(milliseconds)
298
+ raise DeviceNotOpen unless open?
299
+
300
+ mutex.synchronize do
301
+ if input_reports.count > 0
302
+ data = input_reports.delete_at(0)
303
+ HIDAPI.debug "read data from device #{path}: #{data.inspect}"
304
+ return data
305
+ end
306
+
307
+ if shutdown_thread
308
+ HIDAPI.debug "read thread for device #{path} is not running"
309
+ return nil
310
+ end
311
+ end
312
+
313
+ # no data to return, do not block.
314
+ return '' if milliseconds == 0
315
+
316
+ if milliseconds < 0
317
+ # wait forever (as long as the read thread doesn't die)
318
+ until shutdown_thread
319
+ mutex.synchronize do
320
+ if input_reports.count > 0
321
+ data = input_reports.delete_at(0)
322
+ HIDAPI.debug "read data from device #{path}: #{data.inspect}"
323
+ return data
324
+ end
325
+ end
326
+ sleep 0
327
+ end
328
+
329
+ # error, return nil
330
+ HIDAPI.debug "read thread ended while waiting on device #{path}"
331
+ nil
332
+ else
333
+ # wait up to so many milliseconds for input.
334
+ stop_at = Time.now + (milliseconds * 0.001)
335
+ while Time.now < stop_at
336
+ mutex.synchronize do
337
+ if input_reports.count > 0
338
+ data = input_reports.delete_at(0)
339
+ HIDAPI.debug "read data from device #{path}: #{data.inspect}"
340
+ return data
341
+ end
342
+ end
343
+ sleep 0
344
+ end
345
+
346
+ # no input, return empty.
347
+ ''
348
+ end
349
+ end
350
+
351
+ ##
352
+ # Reads the next report from the device.
353
+ #
354
+ # In blocking mode, it will wait for a report.
355
+ # In non-blocking mode, it will return immediately with an empty string if there is no report.
356
+ #
357
+ # Returns nil on error.
358
+ def read
359
+ read_timeout blocking? ? -1 : 0
360
+ end
361
+
362
+ ##
363
+ # Is this device in blocking mode (for reading)?
364
+ def blocking?
365
+ !!blocking
366
+ end
367
+
368
+ ##
369
+ # Sends a feature report to the device.
370
+ def send_feature_report(data)
371
+ raise ArgumentError, 'data must not be blank' if data.nil? || data.length < 1
372
+ raise HIDAPI::DeviceNotOpen unless open?
373
+
374
+ data, report_number, skipped_report_id = clean_output_data(data)
375
+
376
+ handle.control_transfer(
377
+ bmRequestType: LIBUSB::REQUEST_TYPE_CLASS | LIBUSB::RECIPIENT_INTERFACE | LIBUSB::ENDPOINT_OUT,
378
+ bRequest: 0x09, # HID Set_Report
379
+ wValue: (3 << 8) | report_number, # HID feature = 3
380
+ wIndex: interface,
381
+ dataOut: data
382
+ )
383
+
384
+ data.length + (skipped_report_id ? 1 : 0)
385
+ end
386
+
387
+ ##
388
+ # Gets a feature report from the device.
389
+ def get_feature_report(report_number, buffer_size = nil)
390
+
391
+ buffer_size ||= input_ep_max_packet_size
392
+
393
+ handle.control_transfer(
394
+ bmRequestType: LIBUSB::REQUEST_TYPE_CLASS | LIBUSB::RECIPIENT_INTERFACE | LIBUSB::ENDPOINT_IN,
395
+ bRequest: 0x01, # HID Get_Report
396
+ wValue: (3 << 8) | report_number,
397
+ wIndex: interface,
398
+ dataIn: buffer_size
399
+ )
400
+
401
+ end
402
+
403
+
404
+ def inspect # :nodoc:
405
+ "#<#{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'})>"
406
+ end
407
+
408
+
409
+ def to_s # :nodoc:
410
+ "#{manufacturer} #{product} (#{serial_number})"
411
+ end
412
+
413
+
414
+ ##
415
+ # Generates a path for a device.
416
+ def self.make_path(usb_dev, interface)
417
+ bus = usb_dev.bus_number
418
+ address = usb_dev.device_address
419
+ "#{bus.to_hex(4)}:#{address.to_hex(4)}:#{interface.to_hex(2)}"
420
+ end
421
+
422
+
423
+ ##
424
+ # Reads a string descriptor from the USB device.
425
+ def read_string(index, on_failure = '')
426
+ begin
427
+ # does not require an interface, so open from the usb_dev instead of using our open method.
428
+ data = usb_device.open { |handle| handle.string_descriptor_ascii(index) }
429
+ HIDAPI.debug("read string at index #{index} for device #{path}: #{data.inspect}")
430
+ data
431
+ rescue =>e
432
+ HIDAPI.debug("failed to read string at index #{index} for device #{path}: #{e.inspect}")
433
+ on_failure || ''
434
+ end
435
+ end
436
+
437
+
438
+ protected
439
+
440
+ ##
441
+ # Defines a hook to execute when data is read from the device.
442
+ #
443
+ # This can be provided as a proc, symbol, or simply as a block.
444
+ #
445
+ # The proc should return a true value if it consumes the data.
446
+ # If it does not consume the data it must return false or nil.
447
+ #
448
+ # If no read_hook proc consumes the data, it will be cached for
449
+ # future calls to +read+ or +read_timeout+.
450
+ #
451
+ # The read hook is called from within the read thread. If it must
452
+ # access resources from another thread, you will want to use
453
+ # a mutex for locking.
454
+ #
455
+ # :yields: a device and the input_report
456
+ #
457
+ # read_hook do |device, input_report|
458
+ # ...
459
+ # true
460
+ # end
461
+ def self.read_hook(proc = nil, &block)
462
+ @read_hook ||= []
463
+
464
+ proc = block if proc.nil? && block_given?
465
+ if proc
466
+ if proc.is_a?(Symbol) || proc.is_a?(String)
467
+ proc_name = proc
468
+ proc = Proc.new do |dev, input_report|
469
+ dev.send(proc_name, dev, input_report)
470
+ end
471
+ end
472
+ @read_hook << proc
473
+ end
474
+
475
+ @read_hook
476
+ end
477
+
478
+ ##
479
+ # Defines a hook to execute when a device is initialized.
480
+ #
481
+ # Yields the device instance.
482
+ def self.init_hook(proc = nil, &block)
483
+ @init_hook ||= []
484
+
485
+ proc = block if proc.nil? && block_given?
486
+ if proc
487
+ if proc.is_a?(Symbol) || proc.is_a?(String)
488
+ proc_name = proc
489
+ proc = Proc.new do |dev|
490
+ dev.send(proc_name, dev)
491
+ end
492
+ end
493
+ @init_hook << proc
494
+ end
495
+
496
+ @init_hook
497
+ end
498
+
499
+ private
500
+
501
+ def clean_output_data(data)
502
+ if data.length == 1 && data.first.is_a?(Array)
503
+ data = data.first
504
+ end
505
+
506
+ if data.length == 1 && data.first.is_a?(String)
507
+ data = data.first
508
+ end
509
+
510
+ data = data.pack('C*') unless data.is_a?(String)
511
+
512
+ skipped_report_id = false
513
+ report_number = data.getbyte(0)
514
+
515
+ if report_number == 0x00
516
+ data = data[1..-1].to_s
517
+ skipped_report_id = true
518
+ end
519
+
520
+ [ data, report_number, skipped_report_id ]
521
+ end
522
+
523
+ def execute_read_thread
524
+
525
+ begin
526
+ # make it available locally, prevent changes while we are running.
527
+ length = input_ep_max_packet_size
528
+ context = usb_device.context
529
+
530
+ # Construct our transfer.
531
+ self.transfer = LIBUSB::InterruptTransfer.new(
532
+ dev_handle: handle,
533
+ endpoint: input_endpoint,
534
+ callback: method(:read_callback),
535
+ timeout: 30000
536
+ )
537
+ transfer.alloc_buffer length
538
+
539
+ # clear flag for transfer cancellation.
540
+ transfer_cancelled.completed = false
541
+
542
+ # perform the initial submission, the callback will resubmit.
543
+ transfer.submit!
544
+ rescue =>e
545
+ HIDAPI.debug "failed to initialize read thread for device #{path}: #{e.inspect}"
546
+ self.shutdown_thread = true
547
+ raise e
548
+ ensure
549
+ # tell the main thread that we are running.
550
+ self.thread_initialized = true
551
+ end
552
+
553
+ # wait for the main thread to kill this thread.
554
+ until shutdown_thread
555
+ begin
556
+ context.handle_events 0
557
+ rescue LIBUSB::ERROR_BUSY, LIBUSB::ERROR_TIMEOUT, LIBUSB::ERROR_OVERFLOW, LIBUSB::ERROR_INTERRUPTED => e
558
+ # non fatal errors.
559
+ HIDAPI.debug "non-fatal error for read_thread on device #{path}: #{e.inspect}"
560
+ rescue => e
561
+ HIDAPI.debug "fatal error for read_thread on device #{path}: #{e.inspect}"
562
+ self.shutdown_thread = true
563
+ raise e
564
+ end
565
+ end
566
+
567
+ # no longer running.
568
+ self.thread_initialized = false
569
+
570
+ # cancel any transfers that may be pending.
571
+ transfer.cancel! rescue nil
572
+
573
+ # wait for the cancellation to complete.
574
+ until transfer_cancelled.completed?
575
+ context.handle_events 0, transfer_cancelled
576
+ end
577
+
578
+ end
579
+
580
+ def read_callback(tr)
581
+ if tr.status == :TRANSFER_COMPLETED
582
+ data = tr.actual_buffer
583
+
584
+ consumed = false
585
+ self.class.read_hook.each do |proc|
586
+ consumed =
587
+ begin
588
+ proc.call(self, data)
589
+ rescue =>e
590
+ HIDAPI.debug "read_hook failed for device #{path}: #{e.inspect}"
591
+ false
592
+ end
593
+ break if consumed
594
+ end
595
+
596
+ unless consumed
597
+ mutex.synchronize do
598
+ input_reports << tr.actual_buffer
599
+ input_reports.delete_at(0) while input_reports.length > 32
600
+ end
601
+ end
602
+ elsif tr.status == :TRANSFER_CANCELLED
603
+ mutex.synchronize do
604
+ self.shutdown_thread = true
605
+ transfer_cancelled.completed = true
606
+ end
607
+ HIDAPI.debug "read transfer cancelled for device #{path}"
608
+ elsif tr.status == :TRANSFER_NO_DEVICE
609
+ mutex.synchronize do
610
+ self.shutdown_thread = true
611
+ transfer_cancelled.completed = true
612
+ end
613
+ HIDAPI.debug "read transfer failed with no device for device #{path}"
614
+ elsif tr.status == :TRANSFER_TIMED_OUT
615
+ # ignore timeouts, they are normal
616
+ else
617
+ HIDAPI.debug "read transfer with unknown transfer code (#{tr.status}) for device #{path}"
618
+ end
619
+
620
+ # resubmit the transfer object.
621
+ begin
622
+ tr.submit!
623
+ rescue =>e
624
+ HIDAPI.debug "failed to resubmit transfer for device #{path}: #{e.inspect}"
625
+ mutex.synchronize do
626
+ self.shutdown_thread = true
627
+ transfer_cancelled.completed = true
628
+ end
629
+ end
630
+ end
631
+
632
+
633
+ end
634
+ end