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
@@ -19,7 +19,12 @@ module RSMP
19
19
 
20
20
  components_by_type.each_pair do |id, component_settings|
21
21
  component_settings ||= {}
22
- @components[id] = build_component(id: id, type: type, settings: component_settings)
22
+ component = build_component(id: id, type: type, settings: component_settings)
23
+ component.update_metadata(
24
+ type: component_settings['type'] || type,
25
+ name: component_settings['name'] || id
26
+ )
27
+ @components[id] = component
23
28
  @main = @components[id] if type == 'main'
24
29
  end
25
30
  end
@@ -36,6 +41,22 @@ module RSMP
36
41
  @components[component.c_id] = component
37
42
  end
38
43
 
44
+ def natural_sort_key(value)
45
+ value.to_s.split(/(\d+)/).map do |part|
46
+ part.match?(/\A\d+\z/) ? part.to_i : part
47
+ end
48
+ end
49
+
50
+ def component_list
51
+ @components.values.sort_by { |component| natural_sort_key(component.c_id) }.map do |component|
52
+ {
53
+ 'id' => component.c_id,
54
+ 'type' => component.component_type || 'component',
55
+ 'name' => component.name || component.c_id
56
+ }
57
+ end
58
+ end
59
+
39
60
  def infer_component_type(component_id)
40
61
  raise UnknownComponent, "Component #{component_id} mising and cannot infer type"
41
62
  end
@@ -103,6 +103,23 @@ module RSMP
103
103
  out["commands/#{key}.json"] = output_json json
104
104
  end
105
105
 
106
+ def self.output_sxl_index(out, sxl)
107
+ out['sxl_index.json'] = output_json({
108
+ 'meta' => sxl[:meta],
109
+ 'statuses' => index_items(sxl[:statuses]),
110
+ 'commands' => index_items(sxl[:commands]),
111
+ 'alarms' => index_items(sxl[:alarms])
112
+ })
113
+ end
114
+
115
+ def self.index_items(items)
116
+ items.keys.sort.to_h do |key|
117
+ [key, {
118
+ 'arguments' => (items[key]['arguments'] || {}).keys.sort
119
+ }]
120
+ end
121
+ end
122
+
106
123
  # output the json schema root
107
124
  def self.output_root(out, meta)
108
125
  json = {
@@ -112,6 +129,8 @@ module RSMP
112
129
  'version' => meta['version'],
113
130
  'allOf' => root_type_rules
114
131
  }
132
+ json['prefix'] = meta['prefix'] if meta['prefix']
133
+ json['minimum_core_version'] = meta['minimum_core_version'] if meta['minimum_core_version']
115
134
  out['rsmp.json'] = output_json json
116
135
  end
117
136
 
@@ -7,7 +7,7 @@ module RSMP
7
7
  def self.build_value(item)
8
8
  out = {}
9
9
  out['description'] = item['description'] if item['description']
10
- if item['type'] =~ /_list$/
10
+ if item['type'] =~ /_list(_as_string)?$/
11
11
  handle_string_list item, out
12
12
  else
13
13
  handle_types item, out
@@ -59,11 +59,13 @@ module RSMP
59
59
  # convert a yaml item with list: true to json schema
60
60
  def self.handle_string_list(item, out)
61
61
  case item['type']
62
- when 'boolean_list'
62
+ when 'boolean_list', 'boolean_list_as_string'
63
63
  out['$ref'] = '../defs/definitions.json#/boolean_list'
64
- when 'integer_list'
64
+ when 'integer_list', 'integer_list_as_string'
65
65
  out['$ref'] = '../defs/definitions.json#/integer_list'
66
- when 'string_list'
66
+ when 'number_list', 'number_list_as_string'
67
+ out['$ref'] = '../defs/definitions.json#/number_list'
68
+ when 'string_list', 'string_list_as_string'
67
69
  out['$ref'] = '../defs/definitions.json#/string_list'
68
70
  else
69
71
  raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
@@ -74,7 +76,7 @@ module RSMP
74
76
  out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
75
77
  end
76
78
 
77
- puts "Warning: Pattern not support for lists: #{item.inspect}" if item['pattern']
79
+ handle_pattern item, out
78
80
  end
79
81
 
80
82
  # convert yaml values to json schema enum
@@ -26,8 +26,18 @@ module RSMP
26
26
  JSON.generate(item, JSON_OPTIONS)
27
27
  end
28
28
 
29
- # Default path to definitions.json bundled with the gem's core schemas
30
- DEFINITIONS_SOURCE = File.expand_path('../../../../schemas/core/3.1.2/definitions.json', __dir__)
29
+ def self.minimum_core_version(sxl)
30
+ sxl.dig(:meta, 'minimum_core_version') || RSMP::Schema.latest_core_version
31
+ end
32
+
33
+ # Path to definitions.json for the fallback bundled core schema version
34
+ def self.definitions_source(sxl)
35
+ version = minimum_core_version(sxl)
36
+ path = File.expand_path("../../../../schemas/core/#{version}/definitions.json", __dir__)
37
+ raise "Missing core definitions for RSMP #{version}" unless File.exist?(path)
38
+
39
+ path
40
+ end
31
41
 
32
42
  # generate the json schema from a string containing yaml
33
43
  def self.generate(sxl)
@@ -36,6 +46,7 @@ module RSMP
36
46
  output_alarms out, sxl[:alarms]
37
47
  output_statuses out, sxl[:statuses]
38
48
  output_commands out, sxl[:commands]
49
+ output_sxl_index out, sxl
39
50
  out
40
51
  end
41
52
 
@@ -50,7 +61,8 @@ module RSMP
50
61
  # Copy definitions.json so each version folder is self-contained
51
62
  defs_dest = File.join(folder, 'defs', 'definitions.json')
52
63
  FileUtils.mkdir_p File.dirname(defs_dest)
53
- FileUtils.cp DEFINITIONS_SOURCE, defs_dest if File.exist?(DEFINITIONS_SOURCE)
64
+ source = definitions_source(sxl)
65
+ FileUtils.cp source, defs_dest
54
66
  end
55
67
  end
56
68
  end
@@ -3,8 +3,8 @@ class Hash
3
3
  def deep_merge(other_hash)
4
4
  return self unless other_hash
5
5
 
6
- merge(other_hash) do |_key, old, fresh|
7
- if old.is_a?(Hash) && fresh.is_a?(Hash)
6
+ merge(other_hash) do |key, old, fresh|
7
+ if key != 'sxls' && old.is_a?(Hash) && fresh.is_a?(Hash)
8
8
  old.deep_merge(fresh)
9
9
  else
10
10
  fresh
data/lib/rsmp/message.rb CHANGED
@@ -30,6 +30,7 @@ module RSMP
30
30
  'MessageAck' => MessageAck,
31
31
  'MessageNotAck' => MessageNotAck,
32
32
  'Version' => Version,
33
+ 'ComponentList' => ComponentList,
33
34
  'AggregatedStatus' => AggregatedStatus,
34
35
  'AggregatedStatusRequest' => AggregatedStatusRequest,
35
36
  'Watchdog' => Watchdog,
@@ -216,6 +217,37 @@ module RSMP
216
217
  def versions
217
218
  attribute('RSMP').map { |item| item['vers'] }
218
219
  end
220
+
221
+ def step
222
+ @attributes['step']
223
+ end
224
+
225
+ def request?
226
+ step == 'Request'
227
+ end
228
+
229
+ def response?
230
+ step == 'Response'
231
+ end
232
+
233
+ def sxls
234
+ (@attributes['SXLS'] || []).map do |item|
235
+ item.transform_keys(&:to_s)
236
+ end
237
+ end
238
+
239
+ def site_ids
240
+ attribute('siteId').map { |item| item['sId'] }
241
+ end
242
+ end
243
+
244
+ # ComponentList message, lists site components and their component types.
245
+ class ComponentList < Message
246
+ def initialize(attributes = {})
247
+ super({
248
+ 'type' => 'ComponentList'
249
+ }.merge attributes)
250
+ end
219
251
  end
220
252
 
221
253
  # Unknown message type wrapper.
@@ -19,8 +19,16 @@ module RSMP
19
19
  build_proxies
20
20
  end
21
21
 
22
+ def sxls
23
+ @site_settings['sxls']
24
+ end
25
+
26
+ def primary_sxl
27
+ sxls.first
28
+ end
29
+
22
30
  def sxl_version
23
- @site_settings['sxl_version']
31
+ primary_sxl && primary_sxl['version']
24
32
  end
25
33
 
26
34
  def site_id
@@ -30,18 +38,34 @@ module RSMP
30
38
  def handle_site_settings(options = {})
31
39
  options_class = self.class.options_class
32
40
  settings = options[:site_settings] || {}
41
+ settings = denormalize_sxls(settings)
33
42
  @site_options = options_class.new(settings)
34
43
  @site_settings = @site_options.to_h
35
44
 
36
- check_sxl_version
45
+ check_sxls
37
46
  check_core_versions
38
47
  setup_components @site_settings['components']
39
48
  end
40
49
 
41
- def check_sxl_version
42
- sxl = @site_settings['sxl']
43
- version = @site_settings['sxl_version'].to_s
44
- RSMP::Schema.find_schema! sxl, version, lenient: true
50
+ def denormalize_sxls(settings)
51
+ sxls = settings['sxls']
52
+ return settings unless sxls.is_a?(Array)
53
+
54
+ settings.merge(
55
+ 'sxls' => sxls.to_h { |sxl| [sxl['name'], sxl['version']] }
56
+ )
57
+ end
58
+
59
+ def check_sxls
60
+ raise RSMP::ConfigurationError, 'No SXLs specified' unless sxls
61
+
62
+ sxls.each do |sxl|
63
+ name = sxl['name']
64
+ version = sxl['version'].to_s
65
+ raise RSMP::ConfigurationError, 'SXL name cannot be core' if name.to_s == 'core'
66
+
67
+ RSMP::Schema.find_schema! name, version, lenient: true
68
+ end
45
69
  end
46
70
 
47
71
  def check_core_versions
@@ -60,7 +84,7 @@ module RSMP
60
84
 
61
85
  def log_site_starting
62
86
  log "Starting #{site_type_name} #{@site_settings['site_id']}", level: :info, timestamp: @clock.now
63
- sxl = "Using #{@site_settings['sxl']} sxl #{@site_settings['sxl_version']}"
87
+ sxl = "Using SXLs #{sxls.map { |item| "#{item['name']} #{item['version']}" }.join(', ')}"
64
88
  version = @site_settings['core_version']
65
89
  core = if version
66
90
  "accepting only core version #{version}"
@@ -113,7 +137,7 @@ module RSMP
113
137
 
114
138
  def send_alarm(alarm)
115
139
  @proxies.each do |proxy|
116
- proxy.send_message alarm if proxy.ready?
140
+ proxy.send_message alarm if proxy.ready? && proxy.receive_alarms?
117
141
  end
118
142
  end
119
143
 
@@ -158,10 +182,10 @@ module RSMP
158
182
  def build_component(id:, type:, settings:)
159
183
  settings ||= {}
160
184
  if type == 'main'
161
- Component.new id: id, node: self, grouped: true,
185
+ Component.new id: id, node: self, type: type, name: settings['name'], grouped: true,
162
186
  ntsoid: settings['ntsOId'], xnid: settings['xNId']
163
187
  else
164
- Component.new id: id, node: self, grouped: false
188
+ Component.new id: id, node: self, type: type, name: settings['name'], grouped: false
165
189
  end
166
190
  end
167
191
  end
@@ -4,22 +4,49 @@ module RSMP
4
4
  # Handles supervisor configuration and site settings
5
5
  module Configuration
6
6
  def handle_supervisor_settings(supervisor_settings)
7
+ supervisor_settings = denormalize_supervisor_sxls(supervisor_settings || {})
7
8
  options = RSMP::Supervisor::Options.new(supervisor_settings || {})
8
9
  @supervisor_settings = options.to_h
9
10
  @core_version = @supervisor_settings.dig('default', 'core_version')
10
- check_site_sxl_types
11
+ check_site_sxls
11
12
  end
12
13
 
13
- def check_site_sxl_types
14
+ def denormalize_supervisor_sxls(settings)
15
+ settings = settings.merge('default' => denormalize_site_sxls(settings['default'])) if settings['default']
16
+ return settings unless settings['sites']
17
+
18
+ settings.merge(
19
+ 'sites' => settings['sites'].transform_values { |site_settings| denormalize_site_sxls(site_settings) }
20
+ )
21
+ end
22
+
23
+ def denormalize_site_sxls(settings)
24
+ sxls = settings['sxls']
25
+ return settings unless sxls.is_a?(Array)
26
+
27
+ settings.merge(
28
+ 'sxls' => sxls.to_h { |sxl| [sxl['name'], sxl['version']] }
29
+ )
30
+ end
31
+
32
+ def check_site_sxls
14
33
  sites = @supervisor_settings['sites'].clone || {}
15
34
  sites['default'] = @supervisor_settings['default']
16
35
  sites.each do |site_id, settings|
17
36
  raise RSMP::ConfigurationError, "Configuration for site '#{site_id}' is empty" unless settings
18
37
 
19
- sxl = settings['sxl']
20
- raise RSMP::ConfigurationError, "Configuration error for site '#{site_id}': No SXL specified" unless sxl
38
+ sxls = settings['sxls']
39
+ raise RSMP::ConfigurationError, "Configuration error for site '#{site_id}': No SXLs specified" unless sxls
40
+
41
+ sxls.each do |sxl|
42
+ name = sxl['name']
43
+ if name.to_s == 'core'
44
+ raise RSMP::ConfigurationError,
45
+ "Configuration error for site '#{site_id}': SXL name cannot be core"
46
+ end
21
47
 
22
- RSMP::Schema.find_schemas! sxl if sxl
48
+ RSMP::Schema.find_schema! name, sxl['version'], lenient: true
49
+ end
23
50
  rescue RSMP::Schema::UnknownSchemaError => e
24
51
  raise RSMP::ConfigurationError, "Configuration error for site '#{site_id}': #{e}"
25
52
  end
@@ -128,8 +128,6 @@ module RSMP
128
128
  stop if @supervisor_settings['one_shot']
129
129
  end
130
130
 
131
- # Proxy type is now derived from `site_settings['sxl']` in Supervisor#build_proxy.
132
-
133
131
  def reject_connection(_socket, info)
134
132
  log 'Site rejected', ip: info[:ip], level: :info
135
133
  end
@@ -59,13 +59,6 @@ module RSMP
59
59
  end
60
60
 
61
61
  def build_proxy(settings)
62
- # Determine proxy type from site settings (SXL). Fall back to supervisor
63
- # default settings when site-specific settings are not present.
64
- site_settings = settings[:site_settings] || @supervisor_settings['default']
65
- sxl_type = site_settings && site_settings['sxl']
66
-
67
- return RSMP::TLC::TrafficControllerProxy.new(settings) if sxl_type == 'tlc'
68
-
69
62
  SiteProxy.new settings
70
63
  end
71
64
 
@@ -25,9 +25,9 @@ module RSMP
25
25
  options = extra if options.nil? && extra.any?
26
26
  @source = source
27
27
  @log_settings = normalize(log_settings || {})
28
- normalized = normalize(options || {})
29
- @data = apply_defaults(normalized)
30
- validate! if validate
28
+ config = normalize_config(options || {})
29
+ validate!(config) if validate
30
+ @data = normalize(apply_defaults(config))
31
31
  end
32
32
 
33
33
  def defaults
@@ -44,11 +44,11 @@ module RSMP
44
44
  File.join(SCHEMAS_PATH, schema_file)
45
45
  end
46
46
 
47
- def validate!
47
+ def validate!(data = @data)
48
48
  return unless schema_path && File.exist?(schema_path)
49
49
 
50
50
  schemer = JSONSchemer.schema(Pathname.new(schema_path))
51
- errors = schemer.validate(@data).to_a
51
+ errors = schemer.validate(data).to_a
52
52
  return if errors.empty?
53
53
 
54
54
  message = errors.map { |error| format_error(error) }.join("\n")
@@ -82,7 +82,8 @@ module RSMP
82
82
  case value
83
83
  when Hash
84
84
  value.each_with_object({}) do |(key, val), memo|
85
- memo[key.to_s] = normalize(val)
85
+ normalized_key = key.to_s
86
+ memo[normalized_key] = normalized_key == 'sxls' ? normalize_sxls(val) : normalize(val)
86
87
  end
87
88
  when Array
88
89
  value.map { |item| normalize(item) }
@@ -91,6 +92,54 @@ module RSMP
91
92
  end
92
93
  end
93
94
 
95
+ def normalize_sxls(value)
96
+ case value
97
+ when Hash
98
+ value.map do |name, details|
99
+ normalize_sxl_item(name.to_s, details)
100
+ end
101
+ when Array
102
+ value.map { |item| normalize(item) }
103
+ else
104
+ value
105
+ end
106
+ end
107
+
108
+ def normalize_config(value)
109
+ case value
110
+ when Hash
111
+ value.each_with_object({}) do |(key, val), memo|
112
+ normalized_key = key.to_s
113
+ memo[normalized_key] = normalized_key == 'sxls' ? normalize_config_sxls(val) : normalize_config(val)
114
+ end
115
+ when Array
116
+ value.map { |item| normalize_config(item) }
117
+ else
118
+ value
119
+ end
120
+ end
121
+
122
+ def normalize_config_sxls(value)
123
+ raise RSMP::ConfigurationError, 'sxls must be a hash of SXL names to versions' unless value.is_a?(Hash)
124
+
125
+ value.each_with_object({}) do |(name, details), memo|
126
+ raise RSMP::ConfigurationError, "sxls/#{name} must be a version string" if details.is_a?(Hash)
127
+
128
+ memo[name.to_s] = details.to_s
129
+ end
130
+ end
131
+
132
+ def normalize_sxl_item(name, details)
133
+ case details
134
+ when Hash
135
+ raise RSMP::ConfigurationError, "sxls/#{name} must be a version string"
136
+ when nil
137
+ { 'name' => name }
138
+ else
139
+ { 'name' => name, 'version' => details.to_s }
140
+ end
141
+ end
142
+
94
143
  def format_error(error)
95
144
  pointer = error_pointer(error)
96
145
  details = error_details(error)
@@ -17,8 +17,11 @@
17
17
  "additionalProperties": true
18
18
  }
19
19
  },
20
- "sxl": { "type": "string" },
21
- "sxl_version": { "type": "string" },
20
+ "sxls": {
21
+ "type": "object",
22
+ "propertyNames": { "not": { "const": "core" } },
23
+ "additionalProperties": { "type": "string" }
24
+ },
22
25
  "core_version": { "type": "string" },
23
26
  "intervals": {
24
27
  "type": "object",
@@ -46,4 +49,4 @@
46
49
  "live_output": { "type": ["string", "null"] }
47
50
  },
48
51
  "additionalProperties": true
49
- }
52
+ }
@@ -15,8 +15,11 @@
15
15
  "default": {
16
16
  "type": "object",
17
17
  "properties": {
18
- "sxl": { "type": "string" },
19
- "sxl_version": { "type": "string" },
18
+ "sxls": {
19
+ "type": "object",
20
+ "propertyNames": { "not": { "const": "core" } },
21
+ "additionalProperties": { "type": "string" }
22
+ },
20
23
  "core_version": { "type": "string" },
21
24
  "intervals": {
22
25
  "type": "object",
@@ -3,8 +3,11 @@
3
3
  "$id": "supervisor_site.json",
4
4
  "type": "object",
5
5
  "properties": {
6
- "sxl": { "type": "string" },
7
- "sxl_version": { "type": "string" },
6
+ "sxls": {
7
+ "type": "object",
8
+ "propertyNames": { "not": { "const": "core" } },
9
+ "additionalProperties": { "type": "string" }
10
+ },
8
11
  "type": { "type": "string" },
9
12
  "components": { "type": "object" },
10
13
  "supervisors": {
@@ -8,8 +8,9 @@ module RSMP
8
8
  'supervisors' => [
9
9
  { 'ip' => '127.0.0.1', 'port' => 12_111 }
10
10
  ],
11
- 'sxl' => 'tlc',
12
- 'sxl_version' => RSMP::Schema.latest_version(:tlc),
11
+ 'sxls' => {
12
+ 'tlc' => RSMP::Schema.latest_version(:tlc)
13
+ },
13
14
  'intervals' => {
14
15
  'timer' => 0.1,
15
16
  'watchdog' => 1,
@@ -7,7 +7,9 @@ module RSMP
7
7
  'port' => 12_111,
8
8
  'ips' => 'all',
9
9
  'default' => {
10
- 'sxl' => 'tlc',
10
+ 'sxls' => {
11
+ 'tlc' => RSMP::Schema.latest_version(:tlc)
12
+ },
11
13
  'intervals' => {
12
14
  'timer' => 1,
13
15
  'watchdog' => 1
@@ -86,6 +86,8 @@ module RSMP
86
86
  case original.type
87
87
  when 'Version'
88
88
  version_acknowledged
89
+ when 'ComponentList'
90
+ component_list_acknowledged
89
91
  when 'StatusSubscribe'
90
92
  status_subscribe_acknowledged original
91
93
  end
@@ -5,6 +5,8 @@ module RSMP
5
5
  # Handles receiving and processing incoming messages
6
6
  module Receive
7
7
  def should_validate_ingoing_message?(message)
8
+ return false if message.is_a?(Version) && !@version_determined
9
+
8
10
  return true unless @site_settings
9
11
 
10
12
  skip = @site_settings['skip_validation']
@@ -37,7 +39,8 @@ module RSMP
37
39
  end
38
40
 
39
41
  def handle_schema_error(message, error)
40
- schemas_string = error.schemas.map { |schema| "#{schema.first}: #{schema.last}" }.join(', ')
42
+ failed_schemas = error.respond_to?(:schemas) && error.schemas ? error.schemas : schemas
43
+ schemas_string = failed_schemas.map { |schema| "#{schema.first}: #{schema.last}" }.join(', ')
41
44
  reason = "schema errors (#{schemas_string}): #{error.message}"
42
45
  str = "Received invalid #{message.type}"
43
46
  distribute_error error.exception(str), message: message
@@ -79,7 +82,7 @@ module RSMP
79
82
  handle_malformed_message(attributes, e)
80
83
  rescue SchemaError, RSMP::Schema::Error => e
81
84
  handle_schema_error(message, e)
82
- rescue InvalidMessage => e
85
+ rescue InvalidMessage, MessageRejected => e
83
86
  handle_invalid_message(message, e)
84
87
  rescue FatalError => e
85
88
  handle_fatal_error(message, e)
@@ -17,6 +17,7 @@ module RSMP
17
17
  end
18
18
 
19
19
  def handshake_complete
20
+ build_sxl_interfaces
20
21
  self.state = :ready
21
22
  end
22
23
  end