launchpad_mk2 0.0.1
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/.gitignore +4 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +7 -0
- data/README.rdoc +75 -0
- data/Rakefile +10 -0
- data/examples/binary_clock.rb +41 -0
- data/examples/color_picker.rb +96 -0
- data/examples/colors.rb +27 -0
- data/examples/corners.rb +11 -0
- data/examples/doodle.rb +65 -0
- data/examples/drawing_board.rb +20 -0
- data/examples/feedback.rb +30 -0
- data/examples/pong.rb +193 -0
- data/examples/sysex.rb +34 -0
- data/launchpad-gem-overview.gif +0 -0
- data/launchpad_mk2.gemspec +30 -0
- data/lib/launchpad_mk2.rb +23 -0
- data/lib/launchpad_mk2/device.rb +466 -0
- data/lib/launchpad_mk2/errors.rb +37 -0
- data/lib/launchpad_mk2/interaction.rb +333 -0
- data/lib/launchpad_mk2/logging.rb +27 -0
- data/lib/launchpad_mk2/midi_codes.rb +40 -0
- data/lib/launchpad_mk2/version.rb +3 -0
- data/test/helper.rb +45 -0
- data/test/test_device.rb +446 -0
- data/test/test_interaction.rb +460 -0
- metadata +154 -0
data/test/test_device.rb
ADDED
@@ -0,0 +1,446 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Launchpad::Device do
|
4
|
+
|
5
|
+
CONTROL_BUTTONS = {
|
6
|
+
:up => 0x68,
|
7
|
+
:down => 0x69,
|
8
|
+
:left => 0x6A,
|
9
|
+
:right => 0x6B,
|
10
|
+
:session => 0x6C,
|
11
|
+
:user1 => 0x6D,
|
12
|
+
:user2 => 0x6E,
|
13
|
+
:mixer => 0x6F
|
14
|
+
}
|
15
|
+
|
16
|
+
SCENE_BUTTONS = {
|
17
|
+
:scene1 => 0x59,
|
18
|
+
:scene2 => 0x4F,
|
19
|
+
:scene3 => 0x45,
|
20
|
+
:scene4 => 0x3B,
|
21
|
+
:scene5 => 0x31,
|
22
|
+
:scene6 => 0x27,
|
23
|
+
:scene7 => 0x1D,
|
24
|
+
:scene8 => 0x13
|
25
|
+
}
|
26
|
+
|
27
|
+
STATES = {
|
28
|
+
:down => 127,
|
29
|
+
:up => 0
|
30
|
+
}
|
31
|
+
|
32
|
+
def expects_output(device, *args)
|
33
|
+
args = [args] unless args.first.is_a?(Array)
|
34
|
+
messages = args.collect {|data| {:message => data, :timestamp => 0}}
|
35
|
+
device.instance_variable_get('@output').expects(:write).with(messages)
|
36
|
+
end
|
37
|
+
|
38
|
+
def expects_sysex_message(device, message)
|
39
|
+
device.instance_variable_get('@output').expects(:write_sysex).with(Launchpad::Device::SYSEX_HEADER + message + Launchpad::Device::SYSEX_FOOTER)
|
40
|
+
end
|
41
|
+
|
42
|
+
def stub_input(device, *args)
|
43
|
+
device.instance_variable_get('@input').stubs(:read).returns(args)
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#initialize' do
|
47
|
+
|
48
|
+
it 'tries to initialize both input and output when not specified' do
|
49
|
+
Portmidi.expects(:input_devices).returns(mock_devices)
|
50
|
+
Portmidi.expects(:output_devices).returns(mock_devices)
|
51
|
+
d = Launchpad::Device.new
|
52
|
+
refute_nil d.instance_variable_get('@input')
|
53
|
+
refute_nil d.instance_variable_get('@output')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'does not try to initialize input when set to false' do
|
57
|
+
Portmidi.expects(:input_devices).never
|
58
|
+
d = Launchpad::Device.new(:input => false)
|
59
|
+
assert_nil d.instance_variable_get('@input')
|
60
|
+
refute_nil d.instance_variable_get('@output')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'does not try to initialize output when set to false' do
|
64
|
+
Portmidi.expects(:output_devices).never
|
65
|
+
d = Launchpad::Device.new(:output => false)
|
66
|
+
refute_nil d.instance_variable_get('@input')
|
67
|
+
assert_nil d.instance_variable_get('@output')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'does not try to initialize any of both when set to false' do
|
71
|
+
Portmidi.expects(:input_devices).never
|
72
|
+
Portmidi.expects(:output_devices).never
|
73
|
+
d = Launchpad::Device.new(:input => false, :output => false)
|
74
|
+
assert_nil d.instance_variable_get('@input')
|
75
|
+
assert_nil d.instance_variable_get('@output')
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'initializes the correct input output devices when specified by name' do
|
79
|
+
Portmidi.stubs(:input_devices).returns(mock_devices(:id => 4, :name => 'Launchpad Name'))
|
80
|
+
Portmidi.stubs(:output_devices).returns(mock_devices(:id => 5, :name => 'Launchpad Name'))
|
81
|
+
d = Launchpad::Device.new(:device_name => 'Launchpad Name')
|
82
|
+
assert_equal Portmidi::Input, (input = d.instance_variable_get('@input')).class
|
83
|
+
assert_equal 4, input.device_id
|
84
|
+
assert_equal Portmidi::Output, (output = d.instance_variable_get('@output')).class
|
85
|
+
assert_equal 5, output.device_id
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'initializes the correct input output devices when specified by id' do
|
89
|
+
Portmidi.stubs(:input_devices).returns(mock_devices(:id => 4))
|
90
|
+
Portmidi.stubs(:output_devices).returns(mock_devices(:id => 5))
|
91
|
+
d = Launchpad::Device.new(:input_device_id => 4, :output_device_id => 5, :device_name => 'nonexistant')
|
92
|
+
assert_equal Portmidi::Input, (input = d.instance_variable_get('@input')).class
|
93
|
+
assert_equal 4, input.device_id
|
94
|
+
assert_equal Portmidi::Output, (output = d.instance_variable_get('@output')).class
|
95
|
+
assert_equal 5, output.device_id
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'raises NoSuchDeviceError when requested input device does not exist' do
|
99
|
+
assert_raises Launchpad::NoSuchDeviceError do
|
100
|
+
Portmidi.stubs(:input_devices).returns(mock_devices(:name => 'Launchpad Input'))
|
101
|
+
Launchpad::Device.new
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'raises NoSuchDeviceError when requested output device does not exist' do
|
106
|
+
assert_raises Launchpad::NoSuchDeviceError do
|
107
|
+
Portmidi.stubs(:output_devices).returns(mock_devices(:name => 'Launchpad Output'))
|
108
|
+
Launchpad::Device.new
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'raises DeviceBusyError when requested input device is busy' do
|
113
|
+
assert_raises Launchpad::DeviceBusyError do
|
114
|
+
Portmidi::Input.stubs(:new).raises(RuntimeError)
|
115
|
+
Launchpad::Device.new
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'raises DeviceBusyError when requested output device is busy' do
|
120
|
+
assert_raises Launchpad::DeviceBusyError do
|
121
|
+
Portmidi::Output.stubs(:new).raises(RuntimeError)
|
122
|
+
Launchpad::Device.new
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'stores the logger given' do
|
127
|
+
logger = Logger.new(nil)
|
128
|
+
device = Launchpad::Device.new(:logger => logger)
|
129
|
+
assert_same logger, device.logger
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '#close' do
|
135
|
+
|
136
|
+
it 'does not fail when neither input nor output are there' do
|
137
|
+
Launchpad::Device.new(:input => false, :output => false).close
|
138
|
+
end
|
139
|
+
|
140
|
+
describe 'with input and output devices' do
|
141
|
+
|
142
|
+
before do
|
143
|
+
Portmidi::Input.stubs(:new).returns(@input = mock('input'))
|
144
|
+
Portmidi::Output.stubs(:new).returns(@output = mock('output'))
|
145
|
+
@device = Launchpad::Device.new
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'closes input/output and raise NoInputAllowedError/NoOutputAllowedError on subsequent read/write accesses' do
|
149
|
+
@input.expects(:close)
|
150
|
+
@output.expects(:close)
|
151
|
+
@device.close
|
152
|
+
assert_raises Launchpad::NoInputAllowedError do
|
153
|
+
@device.read_pending_actions
|
154
|
+
end
|
155
|
+
assert_raises Launchpad::NoOutputAllowedError do
|
156
|
+
@device.change(:session)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
describe '#closed?' do
|
165
|
+
|
166
|
+
it 'returns true when neither input nor output are there' do
|
167
|
+
assert Launchpad::Device.new(:input => false, :output => false).closed?
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'returns false when initialized with input' do
|
171
|
+
assert !Launchpad::Device.new(:input => true, :output => false).closed?
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'returns false when initialized with output' do
|
175
|
+
assert !Launchpad::Device.new(:input => false, :output => true).closed?
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'returns false when initialized with both but true after calling close' do
|
179
|
+
d = Launchpad::Device.new
|
180
|
+
assert !d.closed?
|
181
|
+
d.close
|
182
|
+
assert d.closed?
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
describe 'top level API initialized with output' do
|
188
|
+
before do
|
189
|
+
@device = Launchpad::Device.new(:input => false)
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '#pulse1' do
|
193
|
+
[[1, 4, 24], [0, 6, 27]].each do |message|
|
194
|
+
it "sends 40, 0, #{(message[1] + 1) * 10 + (message[0] + 1)}, #{message[2]} when given #{message}" do
|
195
|
+
expects_sysex_message(@device, [40, 0, (message[1] + 1) * 10 + (message[0] + 1), message[2]])
|
196
|
+
@device.pulse1(message[0], message[1], message[2])
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe '#pulsen' do
|
202
|
+
it "sends one message for each set of coordinates received" do
|
203
|
+
[[1, 4], [0, 6]].each do |coords|
|
204
|
+
expects_sysex_message(@device, [40, 0, (coords[1] + 1) * 10 + (coords[0] + 1), 24])
|
205
|
+
end
|
206
|
+
@device.pulsen([[1, 4], [0, 6]], 24)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe '#flash1' do
|
211
|
+
[[1, 4, 24], [0, 6, 27]].each do |message|
|
212
|
+
it "sends 35, 0, #{(message[1] + 1) * 10 + (message[0] + 1)}, #{message[2]} when given #{message}" do
|
213
|
+
expects_sysex_message(@device, [35, 0, (message[1] + 1) * 10 + (message[0] + 1), message[2]])
|
214
|
+
@device.flash1(message[0], message[1], message[2])
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe '#flashn' do
|
220
|
+
it "sends one message for each set of coordinates received" do
|
221
|
+
[[1, 4], [0, 6]].each do |coords|
|
222
|
+
expects_sysex_message(@device, [35, 0, (coords[1] + 1) * 10 + (coords[0] + 1), 24, 0])
|
223
|
+
end
|
224
|
+
@device.flashn([[1, 4], [0, 6]], 24)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe '#light_all' do
|
229
|
+
[24, 27].each do |message|
|
230
|
+
it "sends 14, #{message[0]} when given #{message}" do
|
231
|
+
expects_sysex_message(@device, [14, message[0]])
|
232
|
+
@device.light_all(message[0])
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
describe '#reset_all' do
|
238
|
+
it "sends 14, 0" do
|
239
|
+
expects_sysex_message(@device, [14, 0])
|
240
|
+
@device.reset_all()
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
describe '#light1_row' do
|
245
|
+
[[1, 24], [0, 27]].each do |message|
|
246
|
+
it "sends 13, #{message[0]}, #{message[1]} when given #{message}" do
|
247
|
+
expects_sysex_message(@device, [13, message[0], message[1]])
|
248
|
+
@device.light1_row(message[0], message[1])
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
describe '#light1_column' do
|
254
|
+
[[1, 24], [0, 27]].each do |message|
|
255
|
+
it "sends 12, #{message[0]}, #{message[1]} when given #{message}" do
|
256
|
+
expects_sysex_message(@device, [12, message[0], message[1]])
|
257
|
+
@device.light1_column(message[0], message[1])
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe '#change' do
|
264
|
+
|
265
|
+
it 'raises NoOutputAllowedError when not initialized with output' do
|
266
|
+
assert_raises Launchpad::NoOutputAllowedError do
|
267
|
+
Launchpad::Device.new(:output => false).change(:up)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe 'initialized with output' do
|
272
|
+
|
273
|
+
before do
|
274
|
+
@device = Launchpad::Device.new(:input => false)
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'returns nil' do
|
278
|
+
assert_nil @device.change(:up)
|
279
|
+
end
|
280
|
+
|
281
|
+
describe 'control buttons' do
|
282
|
+
CONTROL_BUTTONS.each do |type, value|
|
283
|
+
it "sends 0xB0, #{value}, 0 when given #{type}" do
|
284
|
+
expects_output(@device, 0xB0, value, 0)
|
285
|
+
@device.change(type)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe 'scene buttons' do
|
291
|
+
SCENE_BUTTONS.each do |type, value|
|
292
|
+
it "sends 0x90, #{value}, 0 when given #{type}" do
|
293
|
+
expects_output(@device, 0x90, value, 0)
|
294
|
+
@device.change(type)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
describe 'grid buttons' do
|
300
|
+
8.times do |x|
|
301
|
+
8.times do |y|
|
302
|
+
it "sends 0x90, #{10 * (y + 1) + (x + 1)}, 0 when given :grid, :x => #{x}, :y => #{y}" do
|
303
|
+
expects_output(@device, 0x90, (10 * (y + 1)) + (x + 1), 0)
|
304
|
+
@device.change(:grid, :x => x, :y => y)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'raises NoValidGridCoordinatesError if x is not specified' do
|
310
|
+
assert_raises Launchpad::NoValidGridCoordinatesError do
|
311
|
+
@device.change(:grid, :y => 1)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
it 'raises NoValidGridCoordinatesError if x is below 0' do
|
316
|
+
assert_raises Launchpad::NoValidGridCoordinatesError do
|
317
|
+
@device.change(:grid, :x => -1, :y => 1)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'raises NoValidGridCoordinatesError if x is above 7' do
|
322
|
+
assert_raises Launchpad::NoValidGridCoordinatesError do
|
323
|
+
@device.change(:grid, :x => 8, :y => 1)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
it 'raises NoValidGridCoordinatesError if y is not specified' do
|
328
|
+
assert_raises Launchpad::NoValidGridCoordinatesError do
|
329
|
+
@device.change(:grid, :x => 1)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'raises NoValidGridCoordinatesError if y is below 0' do
|
334
|
+
assert_raises Launchpad::NoValidGridCoordinatesError do
|
335
|
+
@device.change(:grid, :x => 1, :y => -1)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'raises NoValidGridCoordinatesError if y is above 7' do
|
340
|
+
assert_raises Launchpad::NoValidGridCoordinatesError do
|
341
|
+
@device.change(:grid, :x => 1, :y => 8)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
|
347
|
+
describe 'colors' do
|
348
|
+
(0..127).each do |color_key|
|
349
|
+
it "sends 0x90, 0, #{color_key} when given :color => #{color_key}" do
|
350
|
+
expects_output(@device, 0x90, 11, color_key)
|
351
|
+
@device.change(:grid, :x => 0, :y => 0, :color => color_key)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
it 'raises NoValidColorError if color is below 0' do
|
356
|
+
assert_raises Launchpad::NoValidColorError do
|
357
|
+
@device.change(:grid, :x => 0, :y => 0, :color => -1)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
it 'raises NoValidColorError if color is above 127' do
|
362
|
+
assert_raises Launchpad::NoValidColorError do
|
363
|
+
@device.change(:grid, :x => 0, :y => 0, :color => 128)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
it 'raises NoValidColorError if color is an unknown symbol' do
|
368
|
+
assert_raises Launchpad::NoValidColorError do
|
369
|
+
@device.change(:grid, :x => 0, :y => 0, :color => :unknown)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
end
|
374
|
+
|
375
|
+
describe 'mode' do
|
376
|
+
|
377
|
+
it 'sends 0 when nothing given' do
|
378
|
+
expects_output(@device, 0x90, 11, 0)
|
379
|
+
@device.change(:grid, :x => 0, :y => 0)
|
380
|
+
end
|
381
|
+
|
382
|
+
end
|
383
|
+
|
384
|
+
end
|
385
|
+
|
386
|
+
end
|
387
|
+
|
388
|
+
describe '#read_pending_actions' do
|
389
|
+
|
390
|
+
it 'raises NoInputAllowedError when not initialized with input' do
|
391
|
+
assert_raises Launchpad::NoInputAllowedError do
|
392
|
+
Launchpad::Device.new(:input => false).read_pending_actions
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
describe 'initialized with input' do
|
397
|
+
|
398
|
+
before do
|
399
|
+
@device = Launchpad::Device.new(:output => false)
|
400
|
+
end
|
401
|
+
|
402
|
+
describe 'control buttons' do
|
403
|
+
CONTROL_BUTTONS.each do |type, value|
|
404
|
+
STATES.each do |state, velocity|
|
405
|
+
it "builds proper action for control button #{type}, #{state}" do
|
406
|
+
stub_input(@device, {:timestamp => 0, :message => [0xB0, value, velocity]})
|
407
|
+
assert_equal [{:timestamp => 0, :state => state, :type => type}], @device.read_pending_actions
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
describe 'scene buttons' do
|
414
|
+
SCENE_BUTTONS.each do |type, value|
|
415
|
+
STATES.each do |state, velocity|
|
416
|
+
it "builds proper action for scene button #{type}, #{state}" do
|
417
|
+
stub_input(@device, {:timestamp => 0, :message => [0x90, value, velocity]})
|
418
|
+
assert_equal [{:timestamp => 0, :state => state, :type => type}], @device.read_pending_actions
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
describe '#grid buttons' do
|
425
|
+
8.times do |x|
|
426
|
+
8.times do |y|
|
427
|
+
STATES.each do |state, velocity|
|
428
|
+
it "builds proper action for grid button #{x},#{y}, #{state}" do
|
429
|
+
stub_input(@device, {:timestamp => 0, :message => [0x90, 10 * (y + 1) + (x + 1), velocity]})
|
430
|
+
assert_equal [{:timestamp => 0, :state => state, :type => :grid, :x => x, :y => y}], @device.read_pending_actions
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
it 'builds proper actions for multiple pending actions' do
|
438
|
+
stub_input(@device, {:timestamp => 1, :message => [0x90, 11, 127]}, {:timestamp => 2, :message => [0xB0, 0x68, 0]})
|
439
|
+
assert_equal [{:timestamp => 1, :state => :down, :type => :grid, :x => 0, :y => 0}, {:timestamp => 2, :state => :up, :type => :up}], @device.read_pending_actions
|
440
|
+
end
|
441
|
+
|
442
|
+
end
|
443
|
+
|
444
|
+
end
|
445
|
+
|
446
|
+
end
|
@@ -0,0 +1,460 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
class BreakError < StandardError; end
|
5
|
+
|
6
|
+
describe Launchpad::Interaction do
|
7
|
+
|
8
|
+
before do
|
9
|
+
@interaction = nil
|
10
|
+
@reader = nil
|
11
|
+
@grid_down = nil
|
12
|
+
@mixer_down = nil
|
13
|
+
@mixer_up = nil
|
14
|
+
@mixer = nil
|
15
|
+
@all_up = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# returns true/false whether the operation ended or the timeout was hit
|
20
|
+
def timeout(timeout = 0.02, &block)
|
21
|
+
Timeout.timeout(timeout, &block)
|
22
|
+
true
|
23
|
+
rescue Timeout::Error
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def press(interaction, type, opts = nil)
|
28
|
+
interaction.respond_to(type, :down, opts)
|
29
|
+
interaction.respond_to(type, :up, opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def press_all(interaction)
|
34
|
+
%w(up down left right session user1 user2 mixer).each do |type|
|
35
|
+
press(interaction, type.to_sym)
|
36
|
+
end
|
37
|
+
8.times do |y|
|
38
|
+
8.times do |x|
|
39
|
+
press(interaction, :grid, :x => x, :y => y)
|
40
|
+
end
|
41
|
+
press(interaction, :"scene#{y + 1}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#initialize' do
|
46
|
+
|
47
|
+
it 'creates device if not given' do
|
48
|
+
device = Launchpad::Device.new
|
49
|
+
Launchpad::Device.expects(:new).
|
50
|
+
with(:input => true, :output => true, :logger => nil).
|
51
|
+
returns(device)
|
52
|
+
interaction = Launchpad::Interaction.new
|
53
|
+
assert_same device, interaction.device
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'creates device with given device_name' do
|
57
|
+
device = Launchpad::Device.new
|
58
|
+
Launchpad::Device.expects(:new).
|
59
|
+
with(:device_name => 'device', :input => true, :output => true, :logger => nil).
|
60
|
+
returns(device)
|
61
|
+
interaction = Launchpad::Interaction.new(:device_name => 'device')
|
62
|
+
assert_same device, interaction.device
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'creates device with given input_device_id' do
|
66
|
+
device = Launchpad::Device.new
|
67
|
+
Launchpad::Device.expects(:new).
|
68
|
+
with(:input_device_id => 'in', :input => true, :output => true, :logger => nil).
|
69
|
+
returns(device)
|
70
|
+
interaction = Launchpad::Interaction.new(:input_device_id => 'in')
|
71
|
+
assert_same device, interaction.device
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'creates device with given output_device_id' do
|
75
|
+
device = Launchpad::Device.new
|
76
|
+
Launchpad::Device.expects(:new).
|
77
|
+
with(:output_device_id => 'out', :input => true, :output => true, :logger => nil).
|
78
|
+
returns(device)
|
79
|
+
interaction = Launchpad::Interaction.new(:output_device_id => 'out')
|
80
|
+
assert_same device, interaction.device
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'creates device with given input_device_id/output_device_id' do
|
84
|
+
device = Launchpad::Device.new
|
85
|
+
Launchpad::Device.expects(:new).
|
86
|
+
with(:input_device_id => 'in', :output_device_id => 'out', :input => true, :output => true, :logger => nil).
|
87
|
+
returns(device)
|
88
|
+
interaction = Launchpad::Interaction.new(:input_device_id => 'in', :output_device_id => 'out')
|
89
|
+
assert_same device, interaction.device
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'initializes device if given' do
|
93
|
+
device = Launchpad::Device.new
|
94
|
+
interaction = Launchpad::Interaction.new(:device => device)
|
95
|
+
assert_same device, interaction.device
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'stores the logger given' do
|
99
|
+
logger = Logger.new(nil)
|
100
|
+
interaction = Launchpad::Interaction.new(:logger => logger)
|
101
|
+
assert_same logger, interaction.logger
|
102
|
+
assert_same logger, interaction.device.logger
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'doesn\'t activate the interaction' do
|
106
|
+
assert !Launchpad::Interaction.new.active
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
describe '#logger=' do
|
112
|
+
|
113
|
+
it 'stores the logger and passes it to the device as well' do
|
114
|
+
logger = Logger.new(nil)
|
115
|
+
interaction = Launchpad::Interaction.new
|
116
|
+
interaction.logger = logger
|
117
|
+
assert_same logger, interaction.logger
|
118
|
+
assert_same logger, interaction.device.logger
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
describe '#close' do
|
124
|
+
|
125
|
+
it 'stops the interaction' do
|
126
|
+
interaction = Launchpad::Interaction.new
|
127
|
+
interaction.expects(:stop)
|
128
|
+
interaction.close
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'closes the device' do
|
132
|
+
interaction = Launchpad::Interaction.new
|
133
|
+
interaction.device.expects(:close)
|
134
|
+
interaction.close
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
describe '#closed?' do
|
140
|
+
|
141
|
+
it 'returns false on a newly created interaction, but true after closing' do
|
142
|
+
interaction = Launchpad::Interaction.new
|
143
|
+
assert !interaction.closed?
|
144
|
+
interaction.close
|
145
|
+
assert interaction.closed?
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
describe '#start' do
|
151
|
+
|
152
|
+
before do
|
153
|
+
@interaction = Launchpad::Interaction.new
|
154
|
+
end
|
155
|
+
|
156
|
+
after do
|
157
|
+
mocha_teardown # so that expectations on Thread.join don't fail in here
|
158
|
+
begin
|
159
|
+
@interaction.close
|
160
|
+
rescue
|
161
|
+
# ignore, should be handled in tests, this is just to close all the spawned threads
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'sets active to true in blocking mode' do
|
166
|
+
refute @interaction.active
|
167
|
+
erg = timeout { @interaction.start }
|
168
|
+
refute erg, 'there was no timeout'
|
169
|
+
assert @interaction.active
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'sets active to true in detached mode' do
|
173
|
+
refute @interaction.active
|
174
|
+
@interaction.start(:detached => true)
|
175
|
+
assert @interaction.active
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'blocks in blocking mode' do
|
179
|
+
erg = timeout { @interaction.start }
|
180
|
+
refute erg, 'there was no timeout'
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'returns immediately in detached mode' do
|
184
|
+
erg = timeout { @interaction.start(:detached => true) }
|
185
|
+
assert erg, 'there was a timeout'
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'raises CommunicationError when Portmidi::DeviceError occurs' do
|
189
|
+
@interaction.device.stubs(:read_pending_actions).raises(Portmidi::DeviceError.new(0))
|
190
|
+
assert_raises Launchpad::CommunicationError do
|
191
|
+
@interaction.start
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe 'action handling' do
|
196
|
+
|
197
|
+
before do
|
198
|
+
@interaction.response_to(:mixer, :down) { @mixer_down = true }
|
199
|
+
@interaction.response_to(:mixer, :up) do |i,a|
|
200
|
+
sleep 0.001 # sleep to make "sure" :mixer :down has been processed
|
201
|
+
i.stop
|
202
|
+
end
|
203
|
+
@interaction.device.expects(:read_pending_actions).
|
204
|
+
at_least_once.
|
205
|
+
returns([
|
206
|
+
{
|
207
|
+
:timestamp => 0,
|
208
|
+
:state => :down,
|
209
|
+
:type => :mixer
|
210
|
+
},
|
211
|
+
{
|
212
|
+
:timestamp => 0,
|
213
|
+
:state => :up,
|
214
|
+
:type => :mixer
|
215
|
+
}
|
216
|
+
])
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'calls respond_to_action with actions from respond_to_action in blocking mode' do
|
220
|
+
erg = timeout(0.5) { @interaction.start }
|
221
|
+
assert erg, 'the actions weren\'t called'
|
222
|
+
assert @mixer_down, 'the mixer button wasn\'t pressed'
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'calls respond_to_action with actions from respond_to_action in detached mode' do
|
226
|
+
@interaction.start(:detached => true)
|
227
|
+
erg = timeout(0.5) { while @interaction.active; sleep 0.01; end }
|
228
|
+
assert erg, 'there was a timeout'
|
229
|
+
assert @mixer_down, 'the mixer button wasn\'t pressed'
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
describe 'latency' do
|
235
|
+
|
236
|
+
before do
|
237
|
+
@device = @interaction.device
|
238
|
+
@times = []
|
239
|
+
@device.instance_variable_set("@test_interaction_latency_times", @times)
|
240
|
+
def @device.read_pending_actions
|
241
|
+
@test_interaction_latency_times << Time.now.to_f
|
242
|
+
[]
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'sleeps with default latency of 0.001s when none given' do
|
247
|
+
timeout { @interaction.start }
|
248
|
+
assert @times.size > 1
|
249
|
+
@times.each_cons(2) do |a,b|
|
250
|
+
assert_in_delta 0.001, b - a, 0.01
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'sleeps with given latency' do
|
255
|
+
@interaction = Launchpad::Interaction.new(:latency => 0.5, :device => @device)
|
256
|
+
timeout(0.55) { @interaction.start }
|
257
|
+
assert @times.size > 1
|
258
|
+
@times.each_cons(2) do |a,b|
|
259
|
+
assert_in_delta 0.5, b - a, 0.01
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'sleeps with absolute value of given negative latency' do
|
264
|
+
@interaction = Launchpad::Interaction.new(:latency => -0.1, :device => @device)
|
265
|
+
timeout(0.15) { @interaction.start }
|
266
|
+
assert @times.size > 1
|
267
|
+
@times.each_cons(2) do |a,b|
|
268
|
+
assert_in_delta 0.1, b - a, 0.01
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'does not sleep when latency is 0' do
|
273
|
+
@interaction = Launchpad::Interaction.new(:latency => 0, :device => @device)
|
274
|
+
timeout(0.001) { @interaction.start }
|
275
|
+
assert @times.size > 1
|
276
|
+
@times.each_cons(2) do |a,b|
|
277
|
+
assert_in_delta 0, b - a, 0.1
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'raises NoInputAllowedError on closed interaction' do
|
284
|
+
@interaction.close
|
285
|
+
assert_raises Launchpad::NoInputAllowedError do
|
286
|
+
@interaction.start
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
|
292
|
+
describe '#stop' do
|
293
|
+
|
294
|
+
before do
|
295
|
+
@interaction = Launchpad::Interaction.new
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'sets active to false in blocking mode' do
|
299
|
+
erg = timeout { @interaction.start }
|
300
|
+
refute erg, 'there was no timeout'
|
301
|
+
assert @interaction.active
|
302
|
+
@interaction.stop
|
303
|
+
assert !@interaction.active
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'sets active to false in detached mode' do
|
307
|
+
@interaction.start(:detached => true)
|
308
|
+
assert @interaction.active
|
309
|
+
@interaction.stop
|
310
|
+
assert !@interaction.active
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'is callable anytime' do
|
314
|
+
@interaction.stop
|
315
|
+
@interaction.start(:detached => true)
|
316
|
+
@interaction.stop
|
317
|
+
@interaction.stop
|
318
|
+
end
|
319
|
+
|
320
|
+
# this is kinda greybox tested, since I couldn't come up with another way to test tread handling [thomas, 2010-01-24]
|
321
|
+
it 'raises pending exceptions in detached mode' do
|
322
|
+
t = Thread.new {raise BreakError}
|
323
|
+
Thread.expects(:new).returns(t)
|
324
|
+
@interaction.start(:detached => true)
|
325
|
+
assert_raises BreakError do
|
326
|
+
@interaction.stop
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
end
|
331
|
+
|
332
|
+
describe '#response_to/#no_response_to/#respond_to' do
|
333
|
+
|
334
|
+
before do
|
335
|
+
@interaction = Launchpad::Interaction.new
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'calls all responses that match, and not others' do
|
339
|
+
@interaction.response_to(:mixer, :down) {|i, a| @mixer_down = true}
|
340
|
+
@interaction.response_to(:all, :down) {|i, a| @all_down = true}
|
341
|
+
@interaction.response_to(:all, :up) {|i, a| @all_up = true}
|
342
|
+
@interaction.response_to(:grid, :down) {|i, a| @grid_down = true}
|
343
|
+
@interaction.respond_to(:mixer, :down)
|
344
|
+
assert @mixer_down
|
345
|
+
assert @all_down
|
346
|
+
assert !@all_up
|
347
|
+
assert !@grid_down
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'does not call responses when they are deregistered' do
|
351
|
+
@interaction.response_to(:mixer, :down) {|i, a| @mixer_down = true}
|
352
|
+
@interaction.response_to(:mixer, :up) {|i, a| @mixer_up = true}
|
353
|
+
@interaction.response_to(:all, :both) {|i, a| @all_down = a[:state] == :down}
|
354
|
+
@interaction.no_response_to(:mixer, :down)
|
355
|
+
@interaction.respond_to(:mixer, :down)
|
356
|
+
assert !@mixer_down
|
357
|
+
assert !@mixer_up
|
358
|
+
assert @all_down
|
359
|
+
@interaction.respond_to(:mixer, :up)
|
360
|
+
assert !@mixer_down
|
361
|
+
assert @mixer_up
|
362
|
+
assert !@all_down
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'does not call responses registered for both when removing for one of both states' do
|
366
|
+
@interaction.response_to(:mixer, :both) {|i, a| @mixer = true}
|
367
|
+
@interaction.no_response_to(:mixer, :down)
|
368
|
+
@interaction.respond_to(:mixer, :down)
|
369
|
+
assert !@mixer
|
370
|
+
@interaction.respond_to(:mixer, :up)
|
371
|
+
assert @mixer
|
372
|
+
end
|
373
|
+
|
374
|
+
it 'removes other responses when adding a new exclusive response' do
|
375
|
+
@interaction.response_to(:mixer, :both) {|i, a| @mixer = true}
|
376
|
+
@interaction.response_to(:mixer, :down, :exclusive => true) {|i, a| @exclusive_mixer = true}
|
377
|
+
@interaction.respond_to(:mixer, :down)
|
378
|
+
assert !@mixer
|
379
|
+
assert @exclusive_mixer
|
380
|
+
@interaction.respond_to(:mixer, :up)
|
381
|
+
assert @mixer
|
382
|
+
assert @exclusive_mixer
|
383
|
+
end
|
384
|
+
|
385
|
+
it 'allows for multiple types' do
|
386
|
+
@downs = []
|
387
|
+
@interaction.response_to([:up, :down], :down) {|i, a| @downs << a[:type]}
|
388
|
+
@interaction.respond_to(:up, :down)
|
389
|
+
@interaction.respond_to(:down, :down)
|
390
|
+
@interaction.respond_to(:up, :down)
|
391
|
+
assert_equal [:up, :down, :up], @downs
|
392
|
+
end
|
393
|
+
|
394
|
+
describe 'allows to bind to specific grid buttons' do
|
395
|
+
|
396
|
+
before do
|
397
|
+
@downs = []
|
398
|
+
@action = lambda {|i, a| @downs << [a[:x], a[:y]]}
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'one specific grid button' do
|
402
|
+
@interaction.response_to(:grid, :down, :x => 4, :y => 2, &@action)
|
403
|
+
press_all @interaction
|
404
|
+
assert_equal [[4, 2]], @downs
|
405
|
+
end
|
406
|
+
|
407
|
+
it 'a complete row of grid buttons' do
|
408
|
+
@interaction.response_to(:grid, :down, :y => 2, &@action)
|
409
|
+
press_all @interaction
|
410
|
+
assert_equal [[0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2]], @downs
|
411
|
+
end
|
412
|
+
|
413
|
+
it 'a complete column of grid buttons' do
|
414
|
+
@interaction.response_to(:grid, :down, :x => 3, &@action)
|
415
|
+
press_all @interaction
|
416
|
+
assert_equal [[3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6], [3, 7]], @downs
|
417
|
+
end
|
418
|
+
|
419
|
+
it 'a complex range of grid buttons' do
|
420
|
+
@interaction.response_to(:grid, :down, :x => [1,[2]], :y => [1, 3..5], &@action)
|
421
|
+
press_all @interaction
|
422
|
+
assert_equal [[1, 1], [2, 1], [1, 3], [2, 3], [1, 4], [2, 4], [1, 5], [2, 5]], @downs
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'a specific grid buttons, a column, a row, all grid buttons and all buttons' do
|
426
|
+
@interaction.response_to(:all, :down) {|i, a| @downs << [a[:x], a[:y], :all]}
|
427
|
+
@interaction.response_to(:grid, :down) {|i, a| @downs << [a[:x], a[:y], :grid]}
|
428
|
+
@interaction.response_to(:grid, :down, :x => 0) {|i, a| @downs << [a[:x], a[:y], :col]}
|
429
|
+
@interaction.response_to(:grid, :down, :y => 0) {|i, a| @downs << [a[:x], a[:y], :row]}
|
430
|
+
@interaction.response_to(:grid, :down, :x => 0, :y => 0, &@action)
|
431
|
+
press @interaction, :grid, :x => 0, :y => 0
|
432
|
+
assert_equal [[0, 0], [0, 0, :col], [0, 0, :row], [0, 0, :grid], [0, 0, :all]], @downs
|
433
|
+
end
|
434
|
+
|
435
|
+
end
|
436
|
+
|
437
|
+
end
|
438
|
+
|
439
|
+
describe 'regression tests' do
|
440
|
+
|
441
|
+
it 'does not raise an exception or write an error to the logger when calling stop within a response in attached mode' do
|
442
|
+
log = StringIO.new
|
443
|
+
logger = Logger.new(log)
|
444
|
+
logger.level = Logger::ERROR
|
445
|
+
i = Launchpad::Interaction.new(:logger => logger)
|
446
|
+
i.response_to(:mixer, :down) {|current_interaction,a| current_interaction.stop}
|
447
|
+
i.device.expects(:read_pending_actions).
|
448
|
+
at_least_once.
|
449
|
+
returns([{
|
450
|
+
:timestamp => 0,
|
451
|
+
:state => :down,
|
452
|
+
:type => :mixer
|
453
|
+
}])
|
454
|
+
timeout { i.start }
|
455
|
+
assert_equal '', log.string
|
456
|
+
end
|
457
|
+
|
458
|
+
end
|
459
|
+
|
460
|
+
end
|