automate-em 0.0.1
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/LGPL3-LICENSE +165 -0
- data/README.textile +48 -0
- data/Rakefile +40 -0
- data/app/models/control_system.rb +20 -0
- data/app/models/controller_device.rb +21 -0
- data/app/models/controller_http_service.rb +17 -0
- data/app/models/controller_logic.rb +5 -0
- data/app/models/controller_zone.rb +10 -0
- data/app/models/dependency.rb +20 -0
- data/app/models/server.rb +12 -0
- data/app/models/setting.rb +38 -0
- data/app/models/trusted_device.rb +63 -0
- data/app/models/user_zone.rb +10 -0
- data/app/models/zone.rb +16 -0
- data/db/migrate/20111001022500_init.rb +147 -0
- data/db/migrate/20111017213801_one_time_key.rb +9 -0
- data/db/migrate/20111021071632_encrypt_setting.rb +9 -0
- data/db/migrate/20111110075444_servers.rb +15 -0
- data/db/migrate/20111114074538_default_port.rb +9 -0
- data/db/migrate/20111122073055_makebreak.rb +9 -0
- data/db/migrate/20111211062846_create_controller_http_services.rb +18 -0
- data/lib/automate-em.rb +155 -0
- data/lib/automate-em/constants.rb +6 -0
- data/lib/automate-em/core/communicator.rb +318 -0
- data/lib/automate-em/core/modules.rb +373 -0
- data/lib/automate-em/core/resolver_pool.rb +76 -0
- data/lib/automate-em/core/system.rb +356 -0
- data/lib/automate-em/device/datagram_server.rb +111 -0
- data/lib/automate-em/device/device.rb +140 -0
- data/lib/automate-em/device/device_connection.rb +689 -0
- data/lib/automate-em/device/tcp_control.rb +210 -0
- data/lib/automate-em/engine.rb +36 -0
- data/lib/automate-em/interfaces/OLD CODE/deferred.rb +67 -0
- data/lib/automate-em/interfaces/OLD CODE/telnet/ansi.rb +137 -0
- data/lib/automate-em/interfaces/OLD CODE/telnet/telnet.rb +137 -0
- data/lib/automate-em/interfaces/html5.rb +302 -0
- data/lib/automate-em/logic/logic.rb +76 -0
- data/lib/automate-em/service/http_service.rb +584 -0
- data/lib/automate-em/service/service.rb +48 -0
- data/lib/automate-em/status.rb +89 -0
- data/lib/automate-em/utilities.rb +195 -0
- data/lib/automate-em/version.rb +3 -0
- data/lib/generators/module/USAGE +8 -0
- data/lib/generators/module/module_generator.rb +47 -0
- data/lib/tasks/automate-em_tasks.rake +5 -0
- data/test/automate-em_test.rb +7 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +56 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/test_helper.rb +15 -0
- metadata +328 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module AutomateEm
|
4
|
+
|
5
|
+
$datagramServer = nil
|
6
|
+
|
7
|
+
class DatagramBase
|
8
|
+
include Utilities
|
9
|
+
include DeviceConnection
|
10
|
+
|
11
|
+
def do_send_data(data)
|
12
|
+
$datagramServer.do_send_data(DeviceModule.lookup(@parent), data)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module DatagramServer
|
17
|
+
def initialize *args
|
18
|
+
super
|
19
|
+
|
20
|
+
if !$datagramServer.nil?
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
$datagramServer = self
|
25
|
+
@devices = {}
|
26
|
+
@ips = {}
|
27
|
+
|
28
|
+
EM.defer do
|
29
|
+
System.logger.info 'running datagram server on an ephemeral port'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
#
|
35
|
+
# Eventmachine callbacks
|
36
|
+
#
|
37
|
+
def receive_data(data)
|
38
|
+
#ip = get_peername[2,6].unpack "nC4"
|
39
|
+
port, ip = Socket.unpack_sockaddr_in(get_peername)
|
40
|
+
begin
|
41
|
+
@devices["#{ip}:#{port}"].do_receive_data(data)
|
42
|
+
rescue
|
43
|
+
#
|
44
|
+
# TODO:: error messages here if device is null ect
|
45
|
+
#
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
#
|
51
|
+
# Additional controls
|
52
|
+
#
|
53
|
+
def do_send_data(scheme, data)
|
54
|
+
res = ResolverJob.new(scheme.ip)
|
55
|
+
res.callback {|ip|
|
56
|
+
|
57
|
+
#
|
58
|
+
# Just in case the address is a domain name we want to ensure the
|
59
|
+
# IP lookups are always correct and we are always sending to the
|
60
|
+
# specified device
|
61
|
+
#
|
62
|
+
text = "#{scheme.ip}:#{scheme.port}"
|
63
|
+
old_ip = @ips[text]
|
64
|
+
if old_ip != ip
|
65
|
+
EM.schedule do # All modifications are on the reactor thread instead of locking
|
66
|
+
device = @devices.delete("#{old_ip}:#{scheme.port}")
|
67
|
+
@ips[text] = ip
|
68
|
+
@devices["#{ip}:#{scheme.port}"] = device
|
69
|
+
end
|
70
|
+
end
|
71
|
+
send_datagram(data, ip, scheme.port)
|
72
|
+
}
|
73
|
+
res.errback {|error|
|
74
|
+
EM.defer do
|
75
|
+
System.logger.info e.message + " calling UDP send for #{scheme.dependency.actual_name} @ #{scheme.ip} in #{scheme.control_system.name}"
|
76
|
+
end
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_device(scheme, device)
|
81
|
+
EM.schedule do
|
82
|
+
res = ResolverJob.new(scheme.ip)
|
83
|
+
res.callback {|ip|
|
84
|
+
@devices["#{ip}:#{scheme.port}"] = device
|
85
|
+
@ips["#{scheme.ip}:#{scheme.port}"] = ip
|
86
|
+
}
|
87
|
+
res.errback {|error|
|
88
|
+
@devices["#{scheme.ip}:#{scheme.port}"] = device
|
89
|
+
@ips["#{scheme.ip}:#{scheme.port}"] = scheme.ip
|
90
|
+
}
|
91
|
+
|
92
|
+
EM.defer do
|
93
|
+
System.logger.info e.message + " adding UDP #{scheme.dependency.actual_name} @ #{scheme.ip} in #{scheme.control_system.name}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove_device(scheme)
|
99
|
+
EM.schedule do
|
100
|
+
begin
|
101
|
+
ip = @ips.delete("#{scheme.ip}:#{scheme.port}")
|
102
|
+
@devices.delete("#{ip}:#{scheme.port}")
|
103
|
+
rescue
|
104
|
+
EM.defer do
|
105
|
+
System.logger.info e.message + " removing UDP #{scheme.dependency.actual_name} @ #{scheme.ip} in #{scheme.control_system.name}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module AutomateEm
|
2
|
+
module ModuleCore
|
3
|
+
include Status # The observable pattern (Should not be called directly)
|
4
|
+
include Constants
|
5
|
+
include Utilities
|
6
|
+
|
7
|
+
#
|
8
|
+
# Sets up a link for the user code to the eventmachine class
|
9
|
+
# This way the namespace is clean.
|
10
|
+
#
|
11
|
+
def setbase(base)
|
12
|
+
@base = base
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def join_system(system)
|
17
|
+
@system_lock.synchronize {
|
18
|
+
@systems << system
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def leave_system(system)
|
23
|
+
@system_lock.synchronize {
|
24
|
+
@systems.delete(system)
|
25
|
+
return @systems.length
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear_active_timers
|
30
|
+
@schedule.clear_jobs unless @schedule.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
#def command_successful(result) # TODO:: needs a re-think
|
35
|
+
# @base.process_data_result(result)
|
36
|
+
#end
|
37
|
+
|
38
|
+
|
39
|
+
def logger
|
40
|
+
@system_lock.synchronize {
|
41
|
+
return @systems[0].logger unless @systems.empty?
|
42
|
+
}
|
43
|
+
System.logger
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :systems
|
47
|
+
attr_reader :base
|
48
|
+
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
|
53
|
+
#
|
54
|
+
# Configuration and settings
|
55
|
+
# => TODO:: We should get all zones that the pod is in with the setting set and select the first setting (vs first zone)
|
56
|
+
#
|
57
|
+
def setting(name)
|
58
|
+
val = config.settings.where("name = ?", name.to_s).first
|
59
|
+
if val.nil?
|
60
|
+
val = config.control_system.zones.joins(:settings).where('settings.name = ?', name.to_s).first
|
61
|
+
val = val.settings.where("name = ?", name.to_s).first unless val.nil?
|
62
|
+
|
63
|
+
val = config.dependency.settings.where("name = ?", name.to_s).first if val.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
if val.present?
|
67
|
+
case val.value_type
|
68
|
+
when 0
|
69
|
+
return val.text_value
|
70
|
+
when 1
|
71
|
+
return val.integer_value
|
72
|
+
when 2
|
73
|
+
return val.float_value
|
74
|
+
when 3
|
75
|
+
return val.datetime_value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Device
|
84
|
+
include ModuleCore
|
85
|
+
|
86
|
+
def initialize(tls, makebreak)
|
87
|
+
@systems = []
|
88
|
+
|
89
|
+
#
|
90
|
+
# Status variables
|
91
|
+
# NOTE:: if changed then change in logic.rb
|
92
|
+
#
|
93
|
+
@secure_connection = tls
|
94
|
+
@makebreak_connection = makebreak
|
95
|
+
@status = {}
|
96
|
+
@status_lock = Mutex.new
|
97
|
+
@system_lock = Mutex.new
|
98
|
+
@status_waiting = false
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
#
|
105
|
+
# required by base for send logic
|
106
|
+
#
|
107
|
+
attr_reader :secure_connection
|
108
|
+
attr_reader :makebreak_connection
|
109
|
+
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
|
114
|
+
def config
|
115
|
+
DeviceModule.lookup(self)
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
def send(data, options = {}, *args, &block)
|
120
|
+
error = true
|
121
|
+
|
122
|
+
begin
|
123
|
+
error = @base.do_send_command(data, options, *args, &block)
|
124
|
+
rescue => e
|
125
|
+
AutomateEm.print_error(logger, e, {
|
126
|
+
:message => "module #{self.class} in send",
|
127
|
+
:level => Logger::ERROR
|
128
|
+
})
|
129
|
+
ensure
|
130
|
+
if error
|
131
|
+
begin
|
132
|
+
logger.warn "Command send failed for: #{data.inspect}"
|
133
|
+
rescue
|
134
|
+
logger.error "Command send failed, unable to print data"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,689 @@
|
|
1
|
+
require 'atomic'
|
2
|
+
|
3
|
+
#
|
4
|
+
# This contains the basic constructs required for
|
5
|
+
# serialised comms over TCP and UDP
|
6
|
+
#
|
7
|
+
module AutomateEm
|
8
|
+
module DeviceConnection
|
9
|
+
def initialize( parent )
|
10
|
+
super
|
11
|
+
|
12
|
+
@default_send_options = {
|
13
|
+
:wait => true, # Wait for response
|
14
|
+
:delay => 0, # Delay next send by x.y seconds
|
15
|
+
:delay_on_recieve => 0, # Delay next send after a recieve by x.y seconds (only works when we are waiting for responses)
|
16
|
+
#:emit
|
17
|
+
:max_waits => 3,
|
18
|
+
:callback => nil, # Alternative to the received function
|
19
|
+
:retries => 2,
|
20
|
+
:hex_string => false,
|
21
|
+
:timeout => 5, # Timeout in seconds
|
22
|
+
:priority => 50,
|
23
|
+
:retry_on_disconnect => true,
|
24
|
+
:force_disconnect => false # part of make and break options
|
25
|
+
}
|
26
|
+
|
27
|
+
@config = {
|
28
|
+
:max_buffer => 524288, # 512kb
|
29
|
+
:clear_queue_on_disconnect => false,
|
30
|
+
:flush_buffer_on_disconnect => false,
|
31
|
+
:priority_bonus => 20
|
32
|
+
}
|
33
|
+
|
34
|
+
|
35
|
+
#
|
36
|
+
# Queues
|
37
|
+
#
|
38
|
+
@task_queue = EM::Queue.new # basically we add tasks here that we want to run in a strict order (connect, disconnect)
|
39
|
+
@receive_queue = EM::Queue.new # So we can process responses in different ways
|
40
|
+
@wait_queue = EM::Queue.new
|
41
|
+
@send_queue = EM::PriorityQueue.new(:fifo => true) {|x,y| x < y} # regular priority
|
42
|
+
|
43
|
+
#
|
44
|
+
# Named commands
|
45
|
+
# Allowing for state control
|
46
|
+
#
|
47
|
+
@named_commands = {}
|
48
|
+
|
49
|
+
#
|
50
|
+
# Locks
|
51
|
+
#
|
52
|
+
@received_lock = Mutex.new
|
53
|
+
@task_lock = Mutex.new
|
54
|
+
@status_lock = Mutex.new
|
55
|
+
@send_monitor = Object.new.extend(MonitorMixin)
|
56
|
+
|
57
|
+
|
58
|
+
#
|
59
|
+
# State
|
60
|
+
#
|
61
|
+
@connected = false
|
62
|
+
@connecting = false
|
63
|
+
@disconnecting = false
|
64
|
+
@com_paused = true
|
65
|
+
|
66
|
+
@command = nil
|
67
|
+
@waiting = false
|
68
|
+
@processing = false
|
69
|
+
@last_sent_at = 0.0
|
70
|
+
@last_recieve_at = 0.0
|
71
|
+
@timeout = nil
|
72
|
+
|
73
|
+
|
74
|
+
#
|
75
|
+
# Configure links between objects (This is a very loose tie)
|
76
|
+
# Relies on serial loading of modules
|
77
|
+
#
|
78
|
+
@parent = parent
|
79
|
+
@parent.setbase(self)
|
80
|
+
|
81
|
+
@tls_enabled = @parent.secure_connection
|
82
|
+
if @parent.makebreak_connection
|
83
|
+
@make_break = true
|
84
|
+
@first_connect = true
|
85
|
+
else
|
86
|
+
@make_break = false
|
87
|
+
end
|
88
|
+
@make_occured = false
|
89
|
+
|
90
|
+
@shutting_down = Atomic.new(false)
|
91
|
+
|
92
|
+
|
93
|
+
#
|
94
|
+
# Task event loop
|
95
|
+
#
|
96
|
+
@task_queue_proc = Proc.new do |task|
|
97
|
+
if !@shutting_down.value
|
98
|
+
EM.defer do
|
99
|
+
begin
|
100
|
+
@task_lock.synchronize {
|
101
|
+
task.call
|
102
|
+
}
|
103
|
+
rescue => e
|
104
|
+
AutomateEm.print_error(logger, e, {
|
105
|
+
:message => "module #{@parent.class} in device_connection.rb, base : error in task loop",
|
106
|
+
:level => Logger::ERROR
|
107
|
+
})
|
108
|
+
ensure
|
109
|
+
ActiveRecord::Base.clear_active_connections!
|
110
|
+
@task_queue.pop &@task_queue_proc
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
@task_queue.pop &@task_queue_proc # First task is ready
|
116
|
+
|
117
|
+
|
118
|
+
#
|
119
|
+
# send loop
|
120
|
+
#
|
121
|
+
@wait_queue_proc = Proc.new do |ignore|
|
122
|
+
if ignore != :shutdown
|
123
|
+
|
124
|
+
@send_queue.pop {|command|
|
125
|
+
if command != :shutdown
|
126
|
+
begin
|
127
|
+
|
128
|
+
process = true
|
129
|
+
if command[:name].present?
|
130
|
+
begin
|
131
|
+
name = command[:name]
|
132
|
+
@named_commands[name][0].pop # Extract the command data
|
133
|
+
command = @named_commands[name][1]
|
134
|
+
|
135
|
+
if @named_commands[name][0].empty? # See if any more of these commands are queued
|
136
|
+
@named_commands.delete(name) # Delete if there are not
|
137
|
+
else
|
138
|
+
@named_commands[name][1] = nil # Reset if there are
|
139
|
+
end
|
140
|
+
|
141
|
+
if command.nil? # decide if to continue or not
|
142
|
+
command = {}
|
143
|
+
process = false
|
144
|
+
end
|
145
|
+
rescue
|
146
|
+
#
|
147
|
+
# Retry (pop empty, lets let it have it)
|
148
|
+
#
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if process
|
153
|
+
if command[:delay] > 0.0
|
154
|
+
delay = @last_sent_at + command[:delay] - Time.now.to_f
|
155
|
+
if delay > 0.0
|
156
|
+
EM.add_timer delay do
|
157
|
+
process_send(command)
|
158
|
+
end
|
159
|
+
else
|
160
|
+
process_send(command)
|
161
|
+
end
|
162
|
+
else
|
163
|
+
process_send(command)
|
164
|
+
end
|
165
|
+
else
|
166
|
+
process_next_send(command)
|
167
|
+
end
|
168
|
+
rescue => e
|
169
|
+
EM.defer do
|
170
|
+
AutomateEm.print_error(logger, e, {
|
171
|
+
:message => "module #{@parent.class} in device_connection.rb, base : error in send loop",
|
172
|
+
:level => Logger::ERROR
|
173
|
+
})
|
174
|
+
end
|
175
|
+
ensure
|
176
|
+
ActiveRecord::Base.clear_active_connections!
|
177
|
+
@wait_queue.pop &@wait_queue_proc
|
178
|
+
end
|
179
|
+
end
|
180
|
+
}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
#@wait_queue.push(nil) Start paused
|
185
|
+
@wait_queue.pop &@wait_queue_proc
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
def process_send(command) # this is on the reactor thread
|
190
|
+
begin
|
191
|
+
if !error? && @connected
|
192
|
+
do_send_data(command[:data])
|
193
|
+
|
194
|
+
@last_sent_at = Time.now.to_f
|
195
|
+
@waiting = command[:wait]
|
196
|
+
|
197
|
+
if @waiting
|
198
|
+
@command = command
|
199
|
+
@timeout = EM::Timer.new(command[:timeout]) {
|
200
|
+
sending_timeout
|
201
|
+
}
|
202
|
+
else
|
203
|
+
process_next_send(command)
|
204
|
+
end
|
205
|
+
else
|
206
|
+
if @connected
|
207
|
+
process_next_send(command)
|
208
|
+
else
|
209
|
+
if command[:retry_on_disconnect] || @make_break
|
210
|
+
@send_queue.push(command, command[:priority] - (2 * @config[:priority_bonus])) # Double bonus
|
211
|
+
end
|
212
|
+
@com_paused = true
|
213
|
+
end
|
214
|
+
end
|
215
|
+
rescue => e
|
216
|
+
#
|
217
|
+
# Save the thread in case of bad data in that send
|
218
|
+
#
|
219
|
+
EM.defer do
|
220
|
+
AutomateEm.print_error(logger, e, {
|
221
|
+
:message => "module #{@parent.class} in device_connection.rb, process_send : possible bad data",
|
222
|
+
:level => Logger::ERROR
|
223
|
+
})
|
224
|
+
end
|
225
|
+
if @connected
|
226
|
+
process_next_send(command)
|
227
|
+
else
|
228
|
+
@com_paused = true
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def process_next_send(command)
|
234
|
+
if command[:force_disconnect] # Allow connection control
|
235
|
+
close_connection_after_writing
|
236
|
+
@disconnecting = true
|
237
|
+
@com_paused = true
|
238
|
+
else
|
239
|
+
EM.next_tick do
|
240
|
+
@wait_queue.push(nil) # Allows next response to process
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# Data received
|
247
|
+
# Allow modules to set message delimiters for auto-buffering
|
248
|
+
# Default max buffer length == 1mb (setting can be overwritten)
|
249
|
+
# NOTE: The buffer cannot be defered otherwise there are concurrency issues
|
250
|
+
#
|
251
|
+
def do_receive_data(data)
|
252
|
+
@last_recieve_at = Time.now.to_f
|
253
|
+
|
254
|
+
if @parent.respond_to?(:response_delimiter)
|
255
|
+
begin
|
256
|
+
if @buf.nil?
|
257
|
+
del = @parent.response_delimiter
|
258
|
+
if del.class == Array
|
259
|
+
del = array_to_str(del)
|
260
|
+
elsif del.class == Fixnum
|
261
|
+
del = "" << del #array_to_str([del & 0xFF])
|
262
|
+
end
|
263
|
+
@buf = BufferedTokenizer.new(del, @config[:max_buffer]) # Call back for character
|
264
|
+
end
|
265
|
+
data = @buf.extract(data)
|
266
|
+
rescue => e
|
267
|
+
@buf = nil # clear the buffer
|
268
|
+
EM.defer do # Error in a thread
|
269
|
+
AutomateEm.print_error(logger, e, {
|
270
|
+
:message => "module #{@parent.class} error whilst setting delimiter",
|
271
|
+
:level => Logger::ERROR
|
272
|
+
})
|
273
|
+
end
|
274
|
+
data = [data]
|
275
|
+
end
|
276
|
+
else
|
277
|
+
data = [data]
|
278
|
+
end
|
279
|
+
|
280
|
+
if @waiting && data.length > 0
|
281
|
+
if @processing
|
282
|
+
@receive_queue.push(*data)
|
283
|
+
else
|
284
|
+
@processing = true
|
285
|
+
@timeout.cancel
|
286
|
+
process_response(data.shift, @command)
|
287
|
+
if data.length > 0
|
288
|
+
@receive_queue.push(*data)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
else
|
292
|
+
data.each do |result|
|
293
|
+
process_response(result, nil)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
#
|
300
|
+
# Caled from recieve
|
301
|
+
#
|
302
|
+
def process_response(response, command)
|
303
|
+
EM.defer do
|
304
|
+
do_process_response(response, command)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def do_process_response(response, command)
|
309
|
+
return if @shutting_down.value
|
310
|
+
|
311
|
+
@received_lock.synchronize { # This lock protects the send queue lock when we are emiting status
|
312
|
+
@send_monitor.mon_synchronize {
|
313
|
+
result = :abort
|
314
|
+
begin
|
315
|
+
if @parent.respond_to?(:received)
|
316
|
+
if command.present?
|
317
|
+
@parent.mark_emit_start(command[:emit]) if command[:emit].present?
|
318
|
+
if command[:callback].present?
|
319
|
+
result = command[:callback].call(response, command)
|
320
|
+
|
321
|
+
#
|
322
|
+
# The data may still be usefull
|
323
|
+
#
|
324
|
+
if [nil, :ignore].include?(result)
|
325
|
+
@parent.received(response, nil)
|
326
|
+
end
|
327
|
+
else
|
328
|
+
result = @parent.received(response, command)
|
329
|
+
end
|
330
|
+
else
|
331
|
+
# logger.debug "Out of order response received for: #{@parent.class}"
|
332
|
+
result = @parent.received(response, nil)
|
333
|
+
end
|
334
|
+
else
|
335
|
+
if command.present?
|
336
|
+
@parent.mark_emit_start(command[:emit]) if command[:emit].present?
|
337
|
+
if command[:callback].present?
|
338
|
+
result = command[:callback].call(response, command)
|
339
|
+
else
|
340
|
+
result = true
|
341
|
+
end
|
342
|
+
else
|
343
|
+
result = true
|
344
|
+
end
|
345
|
+
end
|
346
|
+
rescue => e
|
347
|
+
#
|
348
|
+
# save from bad user code (don't want to deplete thread pool)
|
349
|
+
# This error should be logged in some consistent manner
|
350
|
+
#
|
351
|
+
AutomateEm.print_error(logger, e, {
|
352
|
+
:message => "module #{@parent.class} error whilst calling: received",
|
353
|
+
:level => Logger::ERROR
|
354
|
+
})
|
355
|
+
ensure
|
356
|
+
if command.present?
|
357
|
+
@parent.mark_emit_end if command[:emit].present?
|
358
|
+
end
|
359
|
+
ActiveRecord::Base.clear_active_connections!
|
360
|
+
end
|
361
|
+
|
362
|
+
if command.present? && command[:wait]
|
363
|
+
EM.schedule do
|
364
|
+
process_result(result)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
}
|
368
|
+
}
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
def sending_timeout
|
373
|
+
@timeout = true
|
374
|
+
if !@processing && @connected && @command.present? # Probably not needed...
|
375
|
+
@processing = true # Ensure responses go into the queue
|
376
|
+
|
377
|
+
command = @command[:data]
|
378
|
+
process_result(:failed)
|
379
|
+
|
380
|
+
EM.defer do
|
381
|
+
logger.info "module #{@parent.class} timeout"
|
382
|
+
logger.info "A response was not received for the command: #{command}" unless command.nil?
|
383
|
+
end
|
384
|
+
elsif !@connected && @command.present? && @command[:wait]
|
385
|
+
if @command[:retry_on_disconnect] || @make_break
|
386
|
+
@send_queue.push(@command, @command[:priority] - (2 * @config[:priority_bonus])) # Double bonus
|
387
|
+
end
|
388
|
+
@com_paused = true
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
def process_result(result)
|
394
|
+
if [nil, :ignore].include?(result) && @command[:max_waits] > 0
|
395
|
+
@command[:max_waits] -= 1
|
396
|
+
|
397
|
+
if @receive_queue.size() > 0
|
398
|
+
@receive_queue.pop { |response|
|
399
|
+
process_response(response, @command)
|
400
|
+
}
|
401
|
+
else
|
402
|
+
@timeout = EM::Timer.new(@command[:timeout]) {
|
403
|
+
sending_timeout
|
404
|
+
}
|
405
|
+
@processing = false
|
406
|
+
end
|
407
|
+
else
|
408
|
+
if [false, :failed].include?(result) && @command[:retries] > 0 # assume command failed, we need to retry
|
409
|
+
@command[:retries] -= 1
|
410
|
+
@send_queue.push(@command, @command[:priority] - @config[:priority_bonus])
|
411
|
+
end
|
412
|
+
|
413
|
+
#else result == :abort || result == :success || result == true || waits and retries exceeded
|
414
|
+
|
415
|
+
@receive_queue.size().times do
|
416
|
+
@receive_queue.pop { |response|
|
417
|
+
process_response(response, nil)
|
418
|
+
}
|
419
|
+
end
|
420
|
+
|
421
|
+
@processing = false
|
422
|
+
@waiting = false
|
423
|
+
|
424
|
+
if @command[:delay_on_recieve] > 0.0
|
425
|
+
delay_for = (@last_recieve_at + @command[:delay_on_recieve] - Time.now.to_f)
|
426
|
+
|
427
|
+
if delay_for > 0.0
|
428
|
+
EM.add_timer delay_for do
|
429
|
+
process_response_complete
|
430
|
+
end
|
431
|
+
else
|
432
|
+
process_response_complete
|
433
|
+
end
|
434
|
+
else
|
435
|
+
process_response_complete
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
def process_response_complete
|
441
|
+
if (@make_break && @send_queue.empty?) || @command[:force_disconnect]
|
442
|
+
if @connected
|
443
|
+
close_connection_after_writing
|
444
|
+
@disconnecting = true
|
445
|
+
end
|
446
|
+
@com_paused = true
|
447
|
+
else
|
448
|
+
EM.next_tick do
|
449
|
+
@wait_queue.push(nil)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
@command = nil # free memory
|
454
|
+
end
|
455
|
+
|
456
|
+
|
457
|
+
|
458
|
+
|
459
|
+
#
|
460
|
+
# ----------------------------------------------------------------
|
461
|
+
# Everything below here is called from a deferred thread
|
462
|
+
#
|
463
|
+
#
|
464
|
+
def logger
|
465
|
+
@parent.logger
|
466
|
+
end
|
467
|
+
|
468
|
+
def received_lock
|
469
|
+
@send_monitor # for monitor use
|
470
|
+
end
|
471
|
+
|
472
|
+
|
473
|
+
#
|
474
|
+
# Processes sends in strict order
|
475
|
+
#
|
476
|
+
def do_send_command(data, options = {}, *args, &block)
|
477
|
+
|
478
|
+
begin
|
479
|
+
@status_lock.synchronize {
|
480
|
+
options = @default_send_options.merge(options)
|
481
|
+
}
|
482
|
+
|
483
|
+
#
|
484
|
+
# Make sure we are sending appropriately formatted data
|
485
|
+
#
|
486
|
+
if data.class == Array
|
487
|
+
data = array_to_str(data)
|
488
|
+
elsif options[:hex_string] == true
|
489
|
+
data = hex_to_byte(data)
|
490
|
+
end
|
491
|
+
|
492
|
+
options[:data] = data
|
493
|
+
options[:retries] = 0 if options[:wait] == false
|
494
|
+
|
495
|
+
if options[:callback].nil? && (args.length > 0 || block.present?)
|
496
|
+
options[:callback] = args[0] unless args.empty? || args[0].class != Proc
|
497
|
+
options[:callback] = block unless block.nil?
|
498
|
+
end
|
499
|
+
|
500
|
+
if options[:name].present?
|
501
|
+
options[:name] = options[:name].to_sym
|
502
|
+
end
|
503
|
+
rescue => e
|
504
|
+
AutomateEm.print_error(logger, e, {
|
505
|
+
:message => "module #{@parent.class} in device_connection.rb, send : possible bad data or options hash",
|
506
|
+
:level => Logger::ERROR
|
507
|
+
})
|
508
|
+
|
509
|
+
return true
|
510
|
+
end
|
511
|
+
|
512
|
+
|
513
|
+
#
|
514
|
+
# Use a monitor here to allow for re-entrant locking
|
515
|
+
# This allows for a priority queue and we guarentee order of operations
|
516
|
+
#
|
517
|
+
bonus = false
|
518
|
+
begin
|
519
|
+
@send_monitor.mon_exit
|
520
|
+
@send_monitor.mon_enter
|
521
|
+
bonus = true
|
522
|
+
rescue
|
523
|
+
end
|
524
|
+
|
525
|
+
EM.schedule do
|
526
|
+
if bonus
|
527
|
+
options[:priority] -= @config[:priority_bonus]
|
528
|
+
end
|
529
|
+
add_to_queue(options)
|
530
|
+
end
|
531
|
+
|
532
|
+
return false
|
533
|
+
rescue => e
|
534
|
+
#
|
535
|
+
# Save from a fatal error
|
536
|
+
#
|
537
|
+
AutomateEm.print_error(logger, e, {
|
538
|
+
:message => "module #{@parent.class} in device_connection.rb, send : something went terribly wrong to get here",
|
539
|
+
:level => Logger::ERROR
|
540
|
+
})
|
541
|
+
return true
|
542
|
+
end
|
543
|
+
|
544
|
+
def add_to_queue(command)
|
545
|
+
begin
|
546
|
+
if @connected || @make_break
|
547
|
+
if @com_paused && !@make_break # We are calling from connected function (and we are connected)
|
548
|
+
command[:priority] -= (2 * @config[:priority_bonus]) # Double bonus
|
549
|
+
elsif @make_break
|
550
|
+
if !@connected && !@connecting
|
551
|
+
EM.next_tick do
|
552
|
+
do_connect
|
553
|
+
end
|
554
|
+
elsif @connected && @disconnecting
|
555
|
+
EM.next_tick do
|
556
|
+
add_to_queue(command)
|
557
|
+
end
|
558
|
+
return # Don't add to queue yet
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
add = true
|
563
|
+
if command[:name].present?
|
564
|
+
name = command[:name]
|
565
|
+
if @named_commands[name].nil?
|
566
|
+
@named_commands[name] = [[command[:priority]], command] #TODO:: we need to deal with the old commands emit values!
|
567
|
+
elsif @named_commands[name][0][-1] > command[:priority]
|
568
|
+
@named_commands[name][0].push(command[:priority])
|
569
|
+
@named_commands[name][1] = command #TODO:: we need to deal with the old commands emit values!
|
570
|
+
else
|
571
|
+
@named_commands[name][1] = command #TODO:: we need to deal with the old commands emit values!
|
572
|
+
add = false
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
@send_queue.push(command, command[:priority]) if add
|
577
|
+
end
|
578
|
+
rescue => e
|
579
|
+
EM.defer do
|
580
|
+
AutomateEm.print_error(logger, e, {
|
581
|
+
:message => "module #{@parent.class} in device_connection.rb, send : something went terribly wrong to get here",
|
582
|
+
:level => Logger::ERROR
|
583
|
+
})
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
|
589
|
+
|
590
|
+
|
591
|
+
#
|
592
|
+
# Connection state
|
593
|
+
#
|
594
|
+
def call_connected(*args) # Called from a deferred thread
|
595
|
+
#
|
596
|
+
# NOTE:: Same as add parent in device module!!!
|
597
|
+
# TODO:: Should break into a module and include it
|
598
|
+
#
|
599
|
+
@task_queue.push lambda {
|
600
|
+
@parent[:connected] = true
|
601
|
+
|
602
|
+
begin
|
603
|
+
@send_monitor.mon_synchronize { # Any sends in here are high priority (no emits as this function must return)
|
604
|
+
@parent.connected(*args) if @parent.respond_to?(:connected)
|
605
|
+
}
|
606
|
+
rescue => e
|
607
|
+
#
|
608
|
+
# save from bad user code (don't want to deplete thread pool)
|
609
|
+
#
|
610
|
+
AutomateEm.print_error(logger, e, {
|
611
|
+
:message => "module #{@parent.class} error whilst calling: connect",
|
612
|
+
:level => Logger::ERROR
|
613
|
+
})
|
614
|
+
ensure
|
615
|
+
EM.schedule do
|
616
|
+
#
|
617
|
+
# First connect if no commands pushed then we disconnect asap
|
618
|
+
#
|
619
|
+
if @make_break && @first_connect && @send_queue.size == 0
|
620
|
+
close_connection_after_writing
|
621
|
+
@disconnecting = true
|
622
|
+
@com_paused = true
|
623
|
+
@first_connect = false
|
624
|
+
elsif @com_paused
|
625
|
+
@com_paused = false
|
626
|
+
@wait_queue.push(nil)
|
627
|
+
else
|
628
|
+
EM.defer do
|
629
|
+
logger.info "Reconnected, communications not paused."
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
end
|
634
|
+
}
|
635
|
+
end
|
636
|
+
|
637
|
+
|
638
|
+
|
639
|
+
def default_send_options= (options)
|
640
|
+
@status_lock.synchronize {
|
641
|
+
@default_send_options.merge!(options)
|
642
|
+
}
|
643
|
+
end
|
644
|
+
|
645
|
+
def config= (options)
|
646
|
+
EM.schedule do
|
647
|
+
@config.merge!(options)
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
|
652
|
+
|
653
|
+
|
654
|
+
def shutdown(system)
|
655
|
+
if @parent.leave_system(system) == 0
|
656
|
+
@shutting_down.value = true
|
657
|
+
|
658
|
+
close_connection
|
659
|
+
|
660
|
+
@wait_queue.push(:shutdown)
|
661
|
+
@send_queue.push(:shutdown, -32768)
|
662
|
+
@task_queue.push(nil)
|
663
|
+
|
664
|
+
EM.defer do
|
665
|
+
begin
|
666
|
+
@parent[:connected] = false # Communicator off at this point
|
667
|
+
if @parent.respond_to?(:disconnected)
|
668
|
+
@task_lock.synchronize {
|
669
|
+
@parent.disconnected
|
670
|
+
}
|
671
|
+
end
|
672
|
+
rescue => e
|
673
|
+
#
|
674
|
+
# save from bad user code (don't want to deplete thread pool)
|
675
|
+
#
|
676
|
+
AutomateEm.print_error(logger, e, {
|
677
|
+
:message => "module #{@parent.class} error whilst calling: disconnected on shutdown",
|
678
|
+
:level => Logger::ERROR
|
679
|
+
})
|
680
|
+
ensure
|
681
|
+
@parent.clear_active_timers
|
682
|
+
ActiveRecord::Base.clear_active_connections!
|
683
|
+
end
|
684
|
+
end
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
end
|
689
|
+
end
|