rsmp 0.43.2 → 0.45.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +11 -11
- data/README.md +19 -3
- data/Rakefile +2 -2
- data/config/supervisor.yaml +2 -1
- data/config/tlc.yaml +2 -2
- data/lib/rsmp/cli.rb +29 -5
- data/lib/rsmp/component/component.rb +0 -4
- data/lib/rsmp/component/component_base.rb +15 -2
- data/lib/rsmp/component/component_proxy.rb +1 -1
- data/lib/rsmp/component/components.rb +22 -1
- data/lib/rsmp/convert/export/json_schema/outputs.rb +1 -0
- data/lib/rsmp/convert/export/json_schema/values.rb +6 -4
- data/lib/rsmp/convert/export/json_schema.rb +7 -3
- data/lib/rsmp/helpers/deep_merge.rb +2 -2
- data/lib/rsmp/message.rb +32 -0
- data/lib/rsmp/node/site/site.rb +34 -10
- data/lib/rsmp/node/supervisor/modules/configuration.rb +32 -5
- data/lib/rsmp/node/supervisor/modules/connection.rb +0 -2
- data/lib/rsmp/node/supervisor/supervisor.rb +0 -7
- data/lib/rsmp/options/options.rb +55 -6
- data/lib/rsmp/options/schemas/site.json +6 -3
- data/lib/rsmp/options/schemas/supervisor.json +5 -2
- data/lib/rsmp/options/schemas/supervisor_site.json +5 -2
- data/lib/rsmp/options/site_options.rb +3 -2
- data/lib/rsmp/options/supervisor_options.rb +3 -1
- data/lib/rsmp/proxy/modules/acknowledgements.rb +2 -0
- data/lib/rsmp/proxy/modules/receive.rb +5 -2
- data/lib/rsmp/proxy/modules/state.rb +1 -0
- data/lib/rsmp/proxy/modules/versions.rb +90 -15
- data/lib/rsmp/proxy/proxy.rb +52 -3
- data/lib/rsmp/proxy/site/modules/status.rb +5 -3
- data/lib/rsmp/proxy/site/site_proxy.rb +68 -35
- data/lib/rsmp/proxy/site/sxl_selection.rb +54 -0
- data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +54 -18
- data/lib/rsmp/schema/message_resolution.rb +104 -0
- data/lib/rsmp/schema.rb +104 -22
- data/lib/rsmp/schema_error.rb +7 -1
- data/lib/rsmp/sxl/interface.rb +48 -0
- data/lib/rsmp/sxl/registry.rb +55 -0
- data/lib/rsmp/sxl/site_interface.rb +10 -0
- data/lib/rsmp/sxl/supervisor_interface.rb +21 -0
- data/lib/rsmp/tlc/detector_logic.rb +2 -2
- data/lib/rsmp/tlc/signal_group.rb +2 -2
- data/lib/rsmp/tlc/site_interface.rb +10 -0
- data/lib/rsmp/tlc/{traffic_controller_proxy.rb → supervisor_interface.rb} +19 -34
- data/lib/rsmp/tlc/traffic_controller.rb +10 -2
- data/lib/rsmp/tlc/traffic_controller_site.rb +4 -2
- data/lib/rsmp/tlc.rb +10 -0
- data/lib/rsmp/version.rb +1 -1
- data/lib/rsmp.rb +8 -1
- data/rsmp.gemspec +5 -5
- data/schemas/core/3.3.0/aggregated_status.json +25 -0
- data/schemas/core/3.3.0/aggregated_status_request.json +9 -0
- data/schemas/core/3.3.0/alarm.json +71 -0
- data/schemas/core/3.3.0/alarm_acknowledge.json +11 -0
- data/schemas/core/3.3.0/alarm_issue.json +44 -0
- data/schemas/core/3.3.0/alarm_request.json +3 -0
- data/schemas/core/3.3.0/alarm_suspend_resume.json +3 -0
- data/schemas/core/3.3.0/alarm_suspended_resumed.json +44 -0
- data/schemas/core/3.3.0/command_request.json +24 -0
- data/schemas/core/3.3.0/command_response.json +35 -0
- data/schemas/core/3.3.0/component_list.json +24 -0
- data/schemas/core/3.3.0/core.json +40 -0
- data/schemas/core/3.3.0/definitions.json +133 -0
- data/schemas/core/3.3.0/message_ack.json +11 -0
- data/schemas/core/3.3.0/message_not_ack.json +15 -0
- data/schemas/core/3.3.0/rsmp.json +142 -0
- data/schemas/core/3.3.0/status.json +21 -0
- data/schemas/core/3.3.0/status_request.json +5 -0
- data/schemas/core/3.3.0/status_response.json +41 -0
- data/schemas/core/3.3.0/status_subscribe.json +31 -0
- data/schemas/core/3.3.0/status_unsubscribe.json +5 -0
- data/schemas/core/3.3.0/status_update.json +41 -0
- data/schemas/core/3.3.0/version.json +144 -0
- data/schemas/core/3.3.0/watchdog.json +9 -0
- data/schemas/tlc/1.3.0/alarms/A0001.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0002.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0003.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0004.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0005.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0006.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0007.json +34 -0
- data/schemas/tlc/1.3.0/alarms/A0008.json +30 -0
- data/schemas/tlc/1.3.0/alarms/A0009.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0010.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0101.json +4 -0
- data/schemas/tlc/1.3.0/alarms/A0201.json +35 -0
- data/schemas/tlc/1.3.0/alarms/A0202.json +35 -0
- data/schemas/tlc/1.3.0/alarms/A0301.json +92 -0
- data/schemas/tlc/1.3.0/alarms/A0302.json +115 -0
- data/schemas/tlc/1.3.0/alarms/A0303.json +92 -0
- data/schemas/tlc/1.3.0/alarms/A0304.json +115 -0
- data/schemas/tlc/1.3.0/alarms/alarms.json +287 -0
- data/schemas/tlc/1.3.0/commands/M0001.json +92 -0
- data/schemas/tlc/1.3.0/commands/M0002.json +69 -0
- data/schemas/tlc/1.3.0/commands/M0003.json +69 -0
- data/schemas/tlc/1.3.0/commands/M0004.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0005.json +69 -0
- data/schemas/tlc/1.3.0/commands/M0006.json +69 -0
- data/schemas/tlc/1.3.0/commands/M0007.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0008.json +87 -0
- data/schemas/tlc/1.3.0/commands/M0010.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0011.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0012.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0013.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0014.json +69 -0
- data/schemas/tlc/1.3.0/commands/M0015.json +69 -0
- data/schemas/tlc/1.3.0/commands/M0016.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0017.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0018.json +69 -0
- data/schemas/tlc/1.3.0/commands/M0019.json +87 -0
- data/schemas/tlc/1.3.0/commands/M0020.json +87 -0
- data/schemas/tlc/1.3.0/commands/M0021.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0022.json +249 -0
- data/schemas/tlc/1.3.0/commands/M0023.json +51 -0
- data/schemas/tlc/1.3.0/commands/M0024.json +33 -0
- data/schemas/tlc/1.3.0/commands/M0103.json +72 -0
- data/schemas/tlc/1.3.0/commands/M0104.json +141 -0
- data/schemas/tlc/1.3.0/commands/command_requests.json +8 -0
- data/schemas/tlc/1.3.0/commands/command_responses.json +8 -0
- data/schemas/tlc/1.3.0/commands/commands.json +415 -0
- data/schemas/tlc/1.3.0/defs/definitions.json +72 -0
- data/schemas/tlc/1.3.0/defs/guards.json +24 -0
- data/schemas/tlc/1.3.0/rsmp.json +74 -0
- data/schemas/tlc/1.3.0/statuses/S0001.json +109 -0
- data/schemas/tlc/1.3.0/statuses/S0002.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0003.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0004.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0005.json +72 -0
- data/schemas/tlc/1.3.0/statuses/S0006.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0007.json +73 -0
- data/schemas/tlc/1.3.0/statuses/S0008.json +73 -0
- data/schemas/tlc/1.3.0/statuses/S0009.json +73 -0
- data/schemas/tlc/1.3.0/statuses/S0010.json +73 -0
- data/schemas/tlc/1.3.0/statuses/S0011.json +73 -0
- data/schemas/tlc/1.3.0/statuses/S0012.json +73 -0
- data/schemas/tlc/1.3.0/statuses/S0013.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0014.json +55 -0
- data/schemas/tlc/1.3.0/statuses/S0015.json +55 -0
- data/schemas/tlc/1.3.0/statuses/S0016.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0017.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0018.json +61 -0
- data/schemas/tlc/1.3.0/statuses/S0019.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0020.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0021.json +37 -0
- data/schemas/tlc/1.3.0/statuses/S0022.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0023.json +37 -0
- data/schemas/tlc/1.3.0/statuses/S0024.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0025.json +162 -0
- data/schemas/tlc/1.3.0/statuses/S0026.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0027.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0028.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0029.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0030.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0031.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0032.json +73 -0
- data/schemas/tlc/1.3.0/statuses/S0033.json +77 -0
- data/schemas/tlc/1.3.0/statuses/S0034.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0035.json +49 -0
- data/schemas/tlc/1.3.0/statuses/S0091.json +40 -0
- data/schemas/tlc/1.3.0/statuses/S0092.json +40 -0
- data/schemas/tlc/1.3.0/statuses/S0095.json +36 -0
- data/schemas/tlc/1.3.0/statuses/S0096.json +126 -0
- data/schemas/tlc/1.3.0/statuses/S0097.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0098.json +72 -0
- data/schemas/tlc/1.3.0/statuses/S0201.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0202.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0203.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0204.json +198 -0
- data/schemas/tlc/1.3.0/statuses/S0205.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0206.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0207.json +54 -0
- data/schemas/tlc/1.3.0/statuses/S0208.json +198 -0
- data/schemas/tlc/1.3.0/statuses/statuses.json +787 -0
- data/schemas/tlc/1.3.0/sxl.yaml +2296 -0
- metadata +144 -12
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
# Selects the SXL versions accepted by a supervisor-side site proxy.
|
|
3
|
+
module SiteSxlSelection
|
|
4
|
+
def check_sxl_version(message)
|
|
5
|
+
if core_3_3?
|
|
6
|
+
select_sxls message
|
|
7
|
+
else
|
|
8
|
+
select_legacy_sxl message
|
|
9
|
+
end
|
|
10
|
+
rescue RSMP::Schema::UnknownSchemaError => e
|
|
11
|
+
dont_acknowledge message, "Rejected #{message.type} message,", e.to_s
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def select_legacy_sxl(message)
|
|
15
|
+
primary = configured_sxls.first
|
|
16
|
+
unless primary
|
|
17
|
+
reason = 'Legacy Version message received, but no SXL is configured'
|
|
18
|
+
dont_acknowledge message, "Rejected #{message.type} message,", reason
|
|
19
|
+
raise HandshakeError, reason
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sanitized_version = RSMP::Schema.sanitize_version(message.attribute('SXL'))
|
|
23
|
+
RSMP::Schema.find_schema! primary['name'], sanitized_version
|
|
24
|
+
@accepted_sxls = [{ 'name' => primary['name'], 'version' => message.attribute('SXL') }]
|
|
25
|
+
@rejected_sxls = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def select_sxls(message)
|
|
29
|
+
selected_sxls = message.sxls.map { |requested| select_sxl(requested) }
|
|
30
|
+
@accepted_sxls, @rejected_sxls = selected_sxls.partition { |item| item['rejected'].nil? }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def select_sxl(requested)
|
|
34
|
+
configured = configured_sxls.find { |item| item['name'] == requested['name'] }
|
|
35
|
+
return rejected_sxl(requested, 1, 'SXL not supported') unless configured
|
|
36
|
+
|
|
37
|
+
if configured['version'].to_s == requested['version'].to_s
|
|
38
|
+
RSMP::Schema.find_schema! requested['name'], requested['version'], lenient: true
|
|
39
|
+
requested.slice('name', 'version', 'prefix')
|
|
40
|
+
else
|
|
41
|
+
rejected_sxl(requested, 2, "Supervisor only supports #{configured['version']}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def rejected_sxl(requested, code, reason)
|
|
46
|
+
{
|
|
47
|
+
'name' => requested['name'],
|
|
48
|
+
'version' => requested['version'],
|
|
49
|
+
'rejected' => code,
|
|
50
|
+
'reason' => reason
|
|
51
|
+
}.compact
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -17,7 +17,9 @@ module RSMP
|
|
|
17
17
|
@ip = options[:ip]
|
|
18
18
|
@port = options[:port]
|
|
19
19
|
@status_subscriptions = {}
|
|
20
|
-
@
|
|
20
|
+
@sxls = configured_sxls
|
|
21
|
+
@accepted_sxls = @sxls.dup
|
|
22
|
+
@rejected_sxls = []
|
|
21
23
|
@synthetic_id = Supervisor.build_id_from_ip_port @ip, @port
|
|
22
24
|
end
|
|
23
25
|
|
|
@@ -46,7 +48,7 @@ module RSMP
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def start_handshake
|
|
49
|
-
|
|
51
|
+
send_version_request @site_settings['site_id'], core_versions
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
# connect to the supervisor and initiate handshake supervisor
|
|
@@ -87,14 +89,14 @@ module RSMP
|
|
|
87
89
|
end
|
|
88
90
|
|
|
89
91
|
def handshake_complete
|
|
90
|
-
|
|
91
|
-
log "Connection to supervisor established, using core #{@core_version},
|
|
92
|
+
sxl_summary = accepted_sxls.map { |item| "#{item['name']} #{item['version']}" }.join(', ')
|
|
93
|
+
log "Connection to supervisor established, using core #{@core_version}, SXLs [#{sxl_summary}]",
|
|
92
94
|
level: :info
|
|
93
95
|
self.state = :ready
|
|
94
96
|
start_watchdog
|
|
95
97
|
if @site_settings['send_after_connect']
|
|
96
98
|
send_all_aggregated_status
|
|
97
|
-
send_active_alarms
|
|
99
|
+
send_active_alarms if receive_alarms?
|
|
98
100
|
end
|
|
99
101
|
super
|
|
100
102
|
end
|
|
@@ -105,10 +107,29 @@ module RSMP
|
|
|
105
107
|
will_not_handle message
|
|
106
108
|
when AggregatedStatusRequest
|
|
107
109
|
process_aggregated_status_request message
|
|
108
|
-
when CommandRequest
|
|
109
|
-
process_command_request message
|
|
110
110
|
when CommandResponse
|
|
111
111
|
process_command_response message
|
|
112
|
+
when CommandRequest, StatusRequest, StatusSubscribe, StatusUnsubscribe,
|
|
113
|
+
Alarm, AlarmAcknowledged, AlarmSuspend, AlarmResume, AlarmRequest
|
|
114
|
+
handle_interface_request message
|
|
115
|
+
else
|
|
116
|
+
super
|
|
117
|
+
end
|
|
118
|
+
rescue UnknownComponent, UnknownCommand, UnknownStatus,
|
|
119
|
+
MessageRejected, MissingAttribute => e
|
|
120
|
+
dont_acknowledge message, '', e.to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def handle_interface_request(message)
|
|
124
|
+
interface = sxl_interface_for message
|
|
125
|
+
interface.validate_message! message
|
|
126
|
+
interface.process_message message
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def process_sxl_request(message)
|
|
130
|
+
case message
|
|
131
|
+
when CommandRequest
|
|
132
|
+
process_command_request message
|
|
112
133
|
when StatusRequest
|
|
113
134
|
process_status_request message
|
|
114
135
|
when StatusSubscribe
|
|
@@ -117,18 +138,17 @@ module RSMP
|
|
|
117
138
|
process_status_unsubcribe message
|
|
118
139
|
when Alarm, AlarmAcknowledged, AlarmSuspend, AlarmResume, AlarmRequest
|
|
119
140
|
process_alarm message
|
|
120
|
-
else
|
|
121
|
-
super
|
|
122
141
|
end
|
|
123
|
-
rescue UnknownComponent, UnknownCommand, UnknownStatus,
|
|
124
|
-
MessageRejected, MissingAttribute => e
|
|
125
|
-
dont_acknowledge message, '', e.to_s
|
|
126
142
|
end
|
|
127
143
|
|
|
128
144
|
def acknowledged_first_ingoing(message)
|
|
129
145
|
case message.type
|
|
130
146
|
when 'Watchdog'
|
|
131
|
-
|
|
147
|
+
if core_3_3?
|
|
148
|
+
send_component_list
|
|
149
|
+
else
|
|
150
|
+
handshake_complete
|
|
151
|
+
end
|
|
132
152
|
end
|
|
133
153
|
end
|
|
134
154
|
|
|
@@ -155,10 +175,6 @@ module RSMP
|
|
|
155
175
|
status_update_timer now if ready?
|
|
156
176
|
end
|
|
157
177
|
|
|
158
|
-
def sxl_version
|
|
159
|
-
@site_settings['sxl_version'].to_s
|
|
160
|
-
end
|
|
161
|
-
|
|
162
178
|
def process_version(message)
|
|
163
179
|
return extraneous_version message if @version_determined
|
|
164
180
|
|
|
@@ -168,7 +184,27 @@ module RSMP
|
|
|
168
184
|
version_accepted message
|
|
169
185
|
end
|
|
170
186
|
|
|
171
|
-
def check_sxl_version(message)
|
|
187
|
+
def check_sxl_version(message)
|
|
188
|
+
if core_3_3?
|
|
189
|
+
@rejected_sxls, @accepted_sxls = message.sxls.partition { |item| item['rejected'] }
|
|
190
|
+
@receive_alarms = message.attributes.fetch('receiveAlarms', true)
|
|
191
|
+
else
|
|
192
|
+
primary = primary_configured_sxl
|
|
193
|
+
raise HandshakeError, 'Legacy Version response received, but no SXL is configured' unless primary
|
|
194
|
+
|
|
195
|
+
@accepted_sxls = [primary]
|
|
196
|
+
@rejected_sxls = []
|
|
197
|
+
end
|
|
198
|
+
build_sxl_interfaces
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def send_component_list
|
|
202
|
+
send_message ComponentList.new('components' => @site.component_list)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def component_list_acknowledged
|
|
206
|
+
handshake_complete
|
|
207
|
+
end
|
|
172
208
|
|
|
173
209
|
def main
|
|
174
210
|
@site.main
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
# Resolves SXL schemas from message code ownership.
|
|
3
|
+
module Schema
|
|
4
|
+
MESSAGE_CODE_EXTRACTORS = {
|
|
5
|
+
'StatusRequest' => ->(message) { status_codes(message) },
|
|
6
|
+
'StatusSubscribe' => ->(message) { status_codes(message) },
|
|
7
|
+
'StatusUnsubscribe' => ->(message) { status_codes(message) },
|
|
8
|
+
'StatusResponse' => ->(message) { status_codes(message) },
|
|
9
|
+
'StatusUpdate' => ->(message) { status_codes(message) },
|
|
10
|
+
'CommandRequest' => ->(message) { request_command_codes(message) },
|
|
11
|
+
'CommandResponse' => ->(message) { response_command_codes(message) },
|
|
12
|
+
'Alarm' => ->(message) { alarm_codes(message) }
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
MESSAGE_CODE_KINDS = {
|
|
16
|
+
'StatusRequest' => :statuses,
|
|
17
|
+
'StatusSubscribe' => :statuses,
|
|
18
|
+
'StatusUnsubscribe' => :statuses,
|
|
19
|
+
'StatusResponse' => :statuses,
|
|
20
|
+
'StatusUpdate' => :statuses,
|
|
21
|
+
'CommandRequest' => :commands,
|
|
22
|
+
'CommandResponse' => :commands,
|
|
23
|
+
'Alarm' => :alarms
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def self.message_codes(message)
|
|
27
|
+
extractor = MESSAGE_CODE_EXTRACTORS[message['type']]
|
|
28
|
+
extractor ? extractor.call(message).uniq : []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.status_codes(message)
|
|
32
|
+
(message['sS'] || []).map { |item| item['sCI'] }.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.request_command_codes(message)
|
|
36
|
+
(message['arg'] || []).map { |item| item['cCI'] }.compact
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.response_command_codes(message)
|
|
40
|
+
(message['rvs'] || []).map { |item| item['cCI'] }.compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.alarm_codes(message)
|
|
44
|
+
[message['aCId']].compact
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.message_code_kind(message)
|
|
48
|
+
MESSAGE_CODE_KINDS[message['type']]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.sxl_defines_codes?(type, version, kind, codes, options)
|
|
52
|
+
version = sanitize_version(version.to_s) if options[:lenient]
|
|
53
|
+
catalogue = sxl_catalogue(type, version, kind)
|
|
54
|
+
prefix = sxl_prefix(type, version, options)
|
|
55
|
+
codes.all? do |code|
|
|
56
|
+
unprefixed = prefix && code.start_with?(prefix) ? code[prefix.length..] : code
|
|
57
|
+
catalogue.key?(code) || catalogue.key?(code.to_sym) ||
|
|
58
|
+
catalogue.key?(unprefixed) || catalogue.key?(unprefixed.to_sym)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.message_code_kind_name(kind)
|
|
63
|
+
{
|
|
64
|
+
statuses: 'status',
|
|
65
|
+
commands: 'command',
|
|
66
|
+
alarms: 'alarm'
|
|
67
|
+
}.fetch(kind) { kind.to_s.delete_suffix('s') }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.resolve_sxl(message, schemas:, **options)
|
|
71
|
+
kind = message_code_kind(message)
|
|
72
|
+
codes = message_codes(message)
|
|
73
|
+
return nil unless kind && codes.any?
|
|
74
|
+
|
|
75
|
+
matches = matching_sxl_schemas(schemas, kind, codes, options)
|
|
76
|
+
raise_if_no_sxl_match(kind, codes) if matches.empty?
|
|
77
|
+
raise_if_ambiguous_sxl_match(codes, matches) if matches.size > 1
|
|
78
|
+
|
|
79
|
+
matches.first
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.matching_sxl_schemas(schemas, kind, codes, options)
|
|
83
|
+
sxl_schemas(schemas).select do |type, version|
|
|
84
|
+
sxl_defines_codes?(type, version, kind, codes, options)
|
|
85
|
+
rescue UnknownSchemaError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.sxl_schemas(schemas)
|
|
91
|
+
schemas.reject { |type, _version| type.to_sym == :core }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.raise_if_no_sxl_match(kind, codes)
|
|
95
|
+
raise UnknownMessageCodeError,
|
|
96
|
+
"No accepted SXL defines #{message_code_kind_name(kind)} code(s) #{codes.join(', ')}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.raise_if_ambiguous_sxl_match(codes, matches)
|
|
100
|
+
names = matches.map { |type, version| "#{type} #{version}" }.join(', ')
|
|
101
|
+
raise AmbiguousMessageCodeError, "Message code(s) #{codes.join(', ')} match multiple accepted SXLs: #{names}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/rsmp/schema.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require 'json_schemer'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'yaml'
|
|
2
4
|
|
|
3
5
|
# RSMP (Road Side Message Protocol) schema validation library.
|
|
4
6
|
module RSMP
|
|
@@ -8,6 +10,7 @@ module RSMP
|
|
|
8
10
|
|
|
9
11
|
def self.setup
|
|
10
12
|
@schemas = {}
|
|
13
|
+
@schema_paths = {}
|
|
11
14
|
schemas_path = File.expand_path(File.join(__dir__, '..', '..', 'schemas'))
|
|
12
15
|
Dir.glob("#{schemas_path}/*").select { |f| File.directory? f }.each do |type_path|
|
|
13
16
|
type = File.basename(type_path).to_sym
|
|
@@ -26,23 +29,37 @@ module RSMP
|
|
|
26
29
|
#
|
|
27
30
|
# an error is raised if the schema type already exists, and force is not set to true
|
|
28
31
|
def self.load_schema_type(type, type_path, force: false)
|
|
29
|
-
|
|
32
|
+
type = type.to_sym
|
|
33
|
+
ensure_schema_type_available(type, force)
|
|
30
34
|
|
|
31
35
|
@schemas[type] = {}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
@schema_paths ||= {}
|
|
37
|
+
@schema_paths[type] = {}
|
|
38
|
+
schema_version_paths(type_path).each { |schema_path| load_schema_version(type, schema_path) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.ensure_schema_type_available(type, force)
|
|
42
|
+
raise "Schema type #{type} already loaded" if @schemas[type] && force != true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.schema_version_paths(type_path)
|
|
46
|
+
Dir.glob("#{type_path}/*").select { |path| File.directory? path }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.load_schema_version(type, schema_path)
|
|
50
|
+
version = File.basename(schema_path)
|
|
51
|
+
file_path = File.join(schema_path, 'rsmp.json')
|
|
52
|
+
return unless File.exist? file_path
|
|
53
|
+
|
|
54
|
+
@schemas[type][version] = JSONSchemer.schema(Pathname.new(file_path))
|
|
55
|
+
@schema_paths[type][version] = schema_path
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
# remove a schema type
|
|
44
59
|
def self.remove_schema_type(type)
|
|
60
|
+
type = type.to_sym
|
|
45
61
|
schemas.delete type
|
|
62
|
+
@schema_paths&.delete type
|
|
46
63
|
end
|
|
47
64
|
|
|
48
65
|
# get schemas types
|
|
@@ -174,37 +191,102 @@ module RSMP
|
|
|
174
191
|
find_schema(type, version, options) != nil
|
|
175
192
|
end
|
|
176
193
|
|
|
194
|
+
def self.sxl_metadata(type, version, options = {})
|
|
195
|
+
version = sanitize_version version if options[:lenient]
|
|
196
|
+
find_schema! type, version
|
|
197
|
+
|
|
198
|
+
path = @schema_paths&.dig(type.to_sym, version)
|
|
199
|
+
return {} unless path
|
|
200
|
+
|
|
201
|
+
yaml_path = File.join(path, 'sxl.yaml')
|
|
202
|
+
return YAML.load_file(yaml_path).fetch('meta', {}) if File.exist?(yaml_path)
|
|
203
|
+
|
|
204
|
+
json_path = File.join(path, 'rsmp.json')
|
|
205
|
+
File.exist?(json_path) ? JSON.parse(File.read(json_path)) : {}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.sxl_prefix(type, version, options = {})
|
|
209
|
+
sxl_metadata(type, version, options)['prefix']
|
|
210
|
+
end
|
|
211
|
+
|
|
177
212
|
# return a catalogue of statuses for a particular schema type and version
|
|
178
213
|
# returns a hash of { status_code_id_sym => [arg_name_sym, ...] }
|
|
179
214
|
# raises an error if the schema type/version is not found, or has no sxl.yaml
|
|
180
215
|
def self.status_catalogue(type, version)
|
|
216
|
+
sxl_catalogue(type, version, :statuses).transform_keys(&:to_sym).transform_values do |status|
|
|
217
|
+
(status['arguments'] || {}).keys.map(&:to_sym)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def self.sxl_catalogue(type, version, kind)
|
|
181
222
|
find_schema! type, version
|
|
182
|
-
|
|
183
|
-
yaml_path = File.join(
|
|
184
|
-
raise "No sxl.yaml for #{type} #{version}" unless File.exist?(yaml_path)
|
|
223
|
+
schema_path = @schema_paths&.dig(type.to_sym, version)
|
|
224
|
+
yaml_path = File.join(schema_path, 'sxl.yaml') if schema_path
|
|
225
|
+
raise "No sxl.yaml for #{type} #{version}" unless yaml_path && File.exist?(yaml_path)
|
|
185
226
|
|
|
186
227
|
sxl = RSMP::Convert::Import::YAML.read(yaml_path)
|
|
187
|
-
sxl
|
|
188
|
-
|
|
228
|
+
sxl.fetch(kind)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.core_message_type?(message)
|
|
232
|
+
type = message['type']
|
|
233
|
+
%w[
|
|
234
|
+
MessageAck
|
|
235
|
+
MessageNotAck
|
|
236
|
+
Version
|
|
237
|
+
ComponentList
|
|
238
|
+
AggregatedStatus
|
|
239
|
+
AggregatedStatusRequest
|
|
240
|
+
Watchdog
|
|
241
|
+
].include?(type)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def self.validate_core(message, schemas, options)
|
|
245
|
+
core_version = schemas[:core] || schemas['core']
|
|
246
|
+
raise ArgumentError, 'schemas must include core' unless core_version
|
|
247
|
+
|
|
248
|
+
schema = find_schema! :core, core_version, options
|
|
249
|
+
validate_using_schema(message, schema)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self.validate_sxls(message, schemas, options)
|
|
253
|
+
sxl_schemas = schemas.reject { |type, _version| type.to_sym == :core }
|
|
254
|
+
return [] if sxl_schemas.empty? || core_message_type?(message)
|
|
255
|
+
|
|
256
|
+
resolved = resolve_sxl(message, schemas: schemas, **options)
|
|
257
|
+
if resolved
|
|
258
|
+
type, version = resolved
|
|
259
|
+
schema = find_schema! type, version, options
|
|
260
|
+
return validate_using_schema(message, schema)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
all_errors = []
|
|
264
|
+
sxl_schemas.each do |type, version|
|
|
265
|
+
schema = find_schema! type, version, options
|
|
266
|
+
errors = validate_using_schema(message, schema)
|
|
267
|
+
return [] if errors.empty?
|
|
268
|
+
|
|
269
|
+
all_errors.concat errors
|
|
189
270
|
end
|
|
271
|
+
all_errors
|
|
190
272
|
end
|
|
191
273
|
|
|
192
|
-
# validate using
|
|
193
|
-
#
|
|
194
|
-
#
|
|
274
|
+
# validate using core and optional SXL schemas.
|
|
275
|
+
# Core must pass. SXL-defined messages pass if at least one SXL schema passes.
|
|
276
|
+
# returns nil if validation succeeds, otherwise returns an array of errors.
|
|
195
277
|
def self.validate(message, schemas, options = {})
|
|
196
278
|
raise ArgumentError, 'message missing' unless message
|
|
197
279
|
raise ArgumentError, 'schemas missing' unless schemas
|
|
198
280
|
raise ArgumentError, 'schemas must be a Hash' unless schemas.is_a?(Hash)
|
|
199
281
|
raise ArgumentError, 'schemas cannot be empty' unless schemas.any?
|
|
200
282
|
|
|
201
|
-
errors = schemas
|
|
202
|
-
|
|
203
|
-
validate_using_schema(message, schema)
|
|
204
|
-
end
|
|
283
|
+
errors = validate_core(message, schemas, options)
|
|
284
|
+
errors.concat validate_sxls(message, schemas, options) if errors.empty?
|
|
205
285
|
return nil if errors.empty?
|
|
206
286
|
|
|
207
287
|
errors
|
|
208
288
|
end
|
|
209
289
|
end
|
|
210
290
|
end
|
|
291
|
+
|
|
292
|
+
require_relative 'schema/message_resolution'
|
data/lib/rsmp/schema_error.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module RSMP
|
|
2
2
|
module Schema
|
|
3
|
-
# Base error class for
|
|
3
|
+
# Base error class for schema validation.
|
|
4
4
|
class Error < StandardError
|
|
5
5
|
end
|
|
6
6
|
|
|
@@ -15,5 +15,11 @@ module RSMP
|
|
|
15
15
|
# Raised when the requested schema version does not exist.
|
|
16
16
|
class UnknownSchemaVersionError < UnknownSchemaError
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
class UnknownMessageCodeError < Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class AmbiguousMessageCodeError < Error
|
|
23
|
+
end
|
|
18
24
|
end
|
|
19
25
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module RSMP
|
|
4
|
+
module SXL
|
|
5
|
+
# Base interface for SXL-specific behavior on a proxy connection.
|
|
6
|
+
class Interface
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def_delegators :proxy, :send_message, :send_message_and_collect, :validate_ready, :log
|
|
10
|
+
|
|
11
|
+
attr_reader :proxy, :name, :version
|
|
12
|
+
|
|
13
|
+
def initialize(proxy:, name:, version:)
|
|
14
|
+
@proxy = proxy
|
|
15
|
+
@name = name
|
|
16
|
+
@version = version
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def node
|
|
20
|
+
proxy.node
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def components
|
|
24
|
+
proxy.respond_to?(:components) ? proxy.components : proxy.site.components
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def main
|
|
28
|
+
proxy.respond_to?(:main) ? proxy.main : proxy.site.main
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sxl_version
|
|
32
|
+
version
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def core_version
|
|
36
|
+
proxy.core_version
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def use_soc?
|
|
40
|
+
return false unless core_version
|
|
41
|
+
|
|
42
|
+
RSMP::Proxy.version_meets_requirement?(core_version, '>=3.1.5')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_message!(_message); end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
module SXL
|
|
3
|
+
# Registry of SXL interface classes keyed by SXL name and connection side.
|
|
4
|
+
module Registry
|
|
5
|
+
@interfaces = {}
|
|
6
|
+
|
|
7
|
+
def self.register(name, side, klass)
|
|
8
|
+
@interfaces[[name.to_s, side.to_sym]] = klass
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register_interface(klass)
|
|
12
|
+
register sxl_name_for(klass), side_for(klass), klass
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.fetch(name, side)
|
|
16
|
+
@interfaces[[name.to_s, side.to_sym]]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.build(proxy, sxl, side)
|
|
20
|
+
klass = fetch(sxl['name'], side) || default_class(side)
|
|
21
|
+
klass.new(proxy: proxy, name: sxl['name'], version: sxl['version'])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.build_for(proxy, sxl)
|
|
25
|
+
case proxy
|
|
26
|
+
when RSMP::SiteProxy
|
|
27
|
+
build(proxy, sxl, :supervisor)
|
|
28
|
+
when RSMP::SupervisorProxy
|
|
29
|
+
build(proxy, sxl, :site)
|
|
30
|
+
else
|
|
31
|
+
raise ArgumentError, "Unknown proxy class #{proxy.class}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.default_class(side)
|
|
36
|
+
side.to_sym == :site ? SiteInterface : SupervisorInterface
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.side_for(klass)
|
|
40
|
+
return :site if klass < SiteInterface
|
|
41
|
+
return :supervisor if klass < SupervisorInterface
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "Cannot infer SXL interface side for #{klass}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.sxl_name_for(klass)
|
|
47
|
+
namespace = klass.name.split('::')[0...-1].join('::')
|
|
48
|
+
owner = Object.const_get(namespace)
|
|
49
|
+
return owner.sxl_name if owner.respond_to?(:sxl_name)
|
|
50
|
+
|
|
51
|
+
raise ArgumentError, "Cannot infer SXL name for #{klass}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module RSMP
|
|
4
|
+
module SXL
|
|
5
|
+
# SXL interface used by a supervisor-side proxy.
|
|
6
|
+
class SupervisorInterface < Interface
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def_delegators :proxy,
|
|
10
|
+
:request_status,
|
|
11
|
+
:request_status_and_collect,
|
|
12
|
+
:subscribe_to_status,
|
|
13
|
+
:subscribe_to_status_and_collect,
|
|
14
|
+
:unsubscribe_to_status,
|
|
15
|
+
:send_command,
|
|
16
|
+
:send_command_and_collect
|
|
17
|
+
|
|
18
|
+
def process_message(_message); end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -4,8 +4,8 @@ module RSMP
|
|
|
4
4
|
class DetectorLogic < Component
|
|
5
5
|
attr_reader :forced, :value
|
|
6
6
|
|
|
7
|
-
def initialize(node:, id:)
|
|
8
|
-
super(node: node, id: id, grouped: false)
|
|
7
|
+
def initialize(node:, id:, type: nil, name: nil)
|
|
8
|
+
super(node: node, id: id, type: type, name: name, grouped: false)
|
|
9
9
|
@forced = 0
|
|
10
10
|
@value = 0
|
|
11
11
|
end
|
|
@@ -5,8 +5,8 @@ module RSMP
|
|
|
5
5
|
attr_reader :plan, :state
|
|
6
6
|
|
|
7
7
|
# plan is a string, with each character representing a signal phase at a particular second in the cycle
|
|
8
|
-
def initialize(node:, id:)
|
|
9
|
-
super(node: node, id: id, grouped: false)
|
|
8
|
+
def initialize(node:, id:, type: nil, name: nil)
|
|
9
|
+
super(node: node, id: id, type: type, name: name, grouped: false)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def timer
|