rsmp 0.1.19 → 0.1.31

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/lib/rsmp/rsmp.rb CHANGED
@@ -1,31 +1,42 @@
1
- # RSMP module
1
+ # Get the current time in UTC, with optional adjustment
2
+ # Convertion to string uses the RSMP format 2015-06-08T12:01:39.654Z
3
+ # Note that using to_s on a my_clock.to_s will not produce an RSMP formatted timestamp,
4
+ # you need to use Clock.to_s my_clock
5
+
6
+ require 'time'
2
7
 
3
8
  module RSMP
4
- WRAPPING_DELIMITER = "\f"
5
9
 
6
- def self.now_object
7
- # date using UTC time zone
8
- Time.now.utc
9
- end
10
+ class Clock
11
+ attr_reader :adjustment
10
12
 
11
- def self.now_object_to_string now
12
- # date in the format required by rsmp, using UTC time zone
13
- # example: 2015-06-08T12:01:39.654Z
14
- time ||= now.utc
15
- time.strftime("%FT%T.%3NZ")
16
- end
13
+ def initialize
14
+ @adjustment = 0
15
+ end
17
16
 
18
- def self.now_string time=nil
19
- time ||= Time.now
20
- now_object_to_string time
21
- end
17
+ def set target
18
+ @adjustment = target - Time.now
19
+ end
22
20
 
23
- def self.parse_time time_str
24
- Time.parse time_str
25
- end
26
-
27
- def self.log_prefix ip
28
- "#{now_string} #{ip.ljust(20)}"
29
- end
21
+ def reset
22
+ @adjustment = 0
23
+ end
30
24
 
25
+ def now
26
+ Time.now.utc + @adjustment
27
+ end
28
+
29
+ def to_s
30
+ Clock.to_s now
31
+ end
32
+
33
+ def self.now
34
+ Time.now.utc
35
+ end
36
+
37
+ def self.to_s time=nil
38
+ (time || now).strftime("%FT%T.%3NZ")
39
+ end
40
+
41
+ end
31
42
  end
data/lib/rsmp/site.rb CHANGED
@@ -4,12 +4,12 @@
4
4
 
5
5
  module RSMP
6
6
  class Site < Node
7
- include SiteBase
7
+ include Components
8
8
 
9
9
  attr_reader :rsmp_versions, :site_settings, :logger, :proxies
10
10
 
11
11
  def initialize options={}
12
- initialize_site
12
+ initialize_components
13
13
  handle_site_settings options
14
14
  super options
15
15
  @proxies = []
@@ -20,43 +20,41 @@ module RSMP
20
20
  @site_settings['site_id']
21
21
  end
22
22
 
23
- def handle_site_settings options
24
- @site_settings = {
23
+ def handle_site_settings options={}
24
+ defaults = {
25
25
  'site_id' => 'RN+SI0001',
26
26
  'supervisors' => [
27
27
  { 'ip' => '127.0.0.1', 'port' => 12111 }
28
28
  ],
29
- 'rsmp_versions' => ['3.1.1','3.1.2','3.1.3','3.1.4'],
30
- 'sxl' => 'traffic_light_controller',
31
- 'sxl_version' => '1.0.7',
32
- 'timer_interval' => 0.1,
33
- 'watchdog_interval' => 1,
34
- 'watchdog_timeout' => 2,
35
- 'acknowledgement_timeout' => 2,
36
- 'command_response_timeout' => 1,
37
- 'status_response_timeout' => 1,
38
- 'status_update_timeout' => 1,
39
- 'site_connect_timeout' => 2,
40
- 'site_ready_timeout' => 1,
41
- 'reconnect_interval' => 0.1,
29
+ 'rsmp_versions' => 'all',
30
+ 'sxl' => 'tlc',
31
+ 'sxl_version' => '1.0.15',
32
+ 'intervals' => {
33
+ 'timer_' => 0.1,
34
+ 'watchdog' => 1,
35
+ 'reconnect' => 0.1
36
+ },
37
+ 'timeouts' => {
38
+ 'watchdog' => 2,
39
+ 'acknowledgement' => 2
40
+ },
42
41
  'send_after_connect' => true,
43
42
  'components' => {
44
43
  'C1' => {}
45
44
  }
46
45
  }
47
- if options[:site_settings]
48
- converted = options[:site_settings].map { |k,v| [k.to_s,v] }.to_h #convert symbol keys to string keys
49
- converted.compact!
50
- @site_settings.merge! converted
51
- end
52
-
53
- required = [:supervisors,:rsmp_versions,:site_id,:watchdog_interval,:watchdog_timeout,
54
- :acknowledgement_timeout,:command_response_timeout]
55
- check_required_settings @site_settings, required
56
-
46
+
47
+ @site_settings = defaults.deep_merge options[:site_settings]
48
+ check_sxl_version
57
49
  setup_components @site_settings['components']
58
50
  end
59
51
 
52
+ def check_sxl_version
53
+ sxl = @site_settings['sxl']
54
+ version = @site_settings['sxl_version']
55
+ RSMP::Schemer::find_schema! sxl, version
56
+ end
57
+
60
58
  def reconnect
61
59
  @sleep_condition.signal
62
60
  end
@@ -64,8 +62,10 @@ module RSMP
64
62
  def start_action
65
63
  @site_settings["supervisors"].each do |supervisor_settings|
66
64
  @task.async do |task|
67
- task.annotate "site_proxy"
65
+ task.annotate "site proxy"
68
66
  connect_to_supervisor task, supervisor_settings
67
+ rescue StandardError => e
68
+ notify_error e, level: :internal
69
69
  end
70
70
  end
71
71
  end
@@ -101,16 +101,14 @@ module RSMP
101
101
  proxy.run # run until disconnected
102
102
  rescue IOError => e
103
103
  log "Stream error: #{e}", level: :warning
104
- rescue SystemCallError => e # all ERRNO errors
105
- log "Reader exception: #{e.to_s}", level: :error
106
104
  rescue StandardError => e
107
- log ["Reader exception: #{e}",e.backtrace].flatten.join("\n"), level: :error
105
+ notify_error e, level: :internal
108
106
  ensure
109
107
  begin
110
- if @site_settings["reconnect_interval"] != :no
108
+ if @site_settings['intervals']['watchdog'] != :no
111
109
  # sleep until waken by reconnect() or the reconnect interval passed
112
110
  proxy.set_state :wait_for_reconnect
113
- task.with_timeout(@site_settings["reconnect_interval"]) do
111
+ task.with_timeout(@site_settings['intervals']['watchdog']) do
114
112
  @sleep_condition.wait
115
113
  end
116
114
  else
@@ -135,7 +133,7 @@ module RSMP
135
133
  def starting
136
134
  log "Starting site #{@site_settings["site_id"]}",
137
135
  level: :info,
138
- timestamp: RSMP.now_object
136
+ timestamp: @clock.now
139
137
  end
140
138
 
141
139
  def alarm
@@ -2,18 +2,24 @@
2
2
 
3
3
  module RSMP
4
4
  class SiteProxy < Proxy
5
- include SiteBase
5
+ include Components
6
+ include SiteProxyWait
6
7
 
7
8
  attr_reader :supervisor, :site_id
8
9
 
9
10
  def initialize options
10
11
  super options
11
- initialize_site
12
+ initialize_components
12
13
  @supervisor = options[:supervisor]
13
14
  @settings = @supervisor.supervisor_settings.clone
14
15
  @site_id = nil
15
16
  end
16
17
 
18
+ def inspect
19
+ "#<#{self.class.name}:#{self.object_id}, #{inspector(
20
+ :@acknowledgements,:@settings,:@site_settings,:@components
21
+ )}>"
22
+ end
17
23
  def node
18
24
  supervisor
19
25
  end
@@ -30,7 +36,7 @@ module RSMP
30
36
 
31
37
  def connection_complete
32
38
  super
33
- log "Connection to site #{@site_id} established", level: :info
39
+ log "Connection to site #{@site_id} established, using core #{@rsmp_version}, #{@sxl} #{@site_sxl_version}", level: :info
34
40
  end
35
41
 
36
42
  def process_message message
@@ -42,6 +48,8 @@ module RSMP
42
48
  will_not_handle message
43
49
  when AggregatedStatus
44
50
  process_aggregated_status message
51
+ when AggregatedStatusRequest
52
+ will_not_handle message
45
53
  when Alarm
46
54
  process_alarm message
47
55
  when CommandResponse
@@ -55,23 +63,44 @@ module RSMP
55
63
  end
56
64
  end
57
65
 
66
+ def process_command_response message
67
+ log "Received #{message.type}", message: message, level: :log
68
+ acknowledge message
69
+ end
70
+
58
71
  def process_deferred
59
72
  supervisor.process_deferred
60
73
  end
61
74
 
62
75
  def version_accepted message
63
- log "Received Version message for site #{@site_id} using RSMP #{@rsmp_version}", message: message, level: :log
76
+ log "Received Version message for site #{@site_id}", message: message, level: :log
64
77
  start_timer
65
78
  acknowledge message
66
- send_version @site_id, @settings['rsmp_versions']
79
+ send_version @site_id, rsmp_versions
67
80
  @version_determined = true
68
81
 
69
- if @settings['sites']
70
- @site_settings = @settings['sites'][@site_id]
71
- @site_settings =@settings['sites'][:any] unless @site_settings
72
- if @site_settings
73
- setup_components @site_settings['components']
82
+ end
83
+
84
+ def request_aggregated_status component, options={}
85
+ raise NotReady unless ready?
86
+ m_id = options[:m_id] || RSMP::Message.make_m_id
87
+
88
+ message = RSMP::AggregatedStatusRequest.new({
89
+ "ntsOId" => '',
90
+ "xNId" => '',
91
+ "cId" => component,
92
+ "mId" => m_id
93
+ })
94
+ if options[:collect]
95
+ result = nil
96
+ task = @task.async do |task|
97
+ wait_for_aggregated_status task, options[:collect]
74
98
  end
99
+ send_message message, validate: options[:validate]
100
+ return message, task.wait
101
+ else
102
+ send_message message, validate: options[:validate]
103
+ message
75
104
  end
76
105
  end
77
106
 
@@ -132,38 +161,39 @@ module RSMP
132
161
  @supervisor.site_ids_changed
133
162
  end
134
163
 
135
- def fetch_status parent_task, options
136
- wait_for_status_responses(parent_task,options) do |m_id|
137
- request_status options.merge(m_id: m_id)
138
- end
139
- end
164
+ def request_status component, status_list, options={}
165
+ raise NotReady unless ready?
166
+ m_id = options[:m_id] || RSMP::Message.make_m_id
140
167
 
141
- # Convert from a short ruby hash:
142
- # {:S0001=>[:signalgroupstatus, :cyclecounter, :basecyclecounter, :stage]}
143
- # to an rsmp-style list:
144
- # [{"sCI"=>"S0001", "n"=>"signalgroupstatus"}, {"sCI"=>"S0001", "n"=>"cyclecounter"}, {"sCI"=>"S0001", "n"=>"basecyclecounter"}, {"sCI"=>"S0001", "n"=>"stage"}]
145
- #
146
- # If the input is already an array, just return it
147
- def convert_status_list list
148
- return list.clone if list.is_a? Array
149
- list.map do |status_code_id,names|
150
- names.map do |name|
151
- { 'sCI' => status_code_id.to_s, 'n' => name.to_s }
152
- end
153
- end.flatten
154
- end
168
+ # additional items can be used when verifying the response,
169
+ # but must to remove from the request
170
+ request_list = status_list.map { |item| item.slice('sCI','n') }
155
171
 
156
- def request_status options
157
- raise NotReady unless ready?
158
172
  message = RSMP::StatusRequest.new({
159
173
  "ntsOId" => '',
160
174
  "xNId" => '',
161
- "cId" => options[:component],
162
- "sS" => convert_status_list(options[:status_list]),
163
- "mId" => options[:m_id]
175
+ "cId" => component,
176
+ "sS" => request_list,
177
+ "mId" => m_id
164
178
  })
165
- send_message message
166
- message
179
+ if options[:collect]
180
+ result = nil
181
+ task = @task.async do |task|
182
+ collect_options = options[:collect].merge status_list: status_list
183
+ collect_status_responses task, collect_options, m_id
184
+ end
185
+ send_message message, validate: options[:validate]
186
+
187
+ # task.wait return the result of the task. if the task raised an exception
188
+ # it will be reraised. but that mechanish does not work if multiple values
189
+ # are returned. so manually raise if first element is an exception
190
+ result = task.wait
191
+ raise result.first if result.first.is_a? Exception
192
+ return message, *result
193
+ else
194
+ send_message message, validate: options[:validate]
195
+ message
196
+ end
167
197
  end
168
198
 
169
199
  def process_status_response message
@@ -173,26 +203,48 @@ module RSMP
173
203
 
174
204
  def subscribe_to_status component, status_list, options={}
175
205
  raise NotReady unless ready?
206
+ m_id = options[:m_id] || RSMP::Message.make_m_id
207
+
208
+ # additional items can be used when verifying the response,
209
+ # but must to remove from the subscribe message
210
+ subscribe_list = status_list.map { |item| item.slice('sCI','n','uRt') }
211
+
176
212
  message = RSMP::StatusSubscribe.new({
177
213
  "ntsOId" => '',
178
214
  "xNId" => '',
179
215
  "cId" => component,
180
- "sS" => convert_status_list(status_list),
181
- 'mId'=>options[:m_id]
216
+ "sS" => subscribe_list,
217
+ 'mId' => m_id
182
218
  })
183
- send_message message
184
- return message
219
+ if options[:collect]
220
+ result = nil
221
+ task = @task.async do |task|
222
+ collect_options = options[:collect].merge status_list: status_list
223
+ collect_status_updates task, collect_options, m_id
224
+ end
225
+ send_message message, validate: options[:validate]
226
+
227
+ # task.wait return the result of the task. if the task raised an exception
228
+ # it will be reraised. but that mechanish does not work if multiple values
229
+ # are returned. so manually raise if first element is an exception
230
+ result = task.wait
231
+ raise result.first if result.first.is_a? Exception
232
+ return message, *result
233
+ else
234
+ send_message message, validate: options[:validate]
235
+ message
236
+ end
185
237
  end
186
238
 
187
- def unsubscribe_to_status component, status_list
239
+ def unsubscribe_to_status component, status_list, options={}
188
240
  raise NotReady unless ready?
189
241
  message = RSMP::StatusUnsubscribe.new({
190
242
  "ntsOId" => '',
191
243
  "xNId" => '',
192
244
  "cId" => component,
193
- "sS" => convert_status_list(status_list)
245
+ "sS" => status_list
194
246
  })
195
- send_message message
247
+ send_message message, validate: options[:validate]
196
248
  message
197
249
  end
198
250
 
@@ -201,37 +253,7 @@ module RSMP
201
253
  acknowledge message
202
254
  end
203
255
 
204
- def status_match? query, item
205
- return false if query[:sCI] && query[:sCI] != item['sCI']
206
- return false if query[:n] && query[:n] != item['n']
207
- return false if query[:q] && query[:q] != item['q']
208
- if query[:s].is_a? Regexp
209
- return false if query[:s] && item['s'] !~ query[:s]
210
- else
211
- return false if query[:s] && item['s'] != query[:s]
212
- end
213
- true
214
- end
215
-
216
- def wait_for_alarm options={}
217
- raise ArgumentError.new("component argument is missing") unless options[:component]
218
- matching_alarm = nil
219
- item = @archive.capture(@task,options.merge(type: "Alarm", with_message: true, num: 1)) do |item|
220
- # TODO check components
221
- matching_alarm = nil
222
- alarm = item[:message]
223
- next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
224
- next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
225
- next if options[:aS] && options[:aS] != alarm.attribute("aS")
226
- matching_alarm = alarm
227
- break
228
- end
229
- if item
230
- { message: item[:message], status: matching_alarm }
231
- end
232
- end
233
-
234
- def send_alarm_acknowledgement component, alarm_code
256
+ def send_alarm_acknowledgement component, alarm_code, options={}
235
257
  message = RSMP::AlarmAcknowledged.new({
236
258
  "ntsOId" => '',
237
259
  "xNId" => '',
@@ -241,44 +263,53 @@ module RSMP
241
263
  "xNACId" => '',
242
264
  "aSp" => 'Acknowledge'
243
265
  })
244
- send_message message
266
+ send_message message, validate: options[:validate]
245
267
  message
246
268
  end
247
269
 
248
- def wait_for_alarm_acknowledgement_response options
249
- raise ArgumentError.new("component argument is missing") unless options[:component]
250
- item = @archive.capture(@task,options.merge(
251
- num: 1,
252
- type: ['AlarmAcknowledgedResponse','MessageNotAck'],
253
- with_message: true
254
- )) do |item|
255
- if item[:message].type == 'MessageNotAck'
256
- next item[:message].attribute('oMId') == options[:message].m_id
257
- elsif item[:message].type == 'AlarmAcknowledgedResponse'
258
- next item[:message].attribute('cId') == options[:message].attribute('cId')
259
- end
260
- end
261
- item[:message] if item
262
- end
263
-
264
- def send_command component, args, options={}
270
+ def send_command component, command_list, options={}
265
271
  raise NotReady unless ready?
272
+ m_id = options[:m_id] || RSMP::Message.make_m_id
266
273
  message = RSMP::CommandRequest.new({
267
274
  "ntsOId" => '',
268
275
  "xNId" => '',
269
276
  "cId" => component,
270
- "arg" => args,
271
- "mId" => options[:m_id]
277
+ "arg" => command_list,
278
+ "mId" => m_id
272
279
  })
273
- send_message message
274
- message
280
+ if options[:collect]
281
+ result = nil
282
+ task = @task.async do |task|
283
+ collect_options = options[:collect].merge command_list: command_list
284
+ collect_command_responses task, collect_options, m_id
285
+ end
286
+ send_message message, validate: options[:validate]
287
+
288
+ # task.wait return the result of the task. if the task raised an exception
289
+ # it will be reraised. but that mechanish does not work if multiple values
290
+ # are returned. so manually raise if first element is an exception
291
+ result = task.wait
292
+ raise result.first if result.first.is_a? Exception
293
+ return message, *result
294
+ else
295
+ send_message message, validate: options[:validate]
296
+ message
297
+ end
275
298
  end
276
299
 
277
300
  def set_watchdog_interval interval
278
- @settings["watchdog_interval"] = interval
301
+ @settings['intervals']['watchdog'] = interval
279
302
  end
280
303
 
281
304
  def check_sxl_version message
305
+
306
+ # check that we have a schema for specified sxl type and version
307
+ # note that the type comes from the site config, while the version
308
+ # comes from the Version message send by the site
309
+ type = 'tlc'
310
+ version = message.attribute 'SXL'
311
+ RSMP::Schemer::find_schema! type, version
312
+
282
313
  # store sxl version requested by site
283
314
  # TODO should check agaist site settings
284
315
  @site_sxl_version = message.attribute 'SXL'
@@ -296,6 +327,8 @@ module RSMP
296
327
  check_rsmp_version message
297
328
  check_sxl_version message
298
329
  version_accepted message
330
+ rescue RSMP::Schemer::UnknownSchemaError => e
331
+ dont_acknowledge message, "Rejected #{message.type} message,", "#{e}"
299
332
  end
300
333
 
301
334
  def check_site_ids message
@@ -303,8 +336,38 @@ module RSMP
303
336
  site_id = message.attribute("siteId").map { |item| item["sId"] }.first
304
337
  @supervisor.check_site_id site_id
305
338
  @site_id = site_id
339
+ setup_site_settings
306
340
  site_ids_changed
307
341
  end
308
342
 
343
+ def find_site_settings site_id
344
+ if @settings['sites'] && @settings['sites'][@site_id]
345
+ log "Using site settings for site id #{@site_id}", level: :debug
346
+ return @settings['sites'][@site_id]
347
+ end
348
+
349
+ settings = @settings['guest']
350
+ if @settings['guest']
351
+ log "Using site settings for guest", level: :debug
352
+ return @settings['guest']
353
+ end
354
+
355
+ nil
356
+ end
357
+
358
+ def setup_site_settings
359
+ @site_settings = find_site_settings @site_id
360
+ if @site_settings
361
+ @sxl = @site_settings['sxl']
362
+ setup_components @site_settings['components']
363
+ else
364
+ dont_acknowledge message, 'Rejected', "No config found for site #{@site_id}"
365
+ end
366
+ end
367
+
368
+ def notify_error e, options={}
369
+ @supervisor.notify_error e, options if @supervisor
370
+ end
371
+
309
372
  end
310
373
  end