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.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +11 -11
  3. data/README.md +19 -3
  4. data/Rakefile +2 -2
  5. data/config/supervisor.yaml +2 -1
  6. data/config/tlc.yaml +2 -2
  7. data/lib/rsmp/cli.rb +29 -5
  8. data/lib/rsmp/component/component.rb +0 -4
  9. data/lib/rsmp/component/component_base.rb +15 -2
  10. data/lib/rsmp/component/component_proxy.rb +1 -1
  11. data/lib/rsmp/component/components.rb +22 -1
  12. data/lib/rsmp/convert/export/json_schema/outputs.rb +1 -0
  13. data/lib/rsmp/convert/export/json_schema/values.rb +6 -4
  14. data/lib/rsmp/convert/export/json_schema.rb +7 -3
  15. data/lib/rsmp/helpers/deep_merge.rb +2 -2
  16. data/lib/rsmp/message.rb +32 -0
  17. data/lib/rsmp/node/site/site.rb +34 -10
  18. data/lib/rsmp/node/supervisor/modules/configuration.rb +32 -5
  19. data/lib/rsmp/node/supervisor/modules/connection.rb +0 -2
  20. data/lib/rsmp/node/supervisor/supervisor.rb +0 -7
  21. data/lib/rsmp/options/options.rb +55 -6
  22. data/lib/rsmp/options/schemas/site.json +6 -3
  23. data/lib/rsmp/options/schemas/supervisor.json +5 -2
  24. data/lib/rsmp/options/schemas/supervisor_site.json +5 -2
  25. data/lib/rsmp/options/site_options.rb +3 -2
  26. data/lib/rsmp/options/supervisor_options.rb +3 -1
  27. data/lib/rsmp/proxy/modules/acknowledgements.rb +2 -0
  28. data/lib/rsmp/proxy/modules/receive.rb +5 -2
  29. data/lib/rsmp/proxy/modules/state.rb +1 -0
  30. data/lib/rsmp/proxy/modules/versions.rb +90 -15
  31. data/lib/rsmp/proxy/proxy.rb +52 -3
  32. data/lib/rsmp/proxy/site/modules/status.rb +5 -3
  33. data/lib/rsmp/proxy/site/site_proxy.rb +68 -35
  34. data/lib/rsmp/proxy/site/sxl_selection.rb +54 -0
  35. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +54 -18
  36. data/lib/rsmp/schema/message_resolution.rb +104 -0
  37. data/lib/rsmp/schema.rb +104 -22
  38. data/lib/rsmp/schema_error.rb +7 -1
  39. data/lib/rsmp/sxl/interface.rb +48 -0
  40. data/lib/rsmp/sxl/registry.rb +55 -0
  41. data/lib/rsmp/sxl/site_interface.rb +10 -0
  42. data/lib/rsmp/sxl/supervisor_interface.rb +21 -0
  43. data/lib/rsmp/tlc/detector_logic.rb +2 -2
  44. data/lib/rsmp/tlc/signal_group.rb +2 -2
  45. data/lib/rsmp/tlc/site_interface.rb +10 -0
  46. data/lib/rsmp/tlc/{traffic_controller_proxy.rb → supervisor_interface.rb} +19 -34
  47. data/lib/rsmp/tlc/traffic_controller.rb +10 -2
  48. data/lib/rsmp/tlc/traffic_controller_site.rb +4 -2
  49. data/lib/rsmp/tlc.rb +10 -0
  50. data/lib/rsmp/version.rb +1 -1
  51. data/lib/rsmp.rb +8 -1
  52. data/rsmp.gemspec +5 -5
  53. data/schemas/core/3.3.0/aggregated_status.json +25 -0
  54. data/schemas/core/3.3.0/aggregated_status_request.json +9 -0
  55. data/schemas/core/3.3.0/alarm.json +71 -0
  56. data/schemas/core/3.3.0/alarm_acknowledge.json +11 -0
  57. data/schemas/core/3.3.0/alarm_issue.json +44 -0
  58. data/schemas/core/3.3.0/alarm_request.json +3 -0
  59. data/schemas/core/3.3.0/alarm_suspend_resume.json +3 -0
  60. data/schemas/core/3.3.0/alarm_suspended_resumed.json +44 -0
  61. data/schemas/core/3.3.0/command_request.json +24 -0
  62. data/schemas/core/3.3.0/command_response.json +35 -0
  63. data/schemas/core/3.3.0/component_list.json +24 -0
  64. data/schemas/core/3.3.0/core.json +40 -0
  65. data/schemas/core/3.3.0/definitions.json +133 -0
  66. data/schemas/core/3.3.0/message_ack.json +11 -0
  67. data/schemas/core/3.3.0/message_not_ack.json +15 -0
  68. data/schemas/core/3.3.0/rsmp.json +142 -0
  69. data/schemas/core/3.3.0/status.json +21 -0
  70. data/schemas/core/3.3.0/status_request.json +5 -0
  71. data/schemas/core/3.3.0/status_response.json +41 -0
  72. data/schemas/core/3.3.0/status_subscribe.json +31 -0
  73. data/schemas/core/3.3.0/status_unsubscribe.json +5 -0
  74. data/schemas/core/3.3.0/status_update.json +41 -0
  75. data/schemas/core/3.3.0/version.json +144 -0
  76. data/schemas/core/3.3.0/watchdog.json +9 -0
  77. data/schemas/tlc/1.3.0/alarms/A0001.json +4 -0
  78. data/schemas/tlc/1.3.0/alarms/A0002.json +4 -0
  79. data/schemas/tlc/1.3.0/alarms/A0003.json +4 -0
  80. data/schemas/tlc/1.3.0/alarms/A0004.json +4 -0
  81. data/schemas/tlc/1.3.0/alarms/A0005.json +4 -0
  82. data/schemas/tlc/1.3.0/alarms/A0006.json +4 -0
  83. data/schemas/tlc/1.3.0/alarms/A0007.json +34 -0
  84. data/schemas/tlc/1.3.0/alarms/A0008.json +30 -0
  85. data/schemas/tlc/1.3.0/alarms/A0009.json +4 -0
  86. data/schemas/tlc/1.3.0/alarms/A0010.json +4 -0
  87. data/schemas/tlc/1.3.0/alarms/A0101.json +4 -0
  88. data/schemas/tlc/1.3.0/alarms/A0201.json +35 -0
  89. data/schemas/tlc/1.3.0/alarms/A0202.json +35 -0
  90. data/schemas/tlc/1.3.0/alarms/A0301.json +92 -0
  91. data/schemas/tlc/1.3.0/alarms/A0302.json +115 -0
  92. data/schemas/tlc/1.3.0/alarms/A0303.json +92 -0
  93. data/schemas/tlc/1.3.0/alarms/A0304.json +115 -0
  94. data/schemas/tlc/1.3.0/alarms/alarms.json +287 -0
  95. data/schemas/tlc/1.3.0/commands/M0001.json +92 -0
  96. data/schemas/tlc/1.3.0/commands/M0002.json +69 -0
  97. data/schemas/tlc/1.3.0/commands/M0003.json +69 -0
  98. data/schemas/tlc/1.3.0/commands/M0004.json +51 -0
  99. data/schemas/tlc/1.3.0/commands/M0005.json +69 -0
  100. data/schemas/tlc/1.3.0/commands/M0006.json +69 -0
  101. data/schemas/tlc/1.3.0/commands/M0007.json +51 -0
  102. data/schemas/tlc/1.3.0/commands/M0008.json +87 -0
  103. data/schemas/tlc/1.3.0/commands/M0010.json +51 -0
  104. data/schemas/tlc/1.3.0/commands/M0011.json +51 -0
  105. data/schemas/tlc/1.3.0/commands/M0012.json +51 -0
  106. data/schemas/tlc/1.3.0/commands/M0013.json +51 -0
  107. data/schemas/tlc/1.3.0/commands/M0014.json +69 -0
  108. data/schemas/tlc/1.3.0/commands/M0015.json +69 -0
  109. data/schemas/tlc/1.3.0/commands/M0016.json +51 -0
  110. data/schemas/tlc/1.3.0/commands/M0017.json +51 -0
  111. data/schemas/tlc/1.3.0/commands/M0018.json +69 -0
  112. data/schemas/tlc/1.3.0/commands/M0019.json +87 -0
  113. data/schemas/tlc/1.3.0/commands/M0020.json +87 -0
  114. data/schemas/tlc/1.3.0/commands/M0021.json +51 -0
  115. data/schemas/tlc/1.3.0/commands/M0022.json +249 -0
  116. data/schemas/tlc/1.3.0/commands/M0023.json +51 -0
  117. data/schemas/tlc/1.3.0/commands/M0024.json +33 -0
  118. data/schemas/tlc/1.3.0/commands/M0103.json +72 -0
  119. data/schemas/tlc/1.3.0/commands/M0104.json +141 -0
  120. data/schemas/tlc/1.3.0/commands/command_requests.json +8 -0
  121. data/schemas/tlc/1.3.0/commands/command_responses.json +8 -0
  122. data/schemas/tlc/1.3.0/commands/commands.json +415 -0
  123. data/schemas/tlc/1.3.0/defs/definitions.json +72 -0
  124. data/schemas/tlc/1.3.0/defs/guards.json +24 -0
  125. data/schemas/tlc/1.3.0/rsmp.json +74 -0
  126. data/schemas/tlc/1.3.0/statuses/S0001.json +109 -0
  127. data/schemas/tlc/1.3.0/statuses/S0002.json +36 -0
  128. data/schemas/tlc/1.3.0/statuses/S0003.json +36 -0
  129. data/schemas/tlc/1.3.0/statuses/S0004.json +36 -0
  130. data/schemas/tlc/1.3.0/statuses/S0005.json +72 -0
  131. data/schemas/tlc/1.3.0/statuses/S0006.json +54 -0
  132. data/schemas/tlc/1.3.0/statuses/S0007.json +73 -0
  133. data/schemas/tlc/1.3.0/statuses/S0008.json +73 -0
  134. data/schemas/tlc/1.3.0/statuses/S0009.json +73 -0
  135. data/schemas/tlc/1.3.0/statuses/S0010.json +73 -0
  136. data/schemas/tlc/1.3.0/statuses/S0011.json +73 -0
  137. data/schemas/tlc/1.3.0/statuses/S0012.json +73 -0
  138. data/schemas/tlc/1.3.0/statuses/S0013.json +54 -0
  139. data/schemas/tlc/1.3.0/statuses/S0014.json +55 -0
  140. data/schemas/tlc/1.3.0/statuses/S0015.json +55 -0
  141. data/schemas/tlc/1.3.0/statuses/S0016.json +36 -0
  142. data/schemas/tlc/1.3.0/statuses/S0017.json +36 -0
  143. data/schemas/tlc/1.3.0/statuses/S0018.json +61 -0
  144. data/schemas/tlc/1.3.0/statuses/S0019.json +36 -0
  145. data/schemas/tlc/1.3.0/statuses/S0020.json +54 -0
  146. data/schemas/tlc/1.3.0/statuses/S0021.json +37 -0
  147. data/schemas/tlc/1.3.0/statuses/S0022.json +36 -0
  148. data/schemas/tlc/1.3.0/statuses/S0023.json +37 -0
  149. data/schemas/tlc/1.3.0/statuses/S0024.json +36 -0
  150. data/schemas/tlc/1.3.0/statuses/S0025.json +162 -0
  151. data/schemas/tlc/1.3.0/statuses/S0026.json +36 -0
  152. data/schemas/tlc/1.3.0/statuses/S0027.json +36 -0
  153. data/schemas/tlc/1.3.0/statuses/S0028.json +36 -0
  154. data/schemas/tlc/1.3.0/statuses/S0029.json +36 -0
  155. data/schemas/tlc/1.3.0/statuses/S0030.json +36 -0
  156. data/schemas/tlc/1.3.0/statuses/S0031.json +36 -0
  157. data/schemas/tlc/1.3.0/statuses/S0032.json +73 -0
  158. data/schemas/tlc/1.3.0/statuses/S0033.json +77 -0
  159. data/schemas/tlc/1.3.0/statuses/S0034.json +36 -0
  160. data/schemas/tlc/1.3.0/statuses/S0035.json +49 -0
  161. data/schemas/tlc/1.3.0/statuses/S0091.json +40 -0
  162. data/schemas/tlc/1.3.0/statuses/S0092.json +40 -0
  163. data/schemas/tlc/1.3.0/statuses/S0095.json +36 -0
  164. data/schemas/tlc/1.3.0/statuses/S0096.json +126 -0
  165. data/schemas/tlc/1.3.0/statuses/S0097.json +54 -0
  166. data/schemas/tlc/1.3.0/statuses/S0098.json +72 -0
  167. data/schemas/tlc/1.3.0/statuses/S0201.json +54 -0
  168. data/schemas/tlc/1.3.0/statuses/S0202.json +54 -0
  169. data/schemas/tlc/1.3.0/statuses/S0203.json +54 -0
  170. data/schemas/tlc/1.3.0/statuses/S0204.json +198 -0
  171. data/schemas/tlc/1.3.0/statuses/S0205.json +54 -0
  172. data/schemas/tlc/1.3.0/statuses/S0206.json +54 -0
  173. data/schemas/tlc/1.3.0/statuses/S0207.json +54 -0
  174. data/schemas/tlc/1.3.0/statuses/S0208.json +198 -0
  175. data/schemas/tlc/1.3.0/statuses/statuses.json +787 -0
  176. data/schemas/tlc/1.3.0/sxl.yaml +2296 -0
  177. 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
- @sxl = @site_settings['sxl']
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
- send_version @site_settings['site_id'], core_versions
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
- sanitized_sxl_version = RSMP::Schema.sanitize_version(sxl_version)
91
- log "Connection to supervisor established, using core #{@core_version}, #{sxl} #{sanitized_sxl_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
- handshake_complete
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); end
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
- raise "Schema type #{type} already loaded" if @schemas[type] && force != true
32
+ type = type.to_sym
33
+ ensure_schema_type_available(type, force)
30
34
 
31
35
  @schemas[type] = {}
32
- Dir.glob("#{type_path}/*").select { |f| File.directory? f }.each do |schema_path|
33
- version = File.basename(schema_path)
34
- file_path = File.join(schema_path, 'rsmp.json')
35
- next unless File.exist? file_path
36
-
37
- @schemas[type][version] = JSONSchemer.schema(
38
- Pathname.new(File.join(schema_path, 'rsmp.json'))
39
- )
40
- end
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
- schemas_path = File.expand_path(File.join(__dir__, '..', '..', 'schemas'))
183
- yaml_path = File.join(schemas_path, type.to_s, version, 'sxl.yaml')
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[:statuses].transform_keys(&:to_sym).transform_values do |status|
188
- (status['arguments'] || {}).keys.map(&:to_sym)
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 a particular schema and version
193
- # raises error if schema is not found
194
- # return nil if validation succeds, otherwise returns an array of errors
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.flat_map do |type, version|
202
- schema = find_schema! type, version, options
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'
@@ -1,6 +1,6 @@
1
1
  module RSMP
2
2
  module Schema
3
- # Base error class for rsmp_schema.
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,10 @@
1
+ module RSMP
2
+ module SXL
3
+ # SXL interface used by a site-side proxy.
4
+ class SiteInterface < Interface
5
+ def process_message(message)
6
+ proxy.process_sxl_request message
7
+ end
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,10 @@
1
+ module RSMP
2
+ # Traffic Light Controller SXL support.
3
+ module TLC
4
+ # Site-side TLC SXL interface.
5
+ class SiteInterface < RSMP::SXL::SiteInterface
6
+ end
7
+
8
+ RSMP::SXL::Registry.register_interface SiteInterface
9
+ end
10
+ end