usb-ruby 0.1.0
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/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +8 -0
- data/lib/usb/bos_descriptor.rb +54 -0
- data/lib/usb/bos_dev_capability.rb +48 -0
- data/lib/usb/config_descriptor.rb +94 -0
- data/lib/usb/constants.rb +129 -0
- data/lib/usb/container_id.rb +43 -0
- data/lib/usb/context.rb +148 -0
- data/lib/usb/device.rb +134 -0
- data/lib/usb/device_descriptor.rb +61 -0
- data/lib/usb/device_handle.rb +246 -0
- data/lib/usb/endpoint_descriptor.rb +91 -0
- data/lib/usb/error.rb +94 -0
- data/lib/usb/event_handling.rb +149 -0
- data/lib/usb/ffi_bindings.rb +319 -0
- data/lib/usb/hotplug.rb +47 -0
- data/lib/usb/interface.rb +31 -0
- data/lib/usb/interface_descriptor.rb +65 -0
- data/lib/usb/iso_packet.rb +26 -0
- data/lib/usb/pollfds.rb +33 -0
- data/lib/usb/ss_device_capability.rb +59 -0
- data/lib/usb/ss_endpoint_companion.rb +51 -0
- data/lib/usb/transfer.rb +201 -0
- data/lib/usb/usb20_extension.rb +47 -0
- data/lib/usb/version.rb +5 -0
- data/lib/usb.rb +49 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 57a269f637b918eeb1bba951a051b436458afe412c425165b2c1c7c2b46fdb25
|
|
4
|
+
data.tar.gz: c5b682b0de82b3579bcf28f45efd92fba56c4e4ac3511fa30d4cc4ffaa2171b8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a7311cda74c4e9481a0c221e417ae04ebdc9efc70599ab287d9b95ce9f79d222e24eb50b3e44ac1cb43e23f64fac4ba48567f04d384530928431c4858f2b0146
|
|
7
|
+
data.tar.gz: da4c60df04e25348cc1c1d2d762466d62aff997b480b7767ba3300c9afcb215f0fa7806b105ed98e3832c5fc65d995e84ea08874a7a2f2f5d81c49c39ddbcdc3
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
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,52 @@
|
|
|
1
|
+
# usb-ruby
|
|
2
|
+
|
|
3
|
+
`usb-ruby` is a Ruby FFI binding for libusb 1.0. It exposes the libusb API through the top-level `USB` module and does not require native extension compilation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add usb-ruby
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install it directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install usb-ruby
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "usb"
|
|
23
|
+
|
|
24
|
+
USB::Context.open do |context|
|
|
25
|
+
context.devices.each do |device|
|
|
26
|
+
descriptor = device.device_descriptor
|
|
27
|
+
puts format("%03d/%03d %04x:%04x",
|
|
28
|
+
device.bus_number,
|
|
29
|
+
device.device_address,
|
|
30
|
+
descriptor.vendor_id,
|
|
31
|
+
descriptor.product_id)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Development
|
|
37
|
+
|
|
38
|
+
Run:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bundle install
|
|
42
|
+
bundle exec rspec
|
|
43
|
+
bundle exec rake install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Contributing
|
|
47
|
+
|
|
48
|
+
Bug reports and pull requests are welcome at https://github.com/ydah/usb.
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module USB
|
|
4
|
+
class BOSDescriptor
|
|
5
|
+
def self.finalizer(ptr)
|
|
6
|
+
proc do
|
|
7
|
+
FFIBindings.libusb_free_bos_descriptor(ptr) unless ptr.nil? || ptr.null?
|
|
8
|
+
rescue StandardError
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(ptr)
|
|
13
|
+
raise ArgumentError, "BOS descriptor pointer is required" if ptr.nil? || ptr.null?
|
|
14
|
+
|
|
15
|
+
@ptr = ptr
|
|
16
|
+
@struct = FFIBindings::BOSDescriptorStruct.new(@ptr)
|
|
17
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@ptr))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def num_device_caps
|
|
21
|
+
@struct[:bNumDeviceCaps]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def device_capabilities
|
|
25
|
+
count = num_device_caps
|
|
26
|
+
base_ptr = @struct[:dev_capability]
|
|
27
|
+
return [] if base_ptr.null?
|
|
28
|
+
|
|
29
|
+
Array.new(count) do |index|
|
|
30
|
+
capability_ptr = base_ptr.get_pointer(index * FFI::Pointer.size)
|
|
31
|
+
BOSDevCapability.new(capability_ptr, self)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def close
|
|
36
|
+
return if @ptr.nil? || @ptr.null?
|
|
37
|
+
|
|
38
|
+
ObjectSpace.undefine_finalizer(self)
|
|
39
|
+
FFIBindings.libusb_free_bos_descriptor(@ptr)
|
|
40
|
+
@ptr = FFI::Pointer::NULL
|
|
41
|
+
@struct = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias free close
|
|
45
|
+
|
|
46
|
+
def to_ptr
|
|
47
|
+
@ptr
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inspect
|
|
51
|
+
"#<USB::BOSDescriptor device_caps=#{num_device_caps}>"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module USB
|
|
4
|
+
class BOSDevCapability
|
|
5
|
+
def initialize(ptr, bos_descriptor = nil)
|
|
6
|
+
raise ArgumentError, "BOS capability pointer is required" if ptr.nil? || ptr.null?
|
|
7
|
+
|
|
8
|
+
@bos_descriptor = bos_descriptor
|
|
9
|
+
@ptr = ptr
|
|
10
|
+
@struct = FFIBindings::BOSDevCapabilityStruct.new(@ptr)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def capability_type
|
|
14
|
+
@struct[:bDevCapabilityType]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def data
|
|
18
|
+
length = @struct[:bLength] - 3
|
|
19
|
+
return "".b if length <= 0
|
|
20
|
+
|
|
21
|
+
(@ptr + 3).read_bytes(length)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def usb_2_0_extension(context)
|
|
25
|
+
fetch_descriptor(context, :libusb_get_usb_2_0_extension_descriptor, USB20Extension)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ss_device_capability(context)
|
|
29
|
+
fetch_descriptor(context, :libusb_get_ss_usb_device_capability_descriptor, SSDeviceCapability)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def container_id(context)
|
|
33
|
+
fetch_descriptor(context, :libusb_get_container_id_descriptor, ContainerID)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def inspect
|
|
37
|
+
"#<USB::BOSDevCapability type=#{capability_type}>"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def fetch_descriptor(context, function_name, klass)
|
|
43
|
+
descriptor_ptr = FFI::MemoryPointer.new(:pointer)
|
|
44
|
+
Error.raise_on_error(FFIBindings.public_send(function_name, context.to_ptr, @ptr, descriptor_ptr))
|
|
45
|
+
klass.new(descriptor_ptr.read_pointer)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module USB
|
|
4
|
+
class ConfigDescriptor
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def self.finalizer(ptr)
|
|
8
|
+
proc do
|
|
9
|
+
FFIBindings.libusb_free_config_descriptor(ptr) unless ptr.nil? || ptr.null?
|
|
10
|
+
rescue StandardError
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(ptr)
|
|
15
|
+
raise ArgumentError, "config descriptor pointer is required" if ptr.nil? || ptr.null?
|
|
16
|
+
|
|
17
|
+
@ptr = ptr
|
|
18
|
+
@struct = FFIBindings::ConfigDescriptorStruct.new(@ptr)
|
|
19
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@ptr))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configuration_value
|
|
23
|
+
@struct[:bConfigurationValue]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def description_index
|
|
27
|
+
@struct[:iConfiguration]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def attributes
|
|
31
|
+
@struct[:bmAttributes]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def max_power
|
|
35
|
+
@struct[:MaxPower]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def num_interfaces
|
|
39
|
+
@struct[:bNumInterfaces]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def interfaces
|
|
43
|
+
count = num_interfaces
|
|
44
|
+
base_ptr = @struct[:interface]
|
|
45
|
+
return [] if base_ptr.null?
|
|
46
|
+
|
|
47
|
+
Array.new(count) do |index|
|
|
48
|
+
offset = index * FFIBindings::InterfaceStruct.size
|
|
49
|
+
Interface.new(self, FFIBindings::InterfaceStruct.new(base_ptr + offset))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def each(&block)
|
|
54
|
+
interfaces.each(&block)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extra
|
|
58
|
+
read_extra(@struct[:extra], @struct[:extra_length])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self_powered?
|
|
62
|
+
(attributes & 0x40) != 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def remote_wakeup?
|
|
66
|
+
(attributes & 0x20) != 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def close
|
|
70
|
+
return if @ptr.nil? || @ptr.null?
|
|
71
|
+
|
|
72
|
+
ObjectSpace.undefine_finalizer(self)
|
|
73
|
+
FFIBindings.libusb_free_config_descriptor(@ptr)
|
|
74
|
+
@ptr = FFI::Pointer::NULL
|
|
75
|
+
@struct = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def inspect
|
|
79
|
+
"#<USB::ConfigDescriptor value=#{configuration_value} interfaces=#{num_interfaces}>"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_ptr
|
|
83
|
+
@ptr
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def read_extra(ptr, length)
|
|
89
|
+
return "".b if ptr.null? || length.zero?
|
|
90
|
+
|
|
91
|
+
ptr.read_bytes(length)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module USB
|
|
4
|
+
LIBUSB_SUCCESS = 0
|
|
5
|
+
LIBUSB_ERROR_IO = -1
|
|
6
|
+
LIBUSB_ERROR_INVALID_PARAM = -2
|
|
7
|
+
LIBUSB_ERROR_ACCESS = -3
|
|
8
|
+
LIBUSB_ERROR_NO_DEVICE = -4
|
|
9
|
+
LIBUSB_ERROR_NOT_FOUND = -5
|
|
10
|
+
LIBUSB_ERROR_BUSY = -6
|
|
11
|
+
LIBUSB_ERROR_TIMEOUT = -7
|
|
12
|
+
LIBUSB_ERROR_OVERFLOW = -8
|
|
13
|
+
LIBUSB_ERROR_PIPE = -9
|
|
14
|
+
LIBUSB_ERROR_INTERRUPTED = -10
|
|
15
|
+
LIBUSB_ERROR_NO_MEM = -11
|
|
16
|
+
LIBUSB_ERROR_NOT_SUPPORTED = -12
|
|
17
|
+
LIBUSB_ERROR_OTHER = -99
|
|
18
|
+
|
|
19
|
+
TRANSFER_COMPLETED = 0
|
|
20
|
+
TRANSFER_ERROR = 1
|
|
21
|
+
TRANSFER_TIMED_OUT = 2
|
|
22
|
+
TRANSFER_CANCELLED = 3
|
|
23
|
+
TRANSFER_STALL = 4
|
|
24
|
+
TRANSFER_NO_DEVICE = 5
|
|
25
|
+
TRANSFER_OVERFLOW = 6
|
|
26
|
+
|
|
27
|
+
TRANSFER_TYPE_CONTROL = 0
|
|
28
|
+
TRANSFER_TYPE_ISOCHRONOUS = 1
|
|
29
|
+
TRANSFER_TYPE_BULK = 2
|
|
30
|
+
TRANSFER_TYPE_INTERRUPT = 3
|
|
31
|
+
TRANSFER_TYPE_BULK_STREAM = 4
|
|
32
|
+
|
|
33
|
+
TRANSFER_SHORT_NOT_OK = 1 << 0
|
|
34
|
+
TRANSFER_FREE_BUFFER = 1 << 1
|
|
35
|
+
TRANSFER_FREE_TRANSFER = 1 << 2
|
|
36
|
+
TRANSFER_ADD_ZERO_PACKET = 1 << 3
|
|
37
|
+
|
|
38
|
+
ENDPOINT_IN = 0x80
|
|
39
|
+
ENDPOINT_OUT = 0x00
|
|
40
|
+
|
|
41
|
+
REQUEST_TYPE_STANDARD = 0x00 << 5
|
|
42
|
+
REQUEST_TYPE_CLASS = 0x01 << 5
|
|
43
|
+
REQUEST_TYPE_VENDOR = 0x02 << 5
|
|
44
|
+
REQUEST_TYPE_RESERVED = 0x03 << 5
|
|
45
|
+
|
|
46
|
+
RECIPIENT_DEVICE = 0x00
|
|
47
|
+
RECIPIENT_INTERFACE = 0x01
|
|
48
|
+
RECIPIENT_ENDPOINT = 0x02
|
|
49
|
+
RECIPIENT_OTHER = 0x03
|
|
50
|
+
|
|
51
|
+
REQUEST_GET_STATUS = 0x00
|
|
52
|
+
REQUEST_CLEAR_FEATURE = 0x01
|
|
53
|
+
REQUEST_SET_FEATURE = 0x03
|
|
54
|
+
REQUEST_SET_ADDRESS = 0x05
|
|
55
|
+
REQUEST_GET_DESCRIPTOR = 0x06
|
|
56
|
+
REQUEST_SET_DESCRIPTOR = 0x07
|
|
57
|
+
REQUEST_GET_CONFIGURATION = 0x08
|
|
58
|
+
REQUEST_SET_CONFIGURATION = 0x09
|
|
59
|
+
REQUEST_GET_INTERFACE = 0x0A
|
|
60
|
+
REQUEST_SET_INTERFACE = 0x0B
|
|
61
|
+
REQUEST_SYNCH_FRAME = 0x0C
|
|
62
|
+
REQUEST_SET_SEL = 0x30
|
|
63
|
+
REQUEST_SET_ISOCH_DELAY = 0x31
|
|
64
|
+
|
|
65
|
+
DT_DEVICE = 0x01
|
|
66
|
+
DT_CONFIG = 0x02
|
|
67
|
+
DT_STRING = 0x03
|
|
68
|
+
DT_INTERFACE = 0x04
|
|
69
|
+
DT_ENDPOINT = 0x05
|
|
70
|
+
DT_BOS = 0x0F
|
|
71
|
+
DT_DEVICE_CAPABILITY = 0x10
|
|
72
|
+
DT_SS_ENDPOINT_COMPANION = 0x30
|
|
73
|
+
DT_SUPERSPEED_HUB = 0x2A
|
|
74
|
+
|
|
75
|
+
CLASS_PER_INTERFACE = 0x00
|
|
76
|
+
CLASS_AUDIO = 0x01
|
|
77
|
+
CLASS_COMM = 0x02
|
|
78
|
+
CLASS_HID = 0x03
|
|
79
|
+
CLASS_PHYSICAL = 0x05
|
|
80
|
+
CLASS_IMAGE = 0x06
|
|
81
|
+
CLASS_PRINTER = 0x07
|
|
82
|
+
CLASS_MASS_STORAGE = 0x08
|
|
83
|
+
CLASS_HUB = 0x09
|
|
84
|
+
CLASS_DATA = 0x0A
|
|
85
|
+
CLASS_SMART_CARD = 0x0B
|
|
86
|
+
CLASS_CONTENT_SECURITY = 0x0D
|
|
87
|
+
CLASS_VIDEO = 0x0E
|
|
88
|
+
CLASS_PERSONAL_HEALTHCARE = 0x0F
|
|
89
|
+
CLASS_DIAGNOSTIC_DEVICE = 0xDC
|
|
90
|
+
CLASS_WIRELESS = 0xE0
|
|
91
|
+
CLASS_MISCELLANEOUS = 0xEF
|
|
92
|
+
CLASS_APPLICATION = 0xFE
|
|
93
|
+
CLASS_VENDOR_SPEC = 0xFF
|
|
94
|
+
|
|
95
|
+
SPEED_UNKNOWN = 0
|
|
96
|
+
SPEED_LOW = 1
|
|
97
|
+
SPEED_FULL = 2
|
|
98
|
+
SPEED_HIGH = 3
|
|
99
|
+
SPEED_SUPER = 4
|
|
100
|
+
SPEED_SUPER_PLUS = 5
|
|
101
|
+
|
|
102
|
+
LOG_LEVEL_NONE = 0
|
|
103
|
+
LOG_LEVEL_ERROR = 1
|
|
104
|
+
LOG_LEVEL_WARNING = 2
|
|
105
|
+
LOG_LEVEL_INFO = 3
|
|
106
|
+
LOG_LEVEL_DEBUG = 4
|
|
107
|
+
|
|
108
|
+
HOTPLUG_EVENT_DEVICE_ARRIVED = 0x01
|
|
109
|
+
HOTPLUG_EVENT_DEVICE_LEFT = 0x02
|
|
110
|
+
|
|
111
|
+
HOTPLUG_ENUMERATE = 1 << 0
|
|
112
|
+
HOTPLUG_MATCH_ANY = -1
|
|
113
|
+
|
|
114
|
+
CAP_HAS_CAPABILITY = 0x0000
|
|
115
|
+
CAP_HAS_HOTPLUG = 0x0001
|
|
116
|
+
CAP_HAS_HID_ACCESS = 0x0100
|
|
117
|
+
CAP_SUPPORTS_DETACH_KERNEL_DRIVER = 0x0101
|
|
118
|
+
|
|
119
|
+
OPTION_LOG_LEVEL = 0
|
|
120
|
+
OPTION_USE_USBDK = 1
|
|
121
|
+
OPTION_NO_DEVICE_DISCOVERY = 2
|
|
122
|
+
OPTION_LOG_CB = 3
|
|
123
|
+
|
|
124
|
+
BT_WIRELESS_USB_DEVICE_CAPABILITY = 1
|
|
125
|
+
BT_USB_2_0_EXTENSION = 2
|
|
126
|
+
BT_SS_USB_DEVICE_CAPABILITY = 3
|
|
127
|
+
BT_CONTAINER_ID = 4
|
|
128
|
+
BT_PLATFORM_DESCRIPTOR = 5
|
|
129
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module USB
|
|
4
|
+
class ContainerID
|
|
5
|
+
def self.finalizer(ptr)
|
|
6
|
+
proc do
|
|
7
|
+
FFIBindings.libusb_free_container_id_descriptor(ptr) unless ptr.nil? || ptr.null?
|
|
8
|
+
rescue StandardError
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(ptr)
|
|
13
|
+
raise ArgumentError, "container ID pointer is required" if ptr.nil? || ptr.null?
|
|
14
|
+
|
|
15
|
+
@ptr = ptr
|
|
16
|
+
@struct = FFIBindings::ContainerIDStruct.new(@ptr)
|
|
17
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@ptr))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def container_id
|
|
21
|
+
@struct[:ContainerID].to_a
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def close
|
|
25
|
+
return if @ptr.nil? || @ptr.null?
|
|
26
|
+
|
|
27
|
+
ObjectSpace.undefine_finalizer(self)
|
|
28
|
+
FFIBindings.libusb_free_container_id_descriptor(@ptr)
|
|
29
|
+
@ptr = FFI::Pointer::NULL
|
|
30
|
+
@struct = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
alias free close
|
|
34
|
+
|
|
35
|
+
def to_ptr
|
|
36
|
+
@ptr
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inspect
|
|
40
|
+
"#<USB::ContainerID #{container_id.map { |byte| format('%02x', byte) }.join}>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/usb/context.rb
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module USB
|
|
4
|
+
class Context
|
|
5
|
+
attr_reader :ptr
|
|
6
|
+
|
|
7
|
+
def self.open(**options)
|
|
8
|
+
context = new(**options)
|
|
9
|
+
return context unless block_given?
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
yield context
|
|
13
|
+
ensure
|
|
14
|
+
context.close
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.finalizer(ptr)
|
|
19
|
+
proc do
|
|
20
|
+
FFIBindings.libusb_exit(ptr) unless ptr.nil? || ptr.null?
|
|
21
|
+
rescue StandardError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(options: nil, **kwargs)
|
|
26
|
+
FFIBindings.ensure_loaded!
|
|
27
|
+
options = (options || {}).merge(kwargs)
|
|
28
|
+
|
|
29
|
+
context_ptr = FFI::MemoryPointer.new(:pointer)
|
|
30
|
+
|
|
31
|
+
if !options.empty? && FFIBindings.function_available?(:libusb_init_context)
|
|
32
|
+
Error.raise_on_error(FFIBindings.libusb_init_context(context_ptr, nil, 0))
|
|
33
|
+
else
|
|
34
|
+
Error.raise_on_error(FFIBindings.libusb_init(context_ptr))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@ptr = context_ptr.read_pointer
|
|
38
|
+
@closed = false
|
|
39
|
+
@hotplug_callbacks = {}
|
|
40
|
+
@pollfd_notifiers = {}
|
|
41
|
+
|
|
42
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@ptr))
|
|
43
|
+
|
|
44
|
+
options.each do |option, value|
|
|
45
|
+
set_option(option, value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close
|
|
50
|
+
return if closed?
|
|
51
|
+
|
|
52
|
+
ObjectSpace.undefine_finalizer(self)
|
|
53
|
+
@hotplug_callbacks.keys.each { |handle| deregister_hotplug(handle) }
|
|
54
|
+
FFIBindings.libusb_exit(@ptr)
|
|
55
|
+
@ptr = FFI::Pointer::NULL
|
|
56
|
+
@closed = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def closed?
|
|
60
|
+
@closed || @ptr.nil? || @ptr.null?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def devices(vendor_id: nil, product_id: nil, device_class: nil)
|
|
64
|
+
all = raw_device_list
|
|
65
|
+
all.select! { |device| device.vendor_id == vendor_id } unless vendor_id.nil?
|
|
66
|
+
all.select! { |device| device.product_id == product_id } unless product_id.nil?
|
|
67
|
+
all.select! { |device| device.device_class == device_class } unless device_class.nil?
|
|
68
|
+
all
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def open_device(vendor_id:, product_id:)
|
|
72
|
+
handle_ptr = FFIBindings.libusb_open_device_with_vid_pid(@ptr, vendor_id, product_id)
|
|
73
|
+
return nil if handle_ptr.null?
|
|
74
|
+
|
|
75
|
+
handle = DeviceHandle.new(handle_ptr)
|
|
76
|
+
return handle unless block_given?
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
yield handle
|
|
80
|
+
ensure
|
|
81
|
+
handle.close
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def set_option(option, value = nil)
|
|
86
|
+
normalized_option = normalize_option(option)
|
|
87
|
+
|
|
88
|
+
if FFIBindings.function_available?(:libusb_set_option)
|
|
89
|
+
result =
|
|
90
|
+
if value.nil?
|
|
91
|
+
FFIBindings.libusb_set_option(@ptr, normalized_option)
|
|
92
|
+
else
|
|
93
|
+
FFIBindings.libusb_set_option(@ptr, normalized_option, :int, value)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Error.raise_on_error(result)
|
|
97
|
+
elsif normalized_option == OPTION_LOG_LEVEL && !value.nil?
|
|
98
|
+
FFIBindings.libusb_set_debug(@ptr, value)
|
|
99
|
+
else
|
|
100
|
+
raise NotImplementedError, "libusb_set_option is not available"
|
|
101
|
+
end
|
|
102
|
+
rescue ArgumentError
|
|
103
|
+
raise unless normalized_option == OPTION_LOG_LEVEL && !value.nil?
|
|
104
|
+
|
|
105
|
+
FFIBindings.libusb_set_debug(@ptr, value)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def debug=(level)
|
|
109
|
+
set_option(OPTION_LOG_LEVEL, level)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def has_capability?(capability)
|
|
113
|
+
FFIBindings.libusb_has_capability(capability) != 0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def to_ptr
|
|
117
|
+
@ptr
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def raw_device_list
|
|
123
|
+
list_ptr = FFI::MemoryPointer.new(:pointer)
|
|
124
|
+
count = Error.raise_on_error(FFIBindings.libusb_get_device_list(@ptr, list_ptr))
|
|
125
|
+
base_ptr = list_ptr.read_pointer
|
|
126
|
+
|
|
127
|
+
Array.new(count) do |index|
|
|
128
|
+
device_ptr = base_ptr.get_pointer(index * FFI::Pointer.size)
|
|
129
|
+
Device.new(self, device_ptr)
|
|
130
|
+
end
|
|
131
|
+
ensure
|
|
132
|
+
if defined?(base_ptr) && base_ptr && !base_ptr.null?
|
|
133
|
+
FFIBindings.libusb_free_device_list(base_ptr, 1)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def normalize_option(option)
|
|
138
|
+
case option
|
|
139
|
+
when :log_level then OPTION_LOG_LEVEL
|
|
140
|
+
when :use_usbdk then OPTION_USE_USBDK
|
|
141
|
+
when :no_device_discovery then OPTION_NO_DEVICE_DISCOVERY
|
|
142
|
+
when :log_callback then OPTION_LOG_CB
|
|
143
|
+
else
|
|
144
|
+
option
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|