qcmd 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,15 @@
1
1
  module Qcmd
2
2
  class Context
3
- attr_accessor :machine, :workspace, :workspace_connected
3
+ attr_accessor :machine, :workspace, :workspace_connected, :cue, :cue_connected, :qlab
4
4
 
5
5
  def reset
6
6
  disconnect_machine
7
7
  disconnect_workspace
8
+ disconnect_cue
8
9
  end
9
10
 
10
11
  def disconnect_machine
12
+ self.qlab.close unless self.qlab.nil?
11
13
  self.machine = nil
12
14
  end
13
15
 
@@ -16,6 +18,11 @@ module Qcmd
16
18
  self.workspace_connected = false
17
19
  end
18
20
 
21
+ def disconnect_cue
22
+ self.cue = nil
23
+ self.cue_connected = false
24
+ end
25
+
19
26
  def machine_connected?
20
27
  !machine.nil?
21
28
  end
@@ -24,27 +31,26 @@ module Qcmd
24
31
  !!workspace_connected
25
32
  end
26
33
 
34
+ def cue_connected?
35
+ !!cue_connected
36
+ end
37
+
27
38
  def connection_state
28
39
  if !machine_connected?
29
40
  :none
30
41
  elsif !workspace_connected?
31
42
  :machine
32
- else
43
+ elsif !cue_connected?
33
44
  :workspace
45
+ else
46
+ :cue
34
47
  end
35
48
  end
36
49
 
37
- def print_workspace_list
38
- Qcmd.print Qcmd.centered_text(" Workspaces ", '-')
39
- Qcmd.print
40
-
41
- machine.workspaces.each_with_index do |ws, n|
42
- Qcmd.print "#{ n + 1 }. #{ ws.name }#{ ws.passcode? ? ' [PROTECTED]' : ''}"
43
- end
44
-
45
- Qcmd.print
46
- Qcmd.print_wrapped('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. Passcode is optional.')
47
- Qcmd.print
50
+ def connect_to_qlab handler=nil
51
+ # get an open connection with the default handler
52
+ handler ||= Qcmd::Handler
53
+ self.qlab = OSC::TCPClient.new(machine.address, machine.port, handler)
48
54
  end
49
55
  end
50
56
  end
@@ -0,0 +1,155 @@
1
+ # An OSC TCP client sends and receives on the same socket using the SLIP
2
+ # protocol.
3
+ #
4
+ # http://www.ietf.org/rfc/rfc1055.txt
5
+
6
+ module OSC
7
+ class TCPClient
8
+
9
+ CHAR_END = 0300 # indicates end of packet
10
+ CHAR_ESC = 0333 # indicates byte stuffing
11
+ CHAR_ESC_END = 0334 # ESC ESC_END means END data byte
12
+ CHAR_ESC_ESC = 0335 # ESC ESC_ESC means ESC data byte
13
+
14
+ CHAR_END_ENC = [0300].pack('C') # indicates end of packet
15
+ CHAR_ESC_ENC = [0333].pack('C') # indicates byte stuffing
16
+ CHAR_ESC_END_ENC = [0334].pack('C') # ESC ESC_END means END data byte
17
+ CHAR_ESC_ESC_ENC = [0335].pack('C') # ESC ESC_ESC means ESC data byte
18
+
19
+ def initialize host, port, handler=nil
20
+ @host = host
21
+ @port = port
22
+ @handler = handler
23
+ @so = TCPSocket.new host, port
24
+ end
25
+
26
+ def close
27
+ @so.close unless closed?
28
+ end
29
+
30
+ def closed?
31
+ @so.closed?
32
+ end
33
+
34
+ # send an OSC::Message
35
+ def send msg
36
+ enc_msg = msg.encode
37
+
38
+ send_char CHAR_END
39
+
40
+ enc_msg.bytes.each do |b|
41
+ case b
42
+ when CHAR_END
43
+ send_char CHAR_ESC
44
+ send_char CHAR_ESC_END
45
+ when CHAR_ESC
46
+ send_char CHAR_ESC
47
+ send_char CHAR_ESC_ESC
48
+ else
49
+ send_char b
50
+ end
51
+ end
52
+
53
+ send_char CHAR_END
54
+
55
+ if block_given? || @handler
56
+ messages = response
57
+ if !messages.nil?
58
+ messages.each do |message|
59
+ if block_given?
60
+ yield message
61
+ else
62
+ @handler.handle message
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def response
70
+ if received_messages = receive_raw
71
+ received_messages.map do |message|
72
+ OSCPacket.messages_from_network(message)
73
+ end.flatten
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ def to_s
80
+ "#<OSC::TCPClient:#{ object_id } @host:#{ @host.inspect }, @port:#{ @port.inspect }, @handler:#{ @handler.to_s }>"
81
+ end
82
+
83
+ private
84
+
85
+ def send_char c
86
+ @so.send [c].pack('C'), 0
87
+ end
88
+
89
+ def receive_raw
90
+ received = 0
91
+ messages = []
92
+ buffer = []
93
+ failed = false
94
+ received_any = false
95
+
96
+ loop do
97
+ begin
98
+ # get a character from the socket, fail if nothing is available
99
+ c = @so.recv_nonblock(1)
100
+
101
+ received_any = true
102
+
103
+ case c
104
+ when CHAR_END_ENC
105
+ if received > 0
106
+ # add SLIP encoded message to list
107
+ messages << buffer.join
108
+
109
+ # reset state and keep reading from the port until there's
110
+ # nothing left
111
+ buffer.clear
112
+ received = 0
113
+ failed = false
114
+ end
115
+ when CHAR_ESC_ENC
116
+ # get next character, blocking is okay
117
+ c = @so.recv(1)
118
+ case c
119
+ when CHAR_ESC_END_ENC
120
+ c = CHAR_END_ENC
121
+ when CHAR_ESC_ESC_ENC
122
+ c = CHAR_ESC_ENC
123
+ else
124
+ received += 1
125
+ buffer << c
126
+ end
127
+ else
128
+ received += 1
129
+ buffer << c
130
+ end
131
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
132
+ # If any messages have been received, assume sender is done sending.
133
+ if failed || received_any
134
+ break
135
+ end
136
+
137
+ # wait one second to see if the socket might become readable (and a
138
+ # response forthcoming). normal usage is send + wait for response,
139
+ # we have to give QLab a reasonable amount of time in which to respond.
140
+
141
+ IO.select([@so], [], [], 1)
142
+ failed = true
143
+ retry
144
+ end
145
+ end
146
+
147
+ if messages.size > 0
148
+ messages
149
+ else
150
+ nil
151
+ end
152
+ end
153
+ end
154
+ end
155
+
@@ -1,86 +1,68 @@
1
1
  module Qcmd
2
2
  class Handler
3
- include Qcmd::Plaintext
3
+ class << self
4
+ include Qcmd::Plaintext
4
5
 
5
- def handle reply
6
- Qcmd.debug "(handling #{ reply })"
6
+ # Handle OSC response message from QLab
7
+ def handle message
8
+ Qcmd.debug "[Handler handle] converting OSC::Message to QLab::Reply"
9
+ reply = QLab::Reply.new(message)
7
10
 
8
- case reply.address
9
- when %r[/workspaces]
10
- Qcmd.context.machine.workspaces = reply.data.map {|ws| Qcmd::QLab::Workspace.new(ws)}
11
+ Qcmd.debug "[Handler handle] handling #{ reply.to_s }"
11
12
 
12
- unless Qcmd.quiet?
13
- Qcmd.context.print_workspace_list
14
- end
15
-
16
- when %r[/workspace/[^/]+/connect]
17
- # connecting to a workspace
18
- if reply.data == 'badpass'
19
- print 'failed to connect to workspace, bad passcode or no passcode given'
20
- Qcmd.context.disconnect_workspace
21
- elsif reply.data == 'ok'
22
- print 'connected to workspace'
23
- Qcmd.context.workspace_connected = true
24
- end
25
-
26
- when %r[/cueLists]
27
- Qcmd.debug "(received cueLists)"
13
+ case reply.address
14
+ when %r[/(selectedCues|runningCues|runningOrPausedCues)]
15
+ Qcmd.debug "[Handler handle] received cue list from #{reply.address}"
28
16
 
29
- # load global cue list
30
- Qcmd.context.workspace.cues = cues = reply.data.map {|cue_list|
31
- cue_list['cues'].map {|cue| Qcmd::QLab::Cue.new(cue)}
32
- }.compact.flatten
17
+ if reply.has_data?
18
+ cues = reply.data.map {|cue| Qcmd::QLab::Cue.new(cue)}
33
19
 
34
- when %r[/(selectedCues|runningCues|runningOrPausedCues)]
35
- cues = reply.data.map {|cue|
36
- cues = [Qcmd::QLab::Cue.new(cue)]
20
+ if cues.size > 0
21
+ title = case reply.address
22
+ when /selectedCues/; "Selected Cues"
23
+ when /runningCues/; "Running Cues"
24
+ when /runningOrPausedCues/; "Running or Paused Cues"
25
+ end
37
26
 
38
- if cue['cues']
39
- cues << cue['cues'].map {|cue| Qcmd::QLab::Cue.new(cue)}
27
+ print
28
+ print centered_text(" #{title} ", '-')
29
+ table(['Number', 'Id', 'Name', 'Type'], cues.map {|cue|
30
+ [cue.number, cue.id, cue.name, cue.type]
31
+ })
32
+ print
33
+ else
34
+ print "no cues found"
35
+ end
40
36
  end
41
37
 
42
- cues
43
- }.compact.flatten
44
-
45
- title = case reply.address
46
- when /selectedCues/; "Selected Cues"
47
- when /runningCues/; "Running Cues"
48
- when /runningOrPausedCues/; "Running or Paused Cues"
49
- end
50
-
51
- print
52
- print centered_text(" #{title} ", '-')
53
- table(['Number', 'Id', 'Name', 'Type'], cues.map {|cue|
54
- [cue.number, cue.id, cue.name, cue.type]
55
- })
56
- print
38
+ when %r[/thump]
39
+ print reply.data
57
40
 
58
- when %r[/(cue|cue_id)/[^/]+/[a-zA-Z]+]
59
- # properties, just print reply data
60
- result = reply.data
61
- if result.is_a?(String) || result.is_a?(Numeric)
62
- print result
63
41
  else
64
- case reply.address
65
- when %r[/basics]
66
- keys = result.keys.sort
67
- table(['Field Name', 'Value'], keys.map {|key|
68
- [key, result[key]]
69
- })
70
- else
71
- begin
72
- print JSON.pretty_generate(result)
73
- rescue JSON::GeneratorError
74
- print result.to_s
75
- end
42
+ Qcmd.debug "[Handler handle] unrecognized message from QLab, cannot handle #{ reply.address }"
43
+
44
+ if !reply.status.nil? && reply.status != 'ok'
45
+ print reply.status
76
46
  end
77
47
  end
48
+ end
78
49
 
79
- when %r[/thump]
80
- print reply.data
50
+ def print_workspace_list
51
+ if Qcmd.context.machine.workspaces.nil? || Qcmd.context.machine.workspaces.empty?
52
+ Qcmd.print "there are no workspaces! you're gonna have a bad time :("
53
+ return
54
+ end
55
+
56
+ Qcmd.print Qcmd.centered_text(" Workspaces ", '-')
57
+ Qcmd.print
58
+
59
+ Qcmd.context.machine.workspaces.each_with_index do |ws, n|
60
+ Qcmd.print "#{ n + 1 }. #{ ws.name }#{ ws.passcode? ? ' [PROTECTED]' : ''}"
61
+ end
81
62
 
82
- else
83
- Qcmd.debug "(unrecognized message from QLab, cannot handle #{ reply.address })"
63
+ Qcmd.print
64
+ Qcmd.print_wrapped('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. Passcode is required if workspace is [PROTECTED].')
65
+ Qcmd.print
84
66
  end
85
67
  end
86
68
  end
@@ -0,0 +1,26 @@
1
+ module Qcmd
2
+ class History
3
+ class << self
4
+ attr_accessor :commands
5
+
6
+ def load
7
+ if File.exists?(Qcmd::Configuration.history_file)
8
+ lines = File.new(Qcmd::Configuration.history_file, 'r').readlines
9
+ else
10
+ lines = []
11
+ end
12
+
13
+ if lines
14
+ lines.reverse[0..100].reverse.each {|hist|
15
+ Readline::HISTORY.push(hist)
16
+ }
17
+ end
18
+ end
19
+
20
+
21
+ def push command
22
+ Qcmd::Configuration.history.puts command
23
+ end
24
+ end
25
+ end
26
+ end
@@ -7,9 +7,18 @@ module Qcmd
7
7
  connect exit quit workspace workspaces disconnect help
8
8
  ]
9
9
 
10
- ReservedWorkspaceWords = Qcmd::Commands::ALL_WORKSPACE_COMMANDS
10
+ ReservedWorkspaceWords = Qcmd::Commands::WORKSPACE
11
11
 
12
- ReservedCueWords = Qcmd::Commands::ALL_CUE_COMMANDS
12
+ ReservedCueWords = Qcmd::Commands::ALL_CUES
13
+
14
+ def self.add_commands commands
15
+ ReservedWords.push(*commands)
16
+ ReservedWords.uniq!
17
+ end
18
+
19
+ def self.add_command command
20
+ add_commands([command])
21
+ end
13
22
 
14
23
  CompletionProc = Proc.new {|input|
15
24
  # puts "input: #{ input }"
@@ -40,6 +49,7 @@ module Qcmd
40
49
  quoted_names = machine_names.map {|mn| %["#{mn}"]}
41
50
  names = (quoted_names + machine_names).grep(matcher)
42
51
  names = quote_if_necessary(names)
52
+
43
53
  # unquote
44
54
  commands = commands + names
45
55
  end
@@ -1,6 +1,7 @@
1
1
  require 'dnssd'
2
2
 
3
3
  module Qcmd
4
+ # Browse the LAN and find open and running QLab instances.
4
5
  class Network
5
6
  BROWSE_TIMEOUT = 2
6
7
 
@@ -10,8 +11,9 @@ module Qcmd
10
11
  # browse can be used alone to populate the machines list
11
12
  def browse
12
13
  self.machines = []
14
+
13
15
  self.browse_thread = Thread.start do
14
- DNSSD.browse! '_qlab._udp' do |b|
16
+ DNSSD.browse! '_qlab._tcp.' do |b|
15
17
  DNSSD.resolve b.name, b.type, b.domain do |r|
16
18
  self.machines << Qcmd::Machine.new(b.name, r.target, r.port)
17
19
  end
@@ -39,7 +41,7 @@ module Qcmd
39
41
  end
40
42
 
41
43
  Qcmd.print
42
- Qcmd.print 'type `connect MACHINE` to connect to a machine'
44
+ Qcmd.print 'Type `connect MACHINE` to connect to a machine'
43
45
  Qcmd.print
44
46
  end
45
47
 
@@ -51,7 +53,11 @@ module Qcmd
51
53
  end
52
54
 
53
55
  def find machine_name
54
- machines.find {|m| m.name == machine_name}
56
+ machines.find {|m| m.name.to_s == machine_name.to_s}
57
+ end
58
+
59
+ def find_by_index idx
60
+ machines[idx] if idx < machines.size
55
61
  end
56
62
 
57
63
  def names