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 +7 -0
- data/.gitignore +12 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +103 -0
- data/Rakefile +11 -0
- data/bin/console +17 -0
- data/bin/setup +8 -0
- data/hidapi.gemspec +27 -0
- data/lib/hidapi.rb +92 -0
- data/lib/hidapi/device.rb +634 -0
- data/lib/hidapi/engine.rb +151 -0
- data/lib/hidapi/errors.rb +18 -0
- data/lib/hidapi/language.rb +221 -0
- data/lib/hidapi/numeric_extensions.rb +10 -0
- data/lib/hidapi/setup_task_helper.rb +240 -0
- data/lib/hidapi/version.rb +3 -0
- data/tmp/.keep +0 -0
- metadata +117 -0
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
data/Gemfile
ADDED
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
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
|