rsmp 0.8.0 → 0.8.4

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.
@@ -8,26 +8,36 @@ module RSMP
8
8
  @want = want
9
9
  @got = nil
10
10
  @message = nil
11
- @done = false
12
11
  end
13
12
 
14
13
  # Are we done, i.e. did the last checked item match?
15
14
  def done?
16
- @done
15
+ @got != nil
17
16
  end
18
17
 
19
18
  # Check an item and set @done to true if it matches
20
19
  # Store the item and corresponding message if there's a positive or negative match
21
- def perform_match item, message
20
+ def perform_match item, message, block
22
21
  matched = match? item
23
22
  if matched != nil
24
- @message = message
25
- @got = item
26
- @done = matched
23
+ if block
24
+ status = block.call(nil,item)
25
+ matched = status if status == true || status == false
26
+ end
27
27
  end
28
28
  matched
29
29
  end
30
30
 
31
+ def keep message, item
32
+ @message = message
33
+ @got = item
34
+ end
35
+
36
+ def forget
37
+ @message = nil
38
+ @got = nil
39
+ end
40
+
31
41
  def match? item
32
42
  end
33
43
  end
@@ -31,8 +31,7 @@ module RSMP
31
31
  # {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>/\d+/} =>
32
32
  # { <StatusResponse message>, {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>"9"} }
33
33
  # }
34
-
35
- class Matcher < Collector
34
+ class StateCollector < Collector
36
35
  attr_reader :queries
37
36
 
38
37
  # Initialize with a list of wanted statuses
@@ -62,7 +61,7 @@ module RSMP
62
61
 
63
62
  # Get messages from results
64
63
  def messages
65
- @queries.map { |query| query.message }.uniq
64
+ @queries.map { |query| query.message }.uniq.compact
66
65
  end
67
66
 
68
67
  # Return progress as completes queries vs. total number of queries
@@ -91,22 +90,33 @@ module RSMP
91
90
  # Check if a messages matches our criteria.
92
91
  # Match each query against each item in the message
93
92
  def perform_match message
94
- return unless type_match?(message)
93
+ return false if super(message) == false
94
+ return unless collecting?
95
95
  @queries.each do |query| # look through queries
96
96
  get_items(message).each do |item| # look through items in message
97
- matched = query.perform_match(item,message)
98
- if matched == true
99
- matched = @block.call(message,item) if @block
100
- end
97
+ matched = query.perform_match(item,message,@block)
98
+ return unless collecting?
101
99
  if matched != nil
102
100
  type = {true=>'match',false=>'mismatch'}[matched]
103
101
  @notifier.log "#{@title.capitalize} #{message.m_id_short} collect #{type} #{query.want}, item #{item}", level: :debug
104
- break
102
+ if matched == true
103
+ query.keep message, item
104
+ elsif matched == false
105
+ query.forget
106
+ end
105
107
  end
106
108
  end
107
109
  end
108
110
  complete if done?
109
111
  @notifier.log "#{@title.capitalize} collect reached #{summary}", level: :debug
110
112
  end
113
+
114
+ # don't collect anything. Query will collect them instead
115
+ def keep message
116
+ end
117
+
118
+ def describe
119
+ @queries.map {|q| q.want.to_s }
120
+ end
111
121
  end
112
122
  end
@@ -0,0 +1,20 @@
1
+ module RSMP
2
+ # Base class for waiting for status updates or responses
3
+ class StatusCollector < StateCollector
4
+ def initialize proxy, want, options={}
5
+ super proxy, want, options.merge(title: 'status response', type:['MessageNotAck'])
6
+
7
+ @options[:type] << 'StatusUpdate' unless options[:updates] == false
8
+ @options[:type] << 'StatusResponse' unless options[:reponses] == false
9
+ end
10
+
11
+ def build_query want
12
+ RSMP::StatusQuery.new want
13
+ end
14
+
15
+ # Get items, in our case status values
16
+ def get_items message
17
+ message.attributes['sS'] || []
18
+ end
19
+ end
20
+ end
@@ -1,19 +1,4 @@
1
1
  module RSMP
2
- # Match a specific command responses
3
- class CommandQuery < Query
4
- # Match a return value item against a query
5
- def match? item
6
- return nil if @want['cCI'] && @want['cCI'] != item['cCI']
7
- return nil if @want['n'] && @want['n'] != item['n']
8
- if @want['v'].is_a? Regexp
9
- return false if @want['v'] && item['v'] !~ @want['v']
10
- else
11
- return false if @want['v'] && item['v'] != @want['v']
12
- end
13
- true
14
- end
15
- end
16
-
17
2
  # Match a specific status response or update
18
3
  class StatusQuery < Query
19
4
  # Match a status value against a query
data/lib/rsmp/node.rb CHANGED
@@ -15,7 +15,6 @@ module RSMP
15
15
  @clock = Clock.new
16
16
  @error_queue = Async::Queue.new
17
17
  @ignore_errors = []
18
- options[:collector]
19
18
  @collect = options[:collect]
20
19
  end
21
20
 
data/lib/rsmp/proxy.rb CHANGED
@@ -52,12 +52,6 @@ module RSMP
52
52
  node.clock
53
53
  end
54
54
 
55
- def collect task, options, &block
56
- collector = RSMP::Collector.new self, options
57
- collector.collect task, &block
58
- collector
59
- end
60
-
61
55
  def run
62
56
  start
63
57
  @reader.wait if @reader
@@ -579,25 +573,27 @@ module RSMP
579
573
  end
580
574
 
581
575
  def wait_for_acknowledgement parent_task, options={}, m_id
582
- collect(parent_task,options.merge({
583
- type: ['MessageAck','MessageNotAck'],
584
- num: 1
585
- })) do |message|
576
+ collector = Collector.new self, options.merge(task: parent_task, type: ['MessageAck','MessageNotAck'])
577
+ collector.collect do |message|
586
578
  if message.is_a?(MessageNotAck)
587
579
  if message.attribute('oMId') == m_id
588
- # set result to an exception, but don't raise it.
589
- # this will be returned by the task and stored as the task result
590
- # when the parent task call wait() on the task, the exception
591
- # will be raised in the parent task, and caught by rspec.
592
- # rspec will then show the error and record the test as failed
593
580
  m_id_short = RSMP::Message.shorten_m_id m_id, 8
594
- result = RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected with '#{message.attribute('rea')}'"
595
- next true # done, no more messages wanted
581
+ raise RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected with '#{message.attribute('rea')}'"
596
582
  end
597
583
  elsif message.is_a?(MessageAck)
598
- next true if message.attribute('oMId') == m_id
584
+ collector.complete if message.attribute('oMId') == m_id
599
585
  end
600
- false
586
+ end
587
+ end
588
+
589
+ def send_and_optionally_collect message, options, &block
590
+ if options[:collect]
591
+ task = @task.async { |task| yield task }
592
+ send_message message, validate: options[:validate]
593
+ { sent: message, collector: task.wait }
594
+ else
595
+ send_message message, validate: options[:validate]
596
+ return { sent: message }
601
597
  end
602
598
  end
603
599
  end
data/lib/rsmp/site.rb CHANGED
@@ -9,9 +9,9 @@ module RSMP
9
9
  attr_reader :rsmp_versions, :site_settings, :logger, :proxies
10
10
 
11
11
  def initialize options={}
12
+ super options
12
13
  initialize_components
13
14
  handle_site_settings options
14
- super options
15
15
  @proxies = []
16
16
  @sleep_condition = Async::Notification.new
17
17
  @proxies_condition = Async::Notification.new
@@ -95,7 +95,8 @@ module RSMP
95
95
  ip: supervisor_settings['ip'],
96
96
  port: supervisor_settings['port'],
97
97
  logger: @logger,
98
- archive: @archive
98
+ archive: @archive,
99
+ collect: @collect
99
100
  })
100
101
  @proxies << proxy
101
102
  @proxies_condition.signal
@@ -101,8 +101,13 @@ module RSMP
101
101
  "cId" => component,
102
102
  "mId" => m_id
103
103
  })
104
- send_and_collect_if_needed message, options do |task|
105
- collect_aggregated_status task, options[:collect].merge(m_id: m_id, num:1)
104
+ send_and_optionally_collect message, options do |task|
105
+ collector = AggregatedStatusCollector.new(
106
+ self,
107
+ options[:collect].merge(task:@task,m_id: m_id, num:1)
108
+ )
109
+ collector.collect
110
+ collector
106
111
  end
107
112
  end
108
113
 
@@ -174,8 +179,14 @@ module RSMP
174
179
  "sS" => request_list,
175
180
  "mId" => m_id
176
181
  })
177
- send_and_collect_if_needed message, options do |task|
178
- collect_status_responses task, status_list, options[:collect].merge(m_id: m_id)
182
+ send_and_optionally_collect message, options do |task|
183
+ collector = StatusCollector.new(
184
+ self,
185
+ status_list,
186
+ options[:collect].merge(task:@task,m_id: m_id)
187
+ )
188
+ collector.collect
189
+ collector
179
190
  end
180
191
  end
181
192
 
@@ -186,17 +197,6 @@ module RSMP
186
197
  acknowledge message
187
198
  end
188
199
 
189
- def send_and_collect_if_needed message, options, &block
190
- if options[:collect]
191
- task = @task.async { |task| yield task }
192
- send_message message, validate: options[:validate]
193
- { sent: message, collector: task.wait }
194
- else
195
- send_message message, validate: options[:validate]
196
- return { sent: message }
197
- end
198
- end
199
-
200
200
  def subscribe_to_status component_id, status_list, options={}
201
201
  validate_ready 'subscribe to status'
202
202
  m_id = options[:m_id] || RSMP::Message.make_m_id
@@ -215,8 +215,14 @@ module RSMP
215
215
  "sS" => subscribe_list,
216
216
  'mId' => m_id
217
217
  })
218
- send_and_collect_if_needed message, options do |task|
219
- collect_status_updates task, status_list, options[:collect].merge(m_id: m_id)
218
+ send_and_optionally_collect message, options do |task|
219
+ collector = StatusCollector.new(
220
+ self,
221
+ status_list,
222
+ options[:collect].merge(task:@task,m_id: m_id)
223
+ )
224
+ collector.collect
225
+ collector
220
226
  end
221
227
  end
222
228
 
@@ -261,8 +267,14 @@ module RSMP
261
267
  "arg" => command_list,
262
268
  "mId" => m_id
263
269
  })
264
- send_and_collect_if_needed message, options do |task|
265
- collect_command_responses task, command_list, options[:collect].merge(m_id: m_id)
270
+ send_and_optionally_collect message, options do |task|
271
+ collector = CommandResponseCollector.new(
272
+ self,
273
+ command_list,
274
+ options[:collect].merge(task:@task,m_id: m_id)
275
+ )
276
+ collector.collect
277
+ collector
266
278
  end
267
279
  end
268
280
 
@@ -339,37 +351,13 @@ module RSMP
339
351
  distribute_error e, options
340
352
  end
341
353
 
342
- def collect_alarms parent_task, options={}
343
- collect(parent_task,options.merge(type: "Alarm")) do |alarm|
354
+ def collect_alarms options={}
355
+ collect(@task,options.merge(type: "Alarm")) do |alarm|
344
356
  next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
345
357
  next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
346
358
  next if options[:aS] && options[:aS] != alarm.attribute("aS")
347
- true
359
+ :keep
348
360
  end
349
361
  end
350
-
351
- def collect_status_updates task, status_list, options
352
- collector = StatusUpdateMatcher.new(self, status_list, options)
353
- collector.collect task
354
- collector
355
- end
356
-
357
- def collect_status_responses task, status_list, options
358
- collector = StatusResponseMatcher.new(self, status_list, options)
359
- collector.collect task
360
- collector
361
- end
362
-
363
- def collect_command_responses task, command_list, options
364
- collector = CommandResponseMatcher.new(self, command_list, options)
365
- collector.collect task
366
- collector
367
- end
368
-
369
- def collect_aggregated_status task, options
370
- collector = AggregatedStatusMatcher.new(self, options)
371
- collector.collect task
372
- collector
373
- end
374
362
  end
375
363
  end
@@ -138,16 +138,8 @@ module RSMP
138
138
  "mId" => m_id,
139
139
  })
140
140
 
141
- if options[:collect]
142
- result = nil
143
- task = @task.async do |task|
144
- wait_for_acknowledgement task, options[:collect], m_id
145
- end
146
- send_message message, validate: options[:validate]
147
- return message, task.wait
148
- else
149
- send_message message, validate: options[:validate]
150
- message
141
+ send_and_optionally_collect message, options do |task|
142
+ wait_for_acknowledgement task, options[:collect], m_id
151
143
  end
152
144
  end
153
145
 
@@ -282,7 +274,7 @@ module RSMP
282
274
  end
283
275
 
284
276
  def fetch_last_sent_status component, code, name
285
- @last_status_sent.dig component, code, name
277
+ @last_status_sent.dig component, code, name if @last_status_sent
286
278
  end
287
279
 
288
280
  def store_last_sent_status message
@@ -6,25 +6,33 @@ module RSMP
6
6
  # plan is a string, with each character representing a signal phase at a particular second in the cycle
7
7
  def initialize node:, id:
8
8
  super node: node, id: id, grouped: false
9
- move 0
10
9
  end
11
10
 
12
- def get_state pos
11
+ def timer
12
+ @state = get_state
13
+ end
14
+
15
+ def get_state
16
+ return 'a' if node.main.dark_mode
17
+ return 'c' if node.main.yellow_flash
18
+
19
+ cycle_counter = node.main.cycle_counter
20
+
21
+ if node.main.startup_sequence_active
22
+ @state = node.main.startup_state || 'a'
23
+ end
24
+
13
25
  default = 'a' # phase a means disabled/dark
14
26
  plan = node.main.current_plan
15
27
  return default unless plan
16
28
  return default unless plan.states
17
29
  states = plan.states[c_id]
18
30
  return default unless states
19
- state =states[pos]
31
+ state = states[cycle_counter]
20
32
  return default unless state =~ /[a-hA-G0-9N-P]/ # valid signal group states
21
33
  state
22
34
  end
23
35
 
24
- def move pos
25
- @state = get_state pos
26
- end
27
-
28
36
  def handle_command command_code, arg
29
37
  case command_code
30
38
  when 'M0010', 'M0011'
@@ -5,9 +5,12 @@ module RSMP
5
5
  # and keeps track of signal plans, detector logics, inputs, etc. which do
6
6
  # not have dedicated components.
7
7
  class TrafficController < Component
8
- attr_reader :pos, :cycle_time, :plan
8
+ attr_reader :pos, :cycle_time, :plan, :cycle_counter,
9
+ :yellow_flash, :dark_mode,
10
+ :startup_sequence_active, :startup_sequence, :startup_sequence_pos
9
11
 
10
- def initialize node:, id:, cycle_time: 10, signal_plans:
12
+ def initialize node:, id:, cycle_time: 10, signal_plans:,
13
+ startup_sequence:, live_output:nil
11
14
  super node: node, id: id, grouped: true
12
15
  @signal_groups = []
13
16
  @detector_logics = []
@@ -15,13 +18,15 @@ module RSMP
15
18
  @cycle_time = cycle_time
16
19
  @num_traffic_situations = 1
17
20
  @num_inputs = 8
21
+ @startup_sequence = startup_sequence
22
+ @live_output = live_output
18
23
  reset
19
24
  end
20
25
 
21
26
  def reset
22
- @pos = 0
23
- @plan = 0
24
- @dark_mode = false
27
+ @cycle_counter = 0
28
+ @plan = 1
29
+ @dark_mode = true
25
30
  @yellow_flash = false
26
31
  @booting = false
27
32
  @control_mode = 'control'
@@ -40,6 +45,12 @@ module RSMP
40
45
  @inputs = '0'*@num_inputs
41
46
  @input_activations = '0'*@num_inputs
42
47
  @input_results = '0'*@num_inputs
48
+
49
+ @day_time_table = {}
50
+ @startup_sequence_active = false
51
+ @startup_sequence_initiated_at = nil
52
+ @startup_sequence_pos = 0
53
+ @time_int = nil
43
54
  end
44
55
 
45
56
  def clock
@@ -64,46 +75,83 @@ module RSMP
64
75
  end
65
76
 
66
77
  def timer now
67
- # TODO
68
- # We should use a monotone timer, to avoid jumps
69
- # in case the user sets the system time
70
- pos = Time.now.to_i % @cycle_time
71
- if pos != @pos
72
- @pos = pos
73
- move pos
74
- end
78
+ # TODO use monotone timer, to avoid jumps in case the user sets the system time
79
+ @signal_groups.each { |group| group.timer }
80
+ time = Time.now.to_i
81
+ return if time == @time_int
82
+ @time_int = time
83
+ move_cycle_counter
84
+ move_startup_sequence if @startup_sequence_active
85
+ output_states
86
+ end
87
+
88
+ def move_cycle_counter
89
+ counter = Time.now.to_i % @cycle_time
90
+ @cycle_counter = counter
91
+ end
92
+
93
+ def startup_state
94
+ return unless @startup_sequence_active
95
+ return unless @startup_sequence_pos
96
+ @startup_sequence[ @startup_sequence_pos ]
75
97
  end
76
98
 
77
- def move pos
78
- @signal_groups.each do |group|
79
- group.move pos
99
+ def initiate_startup_sequence
100
+ log "Initiating startup sequence", level: :info
101
+ @startup_sequence_active = true
102
+ @startup_sequence_initiated_at = nil
103
+ @startup_sequence_pos = nil
104
+ end
105
+
106
+ def end_startup_sequence
107
+ @startup_sequence_active = false
108
+ @startup_sequence_initiated_at = nil
109
+ @startup_sequence_pos = nil
110
+
111
+ @yellow_flash = false
112
+ @dark_mode = false
113
+ end
114
+
115
+ def move_startup_sequence
116
+ was = @startup_sequence_pos
117
+ if @startup_sequence_initiated_at == nil
118
+ @startup_sequence_initiated_at = Time.now.to_i + 1
119
+ @startup_sequence_pos = 0
120
+ else
121
+ @startup_sequence_pos = Time.now.to_i - @startup_sequence_initiated_at
122
+ end
123
+ if @startup_sequence_pos >= @startup_sequence.size
124
+ end_startup_sequence
80
125
  end
81
- #output_states
82
126
  end
83
127
 
84
128
  def output_states
129
+ return unless @live_output
85
130
  str = @signal_groups.map do |group|
86
131
  s = "#{group.c_id}:#{group.state}"
87
132
  if group.state =~ /^[1-9]$/
88
133
  s.colorize(:green)
89
134
  elsif group.state =~ /^[NOP]$/
90
135
  s.colorize(:yellow)
91
- elsif group.state =~ /^[a]$/
92
- s.colorize(color: :black)
136
+ elsif group.state =~ /^[ae]$/
137
+ s.colorize(:black)
138
+ elsif group.state =~ /^[f]$/
139
+ s.colorize(:yellow)
140
+ elsif group.state =~ /^[g]$/
141
+ s.colorize(:red)
93
142
  else
94
143
  s.colorize(:red)
95
144
  end
96
145
  end.join ' '
97
146
  plan = "P#{@plan}"
98
- print "#{plan.rjust(4)} #{pos.to_s.rjust(4)} #{str}\r"
147
+
148
+ File.open @live_output, 'w' do |file|
149
+ file.puts "#{plan.rjust(4)} #{pos.to_s.rjust(4)} #{str}\r"
150
+ end
99
151
  end
100
152
 
101
153
  def format_signal_group_status
102
- if @yellow_flash
103
- 'c' * @signal_groups.size
104
- else
105
- @signal_groups.map { |group| group.state }.join
106
- end
154
+ @signal_groups.map { |group| group.state }.join
107
155
  end
108
156
 
109
157
  def handle_command command_code, arg
@@ -214,6 +262,18 @@ module RSMP
214
262
 
215
263
  def handle_m0017 arg
216
264
  @node.verify_security_code 2, arg['securityCode']
265
+ arg['status'].split(',').each do |item|
266
+ elems = item.split('-')
267
+ nr = elems[0].to_i
268
+ plan = elems[1].to_i
269
+ hour = elems[2].to_i
270
+ min = elems[3].to_i
271
+ if nr<0 || nr>12
272
+ raise InvalidMessage.new "time table id must be between 0 and 12, got #{nr}"
273
+ end
274
+ #p "nr: #{nr}, plan #{plan} at #{hour}:#{min}"
275
+ @day_time_table[nr] = {plan: plan, hour: hour, min:min}
276
+ end
217
277
  end
218
278
 
219
279
  def handle_m0018 arg
@@ -276,6 +336,7 @@ module RSMP
276
336
  log "Switching to mode #{mode}", level: :info
277
337
  case mode
278
338
  when 'NormalControl'
339
+ initiate_startup_sequence if @yellow_flash || @dark_mode
279
340
  @yellow_flash = false
280
341
  @dark_mode = false
281
342
  when 'YellowFlash'
@@ -308,9 +369,9 @@ module RSMP
308
369
  when 'signalgroupstatus'
309
370
  TrafficControllerSite.make_status format_signal_group_status
310
371
  when 'cyclecounter'
311
- TrafficControllerSite.make_status @pos.to_s
372
+ TrafficControllerSite.make_status @cycle_counter.to_s
312
373
  when 'basecyclecounter'
313
- TrafficControllerSite.make_status @pos.to_s
374
+ TrafficControllerSite.make_status @cycle_counter.to_s
314
375
  when 'stage'
315
376
  TrafficControllerSite.make_status 0.to_s
316
377
  end
@@ -511,7 +572,10 @@ module RSMP
511
572
  def handle_s0027 status_code, status_name=nil
512
573
  case status_name
513
574
  when 'status'
514
- TrafficControllerSite.make_status '00-00-00-00'
575
+ status = @day_time_table.map do |i,item|
576
+ "#{i}-#{item[:plan]}-#{item[:hour]}-#{item[:min]}"
577
+ end.join(',')
578
+ TrafficControllerSite.make_status status
515
579
  end
516
580
  end
517
581
 
@@ -5,11 +5,16 @@ module RSMP
5
5
  attr_accessor :main, :signal_plans
6
6
 
7
7
  def initialize options={}
8
+ # setup options before calling super initializer,
9
+ # since build of components depend on options
8
10
  @sxl = 'traffic_light_controller'
9
11
  @security_codes = options[:site_settings]['security_codes']
10
12
  @interval = options[:site_settings].dig('intervals','timer') || 1
13
+ @startup_sequence = options[:site_settings]['startup_sequence'] || 'efg'
11
14
  build_plans options[:site_settings].dig('signal_plans')
15
+
12
16
  super options
17
+
13
18
  unless @main
14
19
  raise ConfigurationError.new "TLC must have a main component"
15
20
  end
@@ -37,7 +42,9 @@ module RSMP
37
42
  when 'main'
38
43
  @main = TrafficController.new node: self, id: id,
39
44
  cycle_time: settings['cycle_time'],
40
- signal_plans: @signal_plans
45
+ startup_sequence: @startup_sequence,
46
+ signal_plans: @signal_plans,
47
+ live_output: @site_settings['live_output']
41
48
  when 'signal_group'
42
49
  group = SignalGroup.new node: self, id: id
43
50
  @main.add_signal_group group
@@ -52,6 +59,7 @@ module RSMP
52
59
  def start_action
53
60
  super
54
61
  start_timer
62
+ @main.initiate_startup_sequence
55
63
  end
56
64
 
57
65
  def start_timer
@@ -134,6 +142,7 @@ module RSMP
134
142
  when :restart
135
143
  log "Restarting TLC", level: :info
136
144
  restart
145
+ initiate_startup_sequence
137
146
  end
138
147
  end
139
148
  end
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.4"
3
3
  end