rsmp 0.1.21 → 0.1.32

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
@@ -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