surface_master 0.2.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0809d0fe24d9da2f41becb4e27a0ec785019c029
4
- data.tar.gz: 43919aa3d44ec3006953501bebf6f3d5a4010f76
3
+ metadata.gz: f2b72c0db5f531114165b1bf813b7723de8daa62
4
+ data.tar.gz: 816e060bcc90c233e3fe80f994295a0d04222334
5
5
  SHA512:
6
- metadata.gz: 0ab45e7ab7dc4761d108f5dd5dc758516bf35a93b6cc4ea0b5142ed80c2ac5a4c11469756762a57aea5de98a7821f79ed12ace9649bb4ec12ae85532b1bd4493
7
- data.tar.gz: 4ede3f9b33450d69c3bcd250494426b4a66cda5d25c1907ce370fd4bc3a8c7a17cc1131d8d077c832fc4dad342854327c57440e25f522651a9a7593118ec2374
6
+ metadata.gz: 7d7e478d03cb7f4bfbb8dd29c527c374d4ad798054b04a3a9c6326d4ed42e56cd971c2830642715df5e9d851de8631342f6d02f215b4cd6c0394ec83e6af7206
7
+ data.tar.gz: 3252e00acc136cf55dee085e073266f65f7cdb63494f84ddcead0fb6bac150e73598fa9e922fc8aefdc58ec6cb9e5e7a608b6c8a4798e2b0141bfc66b8ca0147
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changes
2
2
 
3
+ ## v0.4.0
4
+
5
+ * Jettison threaded input handling.
6
+ * More Numark Orbit handling (output is still a problem).
7
+
8
+
9
+ ## v0.3.0
10
+
11
+ * __BREAKING CHANGE__: Change Orbit interface to more closely follow conventions established with Novation Launchpad driver.
12
+ * Add preliminary `Interaction` class for Numark Orbit.
13
+ * See `examples/orbit_interaction.rb`.
14
+ * Fix exclusive binding in `Interaction` to not nuke coarser bindings entirely.
15
+ * Rename `examples/orbit_testbed.rb` to `examples/orbit_device.rb`.
16
+ * Rename `examples/monitor.rb` to `examples/system_monitor.rb`.
17
+ * Rename `examples/launchpad_testbed.rb` to `examples/launchpad_playground.rb`.
18
+
19
+
3
20
  ## v0.2.1
4
21
 
5
22
  * Missed a file rename.
@@ -1,5 +1,10 @@
1
1
  # Numark Orbit Quick Reference
2
2
 
3
+ ## Mappings
4
+
5
+ Use the file `mappings/Orbit_Preset.json`, and be sure to hit `Send` several times, as it sometimes does not actually take.
6
+
7
+
3
8
  ## Low Battery Indicator
4
9
 
5
10
  Any blinking `Pad Bank Selectors` indicates the battery is low.
@@ -1,32 +1,52 @@
1
1
  #!/usr/bin/env ruby
2
+ #
3
+ # This file provides an example of using the Novation Launchpad Mark 2 using
4
+ # event handlers to respond to buttons, and batch LED updates to update every
5
+ # button on the board quite rapidly.
6
+ #
7
+ # Controls:
8
+ #
9
+ # * Press any grid button and observe that the color changes to white while it's held. Any number
10
+ # of pads may be pressed simultaneously.
11
+ # * The control buttons on the right will each channel-flip a quadrant of the board. They act as
12
+ # toggles, to pressing them again undoes the effect.
13
+ # * The `Mixer` button will terminate the simulation.
14
+ #
15
+ # Effects:
16
+ #
17
+ # * The color of a pad is applied in layers additively, with values clamped to white at the end:
18
+ # 1. The base color for each grid pad is defined by its position with the color getting more
19
+ # green on the X axis, and more blue along the Y axis. See `SCALE` for how steep the change
20
+ # is.
21
+ # 2. Each quadrant adds in a specific color (see `QUADRANTS` below).
22
+ # 3. The red/green/blue channels may be rotated for any given quadrant, if the relevant toggle
23
+ # is active.
24
+ # 4. A sine-wave is applied to one or more channels (see `TIME_SCALE` for the speed of the sine
25
+ # wave).
26
+ # 5. If a particular pad is pressed, the color is set to white.
27
+ #
28
+ # TODO: Input handling seems to interfere with frame rendering, as our FPS seems to "jump" to
29
+ # TODO: insane values when a button is pressed.
30
+ #
2
31
  require "rubygems"
3
32
  require "bundler/setup"
4
33
  Bundler.require(:default, :development)
5
34
 
6
35
  require "surface_master"
7
36
 
8
- # Flash: F0h 00h 20h 29h 02h 18h 23h <LED> <Colour> F7h
9
- # Pulse: F0h 00h 20h 29h 02h 18h 28h <LED> <Colour> F7h
10
- # Set all: F0h 00h 20h 29h 02h 18h 0Eh <Colour> F7h
11
- # Set row: F0h 00h 20h 29h 02h 18h 0Dh <Row> <Colour> F7h
12
- # Set col: F0h 00h 20h 29h 02h 18h 0Ch <Column> <Colour> F7h
13
-
14
- # Configuration
15
- SCALE = 2
37
+ # Animation Configuration
38
+ SCALE = [2, 2]
16
39
  TIME_SCALE = 4.0
40
+ TIME_MASK = { red: 0x3F, green: 0x00, blue: 0x00 }
41
+ QUADRANTS = [[{ red: 0x2F, green: 0x00, blue: 0x00 }, { red: 0x00, green: 0x2F, blue: 0x00 }],
42
+ [{ red: 0x00, green: 0x00, blue: 0x2F }, { red: 0x2F, green: 0x2F, blue: 0x00 }]]
17
43
 
18
44
  # State
19
- QUADRANTS = [
20
- [{ red: 0x2F, green: 0x00, blue: 0x00 }, { red: 0x00, green: 0x2F, blue: 0x00 }],
21
- [{ red: 0x00, green: 0x00, blue: 0x2F }, { red: 0x2F, green: 0x2F, blue: 0x00 }],
22
- ]
23
45
  FLIPPED = [[false, false], [false, false]]
24
46
  PRESSED = (0..7).map { |_x| (0..7).map { |_y| false } }
25
47
  NOW = [Time.now.to_f]
26
48
 
27
49
  # Helpers
28
- CC = %i(up down left right session user1 user2 scene1 scene2 scene3 scene4 scene5 scene6 scene7
29
- scene8)
30
50
  GRID = (0..7).map { |x| (0..7).map { |y| { grid: [x, y] } } }.flatten
31
51
  WHITE = { red: 0x3F, green: 0x3F, blue: 0x3F }
32
52
  BLACK = { red: 0x00, green: 0x00, blue: 0x00 }
@@ -41,15 +61,15 @@ end
41
61
 
42
62
  def positional_color(x, y)
43
63
  { red: 0x00,
44
- green: (x * SCALE),
45
- blue: (y * SCALE) }
64
+ green: (x * SCALE[0]),
65
+ blue: (y * SCALE[1]) }
46
66
  end
47
67
 
48
68
  def temporal_color(_x, _y)
49
69
  s_t = (Math.sin(NOW[0] * TIME_SCALE) * 0.5) + 0.5
50
- { red: (s_t * 0x3F).round,
51
- green: 0x00,
52
- blue: 0x00 }
70
+ { red: (s_t * TIME_MASK[:red]).round,
71
+ green: (s_t * TIME_MASK[:green]).round,
72
+ blue: (s_t * TIME_MASK[:blue]).round }
53
73
  end
54
74
 
55
75
  def clamp_color(color)
@@ -155,18 +175,38 @@ interaction.response_to(:mixer, :down) do |_interaction, _action|
155
175
  interaction.stop
156
176
  end
157
177
  interaction.change(red: 0x03, green: 0x00, blue: 0x00, cc: :mixer)
158
- BTN_COL = { red: 0x03, green: 0x03, blue: 0x03, cc: cc }
178
+ BTN_COL = { red: 0x03, green: 0x03, blue: 0x03 }
159
179
  interaction.changes(%i(scene1 scene2 scene3 scene4).map { |cc| BTN_COL.merge(cc: cc) })
160
180
 
161
181
  init_board(interaction)
162
182
  input_thread = Thread.new do
163
183
  interaction.start
164
184
  end
165
- animation_thread = Thread.new do
185
+ cumulative_time = 0.0
186
+ frame_times = []
187
+ min_frame_time = Float::INFINITY
188
+ max_frame_time = 0.0
189
+ animation_thread = Thread.new do
166
190
  loop do
167
191
  begin
168
192
  NOW[0] = Time.now.to_f
169
193
  init_board(interaction)
194
+ elapsed = Time.now.to_f - NOW[0]
195
+ cumulative_time += elapsed
196
+ min_frame_time = elapsed if elapsed < min_frame_time
197
+ max_frame_time = elapsed if elapsed > max_frame_time
198
+ frame_times.push(elapsed)
199
+ frame_times.shift if frame_times.length > 60
200
+
201
+ # Show avg FPS once per second.
202
+ if cumulative_time >= 1.0
203
+ cumulative_time = 0.0
204
+ avg_frame_time = frame_times.inject(0.0) { |a, e| a + e } / frame_times.length
205
+ avg_rate = (1.0 / avg_frame_time)
206
+ max_rate = (1.0 / min_frame_time)
207
+ min_rate = (1.0 / max_frame_time)
208
+ puts "FPS: Average = %0.1f, Min = %0.1f, Max = %0.1f" % [avg_rate, min_rate, max_rate]
209
+ end
170
210
  rescue StandardError => e
171
211
  puts e.inspect
172
212
  puts e.backtrace.join("\n")
@@ -1,16 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
- # require "bignum"
3
2
  require "rubygems"
4
3
  require "bundler/setup"
5
4
  Bundler.require(:default, :development)
6
5
 
7
6
  require "surface_master"
8
7
 
9
- # Monkey-patching to make debugging easier.
10
- class Fixnum
11
- def to_hex; "%02X" % self; end
12
- end
13
-
14
8
  SurfaceMaster.init!
15
9
  device = SurfaceMaster::Orbit::Device.new
16
10
  loop do
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+ Bundler.require(:default, :development)
5
+
6
+ require "surface_master"
7
+
8
+ SurfaceMaster.init!
9
+ interaction = SurfaceMaster::Orbit::Interaction.new
10
+ interaction.response_to(:pad, :down) do |_inter, action|
11
+ puts "PAD DOWN: #{action.inspect}"
12
+ end
13
+ interaction.response_to(:pad, :up) do |_inter, action|
14
+ puts "PAD UP: #{action.inspect}"
15
+ end
16
+ interaction.response_to(:pad, :up, button: 1..4) do |_inter, action|
17
+ puts "LEFT COLUMN PAD UP: #{action.inspect}"
18
+ end
19
+ interaction.response_to(:pad, :up, button: 5..8, bank: 4, exclusive: true) do |_inter, action|
20
+ puts "SECOND-FROM-LEFT COLUMN PAD UP: #{action.inspect}"
21
+ end
22
+
23
+ interaction.response_to(:vknob, :update) do |_inter, action|
24
+ puts "ANY KNOB, ANY BANK (EXCEPT 2) TURNED: #{action.inspect}"
25
+ end
26
+ interaction.response_to(:vknob, :update, vknob: 3) do |_inter, action|
27
+ puts "KNOB 3, ANY BANK (EXCEPT 2) TURNED: #{action.inspect}"
28
+ end
29
+ interaction.response_to(:vknob, :update, bank: 2, exclusive: true) do |_inter, action|
30
+ puts "ANY KNOB, BANK 2 TURNED: #{action.inspect}"
31
+ end
32
+ interaction.response_to(:vknob, :update, bank: 4, vknob: 1) do |_inter, action|
33
+ puts "KNOB 1, BANK 4 TURNED: #{action.inspect}"
34
+ end
35
+
36
+ interaction.response_to(:accelerometer, :tilt) do |_inter, action|
37
+ puts "ANY AXIS TILT: #{action.inspect}"
38
+ end
39
+ interaction.response_to(:accelerometer, :tilt, axis: :x) do |_inter, action|
40
+ puts "X-AXIS TILT: #{action.inspect}"
41
+ end
42
+
43
+ interaction.response_to(:vknobs, :down) do |_inter, action|
44
+ puts "ANY VKNOB SELECTOR DOWN: #{action.inspect}"
45
+ end
46
+ interaction.response_to(:vknobs, :down, button: 2) do |_inter, action|
47
+ puts "VKNOB 2 SELECTOR DOWN: #{action.inspect}"
48
+ end
49
+
50
+ interaction.response_to(:banks, :down) do |_inter, action|
51
+ puts "ANY BANK SELECTOR DOWN: #{action.inspect}"
52
+ end
53
+ interaction.response_to(:banks, :down, button: 3) do |_inter, action|
54
+ puts "BANK 3 SELECTOR DOWN: #{action.inspect}"
55
+ end
56
+
57
+ interaction.response_to(:shoulder, :down) do |_inter, action|
58
+ puts "ANY SHOULDER DOWN: #{action.inspect}"
59
+ end
60
+ interaction.response_to(:shoulder, :up) do |_inter, action|
61
+ puts "ANY SHOULDER UP: #{action.inspect}"
62
+ end
63
+ interaction.response_to(:shoulder, :down, button: :left) do |_inter, action|
64
+ puts "LEFT SHOULDER DOWN: #{action.inspect}"
65
+ end
66
+ interaction.response_to(:shoulder, :up, button: :left) do |_inter, action|
67
+ puts "LEFT SHOULDER UP: #{action.inspect}"
68
+ end
69
+
70
+ puts "Starting input loop..."
71
+ interaction.start
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+ # Cycle Numark Orbit colors through a wheel.
3
+ #
4
+ # IMPORTANT: Set `MODE` below to `:wired`, or `:wireless`, as appropriate to
5
+ # IMPORTANT: how your Numark Orbit is connected!
6
+ #
7
+ # NOTE: If the lights do not blank out when this starts, your device is in a
8
+ # NOTE: bad state. Push a config to it from the Numark Orbit Editor. If that
9
+ # NOTE: doesn't work, power-cycle it and try again!
10
+ require "rubygems"
11
+ require "bundler/setup"
12
+ Bundler.require(:default, :development)
13
+
14
+ require "surface_master"
15
+
16
+ device = SurfaceMaster::Orbit::Device.new
17
+ # The device seems to be notably less able to accept updates over wireless, and
18
+ # perhaps because CoreMIDI has no backpressure, we can easily wind up hosed.
19
+ # Before settling on new values here, run the process for *several full minutes*
20
+ # and make sure the device continues accepting updates at the end!
21
+ #
22
+ # TODO: More thorough stress testing.
23
+ #
24
+ # TODO: Can we determine if the connection is wired/wireless automatically?
25
+ #
26
+ # TODO: Can we safely get input simultaneously?
27
+ MODE = :wired
28
+
29
+ CONFIGS = { wireless: { delay: 0.75, offset: 0x03, use_read: true, read_delay: 0.1 },
30
+ wired: { delay: 0.1, offset: 0x01, use_read: false, read_delay: 0 } }
31
+ MAPPINGS = [0x03, 0x01, 0x70,
32
+
33
+ 0x00, 0x00, 0x00,
34
+ 0x00, 0x04, 0x04,
35
+ 0x00, 0x08, 0x08,
36
+ 0x00, 0x0C, 0x0C,
37
+ 0x00, 0x01, 0x01,
38
+ 0x00, 0x05, 0x05,
39
+ 0x00, 0x09, 0x09,
40
+ 0x00, 0x0D, 0x0D,
41
+ 0x00, 0x02, 0x02,
42
+ 0x00, 0x06, 0x06,
43
+ 0x00, 0x0A, 0x0A,
44
+ 0x00, 0x0E, 0x0E,
45
+ 0x00, 0x03, 0x03,
46
+ 0x00, 0x07, 0x07,
47
+ 0x00, 0x0B, 0x0B,
48
+ 0x00, 0x0F, 0x0F,
49
+ 0x01, 0x00, 0x10,
50
+ 0x01, 0x04, 0x14,
51
+ 0x01, 0x08, 0x18,
52
+ 0x01, 0x0C, 0x1C,
53
+ 0x01, 0x01, 0x11,
54
+ 0x01, 0x05, 0x15,
55
+ 0x01, 0x09, 0x19,
56
+ 0x01, 0x0D, 0x1D,
57
+ 0x01, 0x02, 0x12,
58
+ 0x01, 0x06, 0x16,
59
+ 0x01, 0x0A, 0x1A,
60
+ 0x01, 0x0E, 0x1E,
61
+ 0x01, 0x03, 0x13,
62
+ 0x01, 0x07, 0x17,
63
+ 0x01, 0x0B, 0x1B,
64
+ 0x01, 0x0F, 0x1F,
65
+ 0x02, 0x00, 0x20,
66
+ 0x02, 0x04, 0x24,
67
+ 0x02, 0x08, 0x28,
68
+ 0x02, 0x0C, 0x2C,
69
+ 0x02, 0x01, 0x21,
70
+ 0x02, 0x05, 0x25,
71
+ 0x02, 0x09, 0x29,
72
+ 0x02, 0x0D, 0x2D,
73
+ 0x02, 0x02, 0x22,
74
+ 0x02, 0x06, 0x26,
75
+ 0x02, 0x0A, 0x2A,
76
+ 0x02, 0x0E, 0x2E,
77
+ 0x02, 0x03, 0x23,
78
+ 0x02, 0x07, 0x27,
79
+ 0x02, 0x0B, 0x2B,
80
+ 0x02, 0x0F, 0x2F,
81
+ 0x03, 0x00, 0x30,
82
+ 0x03, 0x04, 0x34,
83
+ 0x03, 0x08, 0x38,
84
+ 0x03, 0x0C, 0x3C,
85
+ 0x03, 0x01, 0x31,
86
+ 0x03, 0x05, 0x35,
87
+ 0x03, 0x09, 0x39,
88
+ 0x03, 0x0D, 0x3D,
89
+ 0x03, 0x02, 0x32,
90
+ 0x03, 0x06, 0x36,
91
+ 0x03, 0x0A, 0x3A,
92
+ 0x03, 0x0E, 0x3E,
93
+ 0x03, 0x03, 0x33,
94
+ 0x03, 0x07, 0x37,
95
+ 0x03, 0x0B, 0x3B,
96
+ 0x03, 0x0F, 0x3F,
97
+
98
+ 0x00, 0x00, 0x01,
99
+ 0x00, 0x02, 0x00,
100
+ 0x03, 0x00, 0x00,
101
+ 0x01, 0x01, 0x01,
102
+ 0x02, 0x01, 0x03,
103
+ 0x01, 0x00, 0x02,
104
+ 0x01, 0x02, 0x02,
105
+ 0x02, 0x03, 0x02,
106
+ 0x00, 0x03, 0x01,
107
+ 0x03, 0x02, 0x03,
108
+ 0x03, 0x03, 0x0C,
109
+ 0x00, 0x0D, 0x00,
110
+ 0x0C, 0x00, 0x0D,
111
+ 0x00, 0x0C, 0x00,
112
+ 0x0D, 0x00, 0x0C,
113
+ 0x00, 0x0D, 0x00]
114
+ READ_STATE = [0x01, 0x00, 0x00]
115
+
116
+ delay = CONFIGS[MODE][:delay]
117
+ offset = CONFIGS[MODE][:offset]
118
+ use_read = CONFIGS[MODE][:use_read]
119
+ read_delay = CONFIGS[MODE][:read_delay]
120
+
121
+ sleep delay
122
+ puts "Starting..."
123
+ indices = (0..63).map { |n| 5 + (3 * n) }
124
+ loop do
125
+ indices.each do |i|
126
+ MAPPINGS[i] = (MAPPINGS[i] + offset) % 0x3F
127
+ end
128
+ device.sysex!(*MAPPINGS)
129
+ if use_read
130
+ sleep read_delay
131
+ device.sysex!(*READ_STATE)
132
+ end
133
+ printf "."
134
+ sleep delay
135
+ end
@@ -39,14 +39,14 @@ end
39
39
 
40
40
  interaction = SurfaceMaster::Launchpad::Interaction.new
41
41
  cpu_bar = Bar.new(interaction, 0, red: 0x3F, green: 0x00, blue: 0x00)
42
- io_bar = Bar.new(interaction, 0, red: 0x00, green: 0x3F, blue: 0x00)
42
+ io_bar = Bar.new(interaction, 1, red: 0x00, green: 0x3F, blue: 0x00)
43
43
  monitor = Thread.new do
44
44
  loop do
45
45
  fields = `iostat -c 2 disk0`.split(/\n/).last.strip.split(/\s+/)
46
46
  cpu_pct = 100 - fields[-4].to_i
47
47
  cpu_usage = ((cpu_pct / 100.0) * 8.0).round.to_i
48
48
 
49
- disk_pct = (fields[2].to_f / 750.0) * 100.0
49
+ disk_pct = ((fields[2].to_f / 750.0) * 100.0).round.to_i
50
50
  disk_usage = ((disk_pct / 100.0) * 8.0).round.to_i
51
51
 
52
52
  puts "I/O=#{disk_pct}%, CPU=#{cpu_pct}%"
@@ -51,10 +51,13 @@ module SurfaceMaster
51
51
  fail NoOutputAllowedError unless output_enabled?
52
52
  msg = sysex_msg(payload)
53
53
  logger.debug { "#{msg.length}: 0x#{msg.map { |b| '%02X' % b }.join(' ')}" }
54
- @output.write_sysex(msg)
54
+ result = @output.write_sysex(msg)
55
+ if result != 0
56
+ puts "Sysex Error: #{Portmidi::PM_Map.Pm_GetErrorText(result)}"
57
+ end
58
+ result
55
59
  end
56
60
 
57
-
58
61
  def create_input_device(opts)
59
62
  return nil unless opts[:input]
60
63
  create_device(Portmidi.input_devices,
@@ -12,14 +12,11 @@ module SurfaceMaster
12
12
  self.logger = opts[:logger]
13
13
  logger.debug "Initializing #{self.class}##{object_id} with #{opts.inspect}"
14
14
 
15
- @use_threads = opts[:use_threads] || true
16
15
  @device = opts[:device] || @device_class.new(opts.merge(input: true,
17
16
  output: true,
18
17
  logger: opts[:logger]))
19
18
  @latency = (opts[:latency] || 0.001).to_f.abs
20
19
  @active = false
21
-
22
- @action_threads = ThreadGroup.new
23
20
  end
24
21
 
25
22
  def change(opts); @device.change(opts); end
@@ -33,36 +30,29 @@ module SurfaceMaster
33
30
 
34
31
  def closed?; @device.closed?; end
35
32
 
36
- def start(opts = nil)
33
+ def start
37
34
  logger.debug "Starting #{self.class}##{object_id}"
38
35
 
39
- opts = { detached: false }.merge(opts || {})
40
- @active = true
41
- @reader_thread ||= create_reader_thread
42
-
43
- @reader_thread.join unless opts[:detached]
36
+ @active = true
37
+ guard_input_and_reset_at_end! do
38
+ while @active
39
+ @device.read.each { |action| respond_to_action(action) }
40
+ sleep @latency if @latency && @latency > 0.0
41
+ end
42
+ end
44
43
  end
45
44
 
46
45
  def stop
47
46
  logger.debug "Stopping #{self.class}##{object_id}"
48
47
  @active = false
49
- if @reader_thread
50
- # run (resume from sleep) and wait for @reader_thread to end
51
- @reader_thread.run if @reader_thread.alive?
52
- @reader_thread.join
53
- @reader_thread = nil
54
- end
55
- ensure
56
- kill_action_threads!
57
- nil
58
48
  end
59
49
 
60
50
  def response_to(types = :all, state = :both, opts = nil, &block)
61
51
  logger.debug "Setting response to #{types.inspect} for state #{state.inspect} with"\
62
52
  " #{opts.inspect}"
63
- types = Array(types)
64
- opts ||= {}
65
- no_response_to(types, state) if opts[:exclusive] == true
53
+ types = Array(types)
54
+ opts ||= {}
55
+ no_response_to(types, state, opts) if opts[:exclusive] == true
66
56
  expand_states(state).each do |st|
67
57
  add_response_for_state!(types, opts, st, block)
68
58
  end
@@ -71,7 +61,8 @@ module SurfaceMaster
71
61
 
72
62
  def no_response_to(types = nil, state = :both, opts = nil)
73
63
  logger.debug "Removing response to #{types.inspect} for state #{state.inspect}"
74
- types = Array(types)
64
+ types = Array(types)
65
+ opts ||= {}
75
66
  expand_states(state).each do |st|
76
67
  clear_responses_for_state!(types, opts, st)
77
68
  end
@@ -84,15 +75,8 @@ module SurfaceMaster
84
75
 
85
76
  protected
86
77
 
87
- def create_reader_thread
88
- Thread.new do
89
- guard_input_and_reset_at_end! do
90
- while @active
91
- @device.read.each { |action| handle_action(action) }
92
- sleep @latency if @latency && @latency > 0.0
93
- end
94
- end
95
- end
78
+ def expand(list)
79
+ list.map { |ll| ll.respond_to?(:to_a) ? ll.to_a : ll }.flatten
96
80
  end
97
81
 
98
82
  def guard_input_and_reset_at_end!(&block)
@@ -107,17 +91,6 @@ module SurfaceMaster
107
91
  @device.reset!
108
92
  end
109
93
 
110
- def kill_action_threads!
111
- @action_threads.list.each do |thread|
112
- begin
113
- thread.kill
114
- thread.join
115
- rescue StandardException => e # TODO: RuntimeError, Exception, or this?
116
- logger.error "Error when killing action thread: #{e.inspect}"
117
- end
118
- end
119
- end
120
-
121
94
  def add_response_for_state!(types, opts, state, block)
122
95
  response_groups_for(types, opts, state) do |responses|
123
96
  responses << block
@@ -138,17 +111,6 @@ module SurfaceMaster
138
111
 
139
112
  def expand_states(state); Array(state == :both ? %i(down up) : state); end
140
113
 
141
- def handle_action(action)
142
- if @use_threads
143
- action_thread = Thread.new(action) do |act|
144
- respond_to_action(act)
145
- end
146
- @action_threads.add(action_thread)
147
- else
148
- respond_to_action(action)
149
- end
150
- end
151
-
152
114
  def responses
153
115
  # TODO: Generalize for arbitrary actions...
154
116
  @responses ||= Hash.new { |hash, key| hash[key] = { down: [], up: [] } }
@@ -148,7 +148,7 @@ module SurfaceMaster
148
148
  # 0x0D -> Row
149
149
  # 0x0E -> All LEDs
150
150
  [0x0B,
151
- { led: decode_led(opts),
151
+ { led: decode_led(opts)[1],
152
152
  color: [opts[:red] || 0x00, opts[:green] || 0x00, opts[:blue] || 0x00] }]
153
153
  end
154
154
 
@@ -11,11 +11,52 @@ module SurfaceMaster
11
11
  end
12
12
 
13
13
  def reset!
14
- # TODO: This... doesn't appear to work. At all.
15
- if (result = sysex!(*MAPPINGS)) != 0
16
- fail "Expected success (0) setting mappings, got: #{result}"
14
+ # Skip Sysex begin, vendor header, command code, aaaaand sysex end --
15
+ # this will let us compare command vs. response payloads to determine
16
+ # if the state of the device is what we want. Of course, sometimes it
17
+ # lies, but we can't do much about that.
18
+ expected_state = MAPPINGS[6..-2]
19
+ sysex!(MAPPINGS)
20
+ sleep 0.1
21
+ sysex!(READ_STATE)
22
+ current_state = nil
23
+ started_at = Time.now.to_f
24
+ attempts = 1
25
+ loop do
26
+ # TODO: It appears that accessing `buffer` is HIGHLY unsafe! We may
27
+ # TODO: be OK if everyone's waiting on us to come back from this
28
+ # TODO: method before they begin clamoring for input, but that's just
29
+ # TODO: a guess right now.
30
+ if @input.buffer.length == 0
31
+ elapsed = Time.now.to_f - started_at
32
+ if elapsed > 4.0
33
+ logger.warn { "Timeout fetching state of Numark Orbit!" }
34
+ break
35
+ elsif elapsed > (1.0 * attempts)
36
+ logger.warn { "Asking for current state of Numark Orbit again!" }
37
+ attempts += 1
38
+ @output.puts(READ_STATE)
39
+ next
40
+ end
41
+ sleep 0.01
42
+ next
43
+ end
44
+ raw = @input.gets
45
+ current_state = raw.find { |ii| ii[:data][0] == 0xF0 }
46
+ break unless current_state.nil?
47
+ end
48
+
49
+ return unless current_state
50
+
51
+ current_state = current_state[:data][6..-2].dup
52
+ logger.debug { "Got state info from Numark Orbit!" }
53
+ if expected_state != current_state
54
+ logger.error { "UH OH! Numark Orbit state didn't match what we sent!" }
55
+ logger.error { "Expected: #{expected_state.inspect}" }
56
+ logger.error { "Got: #{current_state.inspect}" }
57
+ else
58
+ logger.debug { "Your Numark Orbit should be in the right state now." }
17
59
  end
18
- sysex!(0x01, 0x00, 0x00)
19
60
  end
20
61
 
21
62
  def read
@@ -26,133 +67,140 @@ module SurfaceMaster
26
67
 
27
68
  protected
28
69
 
29
- MAPPINGS = [0x03, 0x01, 0x70,
30
- 0x00, 0x00, 0x00,
31
- 0x00, 0x04, 0x00,
32
- 0x00, 0x08, 0x00,
33
- 0x00, 0x0C, 0x00,
34
- 0x00, 0x01, 0x00,
35
- 0x00, 0x05, 0x00,
36
- 0x00, 0x09, 0x00,
37
- 0x00, 0x0D, 0x00,
38
- 0x00, 0x02, 0x00,
39
- 0x00, 0x06, 0x00,
40
- 0x00, 0x0A, 0x00,
41
- 0x00, 0x0E, 0x00,
42
- 0x00, 0x03, 0x00,
43
- 0x00, 0x07, 0x00,
44
- 0x00, 0x0B, 0x00,
45
- 0x00, 0x0F, 0x00,
46
- 0x01, 0x00, 0x00,
47
- 0x01, 0x04, 0x00,
48
- 0x01, 0x08, 0x00,
49
- 0x01, 0x0C, 0x00,
50
- 0x01, 0x01, 0x00,
51
- 0x01, 0x05, 0x00,
52
- 0x01, 0x09, 0x00,
53
- 0x01, 0x0D, 0x00,
54
- 0x01, 0x02, 0x00,
55
- 0x01, 0x06, 0x00,
56
- 0x01, 0x0A, 0x00,
57
- 0x01, 0x0E, 0x00,
58
- 0x01, 0x03, 0x00,
59
- 0x01, 0x07, 0x00,
60
- 0x01, 0x0B, 0x00,
61
- 0x01, 0x0F, 0x00,
62
- 0x02, 0x00, 0x00,
63
- 0x02, 0x04, 0x00,
64
- 0x02, 0x08, 0x00,
65
- 0x02, 0x0C, 0x00,
66
- 0x02, 0x01, 0x00,
67
- 0x02, 0x05, 0x00,
68
- 0x02, 0x09, 0x00,
69
- 0x02, 0x0D, 0x00,
70
- 0x02, 0x02, 0x00,
71
- 0x02, 0x06, 0x00,
72
- 0x02, 0x0A, 0x00,
73
- 0x02, 0x0E, 0x00,
74
- 0x02, 0x03, 0x00,
75
- 0x02, 0x07, 0x00,
76
- 0x02, 0x0B, 0x00,
77
- 0x02, 0x0F, 0x00,
78
- 0x03, 0x00, 0x00,
79
- 0x03, 0x04, 0x00,
80
- 0x03, 0x08, 0x00,
81
- 0x03, 0x0C, 0x00,
82
- 0x03, 0x01, 0x00,
83
- 0x03, 0x05, 0x00,
84
- 0x03, 0x09, 0x00,
85
- 0x03, 0x0D, 0x00,
86
- 0x03, 0x02, 0x00,
87
- 0x03, 0x06, 0x00,
88
- 0x03, 0x0A, 0x00,
89
- 0x03, 0x0E, 0x00,
90
- 0x03, 0x03, 0x00,
91
- 0x03, 0x07, 0x00,
92
- 0x03, 0x0B, 0x00,
93
- 0x03, 0x0F, 0x00,
94
- 0x00, 0x00, 0x01,
95
- 0x00, 0x02, 0x00,
96
- 0x03, 0x00, 0x00,
97
- 0x01, 0x01, 0x01,
98
- 0x02, 0x01, 0x03,
99
- 0x01, 0x00, 0x02,
100
- 0x01, 0x02, 0x02,
101
- 0x02, 0x03, 0x02,
102
- 0x00, 0x03, 0x01,
103
- 0x03, 0x02, 0x03,
104
- 0x03, 0x03, 0x0C,
105
- 0x00, 0x0D, 0x00,
106
- 0x0C, 0x00, 0x0D,
107
- 0x00, 0x0C, 0x00,
108
- 0x0D, 0x00, 0x0C,
109
- 0x00, 0x0D, 0x00]
70
+ MAPPINGS = [0x03, 0x01, 0x70,
71
+
72
+ 0x00, 0x00, 0x00,
73
+ 0x00, 0x04, 0x00,
74
+ 0x00, 0x08, 0x00,
75
+ 0x00, 0x0C, 0x00,
76
+ 0x00, 0x01, 0x00,
77
+ 0x00, 0x05, 0x00,
78
+ 0x00, 0x09, 0x00,
79
+ 0x00, 0x0D, 0x00,
80
+ 0x00, 0x02, 0x00,
81
+ 0x00, 0x06, 0x00,
82
+ 0x00, 0x0A, 0x00,
83
+ 0x00, 0x0E, 0x00,
84
+ 0x00, 0x03, 0x00,
85
+ 0x00, 0x07, 0x00,
86
+ 0x00, 0x0B, 0x00,
87
+ 0x00, 0x0F, 0x00,
88
+ 0x01, 0x00, 0x00,
89
+ 0x01, 0x04, 0x00,
90
+ 0x01, 0x08, 0x00,
91
+ 0x01, 0x0C, 0x00,
92
+ 0x01, 0x01, 0x00,
93
+ 0x01, 0x05, 0x00,
94
+ 0x01, 0x09, 0x00,
95
+ 0x01, 0x0D, 0x00,
96
+ 0x01, 0x02, 0x00,
97
+ 0x01, 0x06, 0x00,
98
+ 0x01, 0x0A, 0x00,
99
+ 0x01, 0x0E, 0x00,
100
+ 0x01, 0x03, 0x00,
101
+ 0x01, 0x07, 0x00,
102
+ 0x01, 0x0B, 0x00,
103
+ 0x01, 0x0F, 0x00,
104
+ 0x02, 0x00, 0x00,
105
+ 0x02, 0x04, 0x00,
106
+ 0x02, 0x08, 0x00,
107
+ 0x02, 0x0C, 0x00,
108
+ 0x02, 0x01, 0x00,
109
+ 0x02, 0x05, 0x00,
110
+ 0x02, 0x09, 0x00,
111
+ 0x02, 0x0D, 0x00,
112
+ 0x02, 0x02, 0x00,
113
+ 0x02, 0x06, 0x00,
114
+ 0x02, 0x0A, 0x00,
115
+ 0x02, 0x0E, 0x00,
116
+ 0x02, 0x03, 0x00,
117
+ 0x02, 0x07, 0x00,
118
+ 0x02, 0x0B, 0x00,
119
+ 0x02, 0x0F, 0x00,
120
+ 0x03, 0x00, 0x00,
121
+ 0x03, 0x04, 0x00,
122
+ 0x03, 0x08, 0x00,
123
+ 0x03, 0x0C, 0x00,
124
+ 0x03, 0x01, 0x00,
125
+ 0x03, 0x05, 0x00,
126
+ 0x03, 0x09, 0x00,
127
+ 0x03, 0x0D, 0x00,
128
+ 0x03, 0x02, 0x00,
129
+ 0x03, 0x06, 0x00,
130
+ 0x03, 0x0A, 0x00,
131
+ 0x03, 0x0E, 0x00,
132
+ 0x03, 0x03, 0x00,
133
+ 0x03, 0x07, 0x00,
134
+ 0x03, 0x0B, 0x00,
135
+ 0x03, 0x0F, 0x00,
136
+
137
+ 0x00, 0x00, 0x01,
138
+ 0x00, 0x02, 0x00,
139
+ 0x03, 0x00, 0x00,
140
+ 0x01, 0x01, 0x01,
141
+ 0x02, 0x01, 0x03,
142
+ 0x01, 0x00, 0x02,
143
+ 0x01, 0x02, 0x02,
144
+ 0x02, 0x03, 0x02,
145
+ 0x00, 0x03, 0x01,
146
+ 0x03, 0x02, 0x03,
147
+ 0x03, 0x03, 0x0C,
148
+ 0x00, 0x0D, 0x00,
149
+ 0x0C, 0x00, 0x0D,
150
+ 0x00, 0x0C, 0x00,
151
+ 0x0D, 0x00, 0x0C,
152
+ 0x00, 0x0D, 0x00]
153
+ READ_STATE = [0x01, 0x00, 0x00]
110
154
 
111
155
  def sysex_prefix; @sysex_prefix ||= super + [0x00, 0x01, 0x3F, 0x2B]; end
112
156
 
113
157
  def decode_shoulder(decoded, note, _velocity)
114
- decoded[:control].merge!(SurfaceMaster::Orbit::Device::SHOULDERS[note])
158
+ decoded[:control] = decoded[:control].merge(SurfaceMaster::Orbit::Device::SHOULDERS[note])
115
159
  decoded
116
160
  end
117
161
 
118
162
  def decode_pad(decoded, note, _velocity)
119
- decoded[:control][:button] = note
163
+ decoded[:control] = decoded[:control].merge(button: note + 1)
120
164
  decoded
121
165
  end
122
166
 
123
167
  def decode_knob(decoded, note, velocity)
124
- decoded[:control][:bank] = note + 1
125
- decoded[:value] = velocity
168
+ decoded[:control] = decoded[:control].merge(bank: note + 1)
169
+ decoded[:value] = velocity
126
170
  decoded
127
171
  end
128
172
 
129
173
  def decode_control(decoded, note, velocity)
130
- tmp = SurfaceMaster::Orbit::Device::SELECTORS[note]
131
- tmp[:index] = velocity
132
- decoded[:control].merge!(tmp)
174
+ decoded = decoded.merge(SurfaceMaster::Orbit::Device::SELECTORS[note])
175
+ decoded[:control] = { button: velocity }
176
+ decoded
177
+ end
178
+
179
+ def decode_accelerometer(decoded, _note, velocity)
180
+ decoded[:value] = velocity
133
181
  decoded
134
182
  end
135
183
 
136
184
  def enrich_decoded_message(decoded, note, velocity, timestamp)
137
185
  case decoded[:type]
138
- when :shoulder then decoded = decode_shoulder(decoded, note, velocity)
139
- when :pad then decoded = decode_pad(decoded, note, velocity)
140
- when :knob then decoded = decode_knob(decoded, note, velocity)
141
- when :control then decoded = decode_control(decoded, note, velocity)
186
+ when :shoulder then decoded = decode_shoulder(decoded, note, velocity)
187
+ when :pad then decoded = decode_pad(decoded, note, velocity)
188
+ when :vknob then decoded = decode_knob(decoded, note, velocity)
189
+ when :accelerometer then decoded = decode_accelerometer(decoded, note, velocity)
190
+ else decoded = decode_control(decoded, note, velocity)
142
191
  end
143
192
  decoded[:timestamp] = timestamp
144
193
  decoded
145
194
  end
146
195
 
147
196
  def decode_input(input)
148
- # puts [input[:code].to_hex, input[:note].to_hex, input[:velocity].to_hex].join(" ")
149
197
  note = input[:note]
150
198
  velocity = input[:velocity]
151
199
  code_high = input[:code] & 0xF0
152
200
  code_low = input[:code] & 0x0F
153
201
  raw = SurfaceMaster::Orbit::Device::CONTROLS[code_high]
154
202
  raw = raw[code_low] if raw
155
- raw = enrich_decoded_message(raw, note, velocity, input[:timestamp]) if raw
203
+ raw = enrich_decoded_message(raw.dup, note, velocity, input[:timestamp]) if raw
156
204
  raw
157
205
  end
158
206
  end
@@ -7,24 +7,74 @@ module SurfaceMaster
7
7
  super(opts)
8
8
  end
9
9
 
10
- private
10
+ protected
11
11
 
12
+ def combined_types(type, opts = nil)
13
+ tmp = case type
14
+ when :shoulder
15
+ [:"#{type}-#{opts[:button]}"]
16
+ when :accelerometer
17
+ [:"#{type}-#{opts[:axis]}"]
18
+ when :vknob
19
+ knobs = opts[:vknob].nil? ? [1..4] : [opts[:vknob]]
20
+ banks = opts[:bank].nil? ? [1..4] : [opts[:bank]]
21
+
22
+ expand(knobs).product(expand(banks)).map { |k, b| :"#{type}-#{k}-#{b}" }
23
+ when :vknobs, :banks
24
+ buttons = opts[:button].nil? ? [1..4] : [opts[:button]]
25
+
26
+ expand(buttons).map { |b| [:"#{type}-#{b}"] }
27
+ when :pad
28
+ banks = opts[:bank].nil? ? [1..4] : [opts[:bank]]
29
+ buttons = opts[:button].nil? ? [1..16] : [opts[:button]]
30
+
31
+ expand(buttons).product(expand(banks)).map { |p, b| :"#{type}-#{p}-#{b}" }
32
+ else
33
+ [type]
34
+ end
35
+ tmp.flatten.compact
36
+ end
37
+
38
+ def responses_hash
39
+ { down: [],
40
+ up: [],
41
+ update: [],
42
+ tilt: [] }
43
+ end
44
+
45
+ def responses
46
+ @responses ||= Hash.new { |hash, key| hash[key] = responses_hash }
47
+ end
48
+
49
+ # TODO: Allow catching ranges of pads...
50
+ #
51
+ # TODO: Allow differentiating on bank/vknob/shoulder button...
12
52
  def respond_to_action(action)
13
- # type = action[:type].to_sym
14
- # state = action[:state].to_sym
15
- # actions = []
16
- # if type == :grid
17
- # actions += responses[:"grid#{action[:x]}#{action[:y]}"][state]
18
- # actions += responses[:"grid#{action[:x]}-"][state]
19
- # actions += responses[:"grid-#{action[:y]}"][state]
20
- # end
21
- # actions += responses[type][state]
22
- # actions += responses[:all][state]
23
- # actions.compact.each {|block| block.call(self, action)}
53
+ mappings_for_action(action).each do |block|
54
+ block.call(self, action)
55
+ end
56
+ nil
24
57
  rescue Exception => e # TODO: StandardException, RuntimeError, or Exception?
25
58
  logger.error "Error when responding to action #{action.inspect}: #{e.inspect}"
26
59
  raise e
27
60
  end
61
+
62
+ def mappings_for_action(action)
63
+ combined_types = combined_types(action[:type].to_sym, action[:control])
64
+ state = action[:state].to_sym
65
+ actions = []
66
+ actions += combined_types.map { |ct| responses[ct][state] }
67
+ actions += responses[:all][state]
68
+ actions.flatten.compact
69
+ end
70
+
71
+ def expand_states(state)
72
+ case state
73
+ when :both then %i(down up)
74
+ when :all then responses_hash.keys
75
+ else Array(state)
76
+ end
77
+ end
28
78
  end
29
79
  end
30
80
  end
@@ -6,28 +6,28 @@ module SurfaceMaster
6
6
  module MIDICodes
7
7
  # TODO: Use a lib to do a deep-freeze.
8
8
  # rubocop:disable Metrics/LineLength
9
- CONTROLS = { 0x90 => { 0x00 => { type: :pad, action: :down, control: { bank: 1 } },
10
- 0x01 => { type: :pad, action: :down, control: { bank: 2 } },
11
- 0x02 => { type: :pad, action: :down, control: { bank: 3 } },
12
- 0x03 => { type: :pad, action: :down, control: { bank: 4 } },
13
- 0x0F => { type: :shoulder, action: :down, control: {} } },
14
- 0x80 => { 0x00 => { type: :pad, action: :up, control: { bank: 1 } },
15
- 0x01 => { type: :pad, action: :up, control: { bank: 2 } },
16
- 0x02 => { type: :pad, action: :up, control: { bank: 3 } },
17
- 0x03 => { type: :pad, action: :up, control: { bank: 4 } },
18
- 0x0F => { type: :shoulder, action: :up, control: {} } },
19
- 0xB0 => { 0x00 => { type: :knob, action: :update, control: { vknob: 1 } },
20
- 0x01 => { type: :knob, action: :update, control: { vknob: 2 } },
21
- 0x02 => { type: :knob, action: :update, control: { vknob: 3 } },
22
- 0x03 => { type: :knob, action: :update, control: { vknob: 4 } },
23
- 0x0C => { type: :accelerometer, action: :tilt, control: { axis: :x } },
24
- 0x0D => { type: :accelerometer, action: :tilt, control: { axis: :y } },
25
- 0x0F => { type: :control, action: :switch, control: {} } } }.freeze
9
+ CONTROLS = { 0x90 => { 0x00 => { type: :pad, state: :down, control: { bank: 1 } },
10
+ 0x01 => { type: :pad, state: :down, control: { bank: 2 } },
11
+ 0x02 => { type: :pad, state: :down, control: { bank: 3 } },
12
+ 0x03 => { type: :pad, state: :down, control: { bank: 4 } },
13
+ 0x0F => { type: :shoulder, state: :down, control: {} } },
14
+ 0x80 => { 0x00 => { type: :pad, state: :up, control: { bank: 1 } },
15
+ 0x01 => { type: :pad, state: :up, control: { bank: 2 } },
16
+ 0x02 => { type: :pad, state: :up, control: { bank: 3 } },
17
+ 0x03 => { type: :pad, state: :up, control: { bank: 4 } },
18
+ 0x0F => { type: :shoulder, state: :up, control: {} } },
19
+ 0xB0 => { 0x00 => { type: :vknob, state: :update, control: { vknob: 1 } },
20
+ 0x01 => { type: :vknob, state: :update, control: { vknob: 2 } },
21
+ 0x02 => { type: :vknob, state: :update, control: { vknob: 3 } },
22
+ 0x03 => { type: :vknob, state: :update, control: { vknob: 4 } },
23
+ 0x0C => { type: :accelerometer, state: :tilt, control: { axis: :x } },
24
+ 0x0D => { type: :accelerometer, state: :tilt, control: { axis: :y } },
25
+ 0x0F => { state: :down } } }.freeze
26
26
  # rubocop:enable Metrics/LineLength
27
27
  SHOULDERS = { 0x03 => { button: :left },
28
28
  0x04 => { button: :right } }.freeze
29
- SELECTORS = { 0x01 => { selector: :banks },
30
- 0x02 => { selector: :vknobs } }.freeze
29
+ SELECTORS = { 0x01 => { type: :banks },
30
+ 0x02 => { type: :vknobs } }.freeze
31
31
  end
32
32
  end
33
33
  end
@@ -1,4 +1,4 @@
1
1
  #
2
2
  module SurfaceMaster
3
- VERSION = "0.2.1"
3
+ VERSION = "0.4.0"
4
4
  end
@@ -24,3 +24,4 @@ require "surface_master/launchpad/interaction"
24
24
 
25
25
  require "surface_master/orbit/midi_codes"
26
26
  require "surface_master/orbit/device"
27
+ require "surface_master/orbit/interaction"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surface_master
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Frisby
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-09 00:00:00.000000000 Z
11
+ date: 2015-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: portmidi
@@ -73,9 +73,11 @@ files:
73
73
  - debug_tools/decode.rb
74
74
  - debug_tools/extract_midi_monitor_sample.sh
75
75
  - docs/Numark_Orbit_QuickRef.md
76
- - examples/launchpad_testbed.rb
77
- - examples/monitor.rb
78
- - examples/orbit_testbed.rb
76
+ - examples/launchpad_playground.rb
77
+ - examples/orbit_device.rb
78
+ - examples/orbit_interaction.rb
79
+ - examples/orbit_playground.rb
80
+ - examples/system_monitor.rb
79
81
  - lib/surface_master.rb
80
82
  - lib/surface_master/device.rb
81
83
  - lib/surface_master/errors.rb