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 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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
@@ -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