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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "usb"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "base"
|
|
10
|
+
|
|
11
|
+
module UsbKit
|
|
12
|
+
module Backend
|
|
13
|
+
class UsbRuby < Base
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require_relative "usb_ruby/device_wrapper"
|
|
19
|
+
|
|
20
|
+
module UsbKit
|
|
21
|
+
module Backend
|
|
22
|
+
##
|
|
23
|
+
# usb-ruby backed implementation of the UsbKit backend contract.
|
|
24
|
+
#
|
|
25
|
+
class UsbRuby
|
|
26
|
+
REQUEST_TYPE_BITS = {
|
|
27
|
+
standard: 0x00,
|
|
28
|
+
class: 0x20,
|
|
29
|
+
vendor: 0x40
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
RECIPIENT_BITS = {
|
|
33
|
+
device: 0x00,
|
|
34
|
+
interface: 0x01,
|
|
35
|
+
endpoint: 0x02,
|
|
36
|
+
other: 0x03
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# @param usb_context [Object, nil]
|
|
40
|
+
# @param mutex [Mutex]
|
|
41
|
+
# @raise [BackendNotAvailableError] when usb-ruby is not available
|
|
42
|
+
# @return [void]
|
|
43
|
+
def initialize(usb_context: nil, mutex: Mutex.new)
|
|
44
|
+
raise BackendNotAvailableError, "usb-ruby backend not available" unless defined?(USB::Context)
|
|
45
|
+
|
|
46
|
+
@mutex = mutex
|
|
47
|
+
@usb_context = usb_context || USB::Context.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [void]
|
|
51
|
+
def finalize
|
|
52
|
+
synchronize do
|
|
53
|
+
@usb_context&.close if @usb_context.respond_to?(:close)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Array<Hash>]
|
|
58
|
+
def enumerate_devices
|
|
59
|
+
synchronize do
|
|
60
|
+
Array(@usb_context.devices).map { |usb_device| DeviceWrapper.new(usb_device).to_h }
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => error
|
|
63
|
+
raise map_error(error)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param raw_device [Object]
|
|
67
|
+
# @return [Object]
|
|
68
|
+
def open_device(raw_device)
|
|
69
|
+
synchronize { raw_device.open }
|
|
70
|
+
rescue StandardError => error
|
|
71
|
+
raise map_error(error)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param device_handle [Object]
|
|
75
|
+
# @return [void]
|
|
76
|
+
def close_device(device_handle)
|
|
77
|
+
synchronize { device_handle.close }
|
|
78
|
+
rescue StandardError => error
|
|
79
|
+
raise map_error(error)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param device_handle [Object]
|
|
83
|
+
# @param config_value [Integer]
|
|
84
|
+
# @return [void]
|
|
85
|
+
def set_configuration(device_handle, config_value)
|
|
86
|
+
synchronize { device_handle.set_configuration(config_value) }
|
|
87
|
+
rescue StandardError => error
|
|
88
|
+
raise map_error(error)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @param device_handle [Object]
|
|
92
|
+
# @param interface_number [Integer]
|
|
93
|
+
# @return [void]
|
|
94
|
+
def claim_interface(device_handle, interface_number)
|
|
95
|
+
synchronize { device_handle.claim_interface(interface_number) }
|
|
96
|
+
rescue StandardError => error
|
|
97
|
+
raise map_error(error)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @param device_handle [Object]
|
|
101
|
+
# @param interface_number [Integer]
|
|
102
|
+
# @return [void]
|
|
103
|
+
def release_interface(device_handle, interface_number)
|
|
104
|
+
synchronize { device_handle.release_interface(interface_number) }
|
|
105
|
+
rescue StandardError => error
|
|
106
|
+
raise map_error(error)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @param device_handle [Object]
|
|
110
|
+
# @param interface_number [Integer]
|
|
111
|
+
# @param alternate_setting [Integer]
|
|
112
|
+
# @return [void]
|
|
113
|
+
def set_alternate_interface(device_handle, interface_number, alternate_setting)
|
|
114
|
+
synchronize { device_handle.set_interface_alt_setting(interface_number, alternate_setting) }
|
|
115
|
+
rescue StandardError => error
|
|
116
|
+
raise map_error(error)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @param device_handle [Object]
|
|
120
|
+
# @param setup [Hash]
|
|
121
|
+
# @param length [Integer]
|
|
122
|
+
# @return [Hash]
|
|
123
|
+
def control_transfer_in(device_handle, setup, length)
|
|
124
|
+
response = synchronize do
|
|
125
|
+
device_handle.control_transfer(
|
|
126
|
+
bmRequestType: build_request_type(setup, :in),
|
|
127
|
+
bRequest: setup[:request],
|
|
128
|
+
wValue: setup[:value],
|
|
129
|
+
wIndex: setup[:index],
|
|
130
|
+
dataIn: length
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
{ status: TransferStatus::OK, data: response }
|
|
135
|
+
rescue StandardError => error
|
|
136
|
+
raise map_error(error)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @param device_handle [Object]
|
|
140
|
+
# @param setup [Hash]
|
|
141
|
+
# @param data [String, nil]
|
|
142
|
+
# @return [Hash]
|
|
143
|
+
def control_transfer_out(device_handle, setup, data)
|
|
144
|
+
bytes_written = synchronize do
|
|
145
|
+
params = {
|
|
146
|
+
bmRequestType: build_request_type(setup, :out),
|
|
147
|
+
bRequest: setup[:request],
|
|
148
|
+
wValue: setup[:value],
|
|
149
|
+
wIndex: setup[:index]
|
|
150
|
+
}
|
|
151
|
+
params[:dataOut] = data if data
|
|
152
|
+
device_handle.control_transfer(**params)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
{ status: TransferStatus::OK, bytes_written: bytes_written || data&.bytesize.to_i }
|
|
156
|
+
rescue StandardError => error
|
|
157
|
+
raise map_error(error)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @param device_handle [Object]
|
|
161
|
+
# @param endpoint [Integer]
|
|
162
|
+
# @param length [Integer]
|
|
163
|
+
# @param timeout [Integer]
|
|
164
|
+
# @return [Hash]
|
|
165
|
+
def bulk_transfer_in(device_handle, endpoint, length, timeout:)
|
|
166
|
+
data = synchronize do
|
|
167
|
+
device_handle.bulk_transfer(
|
|
168
|
+
endpoint: endpoint | 0x80,
|
|
169
|
+
dataIn: length,
|
|
170
|
+
timeout: timeout
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
{ status: TransferStatus::OK, data: data }
|
|
175
|
+
rescue StandardError => error
|
|
176
|
+
raise map_error(error)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @param device_handle [Object]
|
|
180
|
+
# @param endpoint [Integer]
|
|
181
|
+
# @param data [String]
|
|
182
|
+
# @param timeout [Integer]
|
|
183
|
+
# @return [Hash]
|
|
184
|
+
def bulk_transfer_out(device_handle, endpoint, data, timeout:)
|
|
185
|
+
bytes_written = synchronize do
|
|
186
|
+
device_handle.bulk_transfer(
|
|
187
|
+
endpoint: endpoint,
|
|
188
|
+
dataOut: data,
|
|
189
|
+
timeout: timeout
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
{ status: TransferStatus::OK, bytes_written: bytes_written || data.bytesize }
|
|
194
|
+
rescue StandardError => error
|
|
195
|
+
raise map_error(error)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @param device_handle [Object]
|
|
199
|
+
# @param endpoint [Integer]
|
|
200
|
+
# @param packet_lengths [Array<Integer>]
|
|
201
|
+
# @raise [NotImplementedError] when usb-ruby has no isochronous support
|
|
202
|
+
# @return [Hash]
|
|
203
|
+
def isochronous_transfer_in(device_handle, endpoint, packet_lengths)
|
|
204
|
+
raise NotImplementedError, "isochronous transfers require usb-ruby isochronous support" unless device_handle.respond_to?(:isochronous_transfer)
|
|
205
|
+
|
|
206
|
+
transfer = synchronize do
|
|
207
|
+
device_handle.isochronous_transfer(endpoint: endpoint | 0x80, packet_lengths: packet_lengths)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
{
|
|
211
|
+
data: transfer[:data],
|
|
212
|
+
packets: Array(transfer[:packets]).map { |packet| packet.merge(status: packet.fetch(:status, TransferStatus::OK)) }
|
|
213
|
+
}
|
|
214
|
+
rescue NotImplementedError
|
|
215
|
+
raise
|
|
216
|
+
rescue StandardError => error
|
|
217
|
+
raise map_error(error)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @param device_handle [Object]
|
|
221
|
+
# @param endpoint [Integer]
|
|
222
|
+
# @param data [String]
|
|
223
|
+
# @param packet_lengths [Array<Integer>]
|
|
224
|
+
# @raise [NotImplementedError] when usb-ruby has no isochronous support
|
|
225
|
+
# @return [Hash]
|
|
226
|
+
def isochronous_transfer_out(device_handle, endpoint, data, packet_lengths)
|
|
227
|
+
raise NotImplementedError, "isochronous transfers require usb-ruby isochronous support" unless device_handle.respond_to?(:isochronous_transfer)
|
|
228
|
+
|
|
229
|
+
transfer = synchronize do
|
|
230
|
+
device_handle.isochronous_transfer(
|
|
231
|
+
endpoint: endpoint,
|
|
232
|
+
dataOut: data,
|
|
233
|
+
packet_lengths: packet_lengths
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
{
|
|
238
|
+
packets: Array(transfer[:packets]).map do |packet|
|
|
239
|
+
packet.merge(status: packet.fetch(:status, TransferStatus::OK))
|
|
240
|
+
end
|
|
241
|
+
}
|
|
242
|
+
rescue NotImplementedError
|
|
243
|
+
raise
|
|
244
|
+
rescue StandardError => error
|
|
245
|
+
raise map_error(error)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# @param device_handle [Object]
|
|
249
|
+
# @param endpoint [Integer]
|
|
250
|
+
# @return [void]
|
|
251
|
+
def clear_halt(device_handle, endpoint)
|
|
252
|
+
synchronize { device_handle.clear_halt(endpoint) }
|
|
253
|
+
rescue StandardError => error
|
|
254
|
+
raise map_error(error)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# @param device_handle [Object]
|
|
258
|
+
# @return [void]
|
|
259
|
+
def reset_device(device_handle)
|
|
260
|
+
synchronize { device_handle.reset_device }
|
|
261
|
+
rescue StandardError => error
|
|
262
|
+
raise map_error(error)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @param raw_device [Object]
|
|
266
|
+
# @return [Hash]
|
|
267
|
+
def get_device_descriptor(raw_device)
|
|
268
|
+
DeviceWrapper.new(raw_device).to_h
|
|
269
|
+
rescue StandardError => error
|
|
270
|
+
raise map_error(error)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# @param raw_device [Object]
|
|
274
|
+
# @param index [Integer]
|
|
275
|
+
# @return [Object]
|
|
276
|
+
def get_configuration_descriptor(raw_device, index)
|
|
277
|
+
synchronize do
|
|
278
|
+
if raw_device.respond_to?(:config_descriptor)
|
|
279
|
+
raw_device.config_descriptor(index)
|
|
280
|
+
else
|
|
281
|
+
Array(raw_device.configurations).fetch(index)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
rescue StandardError => error
|
|
285
|
+
raise map_error(error)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def synchronize(&block)
|
|
291
|
+
@mutex.synchronize(&block)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def build_request_type(setup, direction)
|
|
295
|
+
direction_bit = direction == :in ? 0x80 : 0x00
|
|
296
|
+
direction_bit | REQUEST_TYPE_BITS.fetch(setup[:request_type]) | RECIPIENT_BITS.fetch(setup[:recipient])
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def map_error(error)
|
|
300
|
+
return error if error.is_a?(UsbKit::Error)
|
|
301
|
+
|
|
302
|
+
message = error.message.to_s
|
|
303
|
+
lowered = message.downcase
|
|
304
|
+
|
|
305
|
+
return TimeoutError.new(message) if lowered.include?("timeout")
|
|
306
|
+
return DeviceBusyError.new(message) if lowered.include?("busy")
|
|
307
|
+
return DeviceAccessError.new(message) if lowered.include?("access") || lowered.include?("permission")
|
|
308
|
+
return DeviceNotFoundError.new(message) if lowered.include?("not found")
|
|
309
|
+
return TransferError.new(message, status: :stall) if lowered.include?("stall")
|
|
310
|
+
return TransferError.new(message, status: :babble) if lowered.include?("babble")
|
|
311
|
+
|
|
312
|
+
Error.new(message.empty? ? error.class.name : message)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
module Backend
|
|
5
|
+
class Usbfs
|
|
6
|
+
USBDEVFS_CONTROL = 0xC0185500
|
|
7
|
+
USBDEVFS_BULK = 0xC0185502
|
|
8
|
+
USBDEVFS_SETINTF = 0x80085504
|
|
9
|
+
USBDEVFS_SETCONFIG = 0x80045505
|
|
10
|
+
USBDEVFS_CLAIMINTF = 0x8004550F
|
|
11
|
+
USBDEVFS_RELEASEINTF = 0x80045510
|
|
12
|
+
USBDEVFS_CLEAR_HALT = 0x80045515
|
|
13
|
+
USBDEVFS_RESET = 0x00005514
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
module Backend
|
|
5
|
+
class Usbfs
|
|
6
|
+
##
|
|
7
|
+
# Reads USB device metadata from Linux sysfs.
|
|
8
|
+
#
|
|
9
|
+
class SysfsReader
|
|
10
|
+
SYSFS_PATH = "/sys/bus/usb/devices"
|
|
11
|
+
|
|
12
|
+
# @param sysfs_path [String]
|
|
13
|
+
# @return [void]
|
|
14
|
+
def initialize(sysfs_path: SYSFS_PATH)
|
|
15
|
+
@sysfs_path = sysfs_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Array<Hash>]
|
|
19
|
+
def enumerate
|
|
20
|
+
Dir.glob(File.join(@sysfs_path, "[0-9]*-[0-9]*")).filter_map do |path|
|
|
21
|
+
next unless File.exist?(File.join(path, "idVendor"))
|
|
22
|
+
|
|
23
|
+
read_device_info(path)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def read_device_info(path)
|
|
30
|
+
{
|
|
31
|
+
raw_device: {
|
|
32
|
+
sysfs_path: path,
|
|
33
|
+
dev_path: derive_dev_path(path)
|
|
34
|
+
},
|
|
35
|
+
vendor_id: read_hex(path, "idVendor"),
|
|
36
|
+
product_id: read_hex(path, "idProduct"),
|
|
37
|
+
device_class: read_hex(path, "bDeviceClass"),
|
|
38
|
+
device_subclass: read_hex(path, "bDeviceSubClass"),
|
|
39
|
+
device_protocol: read_hex(path, "bDeviceProtocol"),
|
|
40
|
+
device_version_major: bcd_major(read_hex(path, "bcdDevice")),
|
|
41
|
+
device_version_minor: bcd_minor(read_hex(path, "bcdDevice")),
|
|
42
|
+
device_version_subminor: bcd_subminor(read_hex(path, "bcdDevice")),
|
|
43
|
+
usb_version_major: bcd_major(read_hex(path, "version")),
|
|
44
|
+
usb_version_minor: bcd_minor(read_hex(path, "version")),
|
|
45
|
+
usb_version_subminor: bcd_subminor(read_hex(path, "version")),
|
|
46
|
+
manufacturer_name: read_string(path, "manufacturer"),
|
|
47
|
+
product_name: read_string(path, "product"),
|
|
48
|
+
serial_number: read_string(path, "serial"),
|
|
49
|
+
configurations: [],
|
|
50
|
+
configuration_value: nil,
|
|
51
|
+
bus_number: read_decimal(path, "busnum"),
|
|
52
|
+
device_address: read_decimal(path, "devnum")
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def derive_dev_path(path)
|
|
57
|
+
bus_number = read_decimal(path, "busnum")
|
|
58
|
+
device_number = read_decimal(path, "devnum")
|
|
59
|
+
return nil unless bus_number && device_number
|
|
60
|
+
|
|
61
|
+
format("/dev/bus/usb/%<bus>03d/%<device>03d", bus: bus_number, device: device_number)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def read_hex(path, attr)
|
|
65
|
+
content = read_string(path, attr)
|
|
66
|
+
return 0 if content.nil? || content.empty?
|
|
67
|
+
|
|
68
|
+
content.include?(".") ? decimal_version_to_bcd(content) : content.to_i(16)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def read_decimal(path, attr)
|
|
72
|
+
Integer(read_string(path, attr), 10)
|
|
73
|
+
rescue ArgumentError, TypeError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def read_string(path, attr)
|
|
78
|
+
File.read(File.join(path, attr)).strip
|
|
79
|
+
rescue Errno::ENOENT
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def decimal_version_to_bcd(content)
|
|
84
|
+
major, minor = content.split(".", 2)
|
|
85
|
+
((major.to_i & 0xFF) << 8) | ((minor.to_i & 0x0F) << 4)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def bcd_major(value)
|
|
89
|
+
(value >> 8) & 0xFF
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def bcd_minor(value)
|
|
93
|
+
(value >> 4) & 0x0F
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def bcd_subminor(value)
|
|
97
|
+
value & 0x0F
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module UsbKit
|
|
6
|
+
module Backend
|
|
7
|
+
class Usbfs < Base
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative "usbfs/ioctl_commands"
|
|
13
|
+
require_relative "usbfs/sysfs_reader"
|
|
14
|
+
|
|
15
|
+
module UsbKit
|
|
16
|
+
module Backend
|
|
17
|
+
##
|
|
18
|
+
# Linux usbfs fallback backend.
|
|
19
|
+
#
|
|
20
|
+
class Usbfs
|
|
21
|
+
REQUEST_TYPE_BITS = {
|
|
22
|
+
standard: 0x00,
|
|
23
|
+
class: 0x20,
|
|
24
|
+
vendor: 0x40
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
RECIPIENT_BITS = {
|
|
28
|
+
device: 0x00,
|
|
29
|
+
interface: 0x01,
|
|
30
|
+
endpoint: 0x02,
|
|
31
|
+
other: 0x03
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# @param sysfs_reader [SysfsReader]
|
|
35
|
+
# @param file_class [Class]
|
|
36
|
+
# @param linux [Boolean]
|
|
37
|
+
# @raise [BackendNotAvailableError] unless running on Linux
|
|
38
|
+
# @return [void]
|
|
39
|
+
def initialize(sysfs_reader: SysfsReader.new, file_class: File, linux: RUBY_PLATFORM.match?(/linux/i))
|
|
40
|
+
raise BackendNotAvailableError, "usbfs backend is only available on Linux" unless linux
|
|
41
|
+
|
|
42
|
+
@sysfs_reader = sysfs_reader
|
|
43
|
+
@file_class = file_class
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Array<Hash>]
|
|
47
|
+
def enumerate_devices
|
|
48
|
+
@sysfs_reader.enumerate
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param raw_device [Hash, String]
|
|
52
|
+
# @raise [DeviceAccessError, DeviceNotFoundError]
|
|
53
|
+
# @return [File]
|
|
54
|
+
def open_device(raw_device)
|
|
55
|
+
path = raw_device.is_a?(Hash) ? raw_device[:dev_path] : raw_device
|
|
56
|
+
raise DeviceNotFoundError, "usbfs device path is unavailable" unless path
|
|
57
|
+
|
|
58
|
+
@file_class.open(path, "rb+")
|
|
59
|
+
rescue Errno::EACCES => error
|
|
60
|
+
raise DeviceAccessError, error.message
|
|
61
|
+
rescue Errno::ENOENT => error
|
|
62
|
+
raise DeviceNotFoundError, error.message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param device_handle [File]
|
|
66
|
+
# @return [void]
|
|
67
|
+
def close_device(device_handle)
|
|
68
|
+
device_handle.close
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param device_handle [File]
|
|
72
|
+
# @param config_value [Integer]
|
|
73
|
+
# @return [Integer]
|
|
74
|
+
def set_configuration(device_handle, config_value)
|
|
75
|
+
device_handle.ioctl(self.class::USBDEVFS_SETCONFIG, [config_value].pack("I"))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param device_handle [File]
|
|
79
|
+
# @param interface_number [Integer]
|
|
80
|
+
# @return [Integer]
|
|
81
|
+
def claim_interface(device_handle, interface_number)
|
|
82
|
+
device_handle.ioctl(self.class::USBDEVFS_CLAIMINTF, [interface_number].pack("I"))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param device_handle [File]
|
|
86
|
+
# @param interface_number [Integer]
|
|
87
|
+
# @return [Integer]
|
|
88
|
+
def release_interface(device_handle, interface_number)
|
|
89
|
+
device_handle.ioctl(self.class::USBDEVFS_RELEASEINTF, [interface_number].pack("I"))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param device_handle [File]
|
|
93
|
+
# @param interface_number [Integer]
|
|
94
|
+
# @param alternate_setting [Integer]
|
|
95
|
+
# @return [Integer]
|
|
96
|
+
def set_alternate_interface(device_handle, interface_number, alternate_setting)
|
|
97
|
+
payload = [interface_number, alternate_setting].pack("II")
|
|
98
|
+
device_handle.ioctl(self.class::USBDEVFS_SETINTF, payload)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @param device_handle [File]
|
|
102
|
+
# @param setup [Hash]
|
|
103
|
+
# @param length [Integer]
|
|
104
|
+
# @return [Hash]
|
|
105
|
+
def control_transfer_in(device_handle, setup, length)
|
|
106
|
+
payload = build_control_payload(setup, :in, length: length)
|
|
107
|
+
device_handle.ioctl(self.class::USBDEVFS_CONTROL, payload)
|
|
108
|
+
{ status: TransferStatus::OK, data: device_handle.read(length) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param device_handle [File]
|
|
112
|
+
# @param setup [Hash]
|
|
113
|
+
# @param data [String, nil]
|
|
114
|
+
# @return [Hash]
|
|
115
|
+
def control_transfer_out(device_handle, setup, data)
|
|
116
|
+
payload = build_control_payload(setup, :out, data: data.to_s)
|
|
117
|
+
device_handle.ioctl(self.class::USBDEVFS_CONTROL, payload)
|
|
118
|
+
{ status: TransferStatus::OK, bytes_written: data.to_s.bytesize }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @param device_handle [File]
|
|
122
|
+
# @param endpoint [Integer]
|
|
123
|
+
# @param length [Integer]
|
|
124
|
+
# @param timeout [Integer]
|
|
125
|
+
# @return [Hash]
|
|
126
|
+
def bulk_transfer_in(device_handle, endpoint, length, timeout:)
|
|
127
|
+
payload = [endpoint | 0x80, length, timeout].pack("III")
|
|
128
|
+
device_handle.ioctl(self.class::USBDEVFS_BULK, payload)
|
|
129
|
+
{ status: TransferStatus::OK, data: device_handle.read(length) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @param device_handle [File]
|
|
133
|
+
# @param endpoint [Integer]
|
|
134
|
+
# @param data [String]
|
|
135
|
+
# @param timeout [Integer]
|
|
136
|
+
# @return [Hash]
|
|
137
|
+
def bulk_transfer_out(device_handle, endpoint, data, timeout:)
|
|
138
|
+
payload = [endpoint, data.bytesize, timeout].pack("III") + data
|
|
139
|
+
device_handle.ioctl(self.class::USBDEVFS_BULK, payload)
|
|
140
|
+
{ status: TransferStatus::OK, bytes_written: data.bytesize }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @raise [NotImplementedError]
|
|
144
|
+
# @return [void]
|
|
145
|
+
def isochronous_transfer_in(_device_handle, _endpoint, _packet_lengths)
|
|
146
|
+
raise NotImplementedError, "usbfs isochronous transfers are not implemented"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @raise [NotImplementedError]
|
|
150
|
+
# @return [void]
|
|
151
|
+
def isochronous_transfer_out(_device_handle, _endpoint, _data, _packet_lengths)
|
|
152
|
+
raise NotImplementedError, "usbfs isochronous transfers are not implemented"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @param device_handle [File]
|
|
156
|
+
# @param endpoint [Integer]
|
|
157
|
+
# @return [Integer]
|
|
158
|
+
def clear_halt(device_handle, endpoint)
|
|
159
|
+
device_handle.ioctl(self.class::USBDEVFS_CLEAR_HALT, [endpoint].pack("I"))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @param device_handle [File]
|
|
163
|
+
# @return [Integer]
|
|
164
|
+
def reset_device(device_handle)
|
|
165
|
+
device_handle.ioctl(self.class::USBDEVFS_RESET, "".b)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @param device_handle [Object]
|
|
169
|
+
# @return [Object]
|
|
170
|
+
def get_device_descriptor(device_handle)
|
|
171
|
+
device_handle
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @raise [NotImplementedError]
|
|
175
|
+
# @return [void]
|
|
176
|
+
def get_configuration_descriptor(_device_handle, _index)
|
|
177
|
+
raise NotImplementedError, "usbfs configuration descriptors are not implemented"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def build_control_payload(setup, direction, length: 0, data: "".b)
|
|
183
|
+
[
|
|
184
|
+
build_request_type(setup, direction),
|
|
185
|
+
setup[:request],
|
|
186
|
+
setup[:value],
|
|
187
|
+
setup[:index],
|
|
188
|
+
data.bytesize,
|
|
189
|
+
UsbKit.config.transfer_timeout,
|
|
190
|
+
length
|
|
191
|
+
].pack("CCSSIII") + data
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_request_type(setup, direction)
|
|
195
|
+
direction_bit = direction == :in ? 0x80 : 0x00
|
|
196
|
+
direction_bit | REQUEST_TYPE_BITS.fetch(setup[:request_type]) | RECIPIENT_BITS.fetch(setup[:recipient])
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
##
|
|
5
|
+
# Gem-level UsbKit configuration.
|
|
6
|
+
#
|
|
7
|
+
class GemConfiguration
|
|
8
|
+
# @return [Symbol] :auto, :usb_ruby, or :usbfs
|
|
9
|
+
# @return [Symbol] selected log level
|
|
10
|
+
# @return [Integer] transfer timeout in milliseconds
|
|
11
|
+
# @return [Array<Integer>] blocked USB class codes
|
|
12
|
+
attr_accessor :backend, :log_level, :transfer_timeout, :blocked_classes
|
|
13
|
+
|
|
14
|
+
# @return [void]
|
|
15
|
+
def initialize
|
|
16
|
+
@backend = :auto
|
|
17
|
+
@log_level = :warn
|
|
18
|
+
@transfer_timeout = 5000
|
|
19
|
+
@blocked_classes = default_blocked_classes
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def default_blocked_classes
|
|
25
|
+
[
|
|
26
|
+
0x01,
|
|
27
|
+
0x03,
|
|
28
|
+
0x08,
|
|
29
|
+
0x0B,
|
|
30
|
+
0x0E,
|
|
31
|
+
0x10,
|
|
32
|
+
0xE0
|
|
33
|
+
].freeze
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
##
|
|
5
|
+
# Device connection lifecycle event.
|
|
6
|
+
#
|
|
7
|
+
class ConnectionEvent
|
|
8
|
+
EVENT_TYPES = %i[connect disconnect].freeze
|
|
9
|
+
|
|
10
|
+
# @return [Device] related device
|
|
11
|
+
# @return [Symbol] :connect or :disconnect
|
|
12
|
+
attr_reader :device, :type
|
|
13
|
+
|
|
14
|
+
# @param attrs [Hash]
|
|
15
|
+
# @raise [ArgumentError] when the event type is unsupported
|
|
16
|
+
# @return [void]
|
|
17
|
+
def initialize(attrs)
|
|
18
|
+
@device = attrs.fetch(:device)
|
|
19
|
+
@type = validate_type(attrs.fetch(:type))
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Hash]
|
|
24
|
+
def to_h
|
|
25
|
+
{ device: device, type: type }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [String]
|
|
29
|
+
def inspect
|
|
30
|
+
"#<#{self.class} type=#{type.inspect} device=#{device.inspect}>"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate_type(type)
|
|
36
|
+
return type if EVENT_TYPES.include?(type)
|
|
37
|
+
|
|
38
|
+
raise ArgumentError, "Invalid event type: #{type.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|