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.
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
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
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