mrjoy-launchpad 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module Launchpad
2
+ VERSION = '0.4.0'
3
+ end
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env ruby
2
+ # require "bignum"
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+ Bundler.require(:default, :development)
6
+
7
+ require "launchpad"
8
+
9
+ # "Each element has a brightness value from 00h – 3Fh (0 – 63), where 0 is off and 3Fh is full brightness."
10
+ def set_color(device, x, y, r, g, b)
11
+ output = device.instance_variable_get(:@output)
12
+
13
+ led = (y * 10) + x + 11
14
+ x = output.write_sysex([
15
+ # SysEx Begin:
16
+ 0xF0,
17
+ # Manufacturer/Device:
18
+ 0x00,
19
+ 0x20,
20
+ 0x29,
21
+ 0x02,
22
+ 0x18,
23
+ # Command:
24
+ 0x0B,
25
+ # LED:
26
+ led,
27
+ # Red, Green, Blue:
28
+ r,
29
+ g,
30
+ b,
31
+ # SysEx End:
32
+ 0xF7,
33
+ ])
34
+
35
+ puts "ERROR: #{x}" if x != 0
36
+ x
37
+ end
38
+
39
+ def goodbye(interaction)
40
+ (0..7).each do |x|
41
+ (0..7).each do |y|
42
+ set_color(interaction.device, x, y, 0x00, 0x00, 0x00)
43
+ sleep 0.001
44
+ end
45
+ end
46
+ end
47
+
48
+ def bar(interaction, x, val, r, g, b)
49
+ (0..val).each do |y|
50
+ set_color(interaction.device, x, y, r, g, b)
51
+ end
52
+ ((val+1)..7).each do |y|
53
+ set_color(interaction.device, x, y, 0x00, 0x00, 0x00)
54
+ end
55
+ end
56
+
57
+ interaction = Launchpad::Interaction.new(device_name: "Launchpad MK2")
58
+ monitor = Thread.new do
59
+ loop do
60
+ fields = `iostat -c 2 disk0`.split(/\n/).last.strip.split(/\s+/)
61
+ cpu_pct = 100 - fields[-4].to_i
62
+ cpu_usage = ((cpu_pct / 100.0) * 8.0).round.to_i
63
+
64
+ disk_pct = (fields[2].to_f / 750.0) * 100.0
65
+ disk_usage = ((disk_pct / 100.0) * 8.0).round.to_i
66
+
67
+ puts "I/O=#{disk_pct}%, CPU=#{cpu_pct}%"
68
+
69
+ # TODO: Network in/out...
70
+
71
+ # TODO: Make block I/O not be a bar but a fill, with scale indicated by color...
72
+
73
+ bar(interaction, 0, cpu_usage, 0x3F, 0x00, 0x00)
74
+ bar(interaction, 1, disk_usage, 0x00, 0x3F, 0x00)
75
+ end
76
+ end
77
+
78
+ interaction.response_to(:mixer, :down) do |_interaction, action|
79
+ puts "Shutting down"
80
+ begin
81
+ monitor.kill
82
+ goodbye(interaction)
83
+ interaction.stop
84
+ rescue Exception => e
85
+ puts e.inspect
86
+ end
87
+ end
88
+ interaction.start
@@ -0,0 +1,44 @@
1
+ require 'minitest/spec'
2
+ require 'minitest/autorun'
3
+
4
+ begin
5
+ require 'minitest/reporters'
6
+ MiniTest::Reporters.use!
7
+ rescue LoadError
8
+ # ignore when it's not there - must be ruby 1.8
9
+ end
10
+
11
+ require 'mocha/setup'
12
+
13
+ require 'launchpad'
14
+
15
+ # mock Portmidi for tests
16
+ module Portmidi
17
+
18
+ class Input
19
+ attr_accessor :device_id
20
+ def initialize(device_id)
21
+ self.device_id = device_id
22
+ end
23
+ def read(*args); nil; end
24
+ def close; nil; end
25
+ end
26
+
27
+ class Output
28
+ attr_accessor :device_id
29
+ def initialize(device_id)
30
+ self.device_id = device_id
31
+ end
32
+ def write(*args); nil; end
33
+ def close; nil; end
34
+ end
35
+
36
+ def self.input_devices; mock_devices; end
37
+ def self.output_devices; mock_devices; end
38
+ def self.start; end
39
+
40
+ end
41
+
42
+ def mock_devices(opts = {})
43
+ [Portmidi::Device.new(opts[:id] || 1, 0, 0, opts[:name] || 'Launchpad')]
44
+ end
@@ -0,0 +1,530 @@
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
+ SCENE_BUTTONS = {
16
+ :scene1 => 0x08,
17
+ :scene2 => 0x18,
18
+ :scene3 => 0x28,
19
+ :scene4 => 0x38,
20
+ :scene5 => 0x48,
21
+ :scene6 => 0x58,
22
+ :scene7 => 0x68,
23
+ :scene8 => 0x78
24
+ }
25
+ COLORS = {
26
+ nil => 0, 0 => 0, :off => 0,
27
+ 1 => 1, :lo => 1, :low => 1,
28
+ 2 => 2, :med => 2, :medium => 2,
29
+ 3 => 3, :hi => 3, :high => 3
30
+ }
31
+ STATES = {
32
+ :down => 127,
33
+ :up => 0
34
+ }
35
+
36
+ def expects_output(device, *args)
37
+ args = [args] unless args.first.is_a?(Array)
38
+ messages = args.collect {|data| {:message => data, :timestamp => 0}}
39
+ device.instance_variable_get('@output').expects(:write).with(messages)
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', :write => nil))
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
+ {
188
+ :reset => [0xB0, 0x00, 0x00],
189
+ :flashing_on => [0xB0, 0x00, 0x20],
190
+ :flashing_off => [0xB0, 0x00, 0x21],
191
+ :flashing_auto => [0xB0, 0x00, 0x28]
192
+ }.each do |method, codes|
193
+ describe "##{method}" do
194
+
195
+ it 'raises NoOutputAllowedError when not initialized with output' do
196
+ assert_raises Launchpad::NoOutputAllowedError do
197
+ Launchpad::Device.new(:output => false).send(method)
198
+ end
199
+ end
200
+
201
+ it "sends #{codes.inspect}" do
202
+ d = Launchpad::Device.new
203
+ expects_output(d, *codes)
204
+ d.send(method)
205
+ end
206
+
207
+ end
208
+ end
209
+
210
+ describe '#test_leds' do
211
+
212
+ it 'raises NoOutputAllowedError when not initialized with output' do
213
+ assert_raises Launchpad::NoOutputAllowedError do
214
+ Launchpad::Device.new(:output => false).test_leds
215
+ end
216
+ end
217
+
218
+ describe 'initialized with output' do
219
+
220
+ before do
221
+ @device = Launchpad::Device.new(:input => false)
222
+ end
223
+
224
+ it 'returns nil' do
225
+ assert_nil @device.test_leds
226
+ end
227
+
228
+ COLORS.merge(nil => 3).each do |name, value|
229
+ if value == 0
230
+ it "sends 0xB0, 0x00, 0x00 when given #{name}" do
231
+ expects_output(@device, 0xB0, 0x00, 0x00)
232
+ @device.test_leds(value)
233
+ end
234
+ else
235
+ it "sends 0xB0, 0x00, 0x7C + #{value} when given #{name}" do
236
+ d = Launchpad::Device.new
237
+ expects_output(@device, 0xB0, 0x00, 0x7C + value)
238
+ value.nil? ? @device.test_leds : @device.test_leds(value)
239
+ end
240
+ end
241
+ end
242
+
243
+ end
244
+
245
+ end
246
+
247
+ describe '#change' do
248
+
249
+ it 'raises NoOutputAllowedError when not initialized with output' do
250
+ assert_raises Launchpad::NoOutputAllowedError do
251
+ Launchpad::Device.new(:output => false).change(:up)
252
+ end
253
+ end
254
+
255
+ describe 'initialized with output' do
256
+
257
+ before do
258
+ @device = Launchpad::Device.new(:input => false)
259
+ end
260
+
261
+ it 'returns nil' do
262
+ assert_nil @device.change(:up)
263
+ end
264
+
265
+ describe 'control buttons' do
266
+ CONTROL_BUTTONS.each do |type, value|
267
+ it "sends 0xB0, #{value}, 12 when given #{type}" do
268
+ expects_output(@device, 0xB0, value, 12)
269
+ @device.change(type)
270
+ end
271
+ end
272
+ end
273
+
274
+ describe 'scene buttons' do
275
+ SCENE_BUTTONS.each do |type, value|
276
+ it "sends 0x90, #{value}, 12 when given #{type}" do
277
+ expects_output(@device, 0x90, value, 12)
278
+ @device.change(type)
279
+ end
280
+ end
281
+ end
282
+
283
+ describe 'grid buttons' do
284
+ 8.times do |x|
285
+ 8.times do |y|
286
+ it "sends 0x90, #{16 * y + x}, 12 when given :grid, :x => #{x}, :y => #{y}" do
287
+ expects_output(@device, 0x90, 16 * y + x, 12)
288
+ @device.change(:grid, :x => x, :y => y)
289
+ end
290
+ end
291
+ end
292
+
293
+ it 'raises NoValidGridCoordinatesError if x is not specified' do
294
+ assert_raises Launchpad::NoValidGridCoordinatesError do
295
+ @device.change(:grid, :y => 1)
296
+ end
297
+ end
298
+
299
+ it 'raises NoValidGridCoordinatesError if x is below 0' do
300
+ assert_raises Launchpad::NoValidGridCoordinatesError do
301
+ @device.change(:grid, :x => -1, :y => 1)
302
+ end
303
+ end
304
+
305
+ it 'raises NoValidGridCoordinatesError if x is above 7' do
306
+ assert_raises Launchpad::NoValidGridCoordinatesError do
307
+ @device.change(:grid, :x => 8, :y => 1)
308
+ end
309
+ end
310
+
311
+ it 'raises NoValidGridCoordinatesError if y is not specified' do
312
+ assert_raises Launchpad::NoValidGridCoordinatesError do
313
+ @device.change(:grid, :x => 1)
314
+ end
315
+ end
316
+
317
+ it 'raises NoValidGridCoordinatesError if y is below 0' do
318
+ assert_raises Launchpad::NoValidGridCoordinatesError do
319
+ @device.change(:grid, :x => 1, :y => -1)
320
+ end
321
+ end
322
+
323
+ it 'raises NoValidGridCoordinatesError if y is above 7' do
324
+ assert_raises Launchpad::NoValidGridCoordinatesError do
325
+ @device.change(:grid, :x => 1, :y => 8)
326
+ end
327
+ end
328
+
329
+ end
330
+
331
+ describe 'colors' do
332
+ COLORS.each do |red_key, red_value|
333
+ COLORS.each do |green_key, green_value|
334
+ it "sends 0x90, 0, #{16 * green_value + red_value + 12} when given :red => #{red_key}, :green => #{green_key}" do
335
+ expects_output(@device, 0x90, 0, 16 * green_value + red_value + 12)
336
+ @device.change(:grid, :x => 0, :y => 0, :red => red_key, :green => green_key)
337
+ end
338
+ end
339
+ end
340
+
341
+ it 'raises NoValidBrightnessError if red is below 0' do
342
+ assert_raises Launchpad::NoValidBrightnessError do
343
+ @device.change(:grid, :x => 0, :y => 0, :red => -1)
344
+ end
345
+ end
346
+
347
+ it 'raises NoValidBrightnessError if red is above 3' do
348
+ assert_raises Launchpad::NoValidBrightnessError do
349
+ @device.change(:grid, :x => 0, :y => 0, :red => 4)
350
+ end
351
+ end
352
+
353
+ it 'raises NoValidBrightnessError if red is an unknown symbol' do
354
+ assert_raises Launchpad::NoValidBrightnessError do
355
+ @device.change(:grid, :x => 0, :y => 0, :red => :unknown)
356
+ end
357
+ end
358
+
359
+ it 'raises NoValidBrightnessError if green is below 0' do
360
+ assert_raises Launchpad::NoValidBrightnessError do
361
+ @device.change(:grid, :x => 0, :y => 0, :green => -1)
362
+ end
363
+ end
364
+
365
+ it 'raises NoValidBrightnessError if green is above 3' do
366
+ assert_raises Launchpad::NoValidBrightnessError do
367
+ @device.change(:grid, :x => 0, :y => 0, :green => 4)
368
+ end
369
+ end
370
+
371
+ it 'raises NoValidBrightnessError if green is an unknown symbol' do
372
+ assert_raises Launchpad::NoValidBrightnessError do
373
+ @device.change(:grid, :x => 0, :y => 0, :green => :unknown)
374
+ end
375
+ end
376
+
377
+ end
378
+
379
+ describe 'mode' do
380
+
381
+ it 'sends color + 12 when nothing given' do
382
+ expects_output(@device, 0x90, 0, 12)
383
+ @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0)
384
+ end
385
+
386
+ it 'sends color + 12 when given :normal' do
387
+ expects_output(@device, 0x90, 0, 12)
388
+ @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0, :mode => :normal)
389
+ end
390
+
391
+ it 'sends color + 8 when given :flashing' do
392
+ expects_output(@device, 0x90, 0, 8)
393
+ @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0, :mode => :flashing)
394
+ end
395
+
396
+ it 'sends color when given :buffering' do
397
+ expects_output(@device, 0x90, 0, 0)
398
+ @device.change(:grid, :x => 0, :y => 0, :red => 0, :green => 0, :mode => :buffering)
399
+ end
400
+
401
+ end
402
+
403
+ end
404
+
405
+ end
406
+
407
+ describe '#change_all' do
408
+
409
+ it 'raises NoOutputAllowedError when not initialized with output' do
410
+ assert_raises Launchpad::NoOutputAllowedError do
411
+ Launchpad::Device.new(:output => false).change_all
412
+ end
413
+ end
414
+
415
+ describe 'initialized with output' do
416
+
417
+ before do
418
+ @device = Launchpad::Device.new(:input => false)
419
+ end
420
+
421
+ it 'returns nil' do
422
+ assert_nil @device.change_all([0])
423
+ end
424
+
425
+ it 'fills colors with 0, set grid layout to XY and flush colors' do
426
+ expects_output(@device, 0xB0, 0, 0x01)
427
+ expects_output(@device, *([[0x92, 17, 17]] * 20 + [[0x92, 12, 12]] * 20))
428
+ @device.change_all([5] * 40)
429
+ end
430
+
431
+ it 'cuts off exceeding colors, set grid layout to XY and flush colors' do
432
+ expects_output(@device, 0xB0, 0, 0x01)
433
+ expects_output(@device, *([[0x92, 17, 17]] * 40))
434
+ @device.change_all([5] * 100)
435
+ end
436
+
437
+ end
438
+
439
+ end
440
+
441
+ describe '#buffering_mode' do
442
+
443
+ it 'raises NoOutputAllowedError when not initialized with output' do
444
+ assert_raises Launchpad::NoOutputAllowedError do
445
+ Launchpad::Device.new(:output => false).buffering_mode
446
+ end
447
+ end
448
+
449
+ {
450
+ nil => [0xB0, 0x00, 0x20],
451
+ {} => [0xB0, 0x00, 0x20],
452
+ {:display_buffer => 1} => [0xB0, 0x00, 0x21],
453
+ {:update_buffer => 1} => [0xB0, 0x00, 0x24],
454
+ {:copy => true} => [0xB0, 0x00, 0x30],
455
+ {:flashing => true} => [0xB0, 0x00, 0x28],
456
+ {
457
+ :display_buffer => 1,
458
+ :update_buffer => 1,
459
+ :copy => true,
460
+ :flashing => true
461
+ } => [0xB0, 0x00, 0x3D]
462
+ }.each do |opts, codes|
463
+ it "sends #{codes.inspect} when called with #{opts.inspect}" do
464
+ d = Launchpad::Device.new
465
+ expects_output(d, *codes)
466
+ d.buffering_mode(opts)
467
+ end
468
+ end
469
+
470
+ end
471
+
472
+ describe '#read_pending_actions' do
473
+
474
+ it 'raises NoInputAllowedError when not initialized with input' do
475
+ assert_raises Launchpad::NoInputAllowedError do
476
+ Launchpad::Device.new(:input => false).read_pending_actions
477
+ end
478
+ end
479
+
480
+ describe 'initialized with input' do
481
+
482
+ before do
483
+ @device = Launchpad::Device.new(:output => false)
484
+ end
485
+
486
+ describe 'control buttons' do
487
+ CONTROL_BUTTONS.each do |type, value|
488
+ STATES.each do |state, velocity|
489
+ it "builds proper action for control button #{type}, #{state}" do
490
+ stub_input(@device, {:timestamp => 0, :message => [0xB0, value, velocity]})
491
+ assert_equal [{:timestamp => 0, :state => state, :type => type}], @device.read_pending_actions
492
+ end
493
+ end
494
+ end
495
+ end
496
+
497
+ describe 'scene buttons' do
498
+ SCENE_BUTTONS.each do |type, value|
499
+ STATES.each do |state, velocity|
500
+ it "builds proper action for scene button #{type}, #{state}" do
501
+ stub_input(@device, {:timestamp => 0, :message => [0x90, value, velocity]})
502
+ assert_equal [{:timestamp => 0, :state => state, :type => type}], @device.read_pending_actions
503
+ end
504
+ end
505
+ end
506
+ end
507
+
508
+ describe '#grid buttons' do
509
+ 8.times do |x|
510
+ 8.times do |y|
511
+ STATES.each do |state, velocity|
512
+ it "builds proper action for grid button #{x},#{y}, #{state}" do
513
+ stub_input(@device, {:timestamp => 0, :message => [0x90, 16 * y + x, velocity]})
514
+ assert_equal [{:timestamp => 0, :state => state, :type => :grid, :x => x, :y => y}], @device.read_pending_actions
515
+ end
516
+ end
517
+ end
518
+ end
519
+ end
520
+
521
+ it 'builds proper actions for multiple pending actions' do
522
+ stub_input(@device, {:timestamp => 1, :message => [0x90, 0, 127]}, {:timestamp => 2, :message => [0xB0, 0x68, 0]})
523
+ assert_equal [{:timestamp => 1, :state => :down, :type => :grid, :x => 0, :y => 0}, {:timestamp => 2, :state => :up, :type => :up}], @device.read_pending_actions
524
+ end
525
+
526
+ end
527
+
528
+ end
529
+
530
+ end