rsmp 0.1.21 → 0.1.32

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -15,6 +15,11 @@ module RSMP
15
15
  @site_id = nil
16
16
  end
17
17
 
18
+ def inspect
19
+ "#<#{self.class.name}:#{self.object_id}, #{inspector(
20
+ :@acknowledgements,:@settings,:@site_settings,:@components
21
+ )}>"
22
+ end
18
23
  def node
19
24
  supervisor
20
25
  end
@@ -31,7 +36,7 @@ module RSMP
31
36
 
32
37
  def connection_complete
33
38
  super
34
- 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
35
40
  end
36
41
 
37
42
  def process_message message
@@ -43,6 +48,8 @@ module RSMP
43
48
  will_not_handle message
44
49
  when AggregatedStatus
45
50
  process_aggregated_status message
51
+ when AggregatedStatusRequest
52
+ will_not_handle message
46
53
  when Alarm
47
54
  process_alarm message
48
55
  when CommandResponse
@@ -66,18 +73,34 @@ module RSMP
66
73
  end
67
74
 
68
75
  def version_accepted message
69
- 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
70
77
  start_timer
71
78
  acknowledge message
72
- send_version @site_id, @settings['rsmp_versions']
79
+ send_version @site_id, rsmp_versions
73
80
  @version_determined = true
74
81
 
75
- if @settings['sites']
76
- @site_settings = @settings['sites'][@site_id]
77
- @site_settings =@settings['sites'][:any] unless @site_settings
78
- if @site_settings
79
- 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]
80
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
81
104
  end
82
105
  end
83
106
 
@@ -159,10 +182,16 @@ module RSMP
159
182
  collect_options = options[:collect].merge status_list: status_list
160
183
  collect_status_responses task, collect_options, m_id
161
184
  end
162
- send_message message
163
- return message, task.wait
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
164
193
  else
165
- send_message message
194
+ send_message message, validate: options[:validate]
166
195
  message
167
196
  end
168
197
  end
@@ -193,15 +222,21 @@ module RSMP
193
222
  collect_options = options[:collect].merge status_list: status_list
194
223
  collect_status_updates task, collect_options, m_id
195
224
  end
196
- send_message message
197
- return message, task.wait
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
198
233
  else
199
- send_message message
234
+ send_message message, validate: options[:validate]
200
235
  message
201
236
  end
202
237
  end
203
238
 
204
- def unsubscribe_to_status component, status_list
239
+ def unsubscribe_to_status component, status_list, options={}
205
240
  raise NotReady unless ready?
206
241
  message = RSMP::StatusUnsubscribe.new({
207
242
  "ntsOId" => '',
@@ -209,7 +244,7 @@ module RSMP
209
244
  "cId" => component,
210
245
  "sS" => status_list
211
246
  })
212
- send_message message
247
+ send_message message, validate: options[:validate]
213
248
  message
214
249
  end
215
250
 
@@ -218,7 +253,7 @@ module RSMP
218
253
  acknowledge message
219
254
  end
220
255
 
221
- def send_alarm_acknowledgement component, alarm_code
256
+ def send_alarm_acknowledgement component, alarm_code, options={}
222
257
  message = RSMP::AlarmAcknowledged.new({
223
258
  "ntsOId" => '',
224
259
  "xNId" => '',
@@ -228,7 +263,7 @@ module RSMP
228
263
  "xNACId" => '',
229
264
  "aSp" => 'Acknowledge'
230
265
  })
231
- send_message message
266
+ send_message message, validate: options[:validate]
232
267
  message
233
268
  end
234
269
 
@@ -248,19 +283,33 @@ module RSMP
248
283
  collect_options = options[:collect].merge command_list: command_list
249
284
  collect_command_responses task, collect_options, m_id
250
285
  end
251
- send_message message
252
- return message, task.wait
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
253
294
  else
254
- send_message message
295
+ send_message message, validate: options[:validate]
255
296
  message
256
297
  end
257
298
  end
258
299
 
259
300
  def set_watchdog_interval interval
260
- @settings["watchdog_interval"] = interval
301
+ @settings['intervals']['watchdog'] = interval
261
302
  end
262
303
 
263
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
+
264
313
  # store sxl version requested by site
265
314
  # TODO should check agaist site settings
266
315
  @site_sxl_version = message.attribute 'SXL'
@@ -278,6 +327,8 @@ module RSMP
278
327
  check_rsmp_version message
279
328
  check_sxl_version message
280
329
  version_accepted message
330
+ rescue RSMP::Schemer::UnknownSchemaError => e
331
+ dont_acknowledge message, "Rejected #{message.type} message,", "#{e}"
281
332
  end
282
333
 
283
334
  def check_site_ids message
@@ -285,8 +336,38 @@ module RSMP
285
336
  site_id = message.attribute("siteId").map { |item| item["sId"] }.first
286
337
  @supervisor.check_site_id site_id
287
338
  @site_id = site_id
339
+ setup_site_settings
288
340
  site_ids_changed
289
341
  end
290
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
+
291
372
  end
292
373
  end
@@ -20,12 +20,12 @@ module RSMP
20
20
  end
21
21
  end
22
22
 
23
- def wait_for_alarm options={}
23
+ def wait_for_alarm parent_task, options={}
24
24
  matching_alarm = nil
25
- item = collect(@task,options.merge(type: "Alarm", with_message: true, num: 1)) do |item|
25
+ message = collect(parent_task,options.merge(type: "Alarm", with_message: true, num: 1)) do |message|
26
26
  # TODO check components
27
27
  matching_alarm = nil
28
- alarm = item[:message]
28
+ alarm = message
29
29
  next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
30
30
  next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
31
31
  next if options[:aS] && options[:aS] != alarm.attribute("aS")
@@ -33,7 +33,7 @@ module RSMP
33
33
  break
34
34
  end
35
35
  if item
36
- { message: item[:message], status: matching_alarm }
36
+ { message: message, status: matching_alarm }
37
37
  end
38
38
  end
39
39
 
@@ -49,11 +49,11 @@ module RSMP
49
49
  task.annotate "wait for command response"
50
50
  want = options[:command_list].clone
51
51
  result = {}
52
- item = collect(parent_task,options.merge({
52
+ messages = []
53
+ collect(parent_task,options.merge({
53
54
  type: ['CommandResponse','MessageNotAck'],
54
55
  num: 1
55
- })) do |item|
56
- message = item[:message]
56
+ })) do |message|
57
57
  if message.is_a?(MessageNotAck)
58
58
  if message.attribute('oMId') == m_id
59
59
  # set result to an exception, but don't raise it.
@@ -68,39 +68,38 @@ module RSMP
68
68
  false
69
69
  end
70
70
  else
71
- found = []
71
+ add = false
72
72
  # look through querues
73
73
  want.each_with_index do |query,i|
74
74
  # look through items in message
75
- item[:message].attributes['rvs'].each do |input|
76
- ok = command_match? query, input
77
- if ok
75
+ message.attributes['rvs'].each do |input|
76
+ matching = command_match? query, input
77
+ if matching == true
78
78
  result[query] = input
79
- found << i # record which queries where matched succesfully
79
+ add = true
80
+ elsif matching == false
81
+ result.delete query
80
82
  end
81
83
  end
82
84
  end
83
- # remove queries that where matched
84
- found.sort.reverse.each do |i|
85
- want.delete_at i
86
- end
87
- want.empty? # any queries left to match?
85
+ messages << message if add
86
+ result.size == want.size # any queries left to match?
88
87
  end
89
88
  end
90
- result
89
+ return result, messages
91
90
  rescue Async::TimeoutError
92
- raise RSMP::TimeoutError.new "Did not receive command response to #{m_id} within #{options[:timeout]}s"
91
+ raise RSMP::TimeoutError.new "Did not receive correct command response to #{m_id} within #{options[:timeout]}s"
93
92
  end
94
93
 
95
94
  def collect_status_updates_or_responses task, type, options, m_id
96
- want = options[:status_list]
95
+ want = options[:status_list].clone
97
96
  result = {}
97
+ messages = []
98
98
  # wait for a status update
99
- item = collect(task,options.merge({
99
+ collect(task,options.merge({
100
100
  type: [type,'MessageNotAck'],
101
101
  num: 1
102
- })) do |item|
103
- message = item[:message]
102
+ })) do |message|
104
103
  if message.is_a?(MessageNotAck)
105
104
  if message.attribute('oMId') == m_id
106
105
  # set result to an exception, but don't raise it.
@@ -115,33 +114,33 @@ module RSMP
115
114
  false
116
115
  else
117
116
  found = []
117
+ add = false
118
118
  # look through querues
119
119
  want.each_with_index do |query,i|
120
120
  # look through status items in message
121
- item[:message].attributes['sS'].each do |input|
122
- ok = status_match? query, input
123
- if ok
121
+ message.attributes['sS'].each do |input|
122
+ matching = status_match? query, input
123
+ if matching == true
124
124
  result[query] = input
125
- found << i # record which queries where matched succesfully
125
+ add = true
126
+ elsif matching == false
127
+ result.delete query
126
128
  end
127
129
  end
128
130
  end
129
- # remove queries that where matched
130
- found.sort.reverse.each do |i|
131
- want.delete_at i
132
- end
133
- want.empty? # any queries left to match?
131
+ messages << message if add
132
+ result.size == want.size # any queries left to match?
134
133
  end
135
134
  end
136
- result
135
+ return result, messages
137
136
  rescue Async::TimeoutError
138
137
  type_str = {'StatusUpdate'=>'update', 'StatusResponse'=>'response'}[type]
139
- raise RSMP::TimeoutError.new "Did not received status #{type_str} in reply to #{m_id} within #{options[:timeout]}s"
138
+ raise RSMP::TimeoutError.new "Did not received correct status #{type_str} in reply to #{m_id} within #{options[:timeout]}s"
140
139
  end
141
140
 
142
141
  def status_match? query, item
143
- return false if query['sCI'] && query['sCI'] != item['sCI']
144
- return false if query['n'] && query['n'] != item['n']
142
+ return nil if query['sCI'] && query['sCI'] != item['sCI']
143
+ return nil if query['n'] && query['n'] != item['n']
145
144
  return false if query['q'] && query['q'] != item['q']
146
145
  if query['s'].is_a? Regexp
147
146
  return false if query['s'] && item['s'] !~ query['s']
@@ -152,8 +151,8 @@ module RSMP
152
151
  end
153
152
 
154
153
  def command_match? query, item
155
- return false if query['cCI'] && query['cCI'] != item['cCI']
156
- return false if query['n'] && query['n'] != item['n']
154
+ return nil if query['cCI'] && query['cCI'] != item['cCI']
155
+ return nil if query['n'] && query['n'] != item['n']
157
156
  if query['v'].is_a? Regexp
158
157
  return false if query['v'] && item['v'] !~ query['v']
159
158
  else
@@ -168,6 +167,8 @@ module RSMP
168
167
  # wait for command responses in an async task
169
168
  task = parent_task.async do |task|
170
169
  collect_block.call task, m_id
170
+ rescue StandardError => e
171
+ notify_error e, level: :internal
171
172
  end
172
173
 
173
174
  # call block, it should send command request using the given m_id
@@ -177,5 +178,29 @@ module RSMP
177
178
  task.wait
178
179
  end
179
180
 
181
+ def wait_for_aggregated_status parent_task, options={}
182
+ collect(parent_task,options.merge({
183
+ type: ['AggregatedStatus','MessageNotAck'],
184
+ num: 1
185
+ })) do |message|
186
+ if message.is_a?(MessageNotAck)
187
+ if message.attribute('oMId') == m_id
188
+ # set result to an exception, but don't raise it.
189
+ # this will be returned by the task and stored as the task result
190
+ # when the parent task call wait() on the task, the exception
191
+ # will be raised in the parent task, and caught by rspec.
192
+ # rspec will then show the error and record the test as failed
193
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
194
+ result = RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected: #{message.attribute('rea')}"
195
+ next true # done, no more messages wanted
196
+ else
197
+ false
198
+ end
199
+ else
200
+ true
201
+ end
202
+ end
203
+ end
204
+
180
205
  end
181
206
  end