rsmp 0.40.0 → 0.41.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.
@@ -0,0 +1,58 @@
1
+ module RSMP
2
+ module TLC
3
+ module Proxy
4
+ # Command methods for operational control of a remote TLC.
5
+ # Covers functional position, emergency routes, I/O modes, signal group orders, and system settings.
6
+ module Detectors
7
+ # M0008 — Force detector logic to a given mode and status.
8
+ # component_id must refer to the detector logic component, not main.
9
+ def force_detector_logic(component_id, status:, mode:, options: {})
10
+ validate_ready 'force detector logic'
11
+
12
+ security_code = security_code_for(2)
13
+
14
+ command_list = [{
15
+ 'cCI' => 'M0008',
16
+ 'cO' => 'setForceDetectorLogic',
17
+ 'n' => 'status',
18
+ 'v' => status.to_s
19
+ }, {
20
+ 'cCI' => 'M0008',
21
+ 'cO' => 'setForceDetectorLogic',
22
+ 'n' => 'securityCode',
23
+ 'v' => security_code.to_s
24
+ }, {
25
+ 'cCI' => 'M0008',
26
+ 'cO' => 'setForceDetectorLogic',
27
+ 'n' => 'mode',
28
+ 'v' => mode.to_s
29
+ }]
30
+
31
+ send_command_with_confirm component_id, command_list, options, "force detector logic #{component_id}", nil
32
+ end
33
+
34
+ # M0021 — Set the trigger level for traffic counting.
35
+ def set_trigger_level(status, options: {})
36
+ validate_ready 'set trigger level'
37
+ raise 'TLC main component not found' unless main
38
+
39
+ security_code = security_code_for(2)
40
+
41
+ command_list = [{
42
+ 'cCI' => 'M0021',
43
+ 'cO' => 'setLevel',
44
+ 'n' => 'status',
45
+ 'v' => status.to_s
46
+ }, {
47
+ 'cCI' => 'M0021',
48
+ 'cO' => 'setLevel',
49
+ 'n' => 'securityCode',
50
+ 'v' => security_code.to_s
51
+ }]
52
+
53
+ send_command_with_confirm main.c_id, command_list, options, "trigger level #{status}", nil
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,119 @@
1
+ module RSMP
2
+ module TLC
3
+ module Proxy
4
+ # Command methods for I/O control of a remote TLC.
5
+ # Covers detector logic, input/output forcing and setting.
6
+ module IO
7
+ # M0006 — Set a single input to a given status.
8
+ def set_input(input:, status:, options: {})
9
+ validate_ready 'set input'
10
+ raise 'TLC main component not found' unless main
11
+
12
+ security_code = security_code_for(2)
13
+
14
+ command_list = [{
15
+ 'cCI' => 'M0006',
16
+ 'cO' => 'setInput',
17
+ 'n' => 'status',
18
+ 'v' => status.to_s
19
+ }, {
20
+ 'cCI' => 'M0006',
21
+ 'cO' => 'setInput',
22
+ 'n' => 'securityCode',
23
+ 'v' => security_code.to_s
24
+ }, {
25
+ 'cCI' => 'M0006',
26
+ 'cO' => 'setInput',
27
+ 'n' => 'input',
28
+ 'v' => input.to_s
29
+ }]
30
+
31
+ send_command_with_confirm main.c_id, command_list, options, "input #{input} set to #{status}", nil
32
+ end
33
+
34
+ # M0013 — Set all inputs via a bit-pattern string.
35
+ def set_inputs(status, options: {})
36
+ validate_ready 'set inputs'
37
+ raise 'TLC main component not found' unless main
38
+
39
+ security_code = security_code_for(2)
40
+
41
+ command_list = [{
42
+ 'cCI' => 'M0013',
43
+ 'cO' => 'setInputs',
44
+ 'n' => 'status',
45
+ 'v' => status.to_s
46
+ }, {
47
+ 'cCI' => 'M0013',
48
+ 'cO' => 'setInputs',
49
+ 'n' => 'securityCode',
50
+ 'v' => security_code.to_s
51
+ }]
52
+
53
+ send_command_with_confirm main.c_id, command_list, options, "inputs #{status}", nil
54
+ end
55
+
56
+ # M0019 — Force an input to a given value.
57
+ def force_input(input:, status:, value:, options: {})
58
+ validate_ready 'force input'
59
+ raise 'TLC main component not found' unless main
60
+
61
+ command_list = force_input_command_list(input, status, value)
62
+ confirm_status = force_input_confirm_status(input, status, value)
63
+ send_command_with_confirm main.c_id, command_list, options, "force input #{input}", confirm_status
64
+ end
65
+
66
+ # M0020 — Force an output to a given value.
67
+ def force_output(output:, status:, value:, options: {})
68
+ validate_ready 'force output'
69
+ raise 'TLC main component not found' unless main
70
+
71
+ security_code = security_code_for(2)
72
+
73
+ command_list = [{
74
+ 'cCI' => 'M0020',
75
+ 'cO' => 'setOutput',
76
+ 'n' => 'status',
77
+ 'v' => status.to_s
78
+ }, {
79
+ 'cCI' => 'M0020',
80
+ 'cO' => 'setOutput',
81
+ 'n' => 'securityCode',
82
+ 'v' => security_code.to_s
83
+ }, {
84
+ 'cCI' => 'M0020',
85
+ 'cO' => 'setOutput',
86
+ 'n' => 'output',
87
+ 'v' => output.to_s
88
+ }, {
89
+ 'cCI' => 'M0020',
90
+ 'cO' => 'setOutput',
91
+ 'n' => 'outputValue',
92
+ 'v' => value.to_s
93
+ }]
94
+
95
+ send_command_with_confirm main.c_id, command_list, options, "force output #{output}", nil
96
+ end
97
+
98
+ private
99
+
100
+ def force_input_command_list(input, status, value)
101
+ security_code = security_code_for(2)
102
+ [
103
+ { 'cCI' => 'M0019', 'cO' => 'setInput', 'n' => 'status', 'v' => status.to_s },
104
+ { 'cCI' => 'M0019', 'cO' => 'setInput', 'n' => 'securityCode', 'v' => security_code.to_s },
105
+ { 'cCI' => 'M0019', 'cO' => 'setInput', 'n' => 'input', 'v' => input.to_s },
106
+ { 'cCI' => 'M0019', 'cO' => 'setInput', 'n' => 'inputValue', 'v' => value.to_s }
107
+ ]
108
+ end
109
+
110
+ def force_input_confirm_status(input, status, value)
111
+ [
112
+ { 'sCI' => 'S0029', 'n' => 'status', 's' => /^.{#{input.to_i - 1}}#{status == 'True' ? '1' : '0'}/ },
113
+ { 'sCI' => 'S0003', 'n' => 'inputstatus', 's' => /^.{#{input.to_i - 1}}#{value == 'True' ? '1' : '0'}/ }
114
+ ]
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,226 @@
1
+ module RSMP
2
+ module TLC
3
+ module Proxy
4
+ # Command methods for signal plans.
5
+ # Covers time plans, week/day tables, bands, offsets, and cycle times.
6
+ module Plans
7
+ # M0014 — Set dynamic bands for a signal plan.
8
+ def set_dynamic_bands(plan:, status:, options: {})
9
+ validate_ready 'set dynamic bands'
10
+ raise 'TLC main component not found' unless main
11
+
12
+ security_code = security_code_for(2)
13
+
14
+ command_list = [{
15
+ 'cCI' => 'M0014',
16
+ 'cO' => 'setCommands',
17
+ 'n' => 'status',
18
+ 'v' => status.to_s
19
+ }, {
20
+ 'cCI' => 'M0014',
21
+ 'cO' => 'setCommands',
22
+ 'n' => 'securityCode',
23
+ 'v' => security_code.to_s
24
+ }, {
25
+ 'cCI' => 'M0014',
26
+ 'cO' => 'setCommands',
27
+ 'n' => 'plan',
28
+ 'v' => plan.to_s
29
+ }]
30
+
31
+ send_command_with_confirm main.c_id, command_list, options, "dynamic bands plan #{plan}", nil
32
+ end
33
+
34
+ # M0023 — Set timeout for dynamic bands.
35
+ def set_dynamic_bands_timeout(status, options: {})
36
+ validate_ready 'set dynamic bands timeout'
37
+ raise 'TLC main component not found' unless main
38
+
39
+ security_code = security_code_for(2)
40
+
41
+ command_list = [{
42
+ 'cCI' => 'M0023',
43
+ 'cO' => 'setTimeout',
44
+ 'n' => 'status',
45
+ 'v' => status.to_s
46
+ }, {
47
+ 'cCI' => 'M0023',
48
+ 'cO' => 'setTimeout',
49
+ 'n' => 'securityCode',
50
+ 'v' => security_code.to_s
51
+ }]
52
+
53
+ send_command_with_confirm main.c_id, command_list, options, "dynamic bands timeout #{status}", nil
54
+ end
55
+
56
+ # M0015 — Set offset for a signal plan.
57
+ def set_offset(plan:, offset:, options: {})
58
+ validate_ready 'set offset'
59
+ raise 'TLC main component not found' unless main
60
+
61
+ security_code = security_code_for(2)
62
+
63
+ command_list = [{
64
+ 'cCI' => 'M0015',
65
+ 'cO' => 'setOffset',
66
+ 'n' => 'status',
67
+ 'v' => offset.to_s
68
+ }, {
69
+ 'cCI' => 'M0015',
70
+ 'cO' => 'setOffset',
71
+ 'n' => 'securityCode',
72
+ 'v' => security_code.to_s
73
+ }, {
74
+ 'cCI' => 'M0015',
75
+ 'cO' => 'setOffset',
76
+ 'n' => 'plan',
77
+ 'v' => plan.to_s
78
+ }]
79
+
80
+ send_command_with_confirm main.c_id, command_list, options, "offset plan #{plan} to #{offset}", nil
81
+ end
82
+
83
+ # Set the timeplan (signal plan) on the remote TLC.
84
+ def set_timeplan(plan_nr, options: {})
85
+ validate_ready 'set timeplan'
86
+ raise 'TLC main component not found' unless main
87
+
88
+ security_code = security_code_for(2)
89
+
90
+ command_list = [{
91
+ 'cCI' => 'M0002',
92
+ 'cO' => 'setPlan',
93
+ 'n' => 'status',
94
+ 'v' => 'True'
95
+ }, {
96
+ 'cCI' => 'M0002',
97
+ 'cO' => 'setPlan',
98
+ 'n' => 'securityCode',
99
+ 'v' => security_code.to_s
100
+ }, {
101
+ 'cCI' => 'M0002',
102
+ 'cO' => 'setPlan',
103
+ 'n' => 'timeplan',
104
+ 'v' => plan_nr.to_s
105
+ }]
106
+
107
+ confirm_status = [{ 'sCI' => 'S0014', 'n' => 'status', 's' => plan_nr.to_s }]
108
+ send_command_with_confirm main.c_id, command_list, options, "timeplan #{plan_nr}", confirm_status
109
+ end
110
+
111
+ # M0016 — Set week table (mapping week days to traffic situations).
112
+ def set_week_table(status, options: {})
113
+ validate_ready 'set week table'
114
+ raise 'TLC main component not found' unless main
115
+
116
+ security_code = security_code_for(2)
117
+
118
+ command_list = [{
119
+ 'cCI' => 'M0016',
120
+ 'cO' => 'setWeekTable',
121
+ 'n' => 'status',
122
+ 'v' => status.to_s
123
+ }, {
124
+ 'cCI' => 'M0016',
125
+ 'cO' => 'setWeekTable',
126
+ 'n' => 'securityCode',
127
+ 'v' => security_code.to_s
128
+ }]
129
+
130
+ send_command_with_confirm main.c_id, command_list, options, 'week table', nil
131
+ end
132
+
133
+ # M0017 — Set day table (mapping time periods to signal plans).
134
+ def set_day_table(status, options: {})
135
+ validate_ready 'set day table'
136
+ raise 'TLC main component not found' unless main
137
+
138
+ security_code = security_code_for(2)
139
+
140
+ command_list = [{
141
+ 'cCI' => 'M0017',
142
+ 'cO' => 'setDayTable',
143
+ 'n' => 'status',
144
+ 'v' => status.to_s
145
+ }, {
146
+ 'cCI' => 'M0017',
147
+ 'cO' => 'setDayTable',
148
+ 'n' => 'securityCode',
149
+ 'v' => security_code.to_s
150
+ }]
151
+
152
+ send_command_with_confirm main.c_id, command_list, options, 'day table', nil
153
+ end
154
+
155
+ # M0018 — Set cycle time for a signal plan.
156
+ def set_cycle_time(plan:, cycle_time:, options: {})
157
+ validate_ready 'set cycle time'
158
+ raise 'TLC main component not found' unless main
159
+
160
+ security_code = security_code_for(2)
161
+
162
+ command_list = [{
163
+ 'cCI' => 'M0018',
164
+ 'cO' => 'setCycleTime',
165
+ 'n' => 'status',
166
+ 'v' => cycle_time.to_s
167
+ }, {
168
+ 'cCI' => 'M0018',
169
+ 'cO' => 'setCycleTime',
170
+ 'n' => 'securityCode',
171
+ 'v' => security_code.to_s
172
+ }, {
173
+ 'cCI' => 'M0018',
174
+ 'cO' => 'setCycleTime',
175
+ 'n' => 'plan',
176
+ 'v' => plan.to_s
177
+ }]
178
+
179
+ send_command_with_confirm main.c_id, command_list, options, "cycle time plan #{plan} to #{cycle_time}", nil
180
+ end
181
+
182
+ # M0010 — Order signal start for a signal group component.
183
+ def order_signal_start(component_id, options: {})
184
+ validate_ready 'order signal start'
185
+
186
+ security_code = security_code_for(2)
187
+
188
+ command_list = [{
189
+ 'cCI' => 'M0010',
190
+ 'cO' => 'setStart',
191
+ 'n' => 'status',
192
+ 'v' => 'True'
193
+ }, {
194
+ 'cCI' => 'M0010',
195
+ 'cO' => 'setStart',
196
+ 'n' => 'securityCode',
197
+ 'v' => security_code.to_s
198
+ }]
199
+
200
+ send_command_with_confirm component_id, command_list, options, "signal start #{component_id}", nil
201
+ end
202
+
203
+ # M0011 — Order signal stop for a signal group component.
204
+ def order_signal_stop(component_id, options: {})
205
+ validate_ready 'order signal stop'
206
+
207
+ security_code = security_code_for(2)
208
+
209
+ command_list = [{
210
+ 'cCI' => 'M0011',
211
+ 'cO' => 'setStop',
212
+ 'n' => 'status',
213
+ 'v' => 'True'
214
+ }, {
215
+ 'cCI' => 'M0011',
216
+ 'cO' => 'setStop',
217
+ 'n' => 'securityCode',
218
+ 'v' => security_code.to_s
219
+ }]
220
+
221
+ send_command_with_confirm component_id, command_list, options, "signal stop #{component_id}", nil
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,120 @@
1
+ module RSMP
2
+ module TLC
3
+ module Proxy
4
+ # Status reading and waiting methods for a remote TLC.
5
+ # Covers status subscriptions, group waits, and plan/band reading.
6
+ module Status
7
+ # Fetch the current signal plan from the remote TLC.
8
+ def fetch_signal_plan(options: {})
9
+ validate_ready 'fetch signal plan'
10
+
11
+ status_list = [{
12
+ 'sCI' => 'S0014',
13
+ 'n' => 'status'
14
+ }, {
15
+ 'sCI' => 'S0014',
16
+ 'n' => 'source'
17
+ }]
18
+
19
+ request_status main.c_id, status_list, @timeouts.merge(options)
20
+ end
21
+
22
+ # Subscribe to one or more statuses and wait until they match the expected values.
23
+ # Raises RSMP::TimeoutError if the values don't match within the timeout.
24
+ #
25
+ # status_list items: { 'sCI' => ..., 'n' => ..., 's' => <expected value or Regexp> }
26
+ # component_id defaults to the main TLC component.
27
+ # timeout defaults to @timeouts['command'].
28
+ def wait_for_status(description, status_list, update_rate: 0, timeout: nil, component_id: nil)
29
+ validate_ready 'wait for status'
30
+ component_id ||= main.c_id
31
+ timeout ||= @timeouts['command']
32
+
33
+ subscribe_list = status_list.map do |item|
34
+ entry = item.merge('uRt' => update_rate.to_s)
35
+ entry = entry.merge('sOc' => true) if use_soc?
36
+ entry
37
+ end
38
+
39
+ log "Wait for #{description}", level: :debug
40
+
41
+ begin
42
+ subscribe_to_status component_id, subscribe_list, collect!: { timeout: timeout }
43
+ ensure
44
+ unsubscribe_list = status_list.map { |item| item.slice('sCI', 'n') }
45
+ unsubscribe_to_status component_id, unsubscribe_list
46
+ end
47
+ end
48
+
49
+ # Wait for all signal groups to match state (as regex string, e.g. 'c' for yellow flash).
50
+ def wait_for_groups(state, timeout:)
51
+ regex = /^#{state}+$/
52
+ wait_for_status(
53
+ "all groups to reach state #{state}",
54
+ [{ 'sCI' => 'S0001', 'n' => 'signalgroupstatus', 's' => regex }],
55
+ timeout: timeout
56
+ )
57
+ end
58
+
59
+ # Wait for the TLC to return to normal control mode (functional position NormalControl,
60
+ # yellow flash off, startup mode off).
61
+ def wait_for_normal_control(timeout: nil)
62
+ wait_for_status(
63
+ 'normal control on, yellow flash off, startup mode off',
64
+ [
65
+ { 'sCI' => 'S0007', 'n' => 'status', 's' => /^True(,True)*$/ },
66
+ { 'sCI' => 'S0011', 'n' => 'status', 's' => /^False(,False)*$/ },
67
+ { 'sCI' => 'S0005', 'n' => 'status', 's' => 'False' }
68
+ ],
69
+ timeout: timeout
70
+ )
71
+ end
72
+
73
+ # Read cycle times for all plans via S0028.
74
+ # Returns a hash of plan_nr (Integer) => cycle_time (Integer, seconds).
75
+ def read_cycle_times(options: {})
76
+ validate_ready 'read cycle times'
77
+ timeout = options[:timeout] || @timeouts['status_response']
78
+ result = request_status main.c_id,
79
+ [{ 'sCI' => 'S0028', 'n' => 'status' }],
80
+ collect!: { timeout: timeout }
81
+ result[:collector].messages.first.attributes['sS'].first['s'].split(',').to_h do |item|
82
+ item.split('-').map(&:to_i)
83
+ end
84
+ end
85
+
86
+ # Read the current signal plan number via S0014.
87
+ # Returns the plan number as an Integer.
88
+ def read_current_plan(options: {})
89
+ validate_ready 'read current plan'
90
+ timeout = options[:timeout] || @timeouts['status_response']
91
+ result = request_status main.c_id,
92
+ [{ 'sCI' => 'S0014', 'n' => 'status' }],
93
+ collect!: { timeout: timeout }
94
+ result[:collector].messages.first.attributes['sS'].first['s'].to_i
95
+ end
96
+
97
+ # Read the value of a single dynamic band for a given plan and band index via S0023.
98
+ # Returns the band value as an Integer, or nil if not found.
99
+ def read_dynamic_band(plan:, band:, options: {})
100
+ validate_ready 'read dynamic band'
101
+ timeout = options[:timeout] || @timeouts['status_response']
102
+ result = request_status main.c_id,
103
+ [{ 'sCI' => 'S0023', 'n' => 'status' }],
104
+ collect!: { timeout: timeout }
105
+ extract_band_value(result, plan, band)
106
+ end
107
+
108
+ private
109
+
110
+ def extract_band_value(result, plan, band)
111
+ result[:collector].messages.first.attributes['sS'].first['s'].split(',').each do |item|
112
+ some_plan, some_band, value = item.split('-')
113
+ return value.to_i if some_plan.to_i == plan.to_i && some_band.to_i == band.to_i
114
+ end
115
+ nil
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,58 @@
1
+ module RSMP
2
+ module TLC
3
+ module Proxy
4
+ # Command methods for operational control of a remote TLC.
5
+ # Covers functional position, emergency routes, I/O modes, signal group orders, and system settings.
6
+ module System
7
+ # M0103 — Change security code for a given level.
8
+ # Does not use security_code_for since the codes are passed explicitly.
9
+ def set_security_code(level:, old_code:, new_code:, options: {})
10
+ validate_ready 'set security code'
11
+ raise 'TLC main component not found' unless main
12
+
13
+ command_list = [{
14
+ 'cCI' => 'M0103',
15
+ 'cO' => 'setSecurityCode',
16
+ 'n' => 'status',
17
+ 'v' => level.to_s
18
+ }, {
19
+ 'cCI' => 'M0103',
20
+ 'cO' => 'setSecurityCode',
21
+ 'n' => 'oldSecurityCode',
22
+ 'v' => old_code.to_s
23
+ }, {
24
+ 'cCI' => 'M0103',
25
+ 'cO' => 'setSecurityCode',
26
+ 'n' => 'newSecurityCode',
27
+ 'v' => new_code.to_s
28
+ }]
29
+
30
+ send_command_with_confirm main.c_id, command_list, options, "security code level #{level}", nil
31
+ end
32
+
33
+ # M0104 — Set the clock on the remote TLC. clock must respond to year/month/day/hour/min/sec.
34
+ def set_clock(clock, options: {})
35
+ validate_ready 'set clock'
36
+ raise 'TLC main component not found' unless main
37
+
38
+ send_command_with_confirm main.c_id, clock_command_list(clock), options, 'clock', nil
39
+ end
40
+
41
+ private
42
+
43
+ def clock_command_list(clock)
44
+ security_code = security_code_for(1)
45
+ [
46
+ { 'cCI' => 'M0104', 'cO' => 'setDate', 'n' => 'securityCode', 'v' => security_code.to_s },
47
+ { 'cCI' => 'M0104', 'cO' => 'setDate', 'n' => 'year', 'v' => clock.year.to_s },
48
+ { 'cCI' => 'M0104', 'cO' => 'setDate', 'n' => 'month', 'v' => clock.month.to_s },
49
+ { 'cCI' => 'M0104', 'cO' => 'setDate', 'n' => 'day', 'v' => clock.day.to_s },
50
+ { 'cCI' => 'M0104', 'cO' => 'setDate', 'n' => 'hour', 'v' => clock.hour.to_s },
51
+ { 'cCI' => 'M0104', 'cO' => 'setDate', 'n' => 'minute', 'v' => clock.min.to_s },
52
+ { 'cCI' => 'M0104', 'cO' => 'setDate', 'n' => 'second', 'v' => clock.sec.to_s }
53
+ ]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end