qcmd 0.1.7 → 0.1.8

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.
@@ -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