hidapi 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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