nl-logic_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE +661 -0
- data/README.md +25 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/list_exports.rb +45 -0
- data/bin/set_multi.rb +18 -0
- data/bin/setup +7 -0
- data/bin/show_info.rb +49 -0
- data/lib/nl/logic_client.rb +586 -0
- data/lib/nl/logic_client/version.rb +5 -0
- data/nl-logic_client.gemspec +28 -0
- metadata +97 -0
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# NL::LogicClient
|
2
|
+
|
3
|
+
This is the Ruby client for the Nitrogen Logic [Automation Controller][1].
|
4
|
+
It's used internally by the Automation Controller's web browser-based interface
|
5
|
+
to connect to the automation logic system.
|
6
|
+
|
7
|
+
# Copying
|
8
|
+
|
9
|
+
©2011-2021 Mike Bourgeous. Released under [AGPLv3][0].
|
10
|
+
|
11
|
+
Use in new projects is not recommended.
|
12
|
+
|
13
|
+
# Usage
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'nl-logic_client'
|
19
|
+
```
|
20
|
+
|
21
|
+
See `bin/set_multi.rb`, `bin/list_exports.rb`, and `bin/show_info.rb` for usage
|
22
|
+
examples.
|
23
|
+
|
24
|
+
[0]: https://www.gnu.org/licenses/agpl-3.0.html
|
25
|
+
[1]: http://www.nitrogenlogic.com/products/automation_controller.html
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "nl/logic_client"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/list_exports.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Prints a list of exported parameters on the specified logic controller.
|
3
|
+
# (C)2011 Mike Bourgeous
|
4
|
+
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'nl/logic_client'
|
7
|
+
|
8
|
+
$list_exports_succeeded = false
|
9
|
+
|
10
|
+
def list_exports hostname=nil
|
11
|
+
hostname ||= 'localhost'
|
12
|
+
|
13
|
+
errback = proc {
|
14
|
+
puts "Connection to the server failed."
|
15
|
+
EM::stop_event_loop
|
16
|
+
}
|
17
|
+
NL::LC.get_connection(hostname, errback) do |c|
|
18
|
+
cmd = c.get_exports do |exports|
|
19
|
+
$list_exports_succeeded = true
|
20
|
+
if ARGV[1] == "--kvp"
|
21
|
+
puts *(exports.map { |e| e.to_kvp })
|
22
|
+
else
|
23
|
+
puts *exports
|
24
|
+
end
|
25
|
+
cmd2 = c.do_command 'bye' do
|
26
|
+
EM::stop_event_loop
|
27
|
+
end
|
28
|
+
cmd2.errback do
|
29
|
+
EM::stop_event_loop
|
30
|
+
end
|
31
|
+
end
|
32
|
+
cmd.errback do
|
33
|
+
EM::stop_event_loop
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if __FILE__ == $0
|
39
|
+
EM::run {
|
40
|
+
list_exports ARGV[0]
|
41
|
+
}
|
42
|
+
|
43
|
+
exit $list_exports_succeeded ? 0 : 7
|
44
|
+
end
|
45
|
+
|
data/bin/set_multi.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'nl/logic_client'
|
5
|
+
|
6
|
+
EM.run {
|
7
|
+
NL::LC.get_connection(
|
8
|
+
ARGV[0] || 'localhost',
|
9
|
+
proc { puts 'Error connecting to server'; EM.stop_event_loop }
|
10
|
+
) do |c|
|
11
|
+
p c
|
12
|
+
c.set_multi [{:objid => 0, :index => 0, :value => '0x55'}] do |count, list|
|
13
|
+
p "#{count} of #{list.length}"
|
14
|
+
p list
|
15
|
+
EM.stop_event_loop
|
16
|
+
end
|
17
|
+
end
|
18
|
+
}
|
data/bin/setup
ADDED
data/bin/show_info.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Displays information about the running graph.
|
3
|
+
# (C)2011 Mike Bourgeous
|
4
|
+
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'nl/logic_client'
|
7
|
+
|
8
|
+
$succeeded = false
|
9
|
+
|
10
|
+
class ShowInfoClient < NL::LC::Client
|
11
|
+
def post_init
|
12
|
+
super
|
13
|
+
get_info do |info|
|
14
|
+
info.each do |k, v|
|
15
|
+
puts "#{k}=#{v}"
|
16
|
+
end
|
17
|
+
do_command 'bye'
|
18
|
+
close_connection_after_writing
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def connection_completed
|
23
|
+
super
|
24
|
+
$succeeded = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def unbind
|
28
|
+
super
|
29
|
+
puts "An error occurred while getting the graph info." unless $succeeded
|
30
|
+
EM::stop_event_loop
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def show_info hostname=nil
|
35
|
+
hostname ||= 'localhost'
|
36
|
+
EM.connect(hostname, 14309, ShowInfoClient)
|
37
|
+
end
|
38
|
+
|
39
|
+
if __FILE__ == $0
|
40
|
+
EM::run {
|
41
|
+
EM.error_handler { |e|
|
42
|
+
puts "Error: "
|
43
|
+
p e
|
44
|
+
}
|
45
|
+
show_info ARGV[0]
|
46
|
+
}
|
47
|
+
|
48
|
+
exit $succeeded ? 0 : 7
|
49
|
+
end
|
@@ -0,0 +1,586 @@
|
|
1
|
+
# Ruby client interface for the logic system protocol, powered by EventMachine.
|
2
|
+
# (C)2012-2016 Mike Bourgeous
|
3
|
+
|
4
|
+
require 'eventmachine'
|
5
|
+
require_relative 'logic_client/version'
|
6
|
+
|
7
|
+
module NL
|
8
|
+
module LogicClient
|
9
|
+
LS_PORT = 14309
|
10
|
+
|
11
|
+
module KeyValueParser
|
12
|
+
KVREGEX = %r{(\A|^|\s)("(\\.|[^"])*"|[^" \t\r\n=][^ \t\r\n=]*)=("(\\.|[^"])*("|$)|[^ \t\r\n]+)}
|
13
|
+
|
14
|
+
# Replacement on the left, original on the right
|
15
|
+
UNESCAPES = {
|
16
|
+
't' => "\t",
|
17
|
+
'n' => "\n",
|
18
|
+
'r' => "\r",
|
19
|
+
'v' => "\v",
|
20
|
+
'f' => "\f",
|
21
|
+
'a' => "\a",
|
22
|
+
'"' => '"'
|
23
|
+
}
|
24
|
+
|
25
|
+
# Original on the left, replacement on the right
|
26
|
+
ESCAPES = UNESCAPES.invert
|
27
|
+
|
28
|
+
# TODO: Replace this key-value parser with the C-based one from knc. See
|
29
|
+
# nl-knd_client.
|
30
|
+
|
31
|
+
# Removes surrounding quotes from and parses C-style escapes within the
|
32
|
+
# given string. Does not handle internal quoting the way a shell would.
|
33
|
+
# Returns an unescaped copy, leaving the original string unmodified.
|
34
|
+
def self.unescape(str, dequote=true, esc="\\")
|
35
|
+
if str.length == 0
|
36
|
+
return str
|
37
|
+
end
|
38
|
+
|
39
|
+
newstr = ''
|
40
|
+
i = 0
|
41
|
+
quoted = false
|
42
|
+
if dequote && str[0] == '"'
|
43
|
+
quoted = true
|
44
|
+
i += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
until i == str.length
|
48
|
+
if str[i] == esc
|
49
|
+
# Remove a lone escape at the end of the string
|
50
|
+
break if i == str.length - 1
|
51
|
+
|
52
|
+
i += 1
|
53
|
+
c = str[i]
|
54
|
+
case c
|
55
|
+
when 'x'
|
56
|
+
puts "Hexadecimal escape", c.inspect
|
57
|
+
# Hexadecimal escape TODO: Get up to two hex digits
|
58
|
+
# TODO: Safe \u unicode escape
|
59
|
+
when esc
|
60
|
+
# Escape character
|
61
|
+
newstr << esc
|
62
|
+
else
|
63
|
+
# Standard escape
|
64
|
+
if UNESCAPES.has_key? c
|
65
|
+
# Matching character -- process escape sequence
|
66
|
+
newstr << UNESCAPES[c]
|
67
|
+
else
|
68
|
+
# No matching character -- pass escape sequence unmodified
|
69
|
+
newstr << esc
|
70
|
+
newstr << c
|
71
|
+
end
|
72
|
+
end
|
73
|
+
else
|
74
|
+
# Ordinary character
|
75
|
+
unless i == str.length - 1 && quoted && str[i] == '"'
|
76
|
+
newstr << str[i]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
i += 1
|
80
|
+
end
|
81
|
+
|
82
|
+
newstr
|
83
|
+
end
|
84
|
+
|
85
|
+
# Parses a multi-element quoted key-value pair string into a hash. Keys
|
86
|
+
# and values should be quoted independently (e.g. "a"="b" not "a=b").
|
87
|
+
def self.kvp(str)
|
88
|
+
pairs = {}
|
89
|
+
str.scan(KVREGEX) do |match|
|
90
|
+
if match[1] != nil && match[3] != nil
|
91
|
+
pairs[unescape(match[1])] = unescape(match[3])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
pairs
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
KVP = KeyValueParser
|
99
|
+
|
100
|
+
# Represents a deferred logic system command. Use the callback and errback
|
101
|
+
# methods from EM::Deferrable to add callbacks to be executed on command
|
102
|
+
# success/failure.
|
103
|
+
class Command
|
104
|
+
include EM::Deferrable
|
105
|
+
|
106
|
+
attr_reader :lines, :data, :data_size, :message, :name, :argstring
|
107
|
+
|
108
|
+
def initialize(name, *args)
|
109
|
+
@name = name
|
110
|
+
@args = args
|
111
|
+
@argstring = (args.length > 0 && " #{@args.map {|s| s.to_s.gsub(',', '') if s != nil }.join(',')}") || ''
|
112
|
+
@linecount = 0
|
113
|
+
@lines = []
|
114
|
+
@data_size = 0
|
115
|
+
@data = nil
|
116
|
+
@message = ""
|
117
|
+
|
118
|
+
timeout(5)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns true if successful, false if failed, nil if the
|
122
|
+
# command isn't finished.
|
123
|
+
def success?
|
124
|
+
case @deferred_status
|
125
|
+
when :succeeded
|
126
|
+
true
|
127
|
+
when :failed
|
128
|
+
false
|
129
|
+
else
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns true if this command is waiting for data.
|
135
|
+
def want_data?
|
136
|
+
@data_size > 0
|
137
|
+
end
|
138
|
+
|
139
|
+
# Called by Client when an OK line is received
|
140
|
+
def ok_line(message)
|
141
|
+
@message = message
|
142
|
+
|
143
|
+
# TODO: Change to a protocol more like the one I designed for HATS
|
144
|
+
# Server, with different response types for text/data/status.
|
145
|
+
case @name
|
146
|
+
when 'stats', 'subs', 'lst', 'lstk', 'help'
|
147
|
+
@linecount = message.to_i
|
148
|
+
when 'download'
|
149
|
+
@data_size = message.gsub(/[^0-9]*(\d+).*/, '\1').to_i
|
150
|
+
end
|
151
|
+
|
152
|
+
if @linecount == 0 && @data_size == 0
|
153
|
+
succeed self
|
154
|
+
return true
|
155
|
+
end
|
156
|
+
|
157
|
+
return false
|
158
|
+
end
|
159
|
+
|
160
|
+
# Called by Client when an ERR line is received
|
161
|
+
def err_line(message)
|
162
|
+
@message = message
|
163
|
+
fail self
|
164
|
+
end
|
165
|
+
|
166
|
+
# Called by Client to add a line
|
167
|
+
# Returns true when enough lines have been received
|
168
|
+
def add_line(line)
|
169
|
+
@lines << line
|
170
|
+
@linecount -= 1
|
171
|
+
if @linecount == 0
|
172
|
+
succeed self
|
173
|
+
end
|
174
|
+
return @linecount <= 0
|
175
|
+
end
|
176
|
+
|
177
|
+
# Called by client to add binary data
|
178
|
+
def add_data(data)
|
179
|
+
@data = @data || ''.force_encoding('BINARY')
|
180
|
+
@data << data
|
181
|
+
@data_size -= data.bytesize
|
182
|
+
if @data_size == 0
|
183
|
+
succeed self
|
184
|
+
elsif @data_size < 0
|
185
|
+
raise "Too many bytes given to data-receiving Command."
|
186
|
+
end
|
187
|
+
return @data_size <= 0
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns the command line sent to the logic system for this command
|
191
|
+
def to_s
|
192
|
+
"#{@name}#{@argstring}"
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns a description of the command's contents, useful for debugging
|
196
|
+
def inspect
|
197
|
+
%Q{#<Command: cmd=#{@name} n_lines=#{@lines.length} n_data=#{@data && @data.length}>}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Internally represents a subscription to a value on the logic system
|
202
|
+
class Subscription
|
203
|
+
attr_reader :value
|
204
|
+
attr_reader :id
|
205
|
+
|
206
|
+
# Initializes a subscription. The block will be called with
|
207
|
+
# object ID, parameter ID, and parameter value.
|
208
|
+
# TODO: Use line as parameter instead?
|
209
|
+
def initialize(obj, id, &block)
|
210
|
+
if obj == nil || id == nil || block == nil
|
211
|
+
raise "Nil parameter to Subscription constructor"
|
212
|
+
end
|
213
|
+
|
214
|
+
unless obj.respond_to?(:to_i) and id.respond_to?(:to_i)
|
215
|
+
raise "Object and ID must be integers"
|
216
|
+
end
|
217
|
+
|
218
|
+
@value = nil
|
219
|
+
@obj = obj.to_i
|
220
|
+
@id = id.to_i
|
221
|
+
@cb = block
|
222
|
+
end
|
223
|
+
|
224
|
+
# Calls the callback when the value is changed. The call to
|
225
|
+
# the callback is deferred using the event reactor.
|
226
|
+
def value=(val)
|
227
|
+
@value = val
|
228
|
+
EM.next_tick {
|
229
|
+
cb.call @obj, @id, @value
|
230
|
+
}
|
231
|
+
# TODO: Support multiple callbacks per parameter using
|
232
|
+
# a reference count to unsubscribe
|
233
|
+
end
|
234
|
+
|
235
|
+
# Parses the key-value pair line from a subscription message
|
236
|
+
def parse(kvpline)
|
237
|
+
# TODO: Parse the message (move kvp tools from KNC
|
238
|
+
# client.rb into a private utility gem?)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Converts a string value to the given logic system type name (int,
|
243
|
+
# float, string, data).
|
244
|
+
def self.string_to_type(str, type)
|
245
|
+
case type
|
246
|
+
when 'int'
|
247
|
+
return str.to_i
|
248
|
+
when 'float'
|
249
|
+
return str.to_f
|
250
|
+
when 'string'
|
251
|
+
return KVP.unescape(str)
|
252
|
+
when 'data'
|
253
|
+
raise "Data type not yet supported."
|
254
|
+
else
|
255
|
+
raise "Unsupported data type: #{type}."
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Represents an exported parameter to be returned by get_exports
|
260
|
+
class Export
|
261
|
+
attr_reader :objid, :obj_name, :param_name, :index, :type, :value
|
262
|
+
attr_reader :min, :max, :def, :hide_in_ui, :read_only
|
263
|
+
|
264
|
+
# Parses an export key-value line received from the logic system server
|
265
|
+
def initialize(line)
|
266
|
+
kvmap = KVP.kvp line
|
267
|
+
|
268
|
+
@objid = kvmap['objid'].to_i
|
269
|
+
@index = kvmap['index'].to_i
|
270
|
+
@type = kvmap['type']
|
271
|
+
@value = LC.string_to_type(kvmap['value'], @type)
|
272
|
+
@min = LC.string_to_type(kvmap['min'], @type)
|
273
|
+
@max = LC.string_to_type(kvmap['max'], @type)
|
274
|
+
@def = LC.string_to_type(kvmap['def'], @type)
|
275
|
+
@obj_name = kvmap['obj_name']
|
276
|
+
@param_name = kvmap['param_name']
|
277
|
+
@hide_in_ui = kvmap['hide_in_ui'] == 'true'
|
278
|
+
@read_only = kvmap['read_only'] == 'true'
|
279
|
+
end
|
280
|
+
|
281
|
+
# Formats this export's info as it would come from the logic system server
|
282
|
+
def to_s
|
283
|
+
# TODO: Quote strings/escape them the same way as the logic system
|
284
|
+
"#{@objid},#{@index},#{@type},#{@value.inspect} (#{@obj_name}: #{@param_name})"
|
285
|
+
end
|
286
|
+
|
287
|
+
# Formats this export's info as key-value pairs
|
288
|
+
def to_kvp
|
289
|
+
%Q{objid=#{@objid} index=#{@index} type=#{@type.inspect} read_only=#{@read_only} } <<
|
290
|
+
%Q{hide_in_ui=#{@hide_in_ui} min=#{@min.inspect} max=#{@max.inspect} def=#{@def.inspect} } <<
|
291
|
+
%Q{obj_name=#{@obj_name.inspect} param_name=#{@param_name.inspect} value=#{@value.inspect}}
|
292
|
+
end
|
293
|
+
|
294
|
+
# Stores this export's info in a hash
|
295
|
+
def to_h
|
296
|
+
{ :objid => @objid, :obj_name => @obj_name, :param_name => @param_name,
|
297
|
+
:min => @min, :max => @max, :def => @def, :hide_in_ui => @hide_in_ui,
|
298
|
+
:read_only => @read_only, :index => @index, :type => @type, :value => @value
|
299
|
+
}
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Manages a connection to a logic system
|
304
|
+
class Client < EM::Connection
|
305
|
+
include EM::P::LineText2
|
306
|
+
|
307
|
+
attr_reader :verstr, :version
|
308
|
+
|
309
|
+
# Conmap parameter is the hash entry in LC::@@connections
|
310
|
+
def initialize(conmap=nil)
|
311
|
+
super
|
312
|
+
@binary = :none
|
313
|
+
@commands = []
|
314
|
+
@active_command = nil
|
315
|
+
@subscriptions = {}
|
316
|
+
@con = conmap
|
317
|
+
@verstr = ''
|
318
|
+
@version = nil
|
319
|
+
end
|
320
|
+
|
321
|
+
# Override this method to implement a connection-completed callback (be
|
322
|
+
# sure to call super)
|
323
|
+
def connection_completed
|
324
|
+
cmd = get_version do |msg|
|
325
|
+
@verstr = msg
|
326
|
+
@version = msg[/[0-9]+\.[0-9]+\.[0-9]+/]
|
327
|
+
end
|
328
|
+
cmd.errback do |cmd|
|
329
|
+
close_connection
|
330
|
+
end
|
331
|
+
|
332
|
+
@con_success = true
|
333
|
+
if @con
|
334
|
+
@con[:connected] = true
|
335
|
+
@con[:callbacks].each do |cb|
|
336
|
+
cb.call(self) if cb && cb.respond_to?(:call)
|
337
|
+
end
|
338
|
+
@con[:callbacks].clear
|
339
|
+
@con[:errbacks].clear
|
340
|
+
end
|
341
|
+
super
|
342
|
+
end
|
343
|
+
|
344
|
+
def unbind
|
345
|
+
# TODO: Call the error handlers for any pending commands
|
346
|
+
# TODO: Add the ability to register unbind handlers
|
347
|
+
|
348
|
+
unless @con_success
|
349
|
+
if @con
|
350
|
+
@con[:errbacks].each do |eb|
|
351
|
+
eb.call() if eb && eb.respond_to?(:call)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
if @con
|
357
|
+
LC.connections.delete(@con[:hostname])
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def receive_line(data)
|
362
|
+
# Feed lines into any command waiting for data
|
363
|
+
if @active_command
|
364
|
+
@active_command = nil if @active_command.add_line data
|
365
|
+
return
|
366
|
+
end
|
367
|
+
|
368
|
+
# No active command, so this must be the beginning of a response (e.g. OK, ERR, SUB)
|
369
|
+
type, message = data.split(" - ", 2)
|
370
|
+
|
371
|
+
case type
|
372
|
+
when "OK"
|
373
|
+
if @commands.length == 0
|
374
|
+
puts '=== ERROR - Received OK when no command was waiting ==='
|
375
|
+
else
|
376
|
+
cmd = @commands.shift
|
377
|
+
unless cmd.ok_line message
|
378
|
+
@active_command = cmd
|
379
|
+
set_binary_mode(cmd.data_size) if cmd.want_data?
|
380
|
+
end
|
381
|
+
end
|
382
|
+
when "ERR"
|
383
|
+
if @commands.length == 0
|
384
|
+
puts '=== ERROR - Received ERR when no command was waiting ==='
|
385
|
+
end
|
386
|
+
@commands.shift.err_line message
|
387
|
+
when "SUB"
|
388
|
+
# TODO: Check for a callback in the subscription table
|
389
|
+
puts "TODO: Implement subscription handling"
|
390
|
+
else
|
391
|
+
puts "=== ERROR - Unknown response '#{data}' ==="
|
392
|
+
# TODO: Clear pending commands or disconnect at this point?
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def receive_binary_data(data)
|
397
|
+
if @active_command
|
398
|
+
raise "Received data for a command not expecting it!" unless @active_command.want_data?
|
399
|
+
@active_command = nil if @active_command.add_data data
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Defers execution of a command. The block, if specified, will be called
|
404
|
+
# with a Command object upon successful completion of the command. For
|
405
|
+
# more control over a command's lifecycle, including specifying an error
|
406
|
+
# callback, see the Command class. Returns the Command object used for
|
407
|
+
# this command. TODO: Add a timeout that calls any error handlers if the
|
408
|
+
# command doesn't return quickly.
|
409
|
+
def do_command(command, *args, &block)
|
410
|
+
if command.is_a? Command
|
411
|
+
cmd = command
|
412
|
+
else
|
413
|
+
cmd = Command.new command, *args
|
414
|
+
end
|
415
|
+
|
416
|
+
if block != nil
|
417
|
+
cmd.callback { |*args|
|
418
|
+
block.call *args
|
419
|
+
}
|
420
|
+
end
|
421
|
+
|
422
|
+
send_data "#{cmd.to_s}\n"
|
423
|
+
@commands << cmd
|
424
|
+
|
425
|
+
return cmd
|
426
|
+
end
|
427
|
+
|
428
|
+
# Calls the given block with the message received from the ver command.
|
429
|
+
# Returns the Command used to process the request.
|
430
|
+
def get_version(&block)
|
431
|
+
return do_command('ver') { |cmd|
|
432
|
+
block.call cmd.message
|
433
|
+
}
|
434
|
+
end
|
435
|
+
|
436
|
+
# Calls the given block with the current list of subscriptions (an array
|
437
|
+
# of lines received from the server). Returns the Command used to
|
438
|
+
# process the request.
|
439
|
+
def get_subscriptions(&block)
|
440
|
+
return do_command("subs") { |cmd|
|
441
|
+
block.call cmd.lines
|
442
|
+
}
|
443
|
+
end
|
444
|
+
|
445
|
+
# Calls the given block with the list of exported parameters (an array of
|
446
|
+
# lines received from the server). Returns the Command used to process
|
447
|
+
# the request.
|
448
|
+
def get_exports(&block)
|
449
|
+
return do_command("lstk") { |cmd|
|
450
|
+
block.call cmd.lines.map { |line| Export.new line }
|
451
|
+
}
|
452
|
+
end
|
453
|
+
|
454
|
+
# Calls the given block with a hash containing information about the
|
455
|
+
# currently-running logic graph. Returns the Command used to process the
|
456
|
+
# request.
|
457
|
+
def get_info(&block)
|
458
|
+
return do_command("inf") { |cmd|
|
459
|
+
info = KVP.kvp cmd.message
|
460
|
+
begin
|
461
|
+
info['id'] = info['id'].to_i
|
462
|
+
info['numobjs'] = info['numobjs'].to_i
|
463
|
+
info['period'] = info['period'].to_i
|
464
|
+
info['avg'] = info['avg'].to_i
|
465
|
+
info['revision'] = info['revision'].split('.', 2).map { |v| v.to_i }
|
466
|
+
rescue
|
467
|
+
end
|
468
|
+
block.call info
|
469
|
+
}
|
470
|
+
end
|
471
|
+
|
472
|
+
# Calls the given block with the requested value. Returns the Command
|
473
|
+
# used to process the request.
|
474
|
+
def get(objid, param_index, &block)
|
475
|
+
return do_command("get", objid, param_index) { |cmd|
|
476
|
+
type, value = cmd.message.split(' - ', 2)
|
477
|
+
block.call LC.string_to_type(value, type)
|
478
|
+
}
|
479
|
+
end
|
480
|
+
|
481
|
+
# Calls the given block with a Command object after successfully setting
|
482
|
+
# the given value. Returns the Command used to process the request.
|
483
|
+
def set(objid, param_index, value, &block)
|
484
|
+
return do_command("set", objid, param_index, value) { |cmd|
|
485
|
+
block.call cmd if block
|
486
|
+
}
|
487
|
+
end
|
488
|
+
|
489
|
+
# Sets multiple parameters and calls the given block with the number of
|
490
|
+
# sucessful sets, and a copy of the multi array with results added to the
|
491
|
+
# individual hashes. Multi should be an array of hashes, with each hash
|
492
|
+
# containing :objid, :index, and :value. A success/error result for each
|
493
|
+
# value will be stored in :result, and the associated Command object
|
494
|
+
# stored in :command. Returns the first Command object in the sequence
|
495
|
+
# (not particularly useful), or nil if multi is empty.
|
496
|
+
def set_multi(multi, &block)
|
497
|
+
raise ArgumentError, "Pass an array of hashes to set_multi" unless multi.is_a? Array
|
498
|
+
|
499
|
+
if multi.length == 0
|
500
|
+
block.call(0, multi) if block
|
501
|
+
return nil
|
502
|
+
end
|
503
|
+
|
504
|
+
iter = multi.each
|
505
|
+
count = 0
|
506
|
+
|
507
|
+
cb = proc { |cmd|
|
508
|
+
begin
|
509
|
+
v = iter.next
|
510
|
+
v[:command] = cmd
|
511
|
+
v[:result] = cmd.success?
|
512
|
+
count += 1 if cmd.success?
|
513
|
+
|
514
|
+
v = iter.peek
|
515
|
+
nc = set(v[:objid], v[:index], v[:value])
|
516
|
+
nc.callback { |*a| cb.call *a }
|
517
|
+
nc.errback { |*a| cb.call *a }
|
518
|
+
rescue ::StopIteration
|
519
|
+
block.call count, multi
|
520
|
+
end
|
521
|
+
}
|
522
|
+
|
523
|
+
v = iter.peek
|
524
|
+
nc = set(v[:objid], v[:index], v[:value])
|
525
|
+
nc.callback { |*a| cb.call *a }
|
526
|
+
nc.errback { |*a| cb.call *a }
|
527
|
+
|
528
|
+
return nc
|
529
|
+
end
|
530
|
+
|
531
|
+
# TODO: Methods for more commands
|
532
|
+
end
|
533
|
+
|
534
|
+
# Key=hostname, value=hash containing:
|
535
|
+
# { :connected => bool,
|
536
|
+
# :client => Client,
|
537
|
+
# :callbacks => [],
|
538
|
+
# :errbacks => []
|
539
|
+
# }
|
540
|
+
@@connections = {}
|
541
|
+
def self.connections
|
542
|
+
@@connections
|
543
|
+
end
|
544
|
+
|
545
|
+
# If a connection to the given exact hostname exists, then the given
|
546
|
+
# block will be called with the corresponding LC::Client object as its
|
547
|
+
# parameter. Otherwise a connection request to the given host name is
|
548
|
+
# queued, and block and errback will be added to the list of success
|
549
|
+
# and error callbacks that will be called when the connection is made
|
550
|
+
# or fails. Success callbacks are called with the Client object as
|
551
|
+
# their first parameter. Error callbacks are called with no
|
552
|
+
# parameters.
|
553
|
+
def self.get_connection(hostname, errback=nil, &block)
|
554
|
+
raise "You must pass a success block to get_connection." if block == nil
|
555
|
+
raise "EventMachine reactor must be running." unless EM.reactor_running?
|
556
|
+
|
557
|
+
con = @@connections[hostname]
|
558
|
+
unless con
|
559
|
+
con = {
|
560
|
+
:hostname => hostname,
|
561
|
+
:connected => false,
|
562
|
+
:callbacks => [],
|
563
|
+
:errbacks => []
|
564
|
+
}
|
565
|
+
con[:client] = EM.connect(hostname, LS_PORT, Client, con)
|
566
|
+
@@connections[hostname] = con
|
567
|
+
end
|
568
|
+
|
569
|
+
if con[:connected]
|
570
|
+
block.call con[:client]
|
571
|
+
else
|
572
|
+
con[:callbacks] << block if block
|
573
|
+
con[:errbacks] << errback if errback
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
# If a connection to the given exact hostname exists, then the
|
578
|
+
# connection's Client object will be returned. Otherwise, nil will be
|
579
|
+
# returned.
|
580
|
+
def self.get_client(hostname)
|
581
|
+
@@connections[hostname] && @@connections[hostname][:client]
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
LC = LogicClient
|
586
|
+
end
|