rsmp 0.43.2 → 0.45.1
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/.github/workflows/rubocop.yaml +1 -1
- data/.github/workflows/sus.yaml +1 -1
- data/.gitignore +0 -1
- data/CHANGELOG.md +12 -1
- data/Gemfile +0 -2
- data/Gemfile.lock +10 -88
- data/README.md +30 -35
- data/Rakefile +2 -2
- data/config/supervisor.yaml +2 -1
- data/config/tlc.yaml +2 -2
- data/documentation/configuration.md +12 -11
- data/documentation/message_distribution.md +1 -2
- data/documentation/tasks.md +1 -2
- data/exe/rsmp +1 -2
- data/lib/rsmp/cli.rb +62 -8
- 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 +19 -0
- data/lib/rsmp/convert/export/json_schema/values.rb +7 -5
- data/lib/rsmp/convert/export/json_schema.rb +15 -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/core_sxl_resolution.rb +69 -0
- data/lib/rsmp/schema/message_resolution.rb +104 -0
- data/lib/rsmp/schema/validation.rb +57 -0
- data/lib/rsmp/schema.rb +87 -32
- 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.0.10/rsmp.json +2 -1
- data/schemas/tlc/1.0.10/sxl.yaml +1 -0
- data/schemas/tlc/1.0.10/sxl_index.json +356 -0
- data/schemas/tlc/1.0.13/rsmp.json +2 -1
- data/schemas/tlc/1.0.13/sxl.yaml +1 -0
- data/schemas/tlc/1.0.13/sxl_index.json +436 -0
- data/schemas/tlc/1.0.14/rsmp.json +2 -1
- data/schemas/tlc/1.0.14/sxl.yaml +1 -0
- data/schemas/tlc/1.0.14/sxl_index.json +468 -0
- data/schemas/tlc/1.0.15/rsmp.json +2 -1
- data/schemas/tlc/1.0.15/sxl.yaml +1 -0
- data/schemas/tlc/1.0.15/sxl_index.json +508 -0
- data/schemas/tlc/1.0.7/rsmp.json +2 -1
- data/schemas/tlc/1.0.7/sxl.yaml +1 -0
- data/schemas/tlc/1.0.7/sxl_index.json +356 -0
- data/schemas/tlc/1.0.8/rsmp.json +2 -1
- data/schemas/tlc/1.0.8/sxl.yaml +1 -0
- data/schemas/tlc/1.0.8/sxl_index.json +356 -0
- data/schemas/tlc/1.0.9/rsmp.json +2 -1
- data/schemas/tlc/1.0.9/sxl.yaml +1 -0
- data/schemas/tlc/1.0.9/sxl_index.json +356 -0
- data/schemas/tlc/1.1.0/rsmp.json +2 -1
- data/schemas/tlc/1.1.0/sxl.yaml +1 -0
- data/schemas/tlc/1.1.0/sxl_index.json +572 -0
- data/schemas/tlc/1.2.0/rsmp.json +2 -1
- data/schemas/tlc/1.2.0/sxl.yaml +1 -0
- data/schemas/tlc/1.2.0/sxl_index.json +571 -0
- data/schemas/tlc/1.2.1/rsmp.json +2 -1
- data/schemas/tlc/1.2.1/sxl.yaml +1 -0
- data/schemas/tlc/1.2.1/sxl_index.json +571 -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 +133 -0
- data/schemas/tlc/1.3.0/defs/guards.json +24 -0
- data/schemas/tlc/1.3.0/rsmp.json +75 -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 +37 -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 +2297 -0
- data/schemas/tlc/1.3.0/sxl_index.json +578 -0
- metadata +157 -15
- data/.github/copilot-instructions.md +0 -33
- data/.rspec +0 -1
- data/cucumber.yml +0 -1
|
@@ -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
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
# Provides JSON Schema validation for RSMP messages across core and SXL versions.
|
|
3
|
+
module Schema
|
|
4
|
+
def self.core_message_type?(message)
|
|
5
|
+
type = message['type']
|
|
6
|
+
%w[
|
|
7
|
+
MessageAck
|
|
8
|
+
MessageNotAck
|
|
9
|
+
Version
|
|
10
|
+
ComponentList
|
|
11
|
+
AggregatedStatus
|
|
12
|
+
AggregatedStatusRequest
|
|
13
|
+
Watchdog
|
|
14
|
+
].include?(type)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.validate_core(message, schemas, options)
|
|
18
|
+
core_version = schemas[:core] || schemas['core']
|
|
19
|
+
raise ArgumentError, 'schemas must include core' unless core_version
|
|
20
|
+
|
|
21
|
+
schema = find_schema! :core, core_version, options
|
|
22
|
+
validate_using_schema(message, schema)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.validate_sxls(message, schemas, options)
|
|
26
|
+
sxl_schemas = schemas.reject { |type, _version| type.to_sym == :core }
|
|
27
|
+
return [] if sxl_schemas.empty? || core_message_type?(message)
|
|
28
|
+
|
|
29
|
+
resolved = resolve_sxl(message, schemas: schemas, **options)
|
|
30
|
+
return validate_resolved_sxl(message, resolved, schemas, options) if resolved
|
|
31
|
+
|
|
32
|
+
all_errors = []
|
|
33
|
+
sxl_schemas.each do |type, version|
|
|
34
|
+
schema = find_core_sxl_schema! type, version, schema_core_version(schemas), options
|
|
35
|
+
errors = validate_using_schema(message, schema)
|
|
36
|
+
return [] if errors.empty?
|
|
37
|
+
|
|
38
|
+
all_errors.concat errors
|
|
39
|
+
end
|
|
40
|
+
all_errors
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Core must pass. SXL-defined messages pass if at least one SXL schema passes.
|
|
44
|
+
def self.validate(message, schemas, options = {})
|
|
45
|
+
raise ArgumentError, 'message missing' unless message
|
|
46
|
+
raise ArgumentError, 'schemas missing' unless schemas
|
|
47
|
+
raise ArgumentError, 'schemas must be a Hash' unless schemas.is_a?(Hash)
|
|
48
|
+
raise ArgumentError, 'schemas cannot be empty' unless schemas.any?
|
|
49
|
+
|
|
50
|
+
errors = validate_core(message, schemas, options)
|
|
51
|
+
errors.concat validate_sxls(message, schemas, options) if errors.empty?
|
|
52
|
+
return nil if errors.empty?
|
|
53
|
+
|
|
54
|
+
errors
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/rsmp/schema.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'json_schemer'
|
|
2
|
+
require 'json'
|
|
2
3
|
|
|
3
4
|
# RSMP (Road Side Message Protocol) schema validation library.
|
|
4
5
|
module RSMP
|
|
@@ -8,13 +9,20 @@ module RSMP
|
|
|
8
9
|
|
|
9
10
|
def self.setup
|
|
10
11
|
@schemas = {}
|
|
11
|
-
|
|
12
|
+
@schema_paths = {}
|
|
13
|
+
@sxl_indexes = {}
|
|
14
|
+
@core_sxl_schemas = {}
|
|
15
|
+
schemas_path = schema_root_path
|
|
12
16
|
Dir.glob("#{schemas_path}/*").select { |f| File.directory? f }.each do |type_path|
|
|
13
17
|
type = File.basename(type_path).to_sym
|
|
14
18
|
load_schema_type type, type_path
|
|
15
19
|
end
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
def self.schema_root_path
|
|
23
|
+
File.expand_path(File.join(__dir__, '..', '..', 'schemas'))
|
|
24
|
+
end
|
|
25
|
+
|
|
18
26
|
# load an schema from a folder. schemas are organized by version, and contain
|
|
19
27
|
# json schema files, with the entry point being rsmp.jspon, eg:
|
|
20
28
|
# tlc
|
|
@@ -26,23 +34,43 @@ module RSMP
|
|
|
26
34
|
#
|
|
27
35
|
# an error is raised if the schema type already exists, and force is not set to true
|
|
28
36
|
def self.load_schema_type(type, type_path, force: false)
|
|
29
|
-
|
|
37
|
+
type = type.to_sym
|
|
38
|
+
ensure_schema_type_available(type, force)
|
|
30
39
|
|
|
31
40
|
@schemas[type] = {}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
@schema_paths ||= {}
|
|
42
|
+
@schema_paths[type] = {}
|
|
43
|
+
clear_sxl_index(type)
|
|
44
|
+
clear_core_sxl_schemas(type)
|
|
45
|
+
schema_version_paths(type_path).each { |schema_path| load_schema_version(type, schema_path) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.ensure_schema_type_available(type, force)
|
|
49
|
+
raise "Schema type #{type} already loaded" if @schemas[type] && force != true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.schema_version_paths(type_path)
|
|
53
|
+
Dir.glob("#{type_path}/*").select { |path| File.directory? path }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.load_schema_version(type, schema_path)
|
|
57
|
+
version = File.basename(schema_path)
|
|
58
|
+
file_path = File.join(schema_path, 'rsmp.json')
|
|
59
|
+
return unless File.exist? file_path
|
|
60
|
+
|
|
61
|
+
@schemas[type][version] = JSONSchemer.schema(Pathname.new(file_path))
|
|
62
|
+
@schema_paths[type][version] = schema_path
|
|
63
|
+
clear_sxl_index(type, version)
|
|
64
|
+
clear_core_sxl_schemas(type, version)
|
|
41
65
|
end
|
|
42
66
|
|
|
43
67
|
# remove a schema type
|
|
44
68
|
def self.remove_schema_type(type)
|
|
69
|
+
type = type.to_sym
|
|
45
70
|
schemas.delete type
|
|
71
|
+
@schema_paths&.delete type
|
|
72
|
+
clear_sxl_index(type)
|
|
73
|
+
clear_core_sxl_schemas(type)
|
|
46
74
|
end
|
|
47
75
|
|
|
48
76
|
# get schemas types
|
|
@@ -174,37 +202,64 @@ module RSMP
|
|
|
174
202
|
find_schema(type, version, options) != nil
|
|
175
203
|
end
|
|
176
204
|
|
|
205
|
+
def self.sxl_metadata(type, version, options = {})
|
|
206
|
+
version = sanitize_version version if options[:lenient]
|
|
207
|
+
find_schema! type, version
|
|
208
|
+
|
|
209
|
+
sxl_index(type, version).fetch('meta', {})
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.sxl_prefix(type, version, options = {})
|
|
213
|
+
sxl_metadata(type, version, options)['prefix']
|
|
214
|
+
end
|
|
215
|
+
|
|
177
216
|
# return a catalogue of statuses for a particular schema type and version
|
|
178
217
|
# returns a hash of { status_code_id_sym => [arg_name_sym, ...] }
|
|
179
|
-
# raises an error if the schema type/version is not found, or has no
|
|
218
|
+
# raises an error if the schema type/version is not found, or has no status catalogue
|
|
180
219
|
def self.status_catalogue(type, version)
|
|
220
|
+
sxl_catalogue(type, version, :statuses).transform_keys(&:to_sym).transform_values do |status|
|
|
221
|
+
status.fetch('arguments', []).map(&:to_sym)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.sxl_catalogue(type, version, kind)
|
|
181
226
|
find_schema! type, version
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
raise "No sxl.yaml for #{type} #{version}" unless File.exist?(yaml_path)
|
|
227
|
+
catalogue = sxl_index(type, version)[kind.to_s]
|
|
228
|
+
raise "No #{kind} catalogue for #{type} #{version}" unless catalogue
|
|
185
229
|
|
|
186
|
-
|
|
187
|
-
sxl[:statuses].transform_keys(&:to_sym).transform_values do |status|
|
|
188
|
-
(status['arguments'] || {}).keys.map(&:to_sym)
|
|
189
|
-
end
|
|
230
|
+
catalogue
|
|
190
231
|
end
|
|
191
232
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def self.validate(message, schemas, options = {})
|
|
196
|
-
raise ArgumentError, 'message missing' unless message
|
|
197
|
-
raise ArgumentError, 'schemas missing' unless schemas
|
|
198
|
-
raise ArgumentError, 'schemas must be a Hash' unless schemas.is_a?(Hash)
|
|
199
|
-
raise ArgumentError, 'schemas cannot be empty' unless schemas.any?
|
|
233
|
+
def self.clear_sxl_index(type = nil, version = nil)
|
|
234
|
+
@sxl_indexes ||= {}
|
|
235
|
+
return @sxl_indexes.clear unless type
|
|
200
236
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
237
|
+
type = type.to_sym
|
|
238
|
+
if version
|
|
239
|
+
@sxl_indexes.delete([type, version.to_s])
|
|
240
|
+
else
|
|
241
|
+
@sxl_indexes.delete_if { |key, _value| key.first == type }
|
|
204
242
|
end
|
|
205
|
-
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def self.sxl_index(type, version)
|
|
246
|
+
key = [type.to_sym, version.to_s]
|
|
247
|
+
@sxl_indexes ||= {}
|
|
248
|
+
@sxl_indexes[key] ||= load_sxl_index(type, version)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def self.load_sxl_index(type, version)
|
|
252
|
+
schema_path = @schema_paths&.dig(type.to_sym, version.to_s)
|
|
253
|
+
raise UnknownSchemaVersionError, "Unknown schema version #{type} #{version}" unless schema_path
|
|
206
254
|
|
|
207
|
-
|
|
255
|
+
index_path = File.join(schema_path, 'sxl_index.json')
|
|
256
|
+
raise Error, "Missing SXL index #{index_path}" unless File.exist?(index_path)
|
|
257
|
+
|
|
258
|
+
JSON.parse(File.read(index_path, encoding: 'UTF-8'))
|
|
208
259
|
end
|
|
209
260
|
end
|
|
210
261
|
end
|
|
262
|
+
|
|
263
|
+
require_relative 'schema/core_sxl_resolution'
|
|
264
|
+
require_relative 'schema/message_resolution'
|
|
265
|
+
require_relative 'schema/validation'
|
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
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
# Proxy for handling communication with a remote Traffic Light Controller (TLC).
|
|
2
|
-
# Provides high-level methods for interacting with TLC functionality.
|
|
3
|
-
# Acts as a mirror of the remote TLC by automatically subscribing to status updates.
|
|
4
|
-
|
|
5
1
|
module RSMP
|
|
2
|
+
# Traffic Light Controller SXL support.
|
|
6
3
|
module TLC
|
|
7
|
-
#
|
|
8
|
-
class
|
|
4
|
+
# Supervisor-side TLC SXL interface.
|
|
5
|
+
class SupervisorInterface < RSMP::SXL::SupervisorInterface
|
|
9
6
|
include Proxy::Control
|
|
10
7
|
include Proxy::IO
|
|
11
8
|
include Proxy::Plans
|
|
@@ -16,16 +13,7 @@ module RSMP
|
|
|
16
13
|
attr_reader :timeplan_source, :timeplan, :timeouts,
|
|
17
14
|
:functional_position, :yellow_flash, :traffic_situation
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
def current_plan
|
|
21
|
-
@timeplan
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def plan_source
|
|
25
|
-
@timeplan_source
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def initialize(options)
|
|
16
|
+
def initialize(...)
|
|
29
17
|
super
|
|
30
18
|
@timeplan_source = nil
|
|
31
19
|
@timeplan = nil
|
|
@@ -35,6 +23,14 @@ module RSMP
|
|
|
35
23
|
@timeouts = node.supervisor_settings.dig('default', 'timeouts') || {}
|
|
36
24
|
end
|
|
37
25
|
|
|
26
|
+
def current_plan
|
|
27
|
+
@timeplan
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def plan_source
|
|
31
|
+
@timeplan_source
|
|
32
|
+
end
|
|
33
|
+
|
|
38
34
|
def subscribe_to_timeplan
|
|
39
35
|
validate_ready 'subscribe to timeplan'
|
|
40
36
|
|
|
@@ -49,32 +45,23 @@ module RSMP
|
|
|
49
45
|
subscribe_to_status status_list, component: main.c_id
|
|
50
46
|
end
|
|
51
47
|
|
|
52
|
-
# Override status update processing to automatically store cached status values.
|
|
53
48
|
def process_status_update(message)
|
|
54
|
-
super
|
|
55
|
-
|
|
56
49
|
status_values = message.attribute('sS')
|
|
57
50
|
return unless status_values
|
|
58
51
|
|
|
59
52
|
status_values.each { |item| cache_status_item(item) }
|
|
60
53
|
end
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
main&.statuses&.dig('S0014') || {}
|
|
55
|
+
def process_message(message)
|
|
56
|
+
process_status_update message if message.is_a?(RSMP::StatusUpdate)
|
|
65
57
|
end
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def use_soc?
|
|
70
|
-
return false unless core_version
|
|
71
|
-
|
|
72
|
-
RSMP::Proxy.version_meets_requirement?(core_version, '>=3.1.5')
|
|
59
|
+
def timeplan_attributes
|
|
60
|
+
main&.statuses&.dig('S0014') || {}
|
|
73
61
|
end
|
|
74
62
|
|
|
75
63
|
private
|
|
76
64
|
|
|
77
|
-
# Automatically subscribe to key TLC statuses to keep proxy in sync.
|
|
78
65
|
def auto_subscribe_to_statuses
|
|
79
66
|
return unless main
|
|
80
67
|
|
|
@@ -82,17 +69,14 @@ module RSMP
|
|
|
82
69
|
subscribe_to_key_statuses
|
|
83
70
|
end
|
|
84
71
|
|
|
85
|
-
# Look up security code for a given level from site settings.
|
|
86
|
-
# Expects @site_settings['security_codes'] = { 1 => 'code1', 2 => 'code2' }
|
|
87
72
|
def security_code_for(level)
|
|
88
|
-
codes =
|
|
73
|
+
codes = proxy.site_settings&.dig('security_codes') || {}
|
|
89
74
|
code = codes[level] || codes[level.to_s]
|
|
90
75
|
raise ArgumentError, "Security code for level #{level} is not configured" unless code
|
|
91
76
|
|
|
92
77
|
code
|
|
93
78
|
end
|
|
94
79
|
|
|
95
|
-
# Process a single status item and update the corresponding cached value.
|
|
96
80
|
def cache_status_item(item)
|
|
97
81
|
case item['sCI']
|
|
98
82
|
when 'S0007' then @functional_position = item['s'] if item['n'] == 'status'
|
|
@@ -102,7 +86,6 @@ module RSMP
|
|
|
102
86
|
end
|
|
103
87
|
end
|
|
104
88
|
|
|
105
|
-
# Update cached values for S0014 (timeplan) status attributes.
|
|
106
89
|
def cache_s0014_attribute(item)
|
|
107
90
|
case item['n']
|
|
108
91
|
when 'status' then @timeplan = item['s'].to_i
|
|
@@ -110,5 +93,7 @@ module RSMP
|
|
|
110
93
|
end
|
|
111
94
|
end
|
|
112
95
|
end
|
|
96
|
+
|
|
97
|
+
RSMP::SXL::Registry.register_interface SupervisorInterface
|
|
113
98
|
end
|
|
114
99
|
end
|
|
@@ -20,8 +20,16 @@ module RSMP
|
|
|
20
20
|
attr_reader :pos, :cycle_time, :plan, :cycle_counter,
|
|
21
21
|
:functional_position, :startup_sequence
|
|
22
22
|
|
|
23
|
-
def initialize(node:, id:,
|
|
24
|
-
super(
|
|
23
|
+
def initialize(node:, id:, **options)
|
|
24
|
+
super(
|
|
25
|
+
node: node,
|
|
26
|
+
id: id,
|
|
27
|
+
type: options[:type],
|
|
28
|
+
name: options[:name],
|
|
29
|
+
ntsoid: options[:ntsoid],
|
|
30
|
+
xnid: options[:xnid],
|
|
31
|
+
grouped: true
|
|
32
|
+
)
|
|
25
33
|
@signal_groups = []
|
|
26
34
|
@detector_logics = []
|
|
27
35
|
@plans = options[:signal_plans]
|
|
@@ -63,6 +63,8 @@ module RSMP
|
|
|
63
63
|
when 'main'
|
|
64
64
|
TrafficController.new node: self,
|
|
65
65
|
id: id,
|
|
66
|
+
type: type,
|
|
67
|
+
name: settings['name'],
|
|
66
68
|
ntsoid: settings['ntsOId'],
|
|
67
69
|
xnid: settings['xNId'],
|
|
68
70
|
startup_sequence: @startup_sequence,
|
|
@@ -70,11 +72,11 @@ module RSMP
|
|
|
70
72
|
live_output: @site_settings['live_output'],
|
|
71
73
|
inputs: @site_settings['inputs']
|
|
72
74
|
when 'signal_group'
|
|
73
|
-
group = SignalGroup.new node: self, id: id
|
|
75
|
+
group = SignalGroup.new node: self, id: id, type: type, name: settings['name']
|
|
74
76
|
main.add_signal_group group
|
|
75
77
|
group
|
|
76
78
|
when 'detector_logic'
|
|
77
|
-
logic = DetectorLogic.new node: self, id: id
|
|
79
|
+
logic = DetectorLogic.new node: self, id: id, type: type, name: settings['name']
|
|
78
80
|
main.add_detector_logic logic
|
|
79
81
|
logic
|
|
80
82
|
end
|