qlab-ruby 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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