nl-logic_client 0.1.0
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.
- 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
|