rsmp 0.37.0 → 0.38.0

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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +22 -0
  3. data/.github/workflows/rubocop.yaml +17 -0
  4. data/.gitignore +5 -6
  5. data/.rubocop.yml +80 -0
  6. data/Gemfile +13 -1
  7. data/Gemfile.lock +34 -1
  8. data/Rakefile +3 -3
  9. data/lib/rsmp/cli.rb +147 -124
  10. data/lib/rsmp/collect/ack_collector.rb +8 -7
  11. data/lib/rsmp/collect/aggregated_status_collector.rb +4 -4
  12. data/lib/rsmp/collect/alarm_collector.rb +31 -23
  13. data/lib/rsmp/collect/alarm_matcher.rb +3 -3
  14. data/lib/rsmp/collect/collector/logging.rb +17 -0
  15. data/lib/rsmp/collect/collector/reporting.rb +44 -0
  16. data/lib/rsmp/collect/collector/status.rb +34 -0
  17. data/lib/rsmp/collect/collector.rb +69 -150
  18. data/lib/rsmp/collect/command_matcher.rb +19 -6
  19. data/lib/rsmp/collect/command_response_collector.rb +7 -7
  20. data/lib/rsmp/collect/distributor.rb +14 -11
  21. data/lib/rsmp/collect/filter.rb +31 -15
  22. data/lib/rsmp/collect/matcher.rb +7 -11
  23. data/lib/rsmp/collect/queue.rb +4 -4
  24. data/lib/rsmp/collect/receiver.rb +10 -12
  25. data/lib/rsmp/collect/state_collector.rb +116 -77
  26. data/lib/rsmp/collect/status_collector.rb +6 -6
  27. data/lib/rsmp/collect/status_matcher.rb +17 -7
  28. data/lib/rsmp/{alarm_state.rb → component/alarm_state.rb} +76 -37
  29. data/lib/rsmp/{component.rb → component/component.rb} +15 -15
  30. data/lib/rsmp/component/component_base.rb +89 -0
  31. data/lib/rsmp/component/component_proxy.rb +75 -0
  32. data/lib/rsmp/component/components.rb +63 -0
  33. data/lib/rsmp/convert/export/json_schema.rb +116 -110
  34. data/lib/rsmp/convert/import/yaml.rb +21 -18
  35. data/lib/rsmp/{rsmp.rb → helpers/clock.rb} +5 -6
  36. data/lib/rsmp/{deep_merge.rb → helpers/deep_merge.rb} +2 -1
  37. data/lib/rsmp/helpers/error.rb +71 -0
  38. data/lib/rsmp/{inspect.rb → helpers/inspect.rb} +6 -10
  39. data/lib/rsmp/log/archive.rb +98 -0
  40. data/lib/rsmp/log/colorization.rb +41 -0
  41. data/lib/rsmp/log/filtering.rb +54 -0
  42. data/lib/rsmp/log/logger.rb +206 -0
  43. data/lib/rsmp/{logging.rb → log/logging.rb} +5 -7
  44. data/lib/rsmp/message.rb +159 -148
  45. data/lib/rsmp/{node.rb → node/node.rb} +19 -17
  46. data/lib/rsmp/{protocol.rb → node/protocol.rb} +5 -3
  47. data/lib/rsmp/node/site/site.rb +195 -0
  48. data/lib/rsmp/node/supervisor/modules/configuration.rb +59 -0
  49. data/lib/rsmp/node/supervisor/modules/connection.rb +140 -0
  50. data/lib/rsmp/node/supervisor/modules/sites.rb +64 -0
  51. data/lib/rsmp/node/supervisor/supervisor.rb +72 -0
  52. data/lib/rsmp/{task.rb → node/task.rb} +12 -14
  53. data/lib/rsmp/proxy/modules/acknowledgements.rb +144 -0
  54. data/lib/rsmp/proxy/modules/receive.rb +119 -0
  55. data/lib/rsmp/proxy/modules/send.rb +76 -0
  56. data/lib/rsmp/proxy/modules/state.rb +25 -0
  57. data/lib/rsmp/proxy/modules/tasks.rb +105 -0
  58. data/lib/rsmp/proxy/modules/versions.rb +69 -0
  59. data/lib/rsmp/proxy/modules/watchdogs.rb +66 -0
  60. data/lib/rsmp/proxy/proxy.rb +199 -0
  61. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +52 -0
  62. data/lib/rsmp/proxy/site/modules/alarms.rb +27 -0
  63. data/lib/rsmp/proxy/site/modules/commands.rb +31 -0
  64. data/lib/rsmp/proxy/site/modules/status.rb +110 -0
  65. data/lib/rsmp/proxy/site/site_proxy.rb +205 -0
  66. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +47 -0
  67. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +73 -0
  68. data/lib/rsmp/proxy/supervisor/modules/commands.rb +53 -0
  69. data/lib/rsmp/proxy/supervisor/modules/status.rb +204 -0
  70. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +178 -0
  71. data/lib/rsmp/tlc/detector_logic.rb +18 -34
  72. data/lib/rsmp/tlc/input_states.rb +126 -0
  73. data/lib/rsmp/tlc/modules/detector_logics.rb +50 -0
  74. data/lib/rsmp/tlc/modules/display.rb +78 -0
  75. data/lib/rsmp/tlc/modules/helpers.rb +41 -0
  76. data/lib/rsmp/tlc/modules/inputs.rb +173 -0
  77. data/lib/rsmp/tlc/modules/modes.rb +253 -0
  78. data/lib/rsmp/tlc/modules/outputs.rb +30 -0
  79. data/lib/rsmp/tlc/modules/plans.rb +218 -0
  80. data/lib/rsmp/tlc/modules/signal_groups.rb +109 -0
  81. data/lib/rsmp/tlc/modules/startup_sequence.rb +22 -0
  82. data/lib/rsmp/tlc/modules/system.rb +140 -0
  83. data/lib/rsmp/tlc/modules/traffic_data.rb +49 -0
  84. data/lib/rsmp/tlc/signal_group.rb +37 -41
  85. data/lib/rsmp/tlc/signal_plan.rb +14 -11
  86. data/lib/rsmp/tlc/signal_priority.rb +39 -35
  87. data/lib/rsmp/tlc/startup_sequence.rb +59 -0
  88. data/lib/rsmp/tlc/traffic_controller.rb +38 -1010
  89. data/lib/rsmp/tlc/traffic_controller_site.rb +58 -57
  90. data/lib/rsmp/version.rb +1 -1
  91. data/lib/rsmp.rb +82 -48
  92. data/rsmp.gemspec +24 -31
  93. metadata +79 -139
  94. data/lib/rsmp/archive.rb +0 -76
  95. data/lib/rsmp/collect/message_matchers.rb +0 -0
  96. data/lib/rsmp/component_base.rb +0 -87
  97. data/lib/rsmp/component_proxy.rb +0 -57
  98. data/lib/rsmp/components.rb +0 -65
  99. data/lib/rsmp/error.rb +0 -71
  100. data/lib/rsmp/logger.rb +0 -216
  101. data/lib/rsmp/proxy.rb +0 -693
  102. data/lib/rsmp/site.rb +0 -188
  103. data/lib/rsmp/site_proxy.rb +0 -389
  104. data/lib/rsmp/supervisor.rb +0 -302
  105. data/lib/rsmp/supervisor_proxy.rb +0 -510
  106. data/lib/rsmp/tlc/inputs.rb +0 -134
@@ -0,0 +1,126 @@
1
+ module RSMP
2
+ module TLC
3
+ # class that maintains the state of TLC inputs
4
+ # indexing is 1-based since that's how the RSMP messages are specified
5
+ class InputStates
6
+ attr_reader :size
7
+
8
+ def initialize(size)
9
+ @size = size
10
+ reset
11
+ end
12
+
13
+ def reset
14
+ string_size = @size + 1
15
+ @value = '0' * string_size
16
+ @forced = '0' * string_size
17
+ @forced_value = '0' * string_size
18
+ @actual = '0' * string_size
19
+ end
20
+
21
+ def set(input, value)
22
+ check_input input
23
+ report_change(input) do
24
+ @value[input] = to_digit value
25
+ update_actual input
26
+ end
27
+ end
28
+
29
+ def set_forcing(input, force: true, forced_value: true)
30
+ check_input input
31
+ report_change(input) do
32
+ @forced[input] = to_digit force
33
+ @forced_value[input] = to_digit forced_value
34
+ update_actual input
35
+ end
36
+ end
37
+
38
+ def force(input, forced_value: true)
39
+ report_change(input) do
40
+ set_forcing input, force: true, forced_value: forced_value
41
+ end
42
+ end
43
+
44
+ def release(input)
45
+ report_change(input) do
46
+ set_forcing input, force: false, forced_value: false
47
+ end
48
+ end
49
+
50
+ def value?(input)
51
+ check_input input
52
+ from_digit? @value[input]
53
+ end
54
+
55
+ def forced?(input)
56
+ check_input input
57
+ from_digit? @forced[input]
58
+ end
59
+
60
+ def forced_value?(input)
61
+ check_input input
62
+ from_digit? @forced_value[input]
63
+ end
64
+
65
+ def actual?(input)
66
+ check_input input
67
+ from_digit? @actual[input]
68
+ end
69
+
70
+ def report(input)
71
+ {
72
+ value: value?(input),
73
+ forced: forced?(input),
74
+ forced_value: forced_value?(input),
75
+ actual: actual?(input)
76
+ }
77
+ end
78
+
79
+ def value_string
80
+ @value[1..]
81
+ end
82
+
83
+ def forced_string
84
+ @forced[1..]
85
+ end
86
+
87
+ def forced_value_string
88
+ @forced[1..]
89
+ end
90
+
91
+ def actual_string
92
+ @actual[1..]
93
+ end
94
+
95
+ protected
96
+
97
+ def check_input(input)
98
+ raise ArgumentError, "Input index #{input} must be in the range 1-#{@size}" if input < 1 || input > @size
99
+ end
100
+
101
+ def from_digit?(input)
102
+ input == '1'
103
+ end
104
+
105
+ def to_digit(input)
106
+ input ? '1' : '0'
107
+ end
108
+
109
+ def update_actual(input)
110
+ @actual[input] = if from_digit? @forced[input]
111
+ @forced_value[input]
112
+ else
113
+ @value[input]
114
+ end
115
+ end
116
+
117
+ def report_change(input)
118
+ before = @actual[input]
119
+ yield
120
+ return unless @actual[input] != before
121
+
122
+ from_digit? @actual[input]
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,50 @@
1
+ module RSMP
2
+ module TLC
3
+ module Modules
4
+ # Detector logic management for traffic controller
5
+ # Handles detector logic status queries and forcing
6
+ module DetectorLogics
7
+ def add_detector_logic(logic)
8
+ @detector_logics << logic
9
+ end
10
+
11
+ # M0021 - Force detector logic
12
+ def handle_m0021(arg, _options = {})
13
+ @node.verify_security_code 2, arg['securityCode']
14
+ end
15
+
16
+ # S0002 - Detector logic status
17
+ def handle_s0002(_status_code, status_name = nil, _options = {})
18
+ case status_name
19
+ when 'detectorlogicstatus'
20
+ TrafficControllerSite.make_status @detector_logics.map { |dl| bool_to_digit(dl.value) }.join
21
+ end
22
+ end
23
+
24
+ # S0016 - Number of detector logics
25
+ def handle_s0016(_status_code, status_name = nil, _options = {})
26
+ case status_name
27
+ when 'number'
28
+ TrafficControllerSite.make_status @detector_logics.size
29
+ end
30
+ end
31
+
32
+ # S0021 - Detector logic forcing status
33
+ def handle_s0021(_status_code, status_name = nil, _options = {})
34
+ case status_name
35
+ when 'detectorlogics'
36
+ TrafficControllerSite.make_status @detector_logics.map { |logic| bool_to_digit(logic.forced) }.join
37
+ end
38
+ end
39
+
40
+ # S0031 - Trigger level sensitivity for loop detector
41
+ def handle_s0031(_status_code, status_name = nil, _options = {})
42
+ case status_name
43
+ when 'status'
44
+ TrafficControllerSite.make_status ''
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,78 @@
1
+ module RSMP
2
+ module TLC
3
+ module Modules
4
+ # Display and output formatting
5
+ module Display
6
+ def output_states
7
+ return unless @live_output
8
+
9
+ str = format_colored_signal_states
10
+ modes = format_mode_indicators
11
+ plan = "P#{@plan}"
12
+
13
+ write_state_output(modes, plan, str)
14
+ end
15
+
16
+ def format_signal_group_status
17
+ @signal_groups.map(&:state).join
18
+ end
19
+
20
+ private
21
+
22
+ def format_colored_signal_states
23
+ @signal_groups.map do |group|
24
+ state = group.state
25
+ s = "#{group.c_id}:#{state}"
26
+ colorize_signal_state(s, state)
27
+ end.join ' '
28
+ end
29
+
30
+ def colorize_signal_state(display_string, state)
31
+ case state
32
+ when /^[1-9]$/
33
+ display_string.colorize(:green)
34
+ when /^[NOPf]$/
35
+ display_string.colorize(:yellow)
36
+ when /^[ae]$/
37
+ display_string.colorize(:light_black)
38
+ else # includes /^g$/ and any other values
39
+ display_string.colorize(:red)
40
+ end
41
+ end
42
+
43
+ def mode_indicators
44
+ {
45
+ 0 => ['N', @function_position == 'NormalControl'],
46
+ 1 => ['Y', @function_position == 'YellowFlash'],
47
+ 2 => ['D', @function_position == 'Dark'],
48
+ 3 => ['B', @booting],
49
+ 4 => ['S', @startup_sequence.active?],
50
+ 5 => ['M', @manual_control],
51
+ 6 => ['F', @fixed_time_control],
52
+ 7 => ['R', @all_red],
53
+ 8 => ['I', @isolated_control],
54
+ 9 => ['P', @police_key != 0]
55
+ }
56
+ end
57
+
58
+ def format_mode_indicators
59
+ modes = '.' * 10
60
+ mode_indicators.each do |pos, (char, active)|
61
+ modes[pos] = char if active
62
+ end
63
+ modes
64
+ end
65
+
66
+ def write_state_output(modes, plan, signal_states)
67
+ # create folders if needed
68
+ FileUtils.mkdir_p File.dirname(@live_output)
69
+
70
+ # append a line with the current state to the file
71
+ File.open @live_output, 'w' do |file|
72
+ file.puts "#{modes} #{plan.rjust(2)} #{@cycle_counter.to_s.rjust(3)} #{signal_states}\r"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,41 @@
1
+ module RSMP
2
+ module TLC
3
+ module Modules
4
+ # Utility helper methods
5
+ module Helpers
6
+ def find_plan(plan_nr)
7
+ plan = @plans[plan_nr.to_i]
8
+ raise InvalidMessage, "unknown signal plan #{plan_nr}, known only [#{@plans.keys.join(', ')}]" unless plan
9
+
10
+ plan
11
+ end
12
+
13
+ def string_to_bool(bool_str)
14
+ case bool_str
15
+ when 'True'
16
+ true
17
+ when 'False'
18
+ false
19
+ else
20
+ raise RSMP::MessageRejected, "Invalid boolean '#{bool}', must be 'True' or 'False'"
21
+ end
22
+ end
23
+
24
+ def bool_string_to_digit(bool)
25
+ case bool
26
+ when 'True'
27
+ '1'
28
+ when 'False'
29
+ '0'
30
+ else
31
+ raise RSMP::MessageRejected, "Invalid boolean '#{bool}', must be 'True' or 'False'"
32
+ end
33
+ end
34
+
35
+ def bool_to_digit(bool)
36
+ bool ? '1' : '0'
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,173 @@
1
+ module RSMP
2
+ module TLC
3
+ module Modules
4
+ # Input programming, control, and status for traffic controllers
5
+ # Handles input commands and queries
6
+ module Inputs
7
+ def setup_inputs(inputs)
8
+ if inputs
9
+ num_inputs = inputs['total']
10
+ @input_programming = inputs['programming']
11
+ else
12
+ @input_programming = nil
13
+ end
14
+ @inputs = TLC::InputStates.new num_inputs || 8
15
+ end
16
+
17
+ def input_logic(input, change)
18
+ return unless @input_programming && !change.nil?
19
+
20
+ action = @input_programming[input]
21
+ return unless action
22
+
23
+ return unless action['raise_alarm']
24
+
25
+ component = if action['component']
26
+ node.find_component action['component']
27
+ else
28
+ node.main
29
+ end
30
+ alarm_code = action['raise_alarm']
31
+ if change
32
+ log "Activating input #{input} is programmed to raise alarm #{alarm_code} on #{component.c_id}",
33
+ level: :info
34
+ component.activate_alarm alarm_code
35
+ else
36
+ log "Deactivating input #{input} is programmed to clear alarm #{alarm_code} on #{component.c_id}",
37
+ level: :info
38
+ component.deactivate_alarm alarm_code
39
+ end
40
+ end
41
+
42
+ # M0006 - Set input
43
+ def handle_m0006(arg, _options = {})
44
+ @node.verify_security_code 2, arg['securityCode']
45
+ input = arg['input'].to_i
46
+ status = string_to_bool arg['status']
47
+ raise MessageRejected, "Input must be in the range 1-#{@inputs.size}" unless input.between?(1, @inputs.size)
48
+
49
+ if status
50
+ log "Activating input #{input}", level: :info
51
+ else
52
+ log "Deactivating input #{input}", level: :info
53
+ end
54
+ change = @inputs.set input, status
55
+ input_logic input, change unless change.nil?
56
+ end
57
+
58
+ # M0012 - Set input (simple)
59
+ def handle_m0012(arg, _options = {})
60
+ @node.verify_security_code 2, arg['securityCode']
61
+ end
62
+
63
+ # M0013 - Set input (complex bit pattern)
64
+ def handle_m0013(arg, _options = {})
65
+ @node.verify_security_code 2, arg['securityCode']
66
+ set, clear = parse_input_status(arg['status'])
67
+ validate_input_ranges(set, clear)
68
+ apply_input_changes(set, clear)
69
+ end
70
+
71
+ # Helper: Parse input status string into set and clear arrays
72
+ def parse_input_status(status_string)
73
+ set = []
74
+ clear = []
75
+ status_string.split(';').each do |part|
76
+ offset, set_bits, clear_bits = part.split(',').map(&:to_i)
77
+ extract_input_bits(set_bits, offset, set)
78
+ extract_input_bits(clear_bits, offset, clear)
79
+ end
80
+
81
+ set = set.uniq.sort
82
+ clear = clear.uniq.sort
83
+ # if input is both activated and deactivated, there is no need to activate first
84
+ set -= (set & clear)
85
+
86
+ [set, clear]
87
+ end
88
+
89
+ # Helper: Extract individual input bits from a bit pattern
90
+ def extract_input_bits(bits, offset, target_array)
91
+ bits.to_s(2).reverse.each_char.with_index do |bit, i|
92
+ target_array << (i + offset) if bit == '1'
93
+ end
94
+ end
95
+
96
+ # Helper: Validate that input indices are in valid range
97
+ def validate_input_ranges(set, clear)
98
+ [set, clear].each do |inputs|
99
+ inputs.each do |input|
100
+ if input < 1
101
+ raise MessageRejected,
102
+ "Cannot activate inputs #{set} and deactivate inputs #{clear}: " \
103
+ "input #{input} is invalid (must be 1 or higher)"
104
+ end
105
+ next unless input > @inputs.size
106
+
107
+ raise MessageRejected,
108
+ "Cannot activate inputs #{set} and deactivate inputs #{clear}: " \
109
+ "input #{input} is invalid (only #{@inputs.size} inputs present)"
110
+ end
111
+ end
112
+ end
113
+
114
+ # Helper: Apply input changes (activate/deactivate)
115
+ def apply_input_changes(set, clear)
116
+ log "Activating inputs #{set} and deactivating inputs #{clear}", level: :info
117
+
118
+ set.each do |input|
119
+ change = @inputs.set input, true
120
+ input_logic input, change unless change.nil?
121
+ end
122
+ clear.each do |input|
123
+ change = @inputs.set input, false
124
+ input_logic input, change unless change.nil?
125
+ end
126
+ end
127
+
128
+ # Helper: Set a specific input value
129
+ def set_input(input_index, _value)
130
+ return unless input_index >= 0 && input_index < @num_inputs
131
+
132
+ @inputs[input_index] = bool_to_digit arg['value']
133
+ end
134
+
135
+ # M0019 - Force input
136
+ def handle_m0019(arg, _options = {})
137
+ @node.verify_security_code 2, arg['securityCode']
138
+ input = arg['input'].to_i
139
+ force = string_to_bool arg['status']
140
+ forced_value = string_to_bool arg['inputValue']
141
+ raise MessageRejected, "Input must be in the range 1-#{@inputs.size}" unless input.between?(1, @inputs.size)
142
+
143
+ if force
144
+ log "Forcing input #{input} to #{forced_value}", level: :info
145
+ else
146
+ log "Releasing input #{input}", level: :info
147
+ end
148
+ change = @inputs.set_forcing input, force: force, forced_value: forced_value
149
+
150
+ input_logic input, change unless change.nil?
151
+ end
152
+
153
+ # S0003 - Input status
154
+ def handle_s0003(_status_code, status_name = nil, _options = {})
155
+ case status_name
156
+ when 'inputstatus'
157
+ TrafficControllerSite.make_status @inputs.actual_string
158
+ when 'extendedinputstatus'
159
+ TrafficControllerSite.make_status 0.to_s
160
+ end
161
+ end
162
+
163
+ # S0029 - Forced input status
164
+ def handle_s0029(_status_code, status_name = nil, _options = {})
165
+ case status_name
166
+ when 'status'
167
+ TrafficControllerSite.make_status @inputs.forced_string
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end