usbkit 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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/lib/usbkit/alternate_interface.rb +56 -0
- data/lib/usbkit/async.rb +57 -0
- data/lib/usbkit/backend/base.rb +133 -0
- data/lib/usbkit/backend/usb_ruby/device_wrapper.rb +148 -0
- data/lib/usbkit/backend/usb_ruby.rb +316 -0
- data/lib/usbkit/backend/usbfs/ioctl_commands.rb +16 -0
- data/lib/usbkit/backend/usbfs/sysfs_reader.rb +102 -0
- data/lib/usbkit/backend/usbfs.rb +200 -0
- data/lib/usbkit/configuration.rb +36 -0
- data/lib/usbkit/connection_event.rb +41 -0
- data/lib/usbkit/constants.rb +88 -0
- data/lib/usbkit/context.rb +168 -0
- data/lib/usbkit/device.rb +479 -0
- data/lib/usbkit/endpoint.rb +54 -0
- data/lib/usbkit/errors.rb +71 -0
- data/lib/usbkit/filter.rb +51 -0
- data/lib/usbkit/interface.rb +52 -0
- data/lib/usbkit/transfer_results.rb +193 -0
- data/lib/usbkit/usb_configuration.rb +45 -0
- data/lib/usbkit/version.rb +5 -0
- data/lib/usbkit.rb +87 -0
- metadata +79 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a316c9064db6d4e7cf6c446bd732a5b90ebd86b7e93d9770f782a339ebbbf4bc
|
|
4
|
+
data.tar.gz: 176a74ed2d34cd1d354b7d2a2162d324357cbc2d512af93603804774323446ea
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a37ddbfd7d868ab4c1a63e8c69c41c768764ff6a11da7c581f15ad27601835691015bb6d699f62c13b3bbf33c2a1d4b6e2e08a303025ea5d1356db67e237ac15
|
|
7
|
+
data.tar.gz: 959235c30c65d104f73d8d17c420284686c8b69d25e4295b7b67460c2904ce3a93c6d99e789d6015c65bfb9c284c8f2ff5daa42c6946cededfb46398d8ef5bde
|
data/CHANGELOG.md
ADDED
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,152 @@
|
|
|
1
|
+
# UsbKit
|
|
2
|
+
|
|
3
|
+
UsbKit is a Ruby gem that brings a WebUSB-style API to Ruby applications. It exposes `Context`, `Device`, descriptor objects, transfer result objects, hotplug callbacks, and an async wrapper while keeping the public API close to the WebUSB mental model.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your application:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "usbkit"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install it directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install usbkit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
UsbKit uses `usb-ruby` as the primary backend and falls back to Linux `usbfs` where available.
|
|
20
|
+
|
|
21
|
+
## System Requirements
|
|
22
|
+
|
|
23
|
+
- Ruby 3.1+
|
|
24
|
+
- `libusb` when using the `usb-ruby` backend
|
|
25
|
+
|
|
26
|
+
Install `libusb` with one of the following:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
brew install libusb
|
|
30
|
+
sudo apt-get install libusb-1.0-0-dev
|
|
31
|
+
choco install libusb
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require "usbkit"
|
|
38
|
+
|
|
39
|
+
context = UsbKit::Context.new
|
|
40
|
+
devices = context.get_devices
|
|
41
|
+
|
|
42
|
+
devices.each do |device|
|
|
43
|
+
puts "#{device.vendor_id.to_s(16)}:#{device.product_id.to_s(16)} #{device.product_name}"
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Request a device and communicate with it:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "usbkit"
|
|
51
|
+
|
|
52
|
+
context = UsbKit::Context.new
|
|
53
|
+
device = context.request_device(filters: [{ vendor_id: 0x2341 }])
|
|
54
|
+
|
|
55
|
+
device.with_session do |usb_device|
|
|
56
|
+
usb_device.select_configuration(1)
|
|
57
|
+
usb_device.claim_interface(0)
|
|
58
|
+
result = usb_device.transfer_in(1, 64)
|
|
59
|
+
puts result.data if result.status == :ok
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API Overview
|
|
64
|
+
|
|
65
|
+
### `UsbKit::Context`
|
|
66
|
+
|
|
67
|
+
- `get_devices(filters: nil)` enumerates visible devices
|
|
68
|
+
- `request_device(filters:)` returns the first matching device
|
|
69
|
+
- `on(:connect)` and `on(:disconnect)` register hotplug callbacks
|
|
70
|
+
- `start_event_monitoring` and `stop_event_monitoring` manage the polling thread
|
|
71
|
+
|
|
72
|
+
### `UsbKit::Device`
|
|
73
|
+
|
|
74
|
+
- Session: `open`, `close`, `forget`, `with_session`
|
|
75
|
+
- Configuration: `select_configuration`
|
|
76
|
+
- Interfaces: `claim_interface`, `release_interface`, `select_alternate_interface`
|
|
77
|
+
- Transfers: `control_transfer_in`, `control_transfer_out`, `transfer_in`, `transfer_out`
|
|
78
|
+
- Utilities: `clear_halt`, `reset`
|
|
79
|
+
- Async: `async.transfer_in(...)`
|
|
80
|
+
|
|
81
|
+
## WebUSB Mapping
|
|
82
|
+
|
|
83
|
+
| WebUSB | UsbKit |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `navigator.usb` | `UsbKit::Context` |
|
|
86
|
+
| `USBDevice` | `UsbKit::Device` |
|
|
87
|
+
| `USBConfiguration` | `UsbKit::Configuration` |
|
|
88
|
+
| `USBInterface` | `UsbKit::Interface` |
|
|
89
|
+
| `USBAlternateInterface` | `UsbKit::AlternateInterface` |
|
|
90
|
+
| `USBEndpoint` | `UsbKit::Endpoint` |
|
|
91
|
+
|
|
92
|
+
## Backend Selection
|
|
93
|
+
|
|
94
|
+
UsbKit resolves a backend automatically by default:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
UsbKit.backend
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You can pin a backend explicitly:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
UsbKit.configure do |config|
|
|
104
|
+
config.backend = :usb_ruby
|
|
105
|
+
config.transfer_timeout = 2_000
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Available values:
|
|
110
|
+
|
|
111
|
+
- `:auto`
|
|
112
|
+
- `:usb_ruby`
|
|
113
|
+
- `:usbfs` on Linux only
|
|
114
|
+
|
|
115
|
+
## Async Usage
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
future = device.async.transfer_in(1, 64)
|
|
119
|
+
result = future.value(timeout: 1.0)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The async wrapper uses a Ruby `Thread` and returns a `UsbKit::Async::Future`.
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
- `BackendNotAvailableError`: install `usb-ruby` and ensure `libusb` is available.
|
|
127
|
+
- `DeviceAccessError`: fix OS-level permissions for the device node.
|
|
128
|
+
- `DeviceNotOpenedError`: call `open` or use `with_session`.
|
|
129
|
+
- `TimeoutError`: increase `UsbKit.config.transfer_timeout`.
|
|
130
|
+
|
|
131
|
+
On Linux you may need a udev rule for non-root access. On macOS you typically need `libusb` installed via Homebrew.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
Install dependencies and run the verification suite:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
bundle install
|
|
139
|
+
bundle exec rspec
|
|
140
|
+
bundle exec yard doc --output-dir doc/
|
|
141
|
+
gem build usbkit.gemspec
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Examples live in `examples/`.
|
|
145
|
+
|
|
146
|
+
## Contributing
|
|
147
|
+
|
|
148
|
+
Bug reports and pull requests are welcome. Keep changes covered by tests and update the README and changelog when behavior changes.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
Released under the MIT License. See `LICENSE.txt`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
##
|
|
5
|
+
# Immutable USB alternate interface descriptor.
|
|
6
|
+
#
|
|
7
|
+
class AlternateInterface
|
|
8
|
+
# @return [Integer] alternate setting number
|
|
9
|
+
# @return [Integer] interface class code
|
|
10
|
+
# @return [Integer] interface subclass code
|
|
11
|
+
# @return [Integer] interface protocol code
|
|
12
|
+
# @return [String, nil] human-readable interface name
|
|
13
|
+
# @return [Array<Endpoint>] endpoints exposed by this alternate
|
|
14
|
+
attr_reader :alternate_setting,
|
|
15
|
+
:interface_class,
|
|
16
|
+
:interface_subclass,
|
|
17
|
+
:interface_protocol,
|
|
18
|
+
:interface_name,
|
|
19
|
+
:endpoints
|
|
20
|
+
|
|
21
|
+
# @param attrs [Hash]
|
|
22
|
+
# @return [void]
|
|
23
|
+
def initialize(attrs)
|
|
24
|
+
@alternate_setting = attrs.fetch(:alternate_setting)
|
|
25
|
+
@interface_class = attrs.fetch(:interface_class)
|
|
26
|
+
@interface_subclass = attrs.fetch(:interface_subclass)
|
|
27
|
+
@interface_protocol = attrs.fetch(:interface_protocol)
|
|
28
|
+
@interface_name = attrs[:interface_name]
|
|
29
|
+
@endpoints = Array(attrs.fetch(:endpoints, [])).map { |endpoint| build_endpoint(endpoint) }.freeze
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Hash]
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
alternate_setting: alternate_setting,
|
|
37
|
+
interface_class: interface_class,
|
|
38
|
+
interface_subclass: interface_subclass,
|
|
39
|
+
interface_protocol: interface_protocol,
|
|
40
|
+
interface_name: interface_name,
|
|
41
|
+
endpoints: endpoints.map(&:to_h)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String]
|
|
46
|
+
def inspect
|
|
47
|
+
"#<#{self.class} alternate_setting=#{alternate_setting.inspect} endpoints=#{endpoints.size}>"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_endpoint(endpoint)
|
|
53
|
+
endpoint.is_a?(Endpoint) ? endpoint : Endpoint.new(endpoint)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/usbkit/async.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
module Async
|
|
5
|
+
##
|
|
6
|
+
# Thread-backed future for asynchronous device operations.
|
|
7
|
+
#
|
|
8
|
+
class Future
|
|
9
|
+
# @yieldreturn [Object]
|
|
10
|
+
# @return [void]
|
|
11
|
+
def initialize(&block)
|
|
12
|
+
@thread = Thread.new(&block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param timeout [Numeric, nil]
|
|
16
|
+
# @raise [TimeoutError] when the background operation does not finish in time
|
|
17
|
+
# @return [Object]
|
|
18
|
+
def value(timeout: nil)
|
|
19
|
+
@thread.join(timeout)
|
|
20
|
+
raise TimeoutError, "Async operation timed out" if @thread.alive?
|
|
21
|
+
|
|
22
|
+
@thread.value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def ready?
|
|
27
|
+
!@thread.alive?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Async wrapper around transfer-oriented device methods.
|
|
33
|
+
#
|
|
34
|
+
class AsyncProxy
|
|
35
|
+
TRANSFER_METHODS = %i[
|
|
36
|
+
transfer_in
|
|
37
|
+
transfer_out
|
|
38
|
+
control_transfer_in
|
|
39
|
+
control_transfer_out
|
|
40
|
+
isochronous_transfer_in
|
|
41
|
+
isochronous_transfer_out
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
# @param device [Device]
|
|
45
|
+
# @return [void]
|
|
46
|
+
def initialize(device)
|
|
47
|
+
@device = device
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
TRANSFER_METHODS.each do |method_name|
|
|
51
|
+
define_method(method_name) do |*args|
|
|
52
|
+
Future.new { @device.public_send(method_name, *args) }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
module Backend
|
|
5
|
+
##
|
|
6
|
+
# Abstract backend contract for USB operations.
|
|
7
|
+
#
|
|
8
|
+
class Base
|
|
9
|
+
# @return [Array<Hash>]
|
|
10
|
+
def enumerate_devices
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param device_handle [Object]
|
|
15
|
+
# @return [Object]
|
|
16
|
+
def open_device(device_handle)
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param device_handle [Object]
|
|
21
|
+
# @return [void]
|
|
22
|
+
def close_device(device_handle)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param device_handle [Object]
|
|
27
|
+
# @param config_value [Integer]
|
|
28
|
+
# @return [void]
|
|
29
|
+
def set_configuration(device_handle, config_value)
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param device_handle [Object]
|
|
34
|
+
# @param interface_number [Integer]
|
|
35
|
+
# @return [void]
|
|
36
|
+
def claim_interface(device_handle, interface_number)
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param device_handle [Object]
|
|
41
|
+
# @param interface_number [Integer]
|
|
42
|
+
# @return [void]
|
|
43
|
+
def release_interface(device_handle, interface_number)
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param device_handle [Object]
|
|
48
|
+
# @param interface_number [Integer]
|
|
49
|
+
# @param alternate_setting [Integer]
|
|
50
|
+
# @return [void]
|
|
51
|
+
def set_alternate_interface(device_handle, interface_number, alternate_setting)
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param device_handle [Object]
|
|
56
|
+
# @param setup [Hash]
|
|
57
|
+
# @param length [Integer]
|
|
58
|
+
# @return [Hash]
|
|
59
|
+
def control_transfer_in(device_handle, setup, length)
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param device_handle [Object]
|
|
64
|
+
# @param setup [Hash]
|
|
65
|
+
# @param data [String, nil]
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
def control_transfer_out(device_handle, setup, data)
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param device_handle [Object]
|
|
72
|
+
# @param endpoint [Integer]
|
|
73
|
+
# @param length [Integer]
|
|
74
|
+
# @param timeout [Integer]
|
|
75
|
+
# @return [Hash]
|
|
76
|
+
def bulk_transfer_in(device_handle, endpoint, length, timeout:)
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param device_handle [Object]
|
|
81
|
+
# @param endpoint [Integer]
|
|
82
|
+
# @param data [String]
|
|
83
|
+
# @param timeout [Integer]
|
|
84
|
+
# @return [Hash]
|
|
85
|
+
def bulk_transfer_out(device_handle, endpoint, data, timeout:)
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param device_handle [Object]
|
|
90
|
+
# @param endpoint [Integer]
|
|
91
|
+
# @param packet_lengths [Array<Integer>]
|
|
92
|
+
# @return [Hash]
|
|
93
|
+
def isochronous_transfer_in(device_handle, endpoint, packet_lengths)
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @param device_handle [Object]
|
|
98
|
+
# @param endpoint [Integer]
|
|
99
|
+
# @param data [String]
|
|
100
|
+
# @param packet_lengths [Array<Integer>]
|
|
101
|
+
# @return [Hash]
|
|
102
|
+
def isochronous_transfer_out(device_handle, endpoint, data, packet_lengths)
|
|
103
|
+
raise NotImplementedError
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @param device_handle [Object]
|
|
107
|
+
# @param endpoint [Integer]
|
|
108
|
+
# @return [void]
|
|
109
|
+
def clear_halt(device_handle, endpoint)
|
|
110
|
+
raise NotImplementedError
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @param device_handle [Object]
|
|
114
|
+
# @return [void]
|
|
115
|
+
def reset_device(device_handle)
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @param device_handle [Object]
|
|
120
|
+
# @return [Hash]
|
|
121
|
+
def get_device_descriptor(device_handle)
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @param device_handle [Object]
|
|
126
|
+
# @param index [Integer]
|
|
127
|
+
# @return [Object]
|
|
128
|
+
def get_configuration_descriptor(device_handle, index)
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
module Backend
|
|
5
|
+
class UsbRuby
|
|
6
|
+
##
|
|
7
|
+
# Adapter that normalizes usb-ruby descriptors into UsbKit hashes.
|
|
8
|
+
#
|
|
9
|
+
class DeviceWrapper
|
|
10
|
+
ENDPOINT_TYPE_MAP = {
|
|
11
|
+
0 => :control,
|
|
12
|
+
1 => EndpointType::ISOCHRONOUS,
|
|
13
|
+
2 => EndpointType::BULK,
|
|
14
|
+
3 => EndpointType::INTERRUPT
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# @param usb_device [Object]
|
|
18
|
+
# @return [void]
|
|
19
|
+
def initialize(usb_device)
|
|
20
|
+
@usb_device = usb_device
|
|
21
|
+
@descriptor = usb_device.device_descriptor
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Hash]
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
raw_device: @usb_device,
|
|
28
|
+
vendor_id: value_for(@descriptor, :vendor_id, :id_vendor),
|
|
29
|
+
product_id: value_for(@descriptor, :product_id, :id_product),
|
|
30
|
+
device_class: value_for(@descriptor, :device_class, :b_device_class, default: 0),
|
|
31
|
+
device_subclass: value_for(@descriptor, :device_sub_class, :device_subclass, :b_device_sub_class, default: 0),
|
|
32
|
+
device_protocol: value_for(@descriptor, :device_protocol, :b_device_protocol, default: 0),
|
|
33
|
+
device_version_major: bcd_major(value_for(@descriptor, :bcd_device, default: 0)),
|
|
34
|
+
device_version_minor: bcd_minor(value_for(@descriptor, :bcd_device, default: 0)),
|
|
35
|
+
device_version_subminor: bcd_subminor(value_for(@descriptor, :bcd_device, default: 0)),
|
|
36
|
+
usb_version_major: bcd_major(value_for(@descriptor, :bcd_usb, default: 0)),
|
|
37
|
+
usb_version_minor: bcd_minor(value_for(@descriptor, :bcd_usb, default: 0)),
|
|
38
|
+
usb_version_subminor: bcd_subminor(value_for(@descriptor, :bcd_usb, default: 0)),
|
|
39
|
+
manufacturer_name: value_for(@usb_device, :manufacturer_name, :manufacturer, default: nil),
|
|
40
|
+
product_name: value_for(@usb_device, :product_name, :product, default: nil),
|
|
41
|
+
serial_number: value_for(@usb_device, :serial_number, default: nil),
|
|
42
|
+
configurations: extract_configurations,
|
|
43
|
+
configuration_value: nil,
|
|
44
|
+
bus_number: value_for(@usb_device, :bus_number, default: nil),
|
|
45
|
+
device_address: value_for(@usb_device, :device_address, :address, default: nil)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def extract_configurations
|
|
52
|
+
configurations = if @usb_device.respond_to?(:configurations)
|
|
53
|
+
Array(@usb_device.configurations)
|
|
54
|
+
else
|
|
55
|
+
count = value_for(@descriptor, :num_configurations, :b_num_configurations, default: 0)
|
|
56
|
+
Array.new(count) { |index| @usb_device.config_descriptor(index) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
configurations.map { |configuration| extract_configuration(configuration) }
|
|
60
|
+
rescue StandardError
|
|
61
|
+
[]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract_configuration(configuration)
|
|
65
|
+
{
|
|
66
|
+
configuration_value: value_for(configuration, :configuration_value, :b_configuration_value, default: 0),
|
|
67
|
+
configuration_name: value_for(configuration, :configuration_name, :description, default: nil),
|
|
68
|
+
interfaces: extract_interfaces(configuration)
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def extract_interfaces(configuration)
|
|
73
|
+
interfaces = collection_for(configuration, :interfaces, :interface_descriptors)
|
|
74
|
+
|
|
75
|
+
interfaces.each_with_index.map do |interface_descriptor, index|
|
|
76
|
+
alternates = collection_for(interface_descriptor, :alternates, :alt_settings, :alternate_settings)
|
|
77
|
+
alternates = [interface_descriptor] if alternates.empty?
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
interface_number: value_for(interface_descriptor, :interface_number, :b_interface_number, default: index),
|
|
81
|
+
alternate_setting: value_for(alternates.first, :alternate_setting, :b_alternate_setting, default: 0),
|
|
82
|
+
alternates: alternates.map { |alternate| extract_alternate(alternate) }
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_alternate(alternate)
|
|
88
|
+
{
|
|
89
|
+
alternate_setting: value_for(alternate, :alternate_setting, :b_alternate_setting, default: 0),
|
|
90
|
+
interface_class: value_for(alternate, :interface_class, :b_interface_class, default: 0),
|
|
91
|
+
interface_subclass: value_for(alternate, :interface_subclass, :b_interface_sub_class, default: 0),
|
|
92
|
+
interface_protocol: value_for(alternate, :interface_protocol, :b_interface_protocol, default: 0),
|
|
93
|
+
interface_name: value_for(alternate, :interface_name, :description, default: nil),
|
|
94
|
+
endpoints: extract_endpoints(alternate)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def extract_endpoints(alternate)
|
|
99
|
+
collection_for(alternate, :endpoints, :endpoint_descriptors).map do |endpoint|
|
|
100
|
+
address = value_for(endpoint, :endpoint_address, :b_endpoint_address, default: 0)
|
|
101
|
+
{
|
|
102
|
+
endpoint_number: address & 0x0F,
|
|
103
|
+
direction: (address & 0x80).zero? ? Direction::OUT : Direction::IN,
|
|
104
|
+
type: endpoint_type(endpoint),
|
|
105
|
+
packet_size: value_for(endpoint, :max_packet_size, :w_max_packet_size, default: 0)
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def endpoint_type(endpoint)
|
|
111
|
+
raw_type = value_for(endpoint, :transfer_type, :attributes, default: EndpointType::BULK)
|
|
112
|
+
return raw_type if EndpointType.valid?(raw_type)
|
|
113
|
+
|
|
114
|
+
mapped = ENDPOINT_TYPE_MAP[raw_type.to_i & 0x03]
|
|
115
|
+
EndpointType.valid?(mapped) ? mapped : EndpointType::BULK
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def collection_for(object, *methods)
|
|
119
|
+
methods.each do |method_name|
|
|
120
|
+
return Array(object.public_send(method_name)) if object.respond_to?(method_name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
[]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def value_for(object, *methods, default: nil)
|
|
127
|
+
methods.each do |method_name|
|
|
128
|
+
return object.public_send(method_name) if object.respond_to?(method_name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
default
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def bcd_major(value)
|
|
135
|
+
(value >> 8) & 0xFF
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def bcd_minor(value)
|
|
139
|
+
(value >> 4) & 0x0F
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def bcd_subminor(value)
|
|
143
|
+
value & 0x0F
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|