qcmd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,116 @@
1
+ module Qcmd
2
+ module Plaintext
3
+ def log message=nil
4
+ if message
5
+ puts message
6
+ else
7
+ puts
8
+ end
9
+ end
10
+
11
+ # always output
12
+ def print message=nil
13
+ log(message)
14
+ end
15
+
16
+ def columns
17
+ begin
18
+ `stty size`.split.last.to_i
19
+ rescue
20
+ 80
21
+ end
22
+ end
23
+
24
+ def pluralize n, word
25
+ "#{n} #{n == 1 ? word : word + 's'}"
26
+ end
27
+
28
+ def word_wrap(text, options={})
29
+ options = {
30
+ :line_width => columns
31
+ }.merge options
32
+
33
+ text.split("\n").collect do |line|
34
+ line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
35
+ end * "\n"
36
+ end
37
+
38
+ def ascii_qlab
39
+ [' .:::: .:: .:: ',
40
+ ' .:: .:: .:: .:: ',
41
+ '.:: .::.:: .:: .:: ',
42
+ '.:: .::.:: .:: .:: .:: .:: ',
43
+ '.:: .::.:: .:: .:: .:: .::',
44
+ ' .:: .: .:: .:: .:: .:: .:: .::',
45
+ ' .:: :: .:::::::: .:: .:::.:: .:: ',
46
+ ' .: '].map {|line|
47
+ print centered_text(line)
48
+ }
49
+ end
50
+
51
+ def joined_wrapped_text line
52
+ wrapped_text(line).join "\n"
53
+ end
54
+
55
+ # turn line into lines of text of columns length
56
+ def wrapped_text line
57
+ word_wrap(line, :line_width => columns).split("\n")
58
+ end
59
+
60
+ def right_text line
61
+ diff = [(columns - line.size), 0].max
62
+ "%s%s" % [' ' * diff, line]
63
+ end
64
+
65
+ def centered_text line, char=' '
66
+ if line.size > columns && line.split(' ').all? {|chunk| chunk.size < columns}
67
+ # wrap the text then center each line, then join
68
+ return wrapped_text(line).map {|l| centered_text(l, char)}.join("\n")
69
+ end
70
+
71
+ diff = (columns - line.size)
72
+
73
+ return line if diff < 0
74
+
75
+ lpad = diff / 2
76
+ rpad = diff - lpad
77
+
78
+ "%s%s%s" % [char * lpad, line, char * rpad]
79
+ end
80
+
81
+ def split_text left, right
82
+ diff = columns - left.size
83
+ if (diff - right.size) < 0
84
+ left_lines = wrapped_text(left)
85
+ diff = columns - left_lines.last.size
86
+
87
+ # still?
88
+ if (diff - right.size) < 0
89
+ diff = ''
90
+ right = "\n" + right_text(right)
91
+ end
92
+
93
+ left = left_lines.join "\n"
94
+ end
95
+ "%s%#{diff}s" % [left, right]
96
+ end
97
+
98
+ def table headers, rows
99
+ print
100
+ columns = headers.map(&:size)
101
+ rows.each do |row|
102
+ columns.each_with_index do |col, n|
103
+ columns[n] = [col, row[n].size].max + 1
104
+ end
105
+ end
106
+
107
+ row_format = columns.map {|n| "%#{n}s\t"}.join('')
108
+ print row_format % headers
109
+ print
110
+ rows.each do |row|
111
+ print row_format % row
112
+ end
113
+ print
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,3 @@
1
+ require 'qcmd/qlab/cue'
2
+ require 'qcmd/qlab/reply'
3
+ require 'qcmd/qlab/workspace'
@@ -0,0 +1,70 @@
1
+ module Qcmd
2
+ module QLab
3
+ # All return an array of cue dictionaries:
4
+ #
5
+ # [
6
+ # {
7
+ # "uniqueID": string,
8
+ # "number": string
9
+ # "name": string
10
+ # "type": string
11
+ # "colorName": string
12
+ # "flagged": number
13
+ # "armed": number
14
+ # }
15
+ # ]
16
+ # If the cue is a group, the dictionary will include an array of cue dictionaries for all children in the group:
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
+ # [{\"number\":\"\",
32
+ # \"uniqueID\":\"1\",
33
+ # \"cues\":[{\"number\":\"1\",
34
+ # \"uniqueID\":\"2\",
35
+ # \"flagged\":false,
36
+ # \"type\":\"Wait\",
37
+ # \"colorName\":\"none\",
38
+ # \"name\":\"boom\",
39
+ # \"armed\":true}],
40
+ # \"flagged\":false,
41
+ # \"type\":\"Group\",
42
+ # \"colorName\":\"none\",
43
+ # \"name\":\"Main Cue List\",
44
+ # \"armed\":true}]
45
+
46
+ class Cue
47
+ attr_accessor :data
48
+
49
+ def initialize options={}
50
+ self.data = options
51
+ end
52
+
53
+ def id
54
+ data['uniqueID']
55
+ end
56
+
57
+ def name
58
+ data['name']
59
+ end
60
+
61
+ def number
62
+ data['number']
63
+ end
64
+
65
+ def type
66
+ data['type']
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ module Qcmd
2
+ module QLab
3
+ class Reply < Struct.new(:osc_message)
4
+ def json
5
+ @json ||= JSON.parse(osc_message.to_a.first)
6
+ end
7
+
8
+ def address
9
+ @address ||= json['address']
10
+ end
11
+
12
+ def data
13
+ @data ||= json['data']
14
+ end
15
+
16
+ def is_cue_command?
17
+ Qcmd::Commands.is_cue_command?(address)
18
+ end
19
+
20
+ def to_s
21
+ "<Qcmd::Qlab::Reply address:'#{address}' data:#{data.inspect}>"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ module Qcmd
2
+ module QLab
3
+ #
4
+ # "uniqueID": string,
5
+ # "displayName": string
6
+ # "hasPasscode": number
7
+ #
8
+ class Workspace
9
+ attr_accessor :data, :passcode, :cue_lists, :cues
10
+
11
+ def initialize options={}
12
+ self.data = options
13
+ end
14
+
15
+ def name
16
+ data['displayName']
17
+ end
18
+
19
+ def passcode?
20
+ !!data['hasPasscode']
21
+ end
22
+
23
+ def id
24
+ data['uniqueID']
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,178 @@
1
+ require 'osc-ruby'
2
+ require 'osc-ruby/em_server'
3
+ require 'ruby-debug'
4
+
5
+ require 'json'
6
+
7
+ module Qcmd
8
+ class TimeoutError < Exception; end
9
+
10
+ class Server
11
+ attr_accessor :receive_channel, :receive_thread, :receive_port, :send_channel, :machine
12
+
13
+ def initialize *args
14
+ options = args.extract_options!
15
+
16
+ self.receive_port = options[:receive]
17
+ connect_to_client
18
+
19
+ @handler = Qcmd::Handler.new
20
+ @sent_messages = []
21
+ @sent_messages_expecting_reply = []
22
+ @received_messages = []
23
+ end
24
+
25
+ def connect_to_client
26
+ self.machine = Qcmd.context.machine
27
+ self.send_channel = OSC::Client.new machine.address, machine.port
28
+
29
+ Qcmd.debug '(setting up listening connection)'
30
+ listen
31
+ end
32
+
33
+ def generic_responding_proc
34
+ proc do |osc_message|
35
+ @received_messages << osc_message
36
+
37
+ begin
38
+ Qcmd.debug "(received message: #{ osc_message.address })"
39
+ reply_received QLab::Reply.new(osc_message)
40
+ rescue => ex
41
+ Qcmd.debug "(ERROR #{ ex.message })"
42
+ end
43
+ end
44
+ end
45
+
46
+ # initialize
47
+ def listen
48
+ if receive_channel
49
+ Qcmd.debug "(stopping existing server)"
50
+ stop
51
+ end
52
+
53
+ self.receive_channel = OSC::EMServer.new(self.receive_port)
54
+
55
+ Qcmd.debug "(opening receiving channel: #{ self.receive_channel.inspect })"
56
+
57
+ receive_channel.add_method %r{/reply/?(.*)}, &generic_responding_proc
58
+ end
59
+
60
+ def replies_expected?
61
+ @sent_messages_expecting_reply.size > 0
62
+ end
63
+
64
+ def reply_received reply
65
+ Qcmd.debug "(receiving #{ reply })"
66
+
67
+ # update world state
68
+ begin
69
+ @handler.handle reply
70
+ rescue => ex
71
+ print "(ERROR: #{ ex.message })"
72
+ end
73
+
74
+ # FIFO
75
+ @sent_messages_expecting_reply.shift
76
+
77
+ Qcmd.debug "(#{ @sent_messages_expecting_reply.size } messages awaiting reply)"
78
+ end
79
+
80
+ def wait_for_replies
81
+ begin
82
+ yield
83
+
84
+ naps = 0
85
+ while replies_expected? do
86
+ if naps > 20
87
+ # FAILED TO GET RESPONSE
88
+ raise TimeoutError.new
89
+ end
90
+
91
+ naps += 1
92
+ sleep 0.1
93
+ end
94
+ rescue TimeoutError => ex
95
+ Qcmd.log "[error: reply timeout]"
96
+ # clear expecting reply item, assume it will never arrive
97
+ @sent_messages_expecting_reply.shift
98
+ end
99
+ end
100
+
101
+ def send_command command, *args
102
+ options = args.extract_options!
103
+
104
+ Qcmd.debug "(building command from command, args, options: #{ command.inspect }, #{ args.inspect }, #{ options.inspect })"
105
+
106
+ # make sure command is valid OSC Address
107
+ if %r[^/] =~ command
108
+ address = command
109
+ else
110
+ address = "/#{ command }"
111
+ end
112
+
113
+ osc_message = OSC::Message.new address, *args
114
+
115
+ send_message osc_message
116
+ end
117
+
118
+ def send_message osc_message
119
+ Qcmd.debug "(sending osc message #{ osc_message.address } #{osc_message.has_arguments? ? 'with' : 'without'} args)"
120
+
121
+ @sent_messages << osc_message
122
+ if Qcmd::Commands.expects_reply?(osc_message)
123
+ Qcmd.debug "(this command expects a reply)"
124
+ @sent_messages_expecting_reply << osc_message
125
+ end
126
+
127
+ wait_for_replies do
128
+ send_channel.send osc_message
129
+ end
130
+ end
131
+
132
+ def stop
133
+ Thread.kill(receive_thread) if receive_thread.alive?
134
+ end
135
+
136
+ def run
137
+ Qcmd.debug '(starting server)'
138
+ self.receive_thread = Thread.new do
139
+ Qcmd.debug '(server is up)'
140
+ receive_channel.run
141
+ end
142
+ end
143
+ alias :start :run
144
+
145
+ def send_workspace_command _command, *args
146
+ command = "workspace/#{ Qcmd.context.workspace.id }/#{ _command }"
147
+ send_command(command, *args)
148
+ end
149
+
150
+ def send_cue_command number, action, *args
151
+ command = "cue/#{ number }/#{ action }"
152
+ send_workspace_command(command, *args)
153
+ end
154
+
155
+ ## QLab commands
156
+
157
+ def load_workspaces
158
+ send_command 'workspaces'
159
+ end
160
+
161
+ def load_cues
162
+ send_workspace_command 'cueLists'
163
+ end
164
+
165
+ def connect_to_workspace workspace
166
+ if workspace.passcode?
167
+ send_command "workspace/#{workspace.id}/connect", workspace.passcode
168
+ else
169
+ send_command "workspace/#{workspace.id}/connect"
170
+ end
171
+
172
+ # if it worked, load cues automatically
173
+ if Qcmd.context.workspace
174
+ load_cues
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,3 @@
1
+ module Qcmd
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'qcmd/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "qcmd"
8
+ gem.version = Qcmd::VERSION
9
+ gem.authors = ["Adam Bachman"]
10
+ gem.email = ["adam.bachman@gmail.com"]
11
+ gem.description = %q{A simple interactive QLab 3 command line controller}
12
+ gem.summary = %q{QLab 3 console}
13
+ gem.homepage = "https://github.com/abachman/qcmd"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency 'dnssd'
21
+ gem.add_runtime_dependency 'eventmachine'
22
+ gem.add_runtime_dependency 'json'
23
+ gem.add_runtime_dependency 'osc-ruby'
24
+ gem.add_runtime_dependency 'trollop'
25
+
26
+ gem.add_development_dependency "rspec"
27
+ gem.add_development_dependency "cucumber"
28
+ gem.add_development_dependency "aruba"
29
+ gem.add_development_dependency 'ruby-debug'
30
+ end