ruby-blinkstick 0.2.0 → 0.3.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 +4 -4
- data/CAPABILITIES.md +13 -13
- data/CHANGELOG.md +19 -0
- data/README.md +162 -11
- data/lib/blinkstick/backend/hidraw.rb +5 -0
- data/lib/blinkstick/backend/iokit.rb +8 -0
- data/lib/blinkstick/cancellation.rb +79 -0
- data/lib/blinkstick/colour.rb +38 -0
- data/lib/blinkstick/errors.rb +6 -2
- data/lib/blinkstick/model.rb +10 -2
- data/lib/blinkstick/names.rb +116 -0
- data/lib/blinkstick/report.rb +40 -0
- data/lib/blinkstick/stick/eeprom.rb +158 -0
- data/lib/blinkstick/stick/effects.rb +151 -0
- data/lib/blinkstick/stick/handover.rb +27 -0
- data/lib/blinkstick/stick/lighting.rb +120 -0
- data/lib/blinkstick/stick/reconnect.rb +126 -0
- data/lib/blinkstick/stick.rb +38 -68
- data/lib/blinkstick/version.rb +1 -1
- data/lib/blinkstick.rb +77 -14
- metadata +16 -7
data/lib/blinkstick/stick.rb
CHANGED
|
@@ -8,18 +8,35 @@ module Blinkstick
|
|
|
8
8
|
# stick (nothing is cached). A stick that is garbage collected without
|
|
9
9
|
# {#close} still releases its OS handle, but call {#close} (or use the
|
|
10
10
|
# block form of {Blinkstick.open}) rather than relying on that.
|
|
11
|
+
#
|
|
12
|
+
# A stick that is unplugged and plugged back in is found again by its
|
|
13
|
+
# serial on the next call, which repaints the last frame written before
|
|
14
|
+
# carrying on. Only when that fails does a call raise {DisconnectedError}.
|
|
11
15
|
class Stick
|
|
16
|
+
include Eeprom
|
|
17
|
+
include Effects
|
|
18
|
+
include Handover
|
|
19
|
+
include Lighting
|
|
20
|
+
include Reconnect
|
|
21
|
+
|
|
12
22
|
# @return [Info] the identity of this stick
|
|
13
23
|
attr_reader :info
|
|
14
24
|
|
|
15
25
|
# @api private
|
|
16
26
|
# @param handle [#write_feature, #read_feature, #close] an open backend handle
|
|
17
27
|
# @param info [Info]
|
|
18
|
-
|
|
28
|
+
# @param reopen [#call, nil] returns a fresh handle for this stick or
|
|
29
|
+
# raises an {Error}; nil means the stick never reconnects
|
|
30
|
+
def initialize(handle, info, reopen: nil)
|
|
19
31
|
@handle = handle
|
|
20
32
|
@info = info
|
|
33
|
+
@reopen = reopen
|
|
34
|
+
@reconnected = false
|
|
21
35
|
@mutex = Mutex.new
|
|
22
36
|
@closed = false
|
|
37
|
+
@limit = 255
|
|
38
|
+
@inverse = false
|
|
39
|
+
@last = nil
|
|
23
40
|
ObjectSpace.define_finalizer(self, self.class.releaser(handle))
|
|
24
41
|
end
|
|
25
42
|
|
|
@@ -44,9 +61,9 @@ module Blinkstick
|
|
|
44
61
|
# @raise [ClosedError] after {#close}
|
|
45
62
|
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
46
63
|
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
47
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
64
|
+
# @raise [DisconnectedError] when the stick stops responding and reconnecting to it fails
|
|
48
65
|
def colour=(colour)
|
|
49
|
-
|
|
66
|
+
write_frame([Colour.parse(colour)] * info.model.leds)
|
|
50
67
|
end
|
|
51
68
|
|
|
52
69
|
# Sets one LED, leaving the others as they are.
|
|
@@ -59,11 +76,10 @@ module Blinkstick
|
|
|
59
76
|
# @raise [ClosedError] after {#close}
|
|
60
77
|
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
61
78
|
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
62
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
79
|
+
# @raise [DisconnectedError] when the stick stops responding and reconnecting to it fails
|
|
63
80
|
def []=(index, colour)
|
|
64
|
-
parsed = Colour.parse(colour)
|
|
65
81
|
check_index(index)
|
|
66
|
-
|
|
82
|
+
write_led(index, Colour.parse(colour))
|
|
67
83
|
colour
|
|
68
84
|
end
|
|
69
85
|
|
|
@@ -76,7 +92,7 @@ module Blinkstick
|
|
|
76
92
|
# @raise [ClosedError] after {#close}
|
|
77
93
|
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
78
94
|
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
79
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
95
|
+
# @raise [DisconnectedError] when the stick stops responding and reconnecting to it fails
|
|
80
96
|
def frame=(colours)
|
|
81
97
|
list = Array.try_convert(colours)
|
|
82
98
|
raise TypeError, "frame must be an Array of colours, got #{colours.class}" if list.nil?
|
|
@@ -86,33 +102,7 @@ module Blinkstick
|
|
|
86
102
|
raise ArgumentError, "a #{model.name} has #{model.leds} LEDs, got #{list.size} colours"
|
|
87
103
|
end
|
|
88
104
|
|
|
89
|
-
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Reads every LED back from the stick. Nothing is cached: each call asks
|
|
93
|
-
# the stick.
|
|
94
|
-
#
|
|
95
|
-
# @return [Array<Colour>] frozen, one per LED of the model
|
|
96
|
-
# @raise [ClosedError] after {#close}
|
|
97
|
-
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
98
|
-
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
99
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
100
|
-
def frame
|
|
101
|
-
Report.decode_frame(read(Report::FRAME_ID, Report::FRAME_SIZE), info.model.leds)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Reads one LED back from the stick, by reading the whole frame.
|
|
105
|
-
#
|
|
106
|
-
# @param index [Integer] LED index, 0 to one less than the model's LED count (a Nano takes 0 or 1, a Square 0 to 7)
|
|
107
|
-
# @return [Colour]
|
|
108
|
-
# @raise [IndexError] before anything is sent, when index is not an Integer within the model's LEDs
|
|
109
|
-
# @raise [ClosedError] after {#close}
|
|
110
|
-
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
111
|
-
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
112
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
113
|
-
def [](index)
|
|
114
|
-
check_index(index)
|
|
115
|
-
frame.fetch(index)
|
|
105
|
+
write_frame(list.map { |colour| Colour.parse(colour) })
|
|
116
106
|
end
|
|
117
107
|
|
|
118
108
|
# Reads the LED mode from the stick.
|
|
@@ -122,7 +112,7 @@ module Blinkstick
|
|
|
122
112
|
# @raise [ClosedError] after {#close}
|
|
123
113
|
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
124
114
|
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
125
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
115
|
+
# @raise [DisconnectedError] when the stick stops responding and reconnecting to it fails
|
|
126
116
|
def mode
|
|
127
117
|
Report.decode_mode(read(Report::MODE_ID, Report::MODE_SIZE))
|
|
128
118
|
end
|
|
@@ -135,7 +125,7 @@ module Blinkstick
|
|
|
135
125
|
# @raise [ClosedError] after {#close}
|
|
136
126
|
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
137
127
|
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
138
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
128
|
+
# @raise [DisconnectedError] when the stick stops responding and reconnecting to it fails
|
|
139
129
|
def info_block(number)
|
|
140
130
|
Report.decode_info_block(read(Report.info_block_id(number), Report::INFO_BLOCK_SIZE))
|
|
141
131
|
end
|
|
@@ -146,13 +136,14 @@ module Blinkstick
|
|
|
146
136
|
# @raise [ClosedError] after {#close}
|
|
147
137
|
# @raise [BusyError] when another program holds the stick mid-transfer
|
|
148
138
|
# @raise [PermissionError] when the system refuses access mid-transfer
|
|
149
|
-
# @raise [DisconnectedError] when the stick stops responding
|
|
139
|
+
# @raise [DisconnectedError] when the stick stops responding and reconnecting to it fails
|
|
150
140
|
def off
|
|
151
141
|
self.colour = Colour::OFF
|
|
152
142
|
nil
|
|
153
143
|
end
|
|
154
144
|
|
|
155
|
-
# Releases the stick. Calling it again does nothing
|
|
145
|
+
# Releases the stick. Calling it again does nothing, and it succeeds on a
|
|
146
|
+
# stick that was unplugged and not found again.
|
|
156
147
|
#
|
|
157
148
|
# @return [void]
|
|
158
149
|
def close
|
|
@@ -161,7 +152,7 @@ module Blinkstick
|
|
|
161
152
|
|
|
162
153
|
@closed = true
|
|
163
154
|
ObjectSpace.undefine_finalizer(self)
|
|
164
|
-
@handle
|
|
155
|
+
@handle&.close
|
|
165
156
|
end
|
|
166
157
|
nil
|
|
167
158
|
end
|
|
@@ -191,39 +182,18 @@ module Blinkstick
|
|
|
191
182
|
#
|
|
192
183
|
# @return [String] at least size bytes, the first being report_id
|
|
193
184
|
def read(report_id, size)
|
|
194
|
-
transfer("reads")
|
|
195
|
-
reply = @handle.read_feature(report_id, size)
|
|
196
|
-
unless reply.bytesize >= size && reply.getbyte(0) == report_id
|
|
197
|
-
raise Backend::TransferFailed,
|
|
198
|
-
"report #{report_id}: got #{reply.bytesize} of #{size} bytes, ID #{reply.getbyte(0).inspect}"
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
reply
|
|
202
|
-
end
|
|
185
|
+
transfer("reads") { fetch(report_id, size) }
|
|
203
186
|
end
|
|
204
187
|
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
raise
|
|
210
|
-
|
|
211
|
-
with_retries(what, &)
|
|
188
|
+
# One read attempt, checked. The caller holds the lock.
|
|
189
|
+
def fetch(report_id, size)
|
|
190
|
+
reply = @handle.read_feature(report_id, size)
|
|
191
|
+
unless reply.bytesize >= size && reply.getbyte(0) == report_id
|
|
192
|
+
raise Backend::TransferFailed,
|
|
193
|
+
"report #{report_id}: got #{reply.bytesize} of #{size} bytes, ID #{reply.getbyte(0).inspect}"
|
|
212
194
|
end
|
|
213
|
-
end
|
|
214
195
|
|
|
215
|
-
|
|
216
|
-
# TransferFailed. The caller must hold the lock.
|
|
217
|
-
def with_retries(what)
|
|
218
|
-
attempt = 0
|
|
219
|
-
begin
|
|
220
|
-
attempt += 1
|
|
221
|
-
yield
|
|
222
|
-
rescue Backend::TransferFailed
|
|
223
|
-
retry if attempt < Backend::ATTEMPTS
|
|
224
|
-
|
|
225
|
-
raise DisconnectedError, "#{info.serial} stopped responding: #{attempt} #{what} failed in a row"
|
|
226
|
-
end
|
|
196
|
+
reply
|
|
227
197
|
end
|
|
228
198
|
end
|
|
229
199
|
end
|
data/lib/blinkstick/version.rb
CHANGED
data/lib/blinkstick.rb
CHANGED
|
@@ -5,7 +5,14 @@ require_relative "blinkstick/errors"
|
|
|
5
5
|
require_relative "blinkstick/colour"
|
|
6
6
|
require_relative "blinkstick/model"
|
|
7
7
|
require_relative "blinkstick/report"
|
|
8
|
+
require_relative "blinkstick/stick/eeprom"
|
|
9
|
+
require_relative "blinkstick/stick/effects"
|
|
10
|
+
require_relative "blinkstick/stick/handover"
|
|
11
|
+
require_relative "blinkstick/stick/lighting"
|
|
12
|
+
require_relative "blinkstick/stick/reconnect"
|
|
8
13
|
require_relative "blinkstick/stick"
|
|
14
|
+
require_relative "blinkstick/names"
|
|
15
|
+
require_relative "blinkstick/cancellation"
|
|
9
16
|
|
|
10
17
|
# Finds and drives BlinkStick Nano and Square USB LED sticks on macOS and Linux.
|
|
11
18
|
#
|
|
@@ -21,29 +28,47 @@ module Blinkstick
|
|
|
21
28
|
# Lists every attached BlinkStick, including models this gem cannot open
|
|
22
29
|
# (their {Info#model} is {Model::UNKNOWN}).
|
|
23
30
|
#
|
|
24
|
-
#
|
|
31
|
+
# With +names: true+ each stick is opened briefly to read its name and then
|
|
32
|
+
# closed, so a stick another program holds shows as busy with an empty name.
|
|
33
|
+
# Unknown models are opened too.
|
|
34
|
+
#
|
|
35
|
+
# @param names [Boolean] read each stick's name
|
|
36
|
+
# @return [Array<Info>, Array<NamedInfo>] NamedInfo when +names+ is true
|
|
25
37
|
# @raise [Error] when the OS cannot list devices
|
|
26
|
-
def self.list
|
|
38
|
+
def self.list(names: false)
|
|
39
|
+
return list_named if names
|
|
40
|
+
|
|
27
41
|
Backend.current.list.map { |device| info_for(device) }
|
|
28
42
|
end
|
|
29
43
|
|
|
30
|
-
# Opens a BlinkStick: the one with the given serial,
|
|
31
|
-
# found. If the first one found is an unknown
|
|
32
|
-
# a supported stick is also attached; pass a
|
|
44
|
+
# Opens a BlinkStick: the one with the given serial, the one with the given
|
|
45
|
+
# name, or else the first one found. If the first one found is an unknown
|
|
46
|
+
# model this raises, even when a supported stick is also attached; pass a
|
|
47
|
+
# serial from {list} to choose.
|
|
33
48
|
#
|
|
34
49
|
# Without a block the caller must {Stick#close} the stick. With a block,
|
|
35
50
|
# like File.open, the stick is yielded, closed afterwards even if the
|
|
36
51
|
# block raises, and the block's value is returned.
|
|
37
52
|
#
|
|
53
|
+
# By name, every stick is opened in turn and its name compared byte for
|
|
54
|
+
# byte; sticks that cannot be opened or read are skipped, and the ones that
|
|
55
|
+
# do not match are closed.
|
|
56
|
+
#
|
|
38
57
|
# @param serial [String, nil] for example "BS072777-3.0"
|
|
58
|
+
# @param name [String, nil] a name stored on the stick, see {Stick#name=}
|
|
39
59
|
# @yieldparam stick [Stick]
|
|
40
60
|
# @return [Stick, Object] the stick, or the block's value
|
|
41
|
-
# @raise [
|
|
61
|
+
# @raise [ArgumentError] when both +serial+ and +name+ are given, or +name+ is empty
|
|
62
|
+
# @raise [TypeError] when +name+ is not a String
|
|
63
|
+
# @raise [NotFoundError] when no stick (or none with that serial or name) is attached;
|
|
64
|
+
# for a name the message says why each skipped stick was skipped
|
|
65
|
+
# @raise [DuplicateNameError] when more than one stick has that name
|
|
42
66
|
# @raise [UnsupportedModelError] when the stick's model is unknown
|
|
43
67
|
# @raise [BusyError] when another program has seized the stick
|
|
44
68
|
# @raise [PermissionError] when the OS refuses access
|
|
45
|
-
def self.open(serial: nil)
|
|
46
|
-
|
|
69
|
+
def self.open(serial: nil, name: nil)
|
|
70
|
+
check_selectors(serial, name)
|
|
71
|
+
stick = name.nil? ? open_stick(serial) : open_named(name)
|
|
47
72
|
return stick unless block_given?
|
|
48
73
|
|
|
49
74
|
begin
|
|
@@ -56,17 +81,55 @@ module Blinkstick
|
|
|
56
81
|
# Checks the model before opening, so an unknown stick is never opened.
|
|
57
82
|
def self.open_stick(serial)
|
|
58
83
|
backend = Backend.current
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
build_stick(backend, find(backend.list, serial))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The one place a Stick for a user is built, however it was found. Checks
|
|
88
|
+
# the model first, then takes the handle from the block (when the caller
|
|
89
|
+
# already holds one open) or opens the device, and gives the Stick a way
|
|
90
|
+
# to find the device again after an unplug.
|
|
91
|
+
def self.build_stick(backend, device)
|
|
92
|
+
info = supported_info(device)
|
|
93
|
+
handle = block_given? ? yield : backend.open(device)
|
|
94
|
+
Stick.new(handle, info, reopen: reopener(device.serial))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A callable that relists through the current backend and opens the one
|
|
98
|
+
# stick with this serial, after the same model check {Blinkstick.open} makes. An
|
|
99
|
+
# empty serial, or one more than one stick shares, could reopen the wrong
|
|
100
|
+
# stick, so those never reconnect.
|
|
101
|
+
def self.reopener(serial)
|
|
102
|
+
return -> { raise DisconnectedError, "this stick has no serial, so it cannot be found again" } if serial.empty?
|
|
62
103
|
|
|
63
|
-
|
|
104
|
+
lambda do
|
|
105
|
+
backend = Backend.current
|
|
106
|
+
device = only_device(backend.list, serial)
|
|
107
|
+
supported_info(device)
|
|
108
|
+
backend.open(device)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The one device with this serial, for {reopener}.
|
|
113
|
+
def self.only_device(devices, serial)
|
|
114
|
+
matches = devices.select { |device| device.serial == serial }
|
|
115
|
+
raise NotFoundError, "no BlinkStick with serial #{serial.inspect}" if matches.empty?
|
|
116
|
+
raise DisconnectedError, "serial #{serial.inspect} is ambiguous: #{matches.size} sticks have it" if matches.size > 1
|
|
117
|
+
|
|
118
|
+
matches.first
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The model check, made before anything is opened.
|
|
122
|
+
def self.supported_info(device)
|
|
123
|
+
info_for(device).tap do |info|
|
|
124
|
+
raise UnsupportedModelError, "#{info.serial} is a model this gem does not support" unless info.model.supported?
|
|
125
|
+
end
|
|
64
126
|
end
|
|
65
127
|
|
|
66
128
|
def self.find(devices, serial)
|
|
67
129
|
return devices.first || raise(NotFoundError, "no BlinkStick attached") if serial.nil?
|
|
68
130
|
|
|
69
|
-
devices.find { |device| device.serial == serial } ||
|
|
131
|
+
devices.find { |device| device.serial == serial } ||
|
|
132
|
+
raise(NotFoundError, "no BlinkStick with serial #{serial.inspect}")
|
|
70
133
|
end
|
|
71
134
|
|
|
72
135
|
def self.info_for(device)
|
|
@@ -74,5 +137,5 @@ module Blinkstick
|
|
|
74
137
|
release: device.release)
|
|
75
138
|
end
|
|
76
139
|
|
|
77
|
-
private_class_method :open_stick, :find, :info_for
|
|
140
|
+
private_class_method :open_stick, :build_stick, :reopener, :only_device, :supported_info, :find, :info_for
|
|
78
141
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-blinkstick
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keith Bawden
|
|
@@ -29,10 +29,12 @@ dependencies:
|
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: 1.17.3
|
|
32
|
-
description: Find, open and
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
description: Find, open and drive BlinkStick Nano and Square USB LEDs from Ruby, on
|
|
33
|
+
macOS and Linux. Sets every LED or one LED from hex, r,g,b or CSS colour names,
|
|
34
|
+
reads them back, names a stick and opens it by name, writes the info blocks and
|
|
35
|
+
mode, limits brightness, runs cancellable blink, pulse and morph effects, and reconnects
|
|
36
|
+
after an unplug. Nothing to install but the gem. Unofficial, and not affiliated
|
|
37
|
+
with Agile Innovative.
|
|
36
38
|
email:
|
|
37
39
|
- keith@arrix.com.au
|
|
38
40
|
executables: []
|
|
@@ -47,12 +49,19 @@ files:
|
|
|
47
49
|
- lib/blinkstick/backend.rb
|
|
48
50
|
- lib/blinkstick/backend/hidraw.rb
|
|
49
51
|
- lib/blinkstick/backend/iokit.rb
|
|
52
|
+
- lib/blinkstick/cancellation.rb
|
|
50
53
|
- lib/blinkstick/colour.rb
|
|
51
54
|
- lib/blinkstick/colour_names.rb
|
|
52
55
|
- lib/blinkstick/errors.rb
|
|
53
56
|
- lib/blinkstick/model.rb
|
|
57
|
+
- lib/blinkstick/names.rb
|
|
54
58
|
- lib/blinkstick/report.rb
|
|
55
59
|
- lib/blinkstick/stick.rb
|
|
60
|
+
- lib/blinkstick/stick/eeprom.rb
|
|
61
|
+
- lib/blinkstick/stick/effects.rb
|
|
62
|
+
- lib/blinkstick/stick/handover.rb
|
|
63
|
+
- lib/blinkstick/stick/lighting.rb
|
|
64
|
+
- lib/blinkstick/stick/reconnect.rb
|
|
56
65
|
- lib/blinkstick/version.rb
|
|
57
66
|
- udev/60-blinkstick.rules
|
|
58
67
|
homepage: https://github.com/bawdo/ruby-blinkstick
|
|
@@ -79,6 +88,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
79
88
|
requirements: []
|
|
80
89
|
rubygems_version: 3.6.9
|
|
81
90
|
specification_version: 4
|
|
82
|
-
summary:
|
|
83
|
-
|
|
91
|
+
summary: 'Drive BlinkStick Nano and Square USB LEDs from Ruby: names, effects and
|
|
92
|
+
reconnect.'
|
|
84
93
|
test_files: []
|