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.
Files changed (222) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yaml +1 -1
  3. data/.github/workflows/sus.yaml +1 -1
  4. data/.gitignore +0 -1
  5. data/CHANGELOG.md +12 -1
  6. data/Gemfile +0 -2
  7. data/Gemfile.lock +10 -88
  8. data/README.md +30 -35
  9. data/Rakefile +2 -2
  10. data/config/supervisor.yaml +2 -1
  11. data/config/tlc.yaml +2 -2
  12. data/documentation/configuration.md +12 -11
  13. data/documentation/message_distribution.md +1 -2
  14. data/documentation/tasks.md +1 -2
  15. data/exe/rsmp +1 -2
  16. data/lib/rsmp/cli.rb +62 -8
  17. data/lib/rsmp/component/component.rb +0 -4
  18. data/lib/rsmp/component/component_base.rb +15 -2
  19. data/lib/rsmp/component/component_proxy.rb +1 -1
  20. data/lib/rsmp/component/components.rb +22 -1
  21. data/lib/rsmp/convert/export/json_schema/outputs.rb +19 -0
  22. data/lib/rsmp/convert/export/json_schema/values.rb +7 -5
  23. data/lib/rsmp/convert/export/json_schema.rb +15 -3
  24. data/lib/rsmp/helpers/deep_merge.rb +2 -2
  25. data/lib/rsmp/message.rb +32 -0
  26. data/lib/rsmp/node/site/site.rb +34 -10
  27. data/lib/rsmp/node/supervisor/modules/configuration.rb +32 -5
  28. data/lib/rsmp/node/supervisor/modules/connection.rb +0 -2
  29. data/lib/rsmp/node/supervisor/supervisor.rb +0 -7
  30. data/lib/rsmp/options/options.rb +55 -6
  31. data/lib/rsmp/options/schemas/site.json +6 -3
  32. data/lib/rsmp/options/schemas/supervisor.json +5 -2
  33. data/lib/rsmp/options/schemas/supervisor_site.json +5 -2
  34. data/lib/rsmp/options/site_options.rb +3 -2
  35. data/lib/rsmp/options/supervisor_options.rb +3 -1
  36. data/lib/rsmp/proxy/modules/acknowledgements.rb +2 -0
  37. data/lib/rsmp/proxy/modules/receive.rb +5 -2
  38. data/lib/rsmp/proxy/modules/state.rb +1 -0
  39. data/lib/rsmp/proxy/modules/versions.rb +90 -15
  40. data/lib/rsmp/proxy/proxy.rb +52 -3
  41. data/lib/rsmp/proxy/site/modules/status.rb +5 -3
  42. data/lib/rsmp/proxy/site/site_proxy.rb +68 -35
  43. data/lib/rsmp/proxy/site/sxl_selection.rb +54 -0
  44. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +54 -18
  45. data/lib/rsmp/schema/core_sxl_resolution.rb +69 -0
  46. data/lib/rsmp/schema/message_resolution.rb +104 -0
  47. data/lib/rsmp/schema/validation.rb +57 -0
  48. data/lib/rsmp/schema.rb +87 -32
  49. data/lib/rsmp/schema_error.rb +7 -1
  50. data/lib/rsmp/sxl/interface.rb +48 -0
  51. data/lib/rsmp/sxl/registry.rb +55 -0
  52. data/lib/rsmp/sxl/site_interface.rb +10 -0
  53. data/lib/rsmp/sxl/supervisor_interface.rb +21 -0
  54. data/lib/rsmp/tlc/detector_logic.rb +2 -2
  55. data/lib/rsmp/tlc/signal_group.rb +2 -2
  56. data/lib/rsmp/tlc/site_interface.rb +10 -0
  57. data/lib/rsmp/tlc/{traffic_controller_proxy.rb → supervisor_interface.rb} +19 -34
  58. data/lib/rsmp/tlc/traffic_controller.rb +10 -2
  59. data/lib/rsmp/tlc/traffic_controller_site.rb +4 -2
  60. data/lib/rsmp/tlc.rb +10 -0
  61. data/lib/rsmp/version.rb +1 -1
  62. data/lib/rsmp.rb +8 -1
  63. data/rsmp.gemspec +5 -5
  64. data/schemas/core/3.3.0/aggregated_status.json +25 -0
  65. data/schemas/core/3.3.0/aggregated_status_request.json +9 -0
  66. data/schemas/core/3.3.0/alarm.json +71 -0
  67. data/schemas/core/3.3.0/alarm_acknowledge.json +11 -0
  68. data/schemas/core/3.3.0/alarm_issue.json +44 -0
  69. data/schemas/core/3.3.0/alarm_request.json +3 -0
  70. data/schemas/core/3.3.0/alarm_suspend_resume.json +3 -0
  71. data/schemas/core/3.3.0/alarm_suspended_resumed.json +44 -0
  72. data/schemas/core/3.3.0/command_request.json +24 -0
  73. data/schemas/core/3.3.0/command_response.json +35 -0
  74. data/schemas/core/3.3.0/component_list.json +24 -0
  75. data/schemas/core/3.3.0/core.json +40 -0
  76. data/schemas/core/3.3.0/definitions.json +133 -0
  77. data/schemas/core/3.3.0/message_ack.json +11 -0
  78. data/schemas/core/3.3.0/message_not_ack.json +15 -0
  79. data/schemas/core/3.3.0/rsmp.json +142 -0
  80. data/schemas/core/3.3.0/status.json +21 -0
  81. data/schemas/core/3.3.0/status_request.json +5 -0
  82. data/schemas/core/3.3.0/status_response.json +41 -0
  83. data/schemas/core/3.3.0/status_subscribe.json +31 -0
  84. data/schemas/core/3.3.0/status_unsubscribe.json +5 -0
  85. data/schemas/core/3.3.0/status_update.json +41 -0
  86. data/schemas/core/3.3.0/version.json +144 -0
  87. data/schemas/core/3.3.0/watchdog.json +9 -0
  88. data/schemas/tlc/1.0.10/rsmp.json +2 -1
  89. data/schemas/tlc/1.0.10/sxl.yaml +1 -0
  90. data/schemas/tlc/1.0.10/sxl_index.json +356 -0
  91. data/schemas/tlc/1.0.13/rsmp.json +2 -1
  92. data/schemas/tlc/1.0.13/sxl.yaml +1 -0
  93. data/schemas/tlc/1.0.13/sxl_index.json +436 -0
  94. data/schemas/tlc/1.0.14/rsmp.json +2 -1
  95. data/schemas/tlc/1.0.14/sxl.yaml +1 -0
  96. data/schemas/tlc/1.0.14/sxl_index.json +468 -0
  97. data/schemas/tlc/1.0.15/rsmp.json +2 -1
  98. data/schemas/tlc/1.0.15/sxl.yaml +1 -0
  99. data/schemas/tlc/1.0.15/sxl_index.json +508 -0
  100. data/schemas/tlc/1.0.7/rsmp.json +2 -1
  101. data/schemas/tlc/1.0.7/sxl.yaml +1 -0
  102. data/schemas/tlc/1.0.7/sxl_index.json +356 -0
  103. data/schemas/tlc/1.0.8/rsmp.json +2 -1
  104. data/schemas/tlc/1.0.8/sxl.yaml +1 -0
  105. data/schemas/tlc/1.0.8/sxl_index.json +356 -0
  106. data/schemas/tlc/1.0.9/rsmp.json +2 -1
  107. data/schemas/tlc/1.0.9/sxl.yaml +1 -0
  108. data/schemas/tlc/1.0.9/sxl_index.json +356 -0
  109. data/schemas/tlc/1.1.0/rsmp.json +2 -1
  110. data/schemas/tlc/1.1.0/sxl.yaml +1 -0
  111. data/schemas/tlc/1.1.0/sxl_index.json +572 -0
  112. data/schemas/tlc/1.2.0/rsmp.json +2 -1
  113. data/schemas/tlc/1.2.0/sxl.yaml +1 -0
  114. data/schemas/tlc/1.2.0/sxl_index.json +571 -0
  115. data/schemas/tlc/1.2.1/rsmp.json +2 -1
  116. data/schemas/tlc/1.2.1/sxl.yaml +1 -0
  117. data/schemas/tlc/1.2.1/sxl_index.json +571 -0
  118. data/schemas/tlc/1.3.0/alarms/A0001.json +4 -0
  119. data/schemas/tlc/1.3.0/alarms/A0002.json +4 -0
  120. data/schemas/tlc/1.3.0/alarms/A0003.json +4 -0
  121. data/schemas/tlc/1.3.0/alarms/A0004.json +4 -0
  122. data/schemas/tlc/1.3.0/alarms/A0005.json +4 -0
  123. data/schemas/tlc/1.3.0/alarms/A0006.json +4 -0
  124. data/schemas/tlc/1.3.0/alarms/A0007.json +34 -0
  125. data/schemas/tlc/1.3.0/alarms/A0008.json +30 -0
  126. data/schemas/tlc/1.3.0/alarms/A0009.json +4 -0
  127. data/schemas/tlc/1.3.0/alarms/A0010.json +4 -0
  128. data/schemas/tlc/1.3.0/alarms/A0101.json +4 -0
  129. data/schemas/tlc/1.3.0/alarms/A0201.json +35 -0
  130. data/schemas/tlc/1.3.0/alarms/A0202.json +35 -0
  131. data/schemas/tlc/1.3.0/alarms/A0301.json +92 -0
  132. data/schemas/tlc/1.3.0/alarms/A0302.json +115 -0
  133. data/schemas/tlc/1.3.0/alarms/A0303.json +92 -0
  134. data/schemas/tlc/1.3.0/alarms/A0304.json +115 -0
  135. data/schemas/tlc/1.3.0/alarms/alarms.json +287 -0
  136. data/schemas/tlc/1.3.0/commands/M0001.json +92 -0
  137. data/schemas/tlc/1.3.0/commands/M0002.json +69 -0
  138. data/schemas/tlc/1.3.0/commands/M0003.json +69 -0
  139. data/schemas/tlc/1.3.0/commands/M0004.json +51 -0
  140. data/schemas/tlc/1.3.0/commands/M0005.json +69 -0
  141. data/schemas/tlc/1.3.0/commands/M0006.json +69 -0
  142. data/schemas/tlc/1.3.0/commands/M0007.json +51 -0
  143. data/schemas/tlc/1.3.0/commands/M0008.json +87 -0
  144. data/schemas/tlc/1.3.0/commands/M0010.json +51 -0
  145. data/schemas/tlc/1.3.0/commands/M0011.json +51 -0
  146. data/schemas/tlc/1.3.0/commands/M0012.json +51 -0
  147. data/schemas/tlc/1.3.0/commands/M0013.json +51 -0
  148. data/schemas/tlc/1.3.0/commands/M0014.json +69 -0
  149. data/schemas/tlc/1.3.0/commands/M0015.json +69 -0
  150. data/schemas/tlc/1.3.0/commands/M0016.json +51 -0
  151. data/schemas/tlc/1.3.0/commands/M0017.json +51 -0
  152. data/schemas/tlc/1.3.0/commands/M0018.json +69 -0
  153. data/schemas/tlc/1.3.0/commands/M0019.json +87 -0
  154. data/schemas/tlc/1.3.0/commands/M0020.json +87 -0
  155. data/schemas/tlc/1.3.0/commands/M0021.json +51 -0
  156. data/schemas/tlc/1.3.0/commands/M0022.json +249 -0
  157. data/schemas/tlc/1.3.0/commands/M0023.json +51 -0
  158. data/schemas/tlc/1.3.0/commands/M0024.json +33 -0
  159. data/schemas/tlc/1.3.0/commands/M0103.json +72 -0
  160. data/schemas/tlc/1.3.0/commands/M0104.json +141 -0
  161. data/schemas/tlc/1.3.0/commands/command_requests.json +8 -0
  162. data/schemas/tlc/1.3.0/commands/command_responses.json +8 -0
  163. data/schemas/tlc/1.3.0/commands/commands.json +415 -0
  164. data/schemas/tlc/1.3.0/defs/definitions.json +133 -0
  165. data/schemas/tlc/1.3.0/defs/guards.json +24 -0
  166. data/schemas/tlc/1.3.0/rsmp.json +75 -0
  167. data/schemas/tlc/1.3.0/statuses/S0001.json +109 -0
  168. data/schemas/tlc/1.3.0/statuses/S0002.json +36 -0
  169. data/schemas/tlc/1.3.0/statuses/S0003.json +36 -0
  170. data/schemas/tlc/1.3.0/statuses/S0004.json +36 -0
  171. data/schemas/tlc/1.3.0/statuses/S0005.json +72 -0
  172. data/schemas/tlc/1.3.0/statuses/S0006.json +54 -0
  173. data/schemas/tlc/1.3.0/statuses/S0007.json +73 -0
  174. data/schemas/tlc/1.3.0/statuses/S0008.json +73 -0
  175. data/schemas/tlc/1.3.0/statuses/S0009.json +73 -0
  176. data/schemas/tlc/1.3.0/statuses/S0010.json +73 -0
  177. data/schemas/tlc/1.3.0/statuses/S0011.json +73 -0
  178. data/schemas/tlc/1.3.0/statuses/S0012.json +73 -0
  179. data/schemas/tlc/1.3.0/statuses/S0013.json +54 -0
  180. data/schemas/tlc/1.3.0/statuses/S0014.json +55 -0
  181. data/schemas/tlc/1.3.0/statuses/S0015.json +55 -0
  182. data/schemas/tlc/1.3.0/statuses/S0016.json +36 -0
  183. data/schemas/tlc/1.3.0/statuses/S0017.json +36 -0
  184. data/schemas/tlc/1.3.0/statuses/S0018.json +61 -0
  185. data/schemas/tlc/1.3.0/statuses/S0019.json +36 -0
  186. data/schemas/tlc/1.3.0/statuses/S0020.json +54 -0
  187. data/schemas/tlc/1.3.0/statuses/S0021.json +37 -0
  188. data/schemas/tlc/1.3.0/statuses/S0022.json +36 -0
  189. data/schemas/tlc/1.3.0/statuses/S0023.json +37 -0
  190. data/schemas/tlc/1.3.0/statuses/S0024.json +37 -0
  191. data/schemas/tlc/1.3.0/statuses/S0025.json +162 -0
  192. data/schemas/tlc/1.3.0/statuses/S0026.json +36 -0
  193. data/schemas/tlc/1.3.0/statuses/S0027.json +36 -0
  194. data/schemas/tlc/1.3.0/statuses/S0028.json +36 -0
  195. data/schemas/tlc/1.3.0/statuses/S0029.json +36 -0
  196. data/schemas/tlc/1.3.0/statuses/S0030.json +36 -0
  197. data/schemas/tlc/1.3.0/statuses/S0031.json +36 -0
  198. data/schemas/tlc/1.3.0/statuses/S0032.json +73 -0
  199. data/schemas/tlc/1.3.0/statuses/S0033.json +77 -0
  200. data/schemas/tlc/1.3.0/statuses/S0034.json +36 -0
  201. data/schemas/tlc/1.3.0/statuses/S0035.json +49 -0
  202. data/schemas/tlc/1.3.0/statuses/S0091.json +40 -0
  203. data/schemas/tlc/1.3.0/statuses/S0092.json +40 -0
  204. data/schemas/tlc/1.3.0/statuses/S0095.json +36 -0
  205. data/schemas/tlc/1.3.0/statuses/S0096.json +126 -0
  206. data/schemas/tlc/1.3.0/statuses/S0097.json +54 -0
  207. data/schemas/tlc/1.3.0/statuses/S0098.json +72 -0
  208. data/schemas/tlc/1.3.0/statuses/S0201.json +54 -0
  209. data/schemas/tlc/1.3.0/statuses/S0202.json +54 -0
  210. data/schemas/tlc/1.3.0/statuses/S0203.json +54 -0
  211. data/schemas/tlc/1.3.0/statuses/S0204.json +198 -0
  212. data/schemas/tlc/1.3.0/statuses/S0205.json +54 -0
  213. data/schemas/tlc/1.3.0/statuses/S0206.json +54 -0
  214. data/schemas/tlc/1.3.0/statuses/S0207.json +54 -0
  215. data/schemas/tlc/1.3.0/statuses/S0208.json +198 -0
  216. data/schemas/tlc/1.3.0/statuses/statuses.json +787 -0
  217. data/schemas/tlc/1.3.0/sxl.yaml +2297 -0
  218. data/schemas/tlc/1.3.0/sxl_index.json +578 -0
  219. metadata +157 -15
  220. data/.github/copilot-instructions.md +0 -33
  221. data/.rspec +0 -1
  222. data/cucumber.yml +0 -1
@@ -15,6 +15,27 @@ module RSMP
15
15
  end
16
16
  end
17
17
 
18
+ def core_3_3?
19
+ core_version && version_meets_requirement?(core_version, '>=3.3.0')
20
+ end
21
+
22
+ def configured_sxls
23
+ (@site_settings['sxls'] || []).map { |item| item.transform_keys(&:to_s) }
24
+ end
25
+
26
+ def primary_configured_sxl
27
+ configured_sxls.first
28
+ end
29
+
30
+ def sxl_request_items
31
+ configured_sxls.map do |sxl|
32
+ item = { 'name' => sxl['name'], 'version' => sxl['version'].to_s }
33
+ prefix = RSMP::Schema.sxl_prefix(sxl['name'], sxl['version'], lenient: true)
34
+ item['prefix'] = prefix if prefix
35
+ item
36
+ end
37
+ end
38
+
18
39
  def check_core_version(message)
19
40
  versions = core_versions
20
41
  # find versions that both we and the client support
@@ -36,33 +57,87 @@ module RSMP
36
57
  end
37
58
 
38
59
  def send_version(site_id, core_versions)
39
- versions = if core_versions == 'latest'
40
- [RSMP::Schema.latest_core_version]
41
- elsif core_versions == 'all'
42
- RSMP::Schema.core_versions
43
- else
44
- [core_versions].flatten
45
- end
46
- versions_array = versions.map { |v| { 'vers' => v } }
60
+ send_version_message(site_id, core_versions, step: nil)
61
+ end
62
+
63
+ def send_version_request(site_id, core_versions)
64
+ send_version_message(site_id, core_versions, step: 'Request')
65
+ end
66
+
67
+ def send_version_response(site_id, core_versions)
68
+ if core_3_3?
69
+ send_message Version.new({
70
+ 'step' => 'Response',
71
+ 'RSMP' => [{ 'vers' => core_version }],
72
+ 'supervisorId' => site_id,
73
+ 'SXLS' => version_response_sxls,
74
+ 'receiveAlarms' => @site_settings['receive_alarms'] != false
75
+ }), validate: false
76
+ else
77
+ send_version_message(site_id, core_versions, step: nil)
78
+ end
79
+ end
80
+
81
+ def send_version_message(site_id, core_versions, step:)
82
+ attributes = version_message_attributes(site_id, core_versions)
83
+ attributes.merge!(version_request_attributes) if step == 'Request'
84
+ send_message Version.new(attributes), validate: false
85
+ end
47
86
 
48
- site_id_array = [site_id].flatten.map { |id| { 'sId' => id } }
87
+ def version_message_attributes(site_id, core_versions)
88
+ primary = primary_configured_sxl
89
+ attributes = {
90
+ 'RSMP' => version_items(core_versions),
91
+ 'siteId' => site_id_items(site_id)
92
+ }
93
+ attributes['SXL'] = primary['version'].to_s if primary
94
+ attributes
95
+ end
49
96
 
50
- version_response = Version.new({
51
- 'RSMP' => versions_array,
52
- 'siteId' => site_id_array,
53
- 'SXL' => sxl_version.to_s
54
- })
55
- send_message version_response
97
+ def version_items(core_versions)
98
+ normalized_core_versions(core_versions).map { |version| { 'vers' => version } }
99
+ end
100
+
101
+ def normalized_core_versions(core_versions)
102
+ case core_versions
103
+ when 'latest'
104
+ [RSMP::Schema.latest_core_version]
105
+ when 'all'
106
+ RSMP::Schema.core_versions
107
+ else
108
+ [core_versions].flatten
109
+ end
110
+ end
111
+
112
+ def site_id_items(site_id)
113
+ [site_id].flatten.map { |id| { 'sId' => id } }
114
+ end
115
+
116
+ def version_request_attributes
117
+ {
118
+ 'step' => 'Request',
119
+ 'SXLS' => sxl_request_items
120
+ }
121
+ end
122
+
123
+ def version_response_sxls
124
+ accepted_sxls + rejected_sxls
56
125
  end
57
126
 
58
127
  def version_acknowledged; end
59
128
 
129
+ def component_list_acknowledged; end
130
+
60
131
  # Use Gem class to check version requirement
61
132
  # Requirement must be a string like '1.1', '>=1.0.3' or '<2.1.4',
62
133
  # or list of strings, like ['<=1.4','<1.5']
63
134
  def self.version_meets_requirement?(version, requirement)
64
135
  Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(version))
65
136
  end
137
+
138
+ def version_meets_requirement?(version, requirement)
139
+ RSMP::Proxy::Modules::Versions.version_meets_requirement?(version, requirement)
140
+ end
66
141
  end
67
142
  end
68
143
  end
@@ -17,7 +17,9 @@ module RSMP
17
17
  include Modules::Versions
18
18
  include Modules::Tasks
19
19
 
20
- attr_reader :state, :archive, :connection_info, :sxl, :collector, :ip, :port, :node, :core_version
20
+ attr_reader :state, :archive, :connection_info, :sxls, :accepted_sxls, :rejected_sxls,
21
+ :collector, :ip, :port, :node, :core_version, :sxl_interfaces,
22
+ :site_settings
21
23
 
22
24
  def initialize(options)
23
25
  @node = options[:node]
@@ -136,6 +138,9 @@ module RSMP
136
138
  @ingoing_acknowledged = {}
137
139
  @outgoing_acknowledged = {}
138
140
  @latest_watchdog_send_at = nil
141
+ @component_list_received = false
142
+ @outgoing_watchdog_acknowledged = false
143
+ @sxl_interfaces = {}
139
144
 
140
145
  @acknowledgements = {}
141
146
  @acknowledgement_condition = Async::Notification.new
@@ -154,7 +159,10 @@ module RSMP
154
159
  @ip = options[:ip]
155
160
  @port = options[:port]
156
161
  @connection_info = options[:info]
157
- @sxl = nil
162
+ @sxls = []
163
+ @accepted_sxls = []
164
+ @rejected_sxls = []
165
+ @receive_alarms = true
158
166
  @site_settings = nil # can't pick until we know the site id
159
167
  return unless options[:collect]
160
168
 
@@ -177,14 +185,55 @@ module RSMP
177
185
  def schemas
178
186
  schemas = { core: RSMP::Schema.latest_core_version } # use latest core
179
187
  schemas[:core] = core_version if core_version
180
- schemas[sxl] = RSMP::Schema.sanitize_version(sxl_version.to_s) if sxl && sxl_version
188
+ accepted_sxls.each do |sxl|
189
+ schemas[sxl['name'].to_sym] = RSMP::Schema.sanitize_version(sxl['version'].to_s)
190
+ end
181
191
  schemas
182
192
  end
183
193
 
194
+ def receive_alarms?
195
+ @receive_alarms != false
196
+ end
197
+
198
+ def primary_sxl
199
+ accepted_sxls.first || sxls.first
200
+ end
201
+
202
+ def sxl
203
+ primary_sxl && primary_sxl['name']
204
+ end
205
+
206
+ def sxl_version
207
+ primary_sxl && primary_sxl['version']
208
+ end
209
+
184
210
  def author
185
211
  @node.site_id
186
212
  end
187
213
 
214
+ def build_sxl_interfaces
215
+ @sxl_interfaces = accepted_sxls.to_h do |sxl|
216
+ [sxl['name'], RSMP::SXL::Registry.build_for(self, sxl)]
217
+ end
218
+ end
219
+
220
+ def sxl_interface(name)
221
+ sxl_interfaces.fetch(name.to_s) do
222
+ raise RSMP::Schema::UnknownSchemaTypeError, "SXL #{name} is not accepted on this connection"
223
+ end
224
+ end
225
+
226
+ def tlc
227
+ sxl_interface 'tlc'
228
+ end
229
+
230
+ def sxl_interface_for(message)
231
+ resolved = RSMP::Schema.resolve_sxl(message.attributes, schemas: schemas)
232
+ return unless resolved
233
+
234
+ sxl_interface resolved.first
235
+ end
236
+
188
237
  # Use Gem class to check version requirement
189
238
  # Requirement must be a string like '1.1', '>=1.0.3' or '<2.1.4',
190
239
  # or list of strings, like ['<=1.4','<1.5']
@@ -125,14 +125,13 @@ module RSMP
125
125
  end
126
126
 
127
127
  def unsubscribe_to_status(status_list, component: nil, validate: nil)
128
+ validate_ready 'unsubscribe to status'
128
129
  component ||= main.c_id
129
130
 
130
131
  status_list.each do |item|
131
132
  remove_subscription_item(component, item['sCI'], item['n'])
132
133
  end
133
134
 
134
- return unless ready? # if the connection is don't we skip sending
135
-
136
135
  message = RSMP::StatusUnsubscribe.new({
137
136
  'cId' => component,
138
137
  'sS' => status_list
@@ -145,7 +144,10 @@ module RSMP
145
144
  # unsubscribes to all statuses (with all attributes) defined in the used SXL
146
145
  def unsubscribe_from_all(component: nil)
147
146
  component ||= main.c_id
148
- catalogue = RSMP::Schema.status_catalogue(@sxl, RSMP::Schema.sanitize_version(sxl_version))
147
+ catalogue = accepted_sxls.each_with_object({}) do |sxl, memo|
148
+ version = RSMP::Schema.sanitize_version(sxl['version'].to_s)
149
+ memo.merge! RSMP::Schema.status_catalogue(sxl['name'], version)
150
+ end
149
151
  status_list = catalogue.flat_map do |status_code_id, names|
150
152
  names.map { |name| { 'sCI' => status_code_id.to_s, 'n' => name.to_s } }
151
153
  end
@@ -6,6 +6,7 @@ module RSMP
6
6
  include Modules::AggregatedStatus
7
7
  include Modules::Alarms
8
8
  include Modules::Commands
9
+ include SiteSxlSelection
9
10
 
10
11
  attr_reader :supervisor, :site_id
11
12
 
@@ -44,7 +45,8 @@ module RSMP
44
45
 
45
46
  def handshake_complete
46
47
  super
47
- log "Connection to site #{@site_id} established, using core #{@core_version}, #{@sxl} #{@site_sxl_version}",
48
+ sxl_summary = accepted_sxls.map { |item| "#{item['name']} #{item['version']}" }.join(', ')
49
+ log "Connection to site #{@site_id} established, using core #{@core_version}, SXLs [#{sxl_summary}]",
48
50
  level: :info
49
51
  start_watchdog
50
52
  end
@@ -55,16 +57,13 @@ module RSMP
55
57
  case message
56
58
  when StatusUnsubscribe, AggregatedStatusRequest
57
59
  will_not_handle message
60
+ when ComponentList
61
+ process_component_list message
58
62
  when AggregatedStatus
59
63
  process_aggregated_status message
60
- when AlarmIssue, AlarmSuspended, AlarmResumed, AlarmAcknowledged
61
- process_alarm message
62
- when CommandResponse
63
- process_command_response message
64
- when StatusResponse
65
- process_status_response message
66
- when StatusUpdate
67
- process_status_update message
64
+ when AlarmIssue, AlarmSuspended, AlarmResumed, AlarmAcknowledged,
65
+ CommandResponse, StatusResponse, StatusUpdate
66
+ handle_interface_message message
68
67
  else
69
68
  super
70
69
  end
@@ -78,11 +77,32 @@ module RSMP
78
77
  message.is_a?(CommandRequest) || message.is_a?(StatusRequest) || message.is_a?(StatusSubscribe)
79
78
  end
80
79
 
80
+ def handle_interface_message(message)
81
+ interface = sxl_interface_for message
82
+ interface.validate_message! message
83
+ record_interface_message message
84
+ interface.process_message message
85
+ end
86
+
87
+ def record_interface_message(message)
88
+ case message
89
+ when AlarmIssue, AlarmSuspended, AlarmResumed, AlarmAcknowledged
90
+ process_alarm message
91
+ when CommandResponse
92
+ process_command_response message
93
+ when StatusResponse
94
+ process_status_response message
95
+ when StatusUpdate
96
+ process_status_update message
97
+ end
98
+ end
99
+
81
100
  def version_accepted(message)
82
101
  log "Received Version message for site #{@site_id}", message: message, level: :log
83
102
  start_timer
84
103
  acknowledge message
85
- send_version @site_id, core_versions
104
+ response_id = core_3_3? ? (@supervisor.site_id || @site_id) : @site_id
105
+ send_version_response response_id, core_versions
86
106
  @version_determined = true
87
107
  end
88
108
 
@@ -96,7 +116,12 @@ module RSMP
96
116
  def acknowledged_first_outgoing(message)
97
117
  case message.type
98
118
  when 'Watchdog'
99
- handshake_complete
119
+ if core_3_3?
120
+ @outgoing_watchdog_acknowledged = true
121
+ handshake_complete if @component_list_received
122
+ else
123
+ handshake_complete
124
+ end
100
125
  end
101
126
  end
102
127
 
@@ -106,6 +131,33 @@ module RSMP
106
131
 
107
132
  def version_acknowledged; end
108
133
 
134
+ def process_component_list(message)
135
+ log "Received #{message.type}", message: message, level: :log
136
+ rebuild_components_from_list message.attributes['components']
137
+ build_sxl_interfaces
138
+ acknowledge message
139
+ @component_list_received = true
140
+ handshake_complete if @outgoing_watchdog_acknowledged
141
+ end
142
+
143
+ def rebuild_components_from_list(items)
144
+ main_id = @site_settings.dig('components', 'main')&.keys&.first
145
+ @components = {}
146
+ @main = nil
147
+ items.each do |item|
148
+ grouped = item['id'] == main_id
149
+ component = ComponentProxy.new(
150
+ id: item['id'],
151
+ node: self,
152
+ type: item['type'],
153
+ name: item['name'],
154
+ grouped: grouped
155
+ )
156
+ @components[component.c_id] = component
157
+ @main = component if grouped
158
+ end
159
+ end
160
+
109
161
  def site_ids_changed
110
162
  @supervisor.site_ids_changed
111
163
  end
@@ -114,27 +166,6 @@ module RSMP
114
166
  @settings['intervals']['watchdog'] = interval
115
167
  end
116
168
 
117
- def check_sxl_version(message)
118
- # check that we have a schema for specified sxl type and version
119
- # note that the type comes from the site config, while the version
120
- # comes from the Version message send by the site
121
- type = @site_settings['sxl']
122
- sanitized_version = RSMP::Schema.sanitize_version(message.attribute('SXL'))
123
- RSMP::Schema.find_schema! type, sanitized_version
124
-
125
- # store raw sxl version from site (may be 2-part like "1.2"), so we echo it back unchanged
126
- # TODO should check agaist site settings
127
- @site_sxl_version = message.attribute('SXL')
128
- rescue RSMP::Schema::UnknownSchemaError => e
129
- dont_acknowledge message, "Rejected #{message.type} message,", e.to_s
130
- end
131
-
132
- def sxl_version
133
- # a supervisor does not maintain it's own sxl version
134
- # instead we use what the site requests
135
- @site_sxl_version
136
- end
137
-
138
169
  def process_version(message)
139
170
  return extraneous_version message if @version_determined
140
171
 
@@ -173,7 +204,9 @@ module RSMP
173
204
  def setup_site_settings
174
205
  @site_settings = find_site_settings @site_id
175
206
  if @site_settings
176
- @sxl = @site_settings['sxl']
207
+ @sxls = configured_sxls
208
+ @accepted_sxls = @sxls.dup
209
+ build_sxl_interfaces
177
210
  setup_components @site_settings['components']
178
211
  else
179
212
  dont_acknowledge message, 'Rejected', "No config found for site #{@site_id}"
@@ -188,10 +221,10 @@ module RSMP
188
221
  def build_component(id:, type:, settings: {})
189
222
  settings ||= {}
190
223
  if type == 'main'
191
- ComponentProxy.new id: id, node: self, grouped: true,
224
+ ComponentProxy.new id: id, node: self, type: type, name: settings['name'], grouped: true,
192
225
  ntsoid: settings['ntsOId'], xnid: settings['xNId']
193
226
  else
194
- ComponentProxy.new id: id, node: self, grouped: false
227
+ ComponentProxy.new id: id, node: self, type: type, name: settings['name'], grouped: false
195
228
  end
196
229
  end
197
230
 
@@ -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,69 @@
1
+ module RSMP
2
+ # Provides JSON Schema validation for RSMP messages across core and SXL versions.
3
+ module Schema
4
+ def self.clear_core_sxl_schemas(type = nil, version = nil)
5
+ @core_sxl_schemas ||= {}
6
+ return @core_sxl_schemas.clear unless type
7
+
8
+ type = type.to_sym
9
+ @core_sxl_schemas.delete_if do |(cached_type, cached_version, _core_version), _schema|
10
+ cached_type == type && (!version || cached_version == version.to_s)
11
+ end
12
+ end
13
+
14
+ def self.schema_core_version(schemas)
15
+ schemas[:core] || schemas['core']
16
+ end
17
+
18
+ def self.validate_resolved_sxl(message, resolved, schemas, options)
19
+ type, version = resolved
20
+ schema = find_core_sxl_schema! type, version, schema_core_version(schemas), options
21
+ validate_using_schema(message, schema)
22
+ end
23
+
24
+ def self.find_core_sxl_schema!(type, version, core_version, options = {})
25
+ raise ArgumentError, 'core version missing' unless core_version
26
+
27
+ version = sanitize_version(version.to_s) if options[:lenient]
28
+ core_version = sanitize_version(core_version.to_s) if options[:lenient]
29
+ find_schema! type, version
30
+ find_schema! :core, core_version
31
+
32
+ key = [type.to_sym, version.to_s, core_version.to_s]
33
+ @core_sxl_schemas ||= {}
34
+ @core_sxl_schemas[key] ||= build_core_sxl_schema(type, version, core_version)
35
+ end
36
+
37
+ def self.build_core_sxl_schema(type, version, core_version)
38
+ schema_path = @schema_paths&.dig(type.to_sym, version.to_s)
39
+ raise UnknownSchemaVersionError, "Unknown schema version #{type} #{version}" unless schema_path
40
+
41
+ file_path = File.join(schema_path, 'rsmp.json')
42
+ JSONSchemer.schema(
43
+ Pathname.new(file_path),
44
+ ref_resolver: core_sxl_ref_resolver(core_version)
45
+ )
46
+ end
47
+
48
+ def self.core_sxl_ref_resolver(core_version)
49
+ proc do |uri|
50
+ if sxl_definitions_ref?(uri)
51
+ JSON.parse(File.read(core_definitions_path(core_version), encoding: 'UTF-8'))
52
+ else
53
+ JSONSchemer::FILE_URI_REF_RESOLVER.call(uri)
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.sxl_definitions_ref?(uri)
59
+ uri.scheme == 'file' && uri.path.end_with?('/defs/definitions.json')
60
+ end
61
+
62
+ def self.core_definitions_path(core_version)
63
+ path = File.join(schema_root_path, 'core', core_version.to_s, 'definitions.json')
64
+ return path if File.exist?(path)
65
+
66
+ raise UnknownSchemaVersionError, "Missing core definitions for RSMP #{core_version}"
67
+ end
68
+ end
69
+ end