rsmp 0.8.5 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yaml +21 -0
  3. data/Gemfile.lock +9 -3
  4. data/README.md +2 -12
  5. data/bin/console +1 -1
  6. data/cucumber.yml +1 -0
  7. data/documentation/classes_and_modules.md +4 -4
  8. data/documentation/collecting_message.md +2 -2
  9. data/documentation/tasks.md +149 -0
  10. data/lib/rsmp/archive.rb +3 -3
  11. data/lib/rsmp/cli.rb +32 -4
  12. data/lib/rsmp/collect/aggregated_status_collector.rb +1 -1
  13. data/lib/rsmp/collect/command_response_collector.rb +1 -1
  14. data/lib/rsmp/collect/state_collector.rb +1 -1
  15. data/lib/rsmp/collect/status_collector.rb +2 -1
  16. data/lib/rsmp/components.rb +3 -3
  17. data/lib/rsmp/convert/export/json_schema.rb +4 -4
  18. data/lib/rsmp/convert/import/yaml.rb +1 -1
  19. data/lib/rsmp/deep_merge.rb +1 -0
  20. data/lib/rsmp/error.rb +0 -3
  21. data/lib/rsmp/inspect.rb +1 -1
  22. data/lib/rsmp/logger.rb +5 -5
  23. data/lib/rsmp/logging.rb +1 -1
  24. data/lib/rsmp/message.rb +1 -1
  25. data/lib/rsmp/node.rb +10 -45
  26. data/lib/rsmp/proxy.rb +176 -133
  27. data/lib/rsmp/rsmp.rb +1 -1
  28. data/lib/rsmp/site.rb +23 -60
  29. data/lib/rsmp/site_proxy.rb +21 -17
  30. data/lib/rsmp/supervisor.rb +25 -21
  31. data/lib/rsmp/supervisor_proxy.rb +58 -29
  32. data/lib/rsmp/task.rb +84 -0
  33. data/lib/rsmp/tlc/signal_group.rb +7 -5
  34. data/lib/rsmp/tlc/signal_plan.rb +2 -2
  35. data/lib/rsmp/tlc/traffic_controller.rb +146 -53
  36. data/lib/rsmp/tlc/traffic_controller_site.rb +43 -36
  37. data/lib/rsmp/version.rb +1 -1
  38. data/lib/rsmp.rb +1 -1
  39. metadata +6 -5
  40. data/lib/rsmp/site_proxy_wait.rb +0 -0
  41. data/lib/rsmp/wait.rb +0 -16
  42. data/test.rb +0 -27
@@ -6,7 +6,7 @@ module RSMP
6
6
  # not have dedicated components.
7
7
  class TrafficController < Component
8
8
  attr_reader :pos, :cycle_time, :plan, :cycle_counter,
9
- :yellow_flash, :dark_mode,
9
+ :functional_position,
10
10
  :startup_sequence_active, :startup_sequence, :startup_sequence_pos
11
11
 
12
12
  def initialize node:, id:, cycle_time: 10, signal_plans:,
@@ -23,27 +23,35 @@ module RSMP
23
23
  reset
24
24
  end
25
25
 
26
- def reset
27
- @cycle_counter = 0
28
- @plan = 1
29
- @dark_mode = true
30
- @yellow_flash = false
26
+ def reset_modes
27
+ @function_position = 'NormalControl'
28
+ @previous_functional_position = nil
29
+ @functional_position_timeout = nil
30
+
31
31
  @booting = false
32
+ @is_starting = false
32
33
  @control_mode = 'control'
34
+ @manual_control = false
35
+ @fixed_time_control = false
36
+ @isolated_control = false
37
+ @all_red = false
33
38
  @police_key = 0
39
+ end
40
+
41
+ def reset
42
+ reset_modes
43
+
44
+ @cycle_counter = 0
45
+ @plan = 1
34
46
  @intersection = 0
35
- @is_starting = false
36
47
  @emergency_route = false
37
48
  @emergency_route_number = 0
38
49
  @traffic_situation = 0
39
- @manual_control = false
40
- @fixed_time_control = false
41
- @isolated_control = false
42
- @yellow_flash = false
43
- @all_red = false
44
50
 
45
51
  @inputs = '0'*@num_inputs
46
52
  @input_activations = '0'*@num_inputs
53
+ @input_forced = '0'*@num_inputs
54
+ @input_forced_values = '0'*@num_inputs
47
55
  @input_results = '0'*@num_inputs
48
56
 
49
57
  @day_time_table = {}
@@ -53,6 +61,18 @@ module RSMP
53
61
  @time_int = nil
54
62
  end
55
63
 
64
+ def dark?
65
+ @function_position == 'Dark'
66
+ end
67
+
68
+ def yellow_flash?
69
+ @function_position == 'YellowFlash'
70
+ end
71
+
72
+ def normal_control?
73
+ @function_position == 'NormalControl'
74
+ end
75
+
56
76
  def clock
57
77
  node.clock
58
78
  end
@@ -76,12 +96,15 @@ module RSMP
76
96
 
77
97
  def timer now
78
98
  # TODO use monotone timer, to avoid jumps in case the user sets the system time
79
- @signal_groups.each { |group| group.timer }
80
99
  time = Time.now.to_i
81
100
  return if time == @time_int
82
101
  @time_int = time
83
102
  move_cycle_counter
103
+ check_functional_position_timeout
84
104
  move_startup_sequence if @startup_sequence_active
105
+
106
+ @signal_groups.each { |group| group.timer }
107
+
85
108
  output_states
86
109
  end
87
110
 
@@ -90,6 +113,15 @@ module RSMP
90
113
  @cycle_counter = counter
91
114
  end
92
115
 
116
+ def check_functional_position_timeout
117
+ return unless @functional_position_timeout
118
+ if clock.now >= @functional_position_timeout
119
+ switch_functional_position @previous_functional_position, reverting: true
120
+ @functional_position_timeout = nil
121
+ @previous_functional_position = nil
122
+ end
123
+ end
124
+
93
125
  def startup_state
94
126
  return unless @startup_sequence_active
95
127
  return unless @startup_sequence_pos
@@ -98,6 +130,7 @@ module RSMP
98
130
 
99
131
  def initiate_startup_sequence
100
132
  log "Initiating startup sequence", level: :info
133
+ reset_modes
101
134
  @startup_sequence_active = true
102
135
  @startup_sequence_initiated_at = nil
103
136
  @startup_sequence_pos = nil
@@ -107,9 +140,6 @@ module RSMP
107
140
  @startup_sequence_active = false
108
141
  @startup_sequence_initiated_at = nil
109
142
  @startup_sequence_pos = nil
110
-
111
- @yellow_flash = false
112
- @dark_mode = false
113
143
  end
114
144
 
115
145
  def move_startup_sequence
@@ -127,26 +157,45 @@ module RSMP
127
157
 
128
158
  def output_states
129
159
  return unless @live_output
160
+
130
161
  str = @signal_groups.map do |group|
131
- s = "#{group.c_id}:#{group.state}"
132
- if group.state =~ /^[1-9]$/
162
+ state = group.state
163
+ s = "#{group.c_id}:#{state}"
164
+ if state =~ /^[1-9]$/
133
165
  s.colorize(:green)
134
- elsif group.state =~ /^[NOP]$/
166
+ elsif state =~ /^[NOP]$/
135
167
  s.colorize(:yellow)
136
- elsif group.state =~ /^[ae]$/
137
- s.colorize(:black)
138
- elsif group.state =~ /^[f]$/
168
+ elsif state =~ /^[ae]$/
169
+ s.colorize(:light_black)
170
+ elsif state =~ /^[f]$/
139
171
  s.colorize(:yellow)
140
- elsif group.state =~ /^[g]$/
172
+ elsif state =~ /^[g]$/
141
173
  s.colorize(:red)
142
174
  else
143
175
  s.colorize(:red)
144
176
  end
145
177
  end.join ' '
178
+
179
+ modes = '.'*9
180
+ modes[0] = 'N' if @function_position == 'NormalControl'
181
+ modes[1] = 'Y' if @function_position == 'YellowFlash'
182
+ modes[2] = 'D' if @function_position == 'Dark'
183
+ modes[3] = 'B' if @booting
184
+ modes[4] = 'S' if @startup_sequence_active
185
+ modes[5] = 'M' if @manual_control
186
+ modes[6] = 'F' if @fixed_time_control
187
+ modes[7] = 'R' if @all_red
188
+ modes[8] = 'I' if @isolated_control
189
+ modes[9] = 'P' if @police_key != 0
190
+
146
191
  plan = "P#{@plan}"
147
192
 
193
+ # create folders if needed
194
+ FileUtils.mkdir_p File.dirname(@live_output)
195
+
196
+ # append a line with the current state to the file
148
197
  File.open @live_output, 'w' do |file|
149
- file.puts "#{plan.rjust(4)} #{pos.to_s.rjust(4)} #{str}\r"
198
+ file.puts "#{modes} #{plan.rjust(2)} #{@cycle_counter.to_s.rjust(3)} #{str}\r"
150
199
  end
151
200
  end
152
201
 
@@ -169,7 +218,7 @@ module RSMP
169
218
 
170
219
  def handle_m0001 arg
171
220
  @node.verify_security_code 2, arg['securityCode']
172
- switch_mode arg['status']
221
+ switch_functional_position arg['status'], timeout: arg['timeout'].to_i*60
173
222
  end
174
223
 
175
224
  def handle_m0002 arg
@@ -206,18 +255,27 @@ module RSMP
206
255
  end
207
256
  end
208
257
 
258
+ def recompute_input idx
259
+ if @input_forced[idx] == '1'
260
+ @input_results[idx] = @input_forced_values[idx]
261
+ elsif @input_activations[idx]=='1'
262
+ @input_results[idx] = '1'
263
+ else
264
+ @input_results[idx] = bool_to_digit( @inputs[idx]=='1' )
265
+ end
266
+ end
267
+
209
268
  def handle_m0006 arg
210
269
  @node.verify_security_code 2, arg['securityCode']
211
270
  input = arg['input'].to_i
212
271
  idx = input - 1
213
272
  return unless idx>=0 && input<@num_inputs # TODO should NotAck
214
- @input_activations[idx] = (arg['status']=='True' ? '1' : '0')
215
- result = @input_activations[idx]=='1' || @inputs[idx]=='1'
216
- @input_results[idx] = (result ? '1' : '0')
273
+ @input_activations[idx] = bool_string_to_digit arg['status']
274
+ recompute_input idx
217
275
  if @input_activations[idx]
218
- log "Activate input #{idx}", level: :info
276
+ log "Activating input #{idx+1}", level: :info
219
277
  else
220
- log "Deactivate input #{idx}", level: :info
278
+ log "Deactivating input #{idx+1}", level: :info
221
279
  end
222
280
  end
223
281
 
@@ -280,8 +338,39 @@ module RSMP
280
338
  @node.verify_security_code 2, arg['securityCode']
281
339
  end
282
340
 
341
+ def bool_string_to_digit bool
342
+ case bool
343
+ when 'True'
344
+ '1'
345
+ when 'False'
346
+ '0'
347
+ else
348
+ raise RSMP::MessageRejected.new "Invalid boolean '#{bool}', must be 'True' or 'False'"
349
+ end
350
+ end
351
+
352
+ def bool_to_digit bool
353
+ bool ? '1' : '0'
354
+ end
355
+
283
356
  def handle_m0019 arg
284
357
  @node.verify_security_code 2, arg['securityCode']
358
+ input = arg['input'].to_i
359
+ idx = input - 1
360
+ unless idx>=0 && input<@num_inputs # TODO should NotAck
361
+ log "Can't force input #{idx+1}, only have #{@num_inputs} inputs", level: :warning
362
+ return
363
+ end
364
+ @input_forced[idx] = bool_string_to_digit arg['status']
365
+ if @input_forced[idx]
366
+ @input_forced_values[idx] = bool_string_to_digit arg['inputValue']
367
+ end
368
+ recompute_input idx
369
+ if @input_forced[idx]
370
+ log "Forcing input #{idx+1} to #{@input_forced_values[idx]}, #{@input_results}", level: :info
371
+ else
372
+ log "Releasing input #{idx+1}", level: :info
373
+ end
285
374
  end
286
375
 
287
376
  def handle_m0020 arg
@@ -308,15 +397,15 @@ module RSMP
308
397
  arg['second'],
309
398
  'UTC'
310
399
  )
311
- @node.clock.set time
312
- log "Clock set to #{time}, (adjustment is #{@node.clock.adjustment}s)", level: :info
400
+ clock.set time
401
+ log "Clock set to #{time}, (adjustment is #{clock.adjustment}s)", level: :info
313
402
  end
314
403
 
315
404
  def set_input i, value
316
405
  return unless i>=0 && i<@num_inputs
317
- @inputs[i] = (arg['value'] ? '1' : '0')
406
+ @inputs[i] = bool_to_digit arg['value']
318
407
  end
319
-
408
+
320
409
  def set_fixed_time_control status
321
410
  @fixed_time_control = status
322
411
  end
@@ -332,20 +421,24 @@ module RSMP
332
421
  @plan = plan_nr
333
422
  end
334
423
 
335
- def switch_mode mode
336
- log "Switching to mode #{mode}", level: :info
337
- case mode
338
- when 'NormalControl'
339
- initiate_startup_sequence if @yellow_flash || @dark_mode
340
- @yellow_flash = false
341
- @dark_mode = false
342
- when 'YellowFlash'
343
- @yellow_flash = true
344
- @dark_mode = false
345
- when 'Dark'
346
- @yellow_flash = false
347
- @dark_mode = true
424
+ def switch_functional_position mode, timeout: nil, reverting: false
425
+ unless ['NormalControl','YellowFlash','Dark'].include? mode
426
+ raise RSMP::MessageRejected.new "Invalid functional position '#{mode}', must be NormalControl, YellowFlash or Dark'"
348
427
  end
428
+ if reverting
429
+ log "Reverting to functional position #{mode} after timeout", level: :info
430
+ elsif timeout && timeout > 0
431
+ log "Switching to functional position #{mode} with timeout #{timeout}min", level: :info
432
+ @previous_functional_position = @function_position
433
+ now = clock.now
434
+ @functional_position_timeout = now + timeout
435
+ else
436
+ log "Switching to functional position #{mode}", level: :info
437
+ end
438
+ if mode == 'NormalControl'
439
+ initiate_startup_sequence if @function_position != 'NormalControl'
440
+ end
441
+ @function_position = mode
349
442
  mode
350
443
  end
351
444
 
@@ -380,7 +473,7 @@ module RSMP
380
473
  def handle_s0002 status_code, status_name=nil
381
474
  case status_name
382
475
  when 'detectorlogicstatus'
383
- TrafficControllerSite.make_status @detector_logics.map { |dl| dl.value ? '1' : '0' }.join
476
+ TrafficControllerSite.make_status @detector_logics.map { |dl| bool_to_digit(dl.value) }.join
384
477
  end
385
478
  end
386
479
 
@@ -423,7 +516,7 @@ module RSMP
423
516
  when 'intersection'
424
517
  TrafficControllerSite.make_status @intersection
425
518
  when 'status'
426
- TrafficControllerSite.make_status !@dark_mode
519
+ TrafficControllerSite.make_status @function_position != 'Dark'
427
520
  end
428
521
  end
429
522
 
@@ -459,7 +552,7 @@ module RSMP
459
552
  when 'intersection'
460
553
  TrafficControllerSite.make_status @intersection
461
554
  when 'status'
462
- TrafficControllerSite.make_status @yellow_flash
555
+ TrafficControllerSite.make_status TrafficControllerSite.to_rmsp_bool( @function_position == 'YellowFlash' )
463
556
  end
464
557
  end
465
558
 
@@ -535,7 +628,7 @@ module RSMP
535
628
  def handle_s0021 status_code, status_name=nil
536
629
  case status_name
537
630
  when 'detectorlogics'
538
- TrafficControllerSite.make_status @detector_logics.map { |logic| logic.forced=='True' ? '1' : '0'}.join
631
+ TrafficControllerSite.make_status @detector_logics.map { |logic| bool_to_digit(logic.forced)}.join
539
632
  end
540
633
  end
541
634
 
@@ -589,7 +682,7 @@ module RSMP
589
682
  def handle_s0029 status_code, status_name=nil
590
683
  case status_name
591
684
  when 'status'
592
- TrafficControllerSite.make_status ''
685
+ TrafficControllerSite.make_status @input_forced
593
686
  end
594
687
  end
595
688
 
@@ -655,7 +748,7 @@ module RSMP
655
748
  when 'checksum'
656
749
  TrafficControllerSite.make_status '1'
657
750
  when 'timestamp'
658
- now = @node.clock.to_s
751
+ now = clock.to_s
659
752
  TrafficControllerSite.make_status now
660
753
  end
661
754
  end
@@ -18,6 +18,18 @@ module RSMP
18
18
  unless @main
19
19
  raise ConfigurationError.new "TLC must have a main component"
20
20
  end
21
+
22
+ end
23
+
24
+ def start
25
+ super
26
+ start_tlc_timer
27
+ @main.initiate_startup_sequence
28
+ end
29
+
30
+ def stop_subtasks
31
+ stop_tlc_timer
32
+ super
21
33
  end
22
34
 
23
35
  def build_plans signal_plans
@@ -56,49 +68,45 @@ module RSMP
56
68
  end
57
69
  end
58
70
 
59
- def start_action
60
- super
61
- start_timer
62
- @main.initiate_startup_sequence
63
- end
64
-
65
- def start_timer
71
+ def start_tlc_timer
66
72
  task_name = "tlc timer"
67
73
  log "Starting #{task_name} with interval #{@interval} seconds", level: :debug
68
74
 
69
75
  @timer = @task.async do |task|
70
- task.annotate task_name
71
- next_time = Time.now.to_f
72
- loop do
73
- begin
74
- timer(@clock.now)
75
- rescue EOFError => e
76
- log "Connection closed: #{e}", level: :warning
77
- rescue IOError => e
78
- log "IOError", level: :warning
79
- rescue Errno::ECONNRESET
80
- log "Connection reset by peer", level: :warning
81
- rescue Errno::EPIPE => e
82
- log "Broken pipe", level: :warning
83
- rescue StandardError => e
84
- notify_error e, level: :internal
85
- ensure
86
- # adjust sleep duration to avoid drift. so wake up always happens on the
87
- # same fractional second.
88
- # note that Time.now is not monotonic. If the clock is changed,
89
- # either manaully or via NTP, the sleep interval might jump.
90
- # an alternative is to use ::Process.clock_gettime(::Process::CLOCK_MONOTONIC),
91
- # to get the current time. this ensures a constant interval, but
92
- # if the clock is changed, the wake up would then happen on a different
93
- # fractional second
94
- next_time += @interval
95
- duration = next_time - Time.now.to_f
96
- task.sleep duration
97
- end
76
+ task.annotate task_name
77
+ run_tlc_timer task
78
+ end
79
+ end
80
+
81
+ def run_tlc_timer task
82
+ next_time = Time.now.to_f
83
+ loop do
84
+ begin
85
+ timer(@clock.now)
86
+ rescue StandardError => e
87
+ notify_error e, level: :internal
88
+ ensure
89
+ # adjust sleep duration to avoid drift. so wake up always happens on the
90
+ # same fractional second.
91
+ # note that Time.now is not monotonic. If the clock is changed,
92
+ # either manaully or via NTP, the sleep interval might jump.
93
+ # an alternative is to use ::Process.clock_gettime(::Process::CLOCK_MONOTONIC),
94
+ # to get the current time. this ensures a constant interval, but
95
+ # if the clock is changed, the wake up would then happen on a different
96
+ # fractional second
97
+ next_time += @interval
98
+ duration = next_time - Time.now.to_f
99
+ task.sleep duration
98
100
  end
99
101
  end
100
102
  end
101
103
 
104
+ def stop_tlc_timer
105
+ return unless @timer
106
+ @timer.stop
107
+ @timer = nil
108
+ end
109
+
102
110
  def timer now
103
111
  return unless @main
104
112
  @main.timer now
@@ -142,7 +150,6 @@ module RSMP
142
150
  when :restart
143
151
  log "Restarting TLC", level: :info
144
152
  restart
145
- initiate_startup_sequence
146
153
  end
147
154
  end
148
155
  end
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = "0.8.5"
2
+ VERSION = "0.9.2"
3
3
  end
data/lib/rsmp.rb CHANGED
@@ -10,10 +10,10 @@ require 'json_schemer'
10
10
  require 'async/queue'
11
11
 
12
12
  require 'rsmp/rsmp'
13
+ require 'rsmp/task'
13
14
  require 'rsmp/deep_merge'
14
15
  require 'rsmp/inspect'
15
16
  require 'rsmp/logging'
16
- require 'rsmp/wait'
17
17
  require 'rsmp/node'
18
18
  require 'rsmp/supervisor'
19
19
  require 'rsmp/components'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsmp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.5
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-26 00:00:00.000000000 Z
11
+ date: 2022-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -186,6 +186,7 @@ executables:
186
186
  extensions: []
187
187
  extra_rdoc_files: []
188
188
  files:
189
+ - ".github/workflows/rspec.yaml"
189
190
  - ".gitignore"
190
191
  - ".gitmodules"
191
192
  - ".rspec"
@@ -200,9 +201,11 @@ files:
200
201
  - bin/setup
201
202
  - config/supervisor.yaml
202
203
  - config/tlc.yaml
204
+ - cucumber.yml
203
205
  - documentation/classes_and_modules.md
204
206
  - documentation/collecting_message.md
205
207
  - documentation/message_distribution.md
208
+ - documentation/tasks.md
206
209
  - exe/rsmp
207
210
  - lib/rsmp.rb
208
211
  - lib/rsmp/archive.rb
@@ -234,18 +237,16 @@ files:
234
237
  - lib/rsmp/rsmp.rb
235
238
  - lib/rsmp/site.rb
236
239
  - lib/rsmp/site_proxy.rb
237
- - lib/rsmp/site_proxy_wait.rb
238
240
  - lib/rsmp/supervisor.rb
239
241
  - lib/rsmp/supervisor_proxy.rb
242
+ - lib/rsmp/task.rb
240
243
  - lib/rsmp/tlc/detector_logic.rb
241
244
  - lib/rsmp/tlc/signal_group.rb
242
245
  - lib/rsmp/tlc/signal_plan.rb
243
246
  - lib/rsmp/tlc/traffic_controller.rb
244
247
  - lib/rsmp/tlc/traffic_controller_site.rb
245
248
  - lib/rsmp/version.rb
246
- - lib/rsmp/wait.rb
247
249
  - rsmp.gemspec
248
- - test.rb
249
250
  homepage: https://github.com/rsmp-nordic/rsmp
250
251
  licenses:
251
252
  - MIT
File without changes
data/lib/rsmp/wait.rb DELETED
@@ -1,16 +0,0 @@
1
- module RSMP
2
- module Wait
3
- # wait for an async condition to signal, then yield to block
4
- # if block returns true we're done. otherwise, wait again
5
- def wait_for condition, timeout, &block
6
- raise RuntimeError.new("Can't wait for condition because task is not running") unless @task.running?
7
- @task.with_timeout(timeout) do
8
- while @task.running? do
9
- value = condition.wait
10
- result = yield value
11
- return result if result # return result of check, if not nil
12
- end
13
- end
14
- end
15
- end
16
- end
data/test.rb DELETED
@@ -1,27 +0,0 @@
1
- class A
2
- def go &block
3
- @block = block # block will be converted automatically to a Proc
4
- indirect
5
- end
6
-
7
- def call
8
- @block.call
9
- end
10
-
11
- def indirect
12
- call
13
- end
14
-
15
- end
16
-
17
- a = A.new
18
-
19
- a.go do
20
- break # this is ok. break causes the block to exit, and the encasing method to return - go() will exit
21
- end
22
-
23
- # this raises an error. the block we passed to go() will be called again, and it tries to break
24
- # but we're not inside a method we can exit from
25
-
26
-
27
- a.indirect