qlab-ruby 0.1.2

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.
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ # local concerns
20
+ .DS_Store
21
+ .rake_tasks~
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in qlab-ruby.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Adam Bachman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,59 @@
1
+ # QLab
2
+
3
+ Interact with QLab from Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'qlab-ruby'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install qlab-ruby
18
+
19
+ ## Usage
20
+
21
+ > require 'qlab-ruby'
22
+ > machine = QLab.connect # defaults to ('localhost', 53000)
23
+ > machine.workspaces.first.go
24
+
25
+ And you're off.
26
+
27
+ A `machine` has one or more `workspaces`. A `workspace` has one or more
28
+ `cue_lists`. A `cue` can respond to any of the QLab OSC commands.
29
+
30
+ > cue.start
31
+ > cue.stop
32
+
33
+ etc.
34
+
35
+ Most OSC commands that accept a single value can be called by their `=`
36
+ versions. For example:
37
+
38
+ > cue.rate(0.5)
39
+
40
+ and
41
+
42
+ > cue.rate = 0.5
43
+
44
+ do the same thing.
45
+
46
+ OSC commands that require multiple arguments will still have to be called
47
+ as methods.
48
+
49
+ > cue.sliderLevel(1, -5)
50
+
51
+ To set slider 1 to the value -5.
52
+
53
+ ## Contributing
54
+
55
+ 1. Fork it
56
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
57
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
58
+ 4. Push to the branch (`git push origin my-new-feature`)
59
+ 5. Create new Pull Request
@@ -0,0 +1,21 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ gem 'rdoc'
5
+ require 'rdoc/task'
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'qlab-ruby'
10
+ t.test_files = FileList['test/*_test.rb']
11
+ t.verbose = true
12
+ end
13
+
14
+ task :default => :test
15
+
16
+ RDoc::Task.new do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = "qlab-ruby #{QLab::VERSION}"
19
+ rdoc.rdoc_files.include('README*')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
@@ -0,0 +1,34 @@
1
+ require "qlab-ruby/version"
2
+ require "qlab-ruby/core-ext/osc-ruby"
3
+
4
+ module QLab
5
+ autoload :Commands, 'qlab-ruby/commands'
6
+ autoload :Communicator, 'qlab-ruby/communicator'
7
+ autoload :Cue, 'qlab-ruby/cue'
8
+ autoload :CueList, 'qlab-ruby/cue_list'
9
+ autoload :Machine, 'qlab-ruby/machine'
10
+ autoload :Reply, 'qlab-ruby/reply'
11
+ autoload :Workspace, 'qlab-ruby/workspace'
12
+
13
+ class << self
14
+ def connect machine_address='localhost', port=53000
15
+ begin
16
+ # global QLab connection
17
+ new_machine = Machine.new(machine_address, port)
18
+ rescue Errno::ECONNREFUSED
19
+ puts "Failed to connect to QLab machine at #{machine_address}:#{port}, please make sure your values are correct and QLab is running."
20
+ raise
21
+ end
22
+
23
+ if new_machine.connected?
24
+ new_machine.alwaysReply = 1
25
+ end
26
+
27
+ new_machine
28
+ end
29
+
30
+ def debug msg
31
+ # puts msg
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,223 @@
1
+ module QLab
2
+ # All commands QLab accepts
3
+ module Commands
4
+ MACHINE = %w(
5
+ alwaysReply
6
+ connect
7
+ version
8
+ workingDirectory
9
+ workspaces
10
+ )
11
+
12
+ WORKSPACE = %w(
13
+ cueLists
14
+ go
15
+ hardStop
16
+ new
17
+ panic
18
+ pause
19
+ reset
20
+ resume
21
+ runningCues
22
+ runningOrPausedCues
23
+ select
24
+ selectedCues
25
+ stop
26
+ thump
27
+ toggleFullScreen
28
+ updates
29
+ )
30
+
31
+ CUE = %w(
32
+ actionElapsed
33
+ allowsEditingDuration
34
+ armed
35
+ children
36
+ colorName
37
+ continueMode
38
+ cueTargetId
39
+ cueTargetNumber
40
+ defaultName
41
+ displayName
42
+ duration
43
+ fileTarget
44
+ flagged
45
+ hardStop
46
+ hasCueTargets
47
+ hasFileTargets
48
+ isBroken
49
+ isLoaded
50
+ isPaused
51
+ isRunning
52
+ listName
53
+ load
54
+ loadAt
55
+ name
56
+ notes
57
+ number
58
+ panic
59
+ pause
60
+ percentActionElapsed
61
+ percentPostWaitElapsed
62
+ percentPreWaitElapsed
63
+ postWait
64
+ postWaitElapsed
65
+ preWait
66
+ preWaitElapsed
67
+ preview
68
+ reset
69
+ resume
70
+ sliderLevel
71
+ sliderLevels
72
+ start
73
+ stop
74
+ togglePause
75
+ type
76
+ uniqueID
77
+ valuesForKeys
78
+ )
79
+
80
+ GROUP_CUE = %w(
81
+ playbackPositionId
82
+ )
83
+
84
+ AUDIO_CUE = %w(
85
+ doFade
86
+ doPitchShift
87
+ endTime
88
+ infiniteLoop
89
+ level
90
+ patch
91
+ playCount
92
+ rate
93
+ sliderLevel
94
+ sliderLevels
95
+ startTime
96
+ )
97
+
98
+ FADE_CUE = %w(
99
+ level
100
+ sliderLevel
101
+ sliderLevels
102
+ )
103
+
104
+ MIC_CUE = %w(
105
+ level
106
+ sliderLevel
107
+ sliderLevels
108
+ )
109
+
110
+ VIDEO_CUE = %w(
111
+ cueSize
112
+ doEffect
113
+ doFade
114
+ doPitchShift
115
+ effect
116
+ effectSet
117
+ endTime
118
+ fullScreen
119
+ infiniteLoop
120
+ layer
121
+ level
122
+ opacity
123
+ patch
124
+ playCount
125
+ preserveAspectRatio
126
+ quaternion
127
+ rate
128
+ scaleX
129
+ scaleY
130
+ sliderLevel
131
+ sliderLevels
132
+ startTime
133
+ surfaceID
134
+ surfaceList
135
+ surfaceSize
136
+ translationX
137
+ translationY
138
+ )
139
+
140
+ # in blocks of: all, group, audio, video, osc, midi, devamp, script
141
+ SET_CUES = %w(
142
+ number
143
+ name
144
+ notes
145
+ fileTarget
146
+ cueTargetNumber
147
+ cueTargetId
148
+ preWait
149
+ duration
150
+ postWait
151
+ continueMode
152
+ flagged
153
+ autoLoad
154
+ armed
155
+ colorName
156
+
157
+ playbackPositionId
158
+
159
+ patch
160
+ startTime
161
+ endTime
162
+ playCount
163
+ infiniteLoop
164
+ rate
165
+ doPitchShift
166
+ doFade
167
+
168
+ surfaceID
169
+ patch
170
+ fullScreen
171
+ layer
172
+ opacity
173
+ preserveAspectRatio
174
+ translationX
175
+ translationY
176
+ scaleX
177
+ scaleY
178
+ doEffect
179
+ effectSet
180
+
181
+ messageType
182
+ qlabCommand
183
+ qlabCueNumber
184
+ qlabCueParameters
185
+ rawString
186
+
187
+ duration
188
+ messageType
189
+ status
190
+ channel
191
+ byte1
192
+ byte2
193
+ byteCombo
194
+ doFade
195
+ endValue
196
+ deviceID
197
+ commandFormat
198
+ command
199
+ qNumber
200
+ qList
201
+ qPath
202
+ macro
203
+ controlNumber
204
+ controlValue
205
+ timecodeString
206
+ hours
207
+ minutes
208
+ seconds
209
+ frames
210
+ subframes
211
+ timecodeFormat
212
+ rawString
213
+
214
+ startNextCueWhenSliceEnds
215
+ stopTargetWhenSliceEnds
216
+
217
+ scriptSource
218
+ )
219
+
220
+ ALL_CUES = (CUE + GROUP_CUE + AUDIO_CUE + FADE_CUE + MIC_CUE + VIDEO_CUE).uniq.sort
221
+ end
222
+ end
223
+
@@ -0,0 +1,51 @@
1
+ module QLab
2
+ # An abstract class providing communication behavior for objects that need to
3
+ # interact with QLab.
4
+ class Communicator
5
+
6
+ def send_message osc_address, *osc_arguments
7
+ osc_address = format_osc_address(osc_address)
8
+
9
+ if osc_arguments.size > 0
10
+ osc_message = OSC::Message.new osc_address, *osc_arguments
11
+ else
12
+ osc_message = OSC::Message.new osc_address
13
+ end
14
+ responses = []
15
+
16
+ QLab.debug "[Action send_message] send #{ osc_message.encode }"
17
+ connection.send(osc_message) do |response|
18
+ responses << QLab::Reply.new(response)
19
+ end
20
+
21
+ if responses.size == 1
22
+ q_reply = responses[0]
23
+ QLab.debug "[Action send_message] got one response: #{q_reply.inspect}"
24
+
25
+ if q_reply.has_data?
26
+ QLab.debug "[Action send_message] single response has data: #{q_reply.data.inspect}"
27
+ q_reply.data
28
+ elsif q_reply.has_status?
29
+ QLab.debug "[Action send_message] single response has status: #{q_reply.status.inspect}"
30
+ q_reply.status
31
+ else
32
+ QLab.debug "[Action send_message] single response has no data or status: #{q_reply.inspect}"
33
+ q_reply
34
+ end
35
+ else
36
+ responses
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def format_osc_address address
43
+ if %r[^/] !~ address.to_s
44
+ "/#{ address }"
45
+ else
46
+ address
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,6 @@
1
+ require 'osc-ruby'
2
+
3
+ require 'qlab-ruby/core-ext/osc-ruby/message'
4
+ require 'qlab-ruby/core-ext/osc-ruby/tcp'
5
+ require 'qlab-ruby/core-ext/osc-ruby/sending_socket'
6
+ require 'qlab-ruby/core-ext/osc-ruby/tcp_client'
@@ -0,0 +1,25 @@
1
+ module OSC
2
+ # Reopen the osc-ruby Message class to provide additional methods to support
3
+ # QLab's use of OSC.
4
+ class Message
5
+ def has_arguments?
6
+ to_a.size > 0
7
+ end
8
+
9
+ # attachable responder, for use with TCP::Server
10
+ def responder
11
+ @responder
12
+ end
13
+
14
+ def responder=(val)
15
+ @responder = val
16
+ end
17
+
18
+ def debug
19
+ types = to_a.map(&:class).map(&:to_s).join(', ')
20
+ args = to_a
21
+
22
+ "#{ip_address}:#{ip_port} -- #{address} -- [#{ types }] -- #{ args.inspect }"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ module OSC
2
+ module TCP
3
+ # A wrapper around an open TCP socket providing SLIP encoding for outbound
4
+ # messages.
5
+ class SendingSocket
6
+ def initialize socket
7
+ @socket = socket
8
+ end
9
+
10
+ # send SLIP encoded OSC messages
11
+ def send msg
12
+ @socket_buffer = []
13
+
14
+ enc_msg = msg.encode
15
+
16
+ send_char CHAR_END
17
+
18
+ enc_msg.bytes.each do |b|
19
+ case b
20
+ when CHAR_END
21
+ send_char CHAR_ESC
22
+ send_char CHAR_ESC_END
23
+ when CHAR_ESC
24
+ send_char CHAR_ESC
25
+ send_char CHAR_ESC_ESC
26
+ else
27
+ send_char b
28
+ end
29
+ end
30
+
31
+ send_char CHAR_END
32
+
33
+ flush
34
+ end
35
+
36
+ private
37
+
38
+ def flush
39
+ @socket.send @socket_buffer.join, 0
40
+ end
41
+
42
+ def send_char c
43
+ @socket_buffer << [c].pack('C')
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ module OSC
2
+ # SLIP encoding constants.
3
+ module TCP
4
+ CHAR_END = 0300 # indicates end of packet
5
+ CHAR_ESC = 0333 # indicates byte stuffing
6
+ CHAR_ESC_END = 0334 # ESC ESC_END means END data byte
7
+ CHAR_ESC_ESC = 0335 # ESC ESC_ESC means ESC data byte
8
+
9
+ CHAR_END_ENC = [0300].pack('C') # indicates end of packet
10
+ CHAR_ESC_ENC = [0333].pack('C') # indicates byte stuffing
11
+ CHAR_ESC_END_ENC = [0334].pack('C') # ESC ESC_END means END data byte
12
+ CHAR_ESC_ESC_ENC = [0335].pack('C') # ESC ESC_ESC means ESC data byte
13
+ end
14
+ end
15
+
@@ -0,0 +1,125 @@
1
+ # A responsive OSC TCP client that sends and receives OSC messages on a single socket using the SLIP protocol.
2
+ #
3
+ # http://www.ietf.org/rfc/rfc1055.txt
4
+ require 'socket'
5
+
6
+ module OSC
7
+ module TCP
8
+ class Client
9
+ def initialize host, port, handler=nil
10
+ @host = host
11
+ @port = port
12
+ @handler = handler
13
+ @socket = TCPSocket.new host, port
14
+ @sending_socket = OSC::TCP::SendingSocket.new @socket
15
+ end
16
+
17
+ def close
18
+ @socket.close unless closed?
19
+ end
20
+
21
+ def closed?
22
+ @socket.closed?
23
+ end
24
+
25
+ # Send an OSC::Message and handle the response if one is given.
26
+ def send msg
27
+ @sending_socket.send(msg)
28
+
29
+ if block_given? || @handler
30
+ messages = response
31
+ if !messages.nil?
32
+ messages.each do |message|
33
+ if block_given?
34
+ yield message
35
+ else
36
+ @handler.handle message
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def response
44
+ if received_messages = receive_raw
45
+ received_messages.map do |message|
46
+ OSCPacket.messages_from_network(message)
47
+ end.flatten
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ def to_s
54
+ "#<OSC::TCP::Client:#{ object_id } @host:#{ @host.inspect }, @port:#{ @port.inspect }, @handler:#{ @handler.to_s }>"
55
+ end
56
+
57
+ private
58
+
59
+ def receive_raw
60
+ received = 0
61
+ messages = []
62
+ buffer = []
63
+ failed = false
64
+ received_any = false
65
+
66
+ loop do
67
+ begin
68
+ # get a character from the socket, fail if nothing is available
69
+ c = @socket.recv_nonblock(1)
70
+
71
+ received_any = true
72
+
73
+ case c
74
+ when CHAR_END_ENC
75
+ if received > 0
76
+ # add SLIP encoded message to list
77
+ messages << buffer.join
78
+
79
+ # reset state and keep reading from the port until there's nothing left
80
+ buffer.clear
81
+ received = 0
82
+ failed = false
83
+ end
84
+ when CHAR_ESC_ENC
85
+ # get next character, blocking is okay
86
+ c = @socket.recv(1)
87
+ case c
88
+ when CHAR_ESC_END_ENC
89
+ c = CHAR_END_ENC
90
+ when CHAR_ESC_ESC_ENC
91
+ c = CHAR_ESC_ENC
92
+ else
93
+ received += 1
94
+ buffer << c
95
+ end
96
+ else
97
+ received += 1
98
+ buffer << c
99
+ end
100
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
101
+ # If any messages have been received, assume sender is done sending.
102
+ if failed || received_any
103
+ break
104
+ end
105
+
106
+ # wait one second to see if the socket might become readable (and a
107
+ # response forthcoming). normal usage is send + receive response, but
108
+ # if app doesn't intend to respond we should eventually ignore it.
109
+
110
+ IO.select([@socket], [], [], 0.2)
111
+ failed = true
112
+ retry
113
+ end
114
+ end
115
+
116
+ if messages.size > 0
117
+ messages
118
+ else
119
+ nil
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
@@ -0,0 +1,87 @@
1
+ module QLab
2
+ class Cue < Communicator
3
+ attr_accessor :data
4
+
5
+ # Load a cue with the attributes given in `data`
6
+ def initialize data, cue_list
7
+ self.data = data
8
+ @cue_list = cue_list
9
+ end
10
+
11
+ # All cue commands for all cue types as listed in Figure53 QLab OSC Reference
12
+ Commands::ALL_CUES.each do |command|
13
+ define_method(command.to_sym) do |*args|
14
+ if args.size > 0
15
+ send_message cue_command(command), *args
16
+ else
17
+ send_message cue_command(command)
18
+ end
19
+ end
20
+ end
21
+
22
+ # Define commands with single settable values as command= methods
23
+ # for convenience. All command-as-method commands will act as setters
24
+ # if given an argument.
25
+ #
26
+ # For exmaple:
27
+ #
28
+ # cue.name = "A Name"
29
+ #
30
+ # is functionally equivalent to
31
+ #
32
+ # cue.name("A Name")
33
+ #
34
+ # as far as QLab is concerned
35
+ #
36
+ Commands::SET_CUES.each do |command|
37
+ define_method("#{ command }=".to_sym) do |value|
38
+ send_message cue_command(command), value
39
+ end
40
+ end
41
+
42
+ # The cue's `uniqueID`.
43
+ def id
44
+ data['uniqueID']
45
+ end
46
+
47
+ # Check whether this cue has nested cues.
48
+ def has_cues?
49
+ cues.size > 0
50
+ end
51
+
52
+ # Get the list of nested cues.
53
+ def cues
54
+ if data['cues'].nil?
55
+ []
56
+ else
57
+ data['cues'].map {|c| Cue.new(c, @cue_list)}
58
+ end
59
+ end
60
+
61
+ # Compare with another Cue.
62
+ def ==(other)
63
+ if other.is_a?(Cue)
64
+ self.data = other.data
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ # A reference to the cue's workspace.
71
+ def workspace
72
+ @cue_list.workspace
73
+ end
74
+
75
+ protected
76
+
77
+ def connection
78
+ workspace.connection
79
+ end
80
+
81
+ private
82
+
83
+ def cue_command command
84
+ "/workspace/#{ workspace.id }/cue_id/#{ id }/#{ command }"
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,72 @@
1
+ module QLab
2
+ # An array of cue objects:
3
+ #
4
+ # [
5
+ # {
6
+ # "uniqueID": string,
7
+ # "number": string
8
+ # "name": string
9
+ # "type": string
10
+ # "colorName": string
11
+ # "flagged": number
12
+ # "armed": number
13
+ # }
14
+ # ]
15
+ #
16
+ # If a given cue is a group, it will include the nested cues:
17
+ #
18
+ # [
19
+ # {
20
+ # "uniqueID": string,
21
+ # "number": string
22
+ # "name": string
23
+ # "type": string
24
+ # "colorName": string
25
+ # "flagged": number
26
+ # "armed": number
27
+ # "cues": [ { }, { }, { } ]
28
+ # }
29
+ # ]
30
+
31
+ class CueList
32
+ attr_accessor :data
33
+
34
+ # Load a cue list with the attributes given in `data`
35
+ def initialize data, workspace
36
+ self.data = data
37
+ @workspace = workspace
38
+ end
39
+
40
+ def workspace
41
+ @workspace
42
+ end
43
+
44
+ def id
45
+ data['uniqueID']
46
+ end
47
+
48
+ def name
49
+ data['listName']
50
+ end
51
+
52
+ def number
53
+ data['number']
54
+ end
55
+
56
+ def type
57
+ data['type']
58
+ end
59
+
60
+ def cues
61
+ if data['cues'].nil?
62
+ []
63
+ else
64
+ data['cues'].map {|c| QLab::Cue.new(c, self)}
65
+ end
66
+ end
67
+
68
+ def has_cues?
69
+ cues.size > 0
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,94 @@
1
+ module QLab
2
+ class Machine < Communicator
3
+ attr_accessor :name, :address, :port
4
+
5
+ # Connect to a running QLab instance. `address` can be a domain name or an
6
+ # IP Address.
7
+ def initialize(_address, _port)
8
+ self.name = _address
9
+ self.address = _address
10
+ self.port = _port
11
+ connect
12
+ end
13
+
14
+ # auto-generate methods first so that manually defined methods will
15
+ # override
16
+ Commands::MACHINE.each do |command|
17
+ define_method(command.to_sym) do |*args|
18
+ if args.size > 0
19
+ send_message command, *args
20
+ else
21
+ send_message command
22
+ end
23
+ end
24
+ end
25
+
26
+ # Open and return a connection to the running QLab instance
27
+ def connect
28
+ if !connected?
29
+ @connection = OSC::TCP::Client.new(@address, @port)
30
+ else
31
+ connection
32
+ end
33
+ end
34
+
35
+ # Reference to the running QLab instance
36
+ def connection
37
+ @connection || connect
38
+ end
39
+
40
+ # The workspaces provided by the connected QLab instance
41
+ def workspaces
42
+ @workspaces || load_workspaces
43
+ end
44
+
45
+
46
+ # Find a workspace according to the given params.
47
+ def find_workspace params={}
48
+ workspaces.find do |ws|
49
+ matches = true
50
+
51
+ # match each key to the given workspace
52
+ params.keys.each do |key|
53
+ matches = matches && (ws.send(key.to_sym) == params[key])
54
+ end
55
+
56
+ matches
57
+ end
58
+ end
59
+
60
+ def connected?
61
+ !(@connection.nil? || send_message('/version').nil?)
62
+ end
63
+
64
+ def close
65
+ @connection.close
66
+ @connection = nil
67
+ end
68
+
69
+ def refresh
70
+ close
71
+ connect
72
+ load_workspaces
73
+ end
74
+
75
+ def alwaysReply=(value)
76
+ # send set command
77
+ alwaysReply(value)
78
+ end
79
+
80
+ private
81
+
82
+ def load_workspaces
83
+ @workspaces = []
84
+
85
+ data = send_message('/workspaces')
86
+
87
+ data.map do |ws|
88
+ @workspaces << QLab::Workspace.new(ws, self)
89
+ end
90
+
91
+ @workspaces
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,51 @@
1
+ require 'json'
2
+
3
+ module QLab
4
+ # QLab OSC reply unpacker.
5
+ class Reply < Struct.new(:osc_message)
6
+ def address
7
+ @address ||= json['address']
8
+ end
9
+
10
+ def data
11
+ @data ||= json['data']
12
+ end
13
+
14
+ def status
15
+ @status ||= json['status']
16
+ end
17
+
18
+ def has_data?
19
+ !data.nil?
20
+ end
21
+
22
+ def has_status?
23
+ !status.nil?
24
+ end
25
+
26
+ def ok?
27
+ status == 'ok'
28
+ end
29
+
30
+ def empty?
31
+ false
32
+ end
33
+
34
+ def to_s
35
+ "<QLab::Reply address:'#{address}' status:'#{status}' data:#{data.inspect}>"
36
+ end
37
+
38
+ protected
39
+
40
+ # Actually perform the message unpacking
41
+ def json
42
+ @json ||= begin
43
+ JSON.parse(osc_message.to_a.first)
44
+ rescue => ex
45
+ puts ex.message
46
+ {}
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module QLab
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,110 @@
1
+ module QLab
2
+ #
3
+ # "uniqueID": string,
4
+ # "displayName": string
5
+ # "hasPasscode": number
6
+ #
7
+ class Workspace < Communicator
8
+ attr_accessor :data, :passcode
9
+
10
+ # Load a workspace with the attributes given in `data`
11
+ def initialize data, machine
12
+ @machine = machine
13
+ self.data = data
14
+ end
15
+
16
+ Commands::WORKSPACE.each do |command|
17
+ define_method(command.to_sym) do |*args|
18
+ if args.size > 0
19
+ send_message workspace_command(command), *args
20
+ else
21
+ send_message workspace_command(command)
22
+ end
23
+ end
24
+ end
25
+
26
+ def connection
27
+ @machine.connection
28
+ end
29
+
30
+ def name
31
+ data['displayName']
32
+ end
33
+
34
+ def passcode?
35
+ !!data['hasPasscode']
36
+ end
37
+
38
+ def id
39
+ data['uniqueID']
40
+ end
41
+
42
+ # all cues in this workspace in a flat list
43
+ def cues
44
+ cue_lists.map do |cl|
45
+ load_cues(cl, [])
46
+ end.flatten.compact
47
+ end
48
+
49
+ def has_cues?
50
+ cues.size > 0
51
+ end
52
+
53
+ def cue_lists
54
+ refresh
55
+ @cue_lists
56
+ end
57
+
58
+ def refresh
59
+ if passcode?
60
+ if passcode.nil?
61
+ raise WorkspaceConnectionError.new("Workspace '#{ name }' requires a passcode.")
62
+ end
63
+
64
+ args = [workspace_command('connect'), ('%04i' % passcode)]
65
+ else
66
+ args = [workspace_command('connect')]
67
+ end
68
+ connect_response = send_message(*args)
69
+
70
+ if connect_response == 'ok'
71
+ @cue_lists = []
72
+
73
+ cues_response = send_message workspace_command('cueLists')
74
+ cues_response.each do |cuelist|
75
+ @cue_lists << QLab::CueList.new(cuelist, self)
76
+ end
77
+ end
78
+ end
79
+
80
+ def find_cue params={}
81
+ cues.find do |cue|
82
+ matches = true
83
+
84
+ # match each key to the given cue
85
+ params.keys.each do |key|
86
+ matches = matches && (cue.send(key.to_sym) == params[key])
87
+ end
88
+
89
+ matches
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def workspace_command command
96
+ "/workspace/#{id}/#{ command }"
97
+ end
98
+
99
+ def load_cues parent_cue, cues
100
+ parent_cue.cues.each {|child_cue|
101
+ cues << child_cue
102
+ load_cues child_cue, cues
103
+ }
104
+
105
+ cues
106
+ end
107
+ end
108
+
109
+ class WorkspaceConnectionError < Exception; end
110
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'qlab-ruby/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "qlab-ruby"
8
+ gem.version = QLab::VERSION
9
+ gem.authors = ["Adam Bachman"]
10
+ gem.email = ["adam@figure53.com"]
11
+ gem.description = %q{Interact with QLab in Ruby.}
12
+ gem.summary = %q{Interact with QLab in Ruby.}
13
+ gem.homepage = "https://github.com/Figure53/qlab-ruby"
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_runtime_dependency 'osc-ruby'
22
+
23
+ gem.add_development_dependency 'rdoc'
24
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+
3
+ class CueTest < Minitest::Test
4
+ def setup
5
+ @machine = QLab.connect 'localhost', 53000
6
+ end
7
+
8
+ def teardown
9
+ @machine.close
10
+ end
11
+
12
+ def test_a_cue_is_reachable
13
+ workspace = @machine.workspaces.first
14
+
15
+ refute_nil workspace.cue_lists
16
+ refute_nil workspace.cue_lists.first
17
+ refute_nil workspace.cue_lists.first.cues
18
+ refute_nil workspace.cue_lists.first.cues.first
19
+ end
20
+
21
+ def test_a_cue_has_a_name
22
+ workspace = @machine.workspaces.first
23
+ cue = workspace.cue_lists.first.cues.first
24
+ assert cue.name
25
+ end
26
+
27
+ def test_changes_are_picked_up_immediately
28
+ original_name = "Name 1"
29
+ new_name = "Name 2"
30
+
31
+ workspace = @machine.workspaces.first
32
+ cue = workspace.cue_lists.first.cues.first
33
+
34
+ cue.name = original_name
35
+ assert_equal original_name, cue.name
36
+
37
+ cue.name = new_name
38
+ assert_equal new_name, cue.name
39
+ end
40
+ end
41
+
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class MachineTest < Minitest::Test
4
+ def test_can_connect
5
+ machine = QLab.connect 'localhost', 53000
6
+ assert machine.connected?
7
+ end
8
+
9
+ def test_can_load_workspaces
10
+ machine = QLab.connect 'localhost', 53000
11
+ assert machine.workspaces.size > 0
12
+ end
13
+
14
+ def test_can_find_workspaces
15
+ machine = QLab.connect 'localhost', 53000
16
+ assert machine.workspaces.size > 0
17
+
18
+ ws = machine.workspaces.first
19
+
20
+ assert_equal ws, machine.find_workspace(id: ws.id)
21
+ assert_equal ws, machine.find_workspace(name: ws.name)
22
+
23
+ refute machine.find_workspace(id: 'nonsense')
24
+ refute machine.find_workspace(name: 'nonsense')
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ require 'qlab-ruby'
2
+
3
+ gem 'minitest'
4
+ require 'minitest/autorun'
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class WorkspaceTest < Minitest::Test
4
+ def setup
5
+ @machine = QLab.connect 'localhost', 53000
6
+ end
7
+
8
+ def teardown
9
+ @machine.close
10
+ end
11
+
12
+ def test_refresh_loads_cues
13
+ workspace = @machine.workspaces.first
14
+
15
+ refute_nil workspace.connection
16
+ refute_nil workspace.name
17
+ refute_nil workspace.id
18
+
19
+ refute_nil workspace.cues
20
+ assert workspace.cues.size > 0
21
+ end
22
+
23
+ def test_finds_cues
24
+ workspace = @machine.workspaces.first
25
+ cue = workspace.cues.first
26
+
27
+ assert_equal cue, workspace.find_cue(id: cue.id)
28
+ assert_equal cue, workspace.find_cue(number: cue.number)
29
+ assert_equal cue, workspace.find_cue(name: cue.name)
30
+
31
+ refute workspace.find_cue(id: 'asdf asdf asdf asdf asdf')
32
+ refute workspace.find_cue(number: 'asdf asdf asdf asdf asdf')
33
+ refute workspace.find_cue(name: 'asdf asdf asdf asdf asdf')
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qlab-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Adam Bachman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: osc-ruby
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rdoc
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Interact with QLab in Ruby.
47
+ email:
48
+ - adam@figure53.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - lib/qlab-ruby.rb
59
+ - lib/qlab-ruby/commands.rb
60
+ - lib/qlab-ruby/communicator.rb
61
+ - lib/qlab-ruby/core-ext/osc-ruby.rb
62
+ - lib/qlab-ruby/core-ext/osc-ruby/message.rb
63
+ - lib/qlab-ruby/core-ext/osc-ruby/sending_socket.rb
64
+ - lib/qlab-ruby/core-ext/osc-ruby/tcp.rb
65
+ - lib/qlab-ruby/core-ext/osc-ruby/tcp_client.rb
66
+ - lib/qlab-ruby/cue.rb
67
+ - lib/qlab-ruby/cue_list.rb
68
+ - lib/qlab-ruby/machine.rb
69
+ - lib/qlab-ruby/reply.rb
70
+ - lib/qlab-ruby/version.rb
71
+ - lib/qlab-ruby/workspace.rb
72
+ - qlab-ruby.gemspec
73
+ - test/cue_test.rb
74
+ - test/machine_test.rb
75
+ - test/test_helper.rb
76
+ - test/workspace_test.rb
77
+ homepage: https://github.com/Figure53/qlab-ruby
78
+ licenses:
79
+ - MIT
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.25
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Interact with QLab in Ruby.
102
+ test_files:
103
+ - test/cue_test.rb
104
+ - test/machine_test.rb
105
+ - test/test_helper.rb
106
+ - test/workspace_test.rb