qcmd 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +131 -0
- data/Rakefile +14 -0
- data/TODO.md +3 -0
- data/bin/qcmd +31 -0
- data/features/hello.feature +13 -0
- data/features/support/setup.rb +2 -0
- data/lib/qcmd.rb +48 -0
- data/lib/qcmd/cli.rb +167 -0
- data/lib/qcmd/commands.rb +103 -0
- data/lib/qcmd/context.rb +37 -0
- data/lib/qcmd/core_ext/array.rb +6 -0
- data/lib/qcmd/core_ext/osc/message.rb +7 -0
- data/lib/qcmd/handler.rb +80 -0
- data/lib/qcmd/input_completer.rb +67 -0
- data/lib/qcmd/machine.rb +27 -0
- data/lib/qcmd/network.rb +70 -0
- data/lib/qcmd/parser.rb +48 -0
- data/lib/qcmd/plaintext.rb +116 -0
- data/lib/qcmd/qlab.rb +3 -0
- data/lib/qcmd/qlab/cue.rb +70 -0
- data/lib/qcmd/qlab/reply.rb +25 -0
- data/lib/qcmd/qlab/workspace.rb +28 -0
- data/lib/qcmd/server.rb +178 -0
- data/lib/qcmd/version.rb +3 -0
- data/qcmd.gemspec +30 -0
- data/sample/dnssd.rb +36 -0
- data/spec/cli_spec.rb +14 -0
- data/spec/commands_spec.rb +38 -0
- data/spec/qcmd_spec.rb +19 -0
- data/spec/qlab_spec.rb +15 -0
- metadata +230 -0
@@ -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
|
data/lib/qcmd/qlab.rb
ADDED
@@ -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
|
data/lib/qcmd/server.rb
ADDED
@@ -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
|
data/lib/qcmd/version.rb
ADDED
data/qcmd.gemspec
ADDED
@@ -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
|