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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
##
|
|
5
|
+
# Direction enum used by endpoints and transfers.
|
|
6
|
+
#
|
|
7
|
+
module Direction
|
|
8
|
+
IN = :in
|
|
9
|
+
OUT = :out
|
|
10
|
+
|
|
11
|
+
ALL = [IN, OUT].freeze
|
|
12
|
+
|
|
13
|
+
# @param value [Object]
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def self.valid?(value)
|
|
16
|
+
ALL.include?(value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Endpoint transfer type enum.
|
|
22
|
+
#
|
|
23
|
+
module EndpointType
|
|
24
|
+
BULK = :bulk
|
|
25
|
+
INTERRUPT = :interrupt
|
|
26
|
+
ISOCHRONOUS = :isochronous
|
|
27
|
+
|
|
28
|
+
ALL = [BULK, INTERRUPT, ISOCHRONOUS].freeze
|
|
29
|
+
|
|
30
|
+
# @param value [Object]
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def self.valid?(value)
|
|
33
|
+
ALL.include?(value)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Control transfer request type enum.
|
|
39
|
+
#
|
|
40
|
+
module RequestType
|
|
41
|
+
STANDARD = :standard
|
|
42
|
+
CLASS = :class
|
|
43
|
+
VENDOR = :vendor
|
|
44
|
+
|
|
45
|
+
ALL = [STANDARD, CLASS, VENDOR].freeze
|
|
46
|
+
|
|
47
|
+
# @param value [Object]
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def self.valid?(value)
|
|
50
|
+
ALL.include?(value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Control transfer recipient enum.
|
|
56
|
+
#
|
|
57
|
+
module Recipient
|
|
58
|
+
DEVICE = :device
|
|
59
|
+
INTERFACE = :interface
|
|
60
|
+
ENDPOINT = :endpoint
|
|
61
|
+
OTHER = :other
|
|
62
|
+
|
|
63
|
+
ALL = [DEVICE, INTERFACE, ENDPOINT, OTHER].freeze
|
|
64
|
+
|
|
65
|
+
# @param value [Object]
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def self.valid?(value)
|
|
68
|
+
ALL.include?(value)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Transfer status enum.
|
|
74
|
+
#
|
|
75
|
+
module TransferStatus
|
|
76
|
+
OK = :ok
|
|
77
|
+
STALL = :stall
|
|
78
|
+
BABBLE = :babble
|
|
79
|
+
|
|
80
|
+
ALL = [OK, STALL, BABBLE].freeze
|
|
81
|
+
|
|
82
|
+
# @param value [Object]
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def self.valid?(value)
|
|
85
|
+
ALL.include?(value)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbKit
|
|
4
|
+
##
|
|
5
|
+
# Entry point for WebUSB-style device discovery and monitoring.
|
|
6
|
+
#
|
|
7
|
+
class Context
|
|
8
|
+
EVENT_TYPES = %i[connect disconnect].freeze
|
|
9
|
+
|
|
10
|
+
# @param backend [Backend::Base]
|
|
11
|
+
# @param config [GemConfiguration]
|
|
12
|
+
# @return [void]
|
|
13
|
+
def initialize(backend: UsbKit.backend, config: UsbKit.config)
|
|
14
|
+
@backend = backend
|
|
15
|
+
@config = config
|
|
16
|
+
@callbacks = Hash.new { |hash, key| hash[key] = [] }
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
@monitoring = false
|
|
19
|
+
@monitor_thread = nil
|
|
20
|
+
@snapshot = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Enumerate visible devices.
|
|
24
|
+
#
|
|
25
|
+
# @param filters [Array<Hash>, nil]
|
|
26
|
+
# @return [Array<Device>]
|
|
27
|
+
def get_devices(filters: nil)
|
|
28
|
+
devices = enumerate_visible_devices
|
|
29
|
+
return devices if filters.nil?
|
|
30
|
+
|
|
31
|
+
matcher = Filter.new(filters)
|
|
32
|
+
devices.select { |device| matcher.match?(device.to_h) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Return the first device that matches the provided filters.
|
|
36
|
+
#
|
|
37
|
+
# @param filters [Array<Hash>]
|
|
38
|
+
# @raise [ArgumentError] when filters are missing
|
|
39
|
+
# @raise [DeviceNotFoundError] when no device matches
|
|
40
|
+
# @return [Device]
|
|
41
|
+
def request_device(filters:)
|
|
42
|
+
raise ArgumentError, "filters are required" if filters.nil? || Array(filters).empty?
|
|
43
|
+
|
|
44
|
+
matcher = Filter.new(filters)
|
|
45
|
+
device = enumerate_visible_devices.find { |candidate| matcher.match?(candidate.to_h) }
|
|
46
|
+
return device if device
|
|
47
|
+
|
|
48
|
+
raise DeviceNotFoundError, "No USB device matched the provided filters"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Register a connection lifecycle callback.
|
|
52
|
+
#
|
|
53
|
+
# @param event_type [Symbol] :connect or :disconnect
|
|
54
|
+
# @yieldparam event [ConnectionEvent]
|
|
55
|
+
# @raise [ArgumentError] when the event type or block is invalid
|
|
56
|
+
# @return [Context]
|
|
57
|
+
def on(event_type, &block)
|
|
58
|
+
raise ArgumentError, "Unsupported event type: #{event_type.inspect}" unless EVENT_TYPES.include?(event_type)
|
|
59
|
+
raise ArgumentError, "A block is required" unless block
|
|
60
|
+
|
|
61
|
+
@mutex.synchronize do
|
|
62
|
+
@callbacks[event_type] << block
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Start polling for device changes in a background thread.
|
|
69
|
+
#
|
|
70
|
+
# @param interval [Float] polling interval in seconds
|
|
71
|
+
# @return [Thread]
|
|
72
|
+
def start_event_monitoring(interval: 1.0)
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
return @monitor_thread if @monitor_thread&.alive?
|
|
75
|
+
|
|
76
|
+
@snapshot = build_snapshot(enumerate_visible_devices)
|
|
77
|
+
@monitoring = true
|
|
78
|
+
@monitor_thread = Thread.new do
|
|
79
|
+
monitor_devices(interval)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Stop the event-monitoring thread.
|
|
85
|
+
#
|
|
86
|
+
# @return [nil]
|
|
87
|
+
def stop_event_monitoring
|
|
88
|
+
thread = @mutex.synchronize do
|
|
89
|
+
@monitoring = false
|
|
90
|
+
thread = @monitor_thread
|
|
91
|
+
@monitor_thread = nil
|
|
92
|
+
thread
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
thread&.join unless thread == Thread.current
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def enumerate_visible_devices
|
|
102
|
+
Array(@backend.enumerate_devices).filter_map do |attributes|
|
|
103
|
+
next if blocked_device?(attributes)
|
|
104
|
+
|
|
105
|
+
Device.new(attributes)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def blocked_device?(attributes)
|
|
110
|
+
@config.blocked_classes.include?(attributes[:device_class])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_snapshot(devices)
|
|
114
|
+
devices.each_with_object({}) do |device, snapshot|
|
|
115
|
+
snapshot[device_identifier(device)] = device
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def device_identifier(device)
|
|
120
|
+
attrs = device.to_h
|
|
121
|
+
[
|
|
122
|
+
attrs[:vendor_id],
|
|
123
|
+
attrs[:product_id],
|
|
124
|
+
attrs[:serial_number],
|
|
125
|
+
attrs[:bus_number],
|
|
126
|
+
attrs[:device_address]
|
|
127
|
+
]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def monitor_devices(interval)
|
|
131
|
+
loop do
|
|
132
|
+
break unless monitoring?
|
|
133
|
+
|
|
134
|
+
current_devices = enumerate_visible_devices
|
|
135
|
+
current_snapshot = build_snapshot(current_devices)
|
|
136
|
+
dispatch_snapshot_changes(current_snapshot)
|
|
137
|
+
@mutex.synchronize do
|
|
138
|
+
@snapshot = current_snapshot
|
|
139
|
+
end
|
|
140
|
+
sleep interval
|
|
141
|
+
rescue StandardError
|
|
142
|
+
sleep interval
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def dispatch_snapshot_changes(current_snapshot)
|
|
147
|
+
previous_snapshot = @mutex.synchronize { @snapshot.dup }
|
|
148
|
+
|
|
149
|
+
(current_snapshot.keys - previous_snapshot.keys).each do |identifier|
|
|
150
|
+
dispatch(:connect, current_snapshot.fetch(identifier))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
(previous_snapshot.keys - current_snapshot.keys).each do |identifier|
|
|
154
|
+
dispatch(:disconnect, previous_snapshot.fetch(identifier))
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def dispatch(event_type, device)
|
|
159
|
+
callbacks = @mutex.synchronize { @callbacks[event_type].dup }
|
|
160
|
+
event = ConnectionEvent.new(type: event_type, device: device)
|
|
161
|
+
callbacks.each { |callback| callback.call(event) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def monitoring?
|
|
165
|
+
@mutex.synchronize { @monitoring }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|