rsmp 0.35.2 → 0.38.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +22 -0
  3. data/.github/copilot-instructions.md +33 -0
  4. data/.github/workflows/copilot-setup-steps.yml +35 -0
  5. data/.github/workflows/rspec.yaml +2 -1
  6. data/.github/workflows/rubocop.yaml +17 -0
  7. data/.gitignore +7 -7
  8. data/.rubocop.yml +80 -0
  9. data/Gemfile +13 -1
  10. data/Gemfile.lock +73 -35
  11. data/Rakefile +3 -3
  12. data/bin/console +2 -4
  13. data/documentation/tasks.md +4 -4
  14. data/lib/rsmp/cli.rb +147 -124
  15. data/lib/rsmp/collect/ack_collector.rb +8 -7
  16. data/lib/rsmp/collect/aggregated_status_collector.rb +4 -4
  17. data/lib/rsmp/collect/alarm_collector.rb +31 -23
  18. data/lib/rsmp/collect/alarm_matcher.rb +3 -3
  19. data/lib/rsmp/collect/collector/logging.rb +17 -0
  20. data/lib/rsmp/collect/collector/reporting.rb +44 -0
  21. data/lib/rsmp/collect/collector/status.rb +34 -0
  22. data/lib/rsmp/collect/collector.rb +69 -150
  23. data/lib/rsmp/collect/command_matcher.rb +19 -6
  24. data/lib/rsmp/collect/command_response_collector.rb +7 -7
  25. data/lib/rsmp/collect/distributor.rb +14 -11
  26. data/lib/rsmp/collect/filter.rb +31 -15
  27. data/lib/rsmp/collect/matcher.rb +7 -11
  28. data/lib/rsmp/collect/queue.rb +4 -4
  29. data/lib/rsmp/collect/receiver.rb +10 -12
  30. data/lib/rsmp/collect/state_collector.rb +116 -77
  31. data/lib/rsmp/collect/status_collector.rb +6 -6
  32. data/lib/rsmp/collect/status_matcher.rb +17 -7
  33. data/lib/rsmp/{alarm_state.rb → component/alarm_state.rb} +76 -37
  34. data/lib/rsmp/{component.rb → component/component.rb} +15 -15
  35. data/lib/rsmp/component/component_base.rb +89 -0
  36. data/lib/rsmp/component/component_proxy.rb +75 -0
  37. data/lib/rsmp/component/components.rb +63 -0
  38. data/lib/rsmp/convert/export/json_schema.rb +116 -110
  39. data/lib/rsmp/convert/import/yaml.rb +21 -18
  40. data/lib/rsmp/{rsmp.rb → helpers/clock.rb} +5 -6
  41. data/lib/rsmp/{deep_merge.rb → helpers/deep_merge.rb} +2 -1
  42. data/lib/rsmp/helpers/error.rb +71 -0
  43. data/lib/rsmp/{inspect.rb → helpers/inspect.rb} +6 -10
  44. data/lib/rsmp/log/archive.rb +98 -0
  45. data/lib/rsmp/log/colorization.rb +41 -0
  46. data/lib/rsmp/log/filtering.rb +54 -0
  47. data/lib/rsmp/log/logger.rb +206 -0
  48. data/lib/rsmp/{logging.rb → log/logging.rb} +5 -7
  49. data/lib/rsmp/message.rb +159 -148
  50. data/lib/rsmp/{node.rb → node/node.rb} +19 -17
  51. data/lib/rsmp/node/protocol.rb +37 -0
  52. data/lib/rsmp/node/site/site.rb +195 -0
  53. data/lib/rsmp/node/supervisor/modules/configuration.rb +59 -0
  54. data/lib/rsmp/node/supervisor/modules/connection.rb +140 -0
  55. data/lib/rsmp/node/supervisor/modules/sites.rb +64 -0
  56. data/lib/rsmp/node/supervisor/supervisor.rb +72 -0
  57. data/lib/rsmp/{task.rb → node/task.rb} +29 -19
  58. data/lib/rsmp/proxy/modules/acknowledgements.rb +144 -0
  59. data/lib/rsmp/proxy/modules/receive.rb +119 -0
  60. data/lib/rsmp/proxy/modules/send.rb +76 -0
  61. data/lib/rsmp/proxy/modules/state.rb +25 -0
  62. data/lib/rsmp/proxy/modules/tasks.rb +105 -0
  63. data/lib/rsmp/proxy/modules/versions.rb +69 -0
  64. data/lib/rsmp/proxy/modules/watchdogs.rb +66 -0
  65. data/lib/rsmp/proxy/proxy.rb +199 -0
  66. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +52 -0
  67. data/lib/rsmp/proxy/site/modules/alarms.rb +27 -0
  68. data/lib/rsmp/proxy/site/modules/commands.rb +31 -0
  69. data/lib/rsmp/proxy/site/modules/status.rb +110 -0
  70. data/lib/rsmp/proxy/site/site_proxy.rb +205 -0
  71. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +47 -0
  72. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +73 -0
  73. data/lib/rsmp/proxy/supervisor/modules/commands.rb +53 -0
  74. data/lib/rsmp/proxy/supervisor/modules/status.rb +204 -0
  75. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +178 -0
  76. data/lib/rsmp/tlc/detector_logic.rb +18 -34
  77. data/lib/rsmp/tlc/input_states.rb +126 -0
  78. data/lib/rsmp/tlc/modules/detector_logics.rb +50 -0
  79. data/lib/rsmp/tlc/modules/display.rb +78 -0
  80. data/lib/rsmp/tlc/modules/helpers.rb +41 -0
  81. data/lib/rsmp/tlc/modules/inputs.rb +173 -0
  82. data/lib/rsmp/tlc/modules/modes.rb +253 -0
  83. data/lib/rsmp/tlc/modules/outputs.rb +30 -0
  84. data/lib/rsmp/tlc/modules/plans.rb +218 -0
  85. data/lib/rsmp/tlc/modules/signal_groups.rb +109 -0
  86. data/lib/rsmp/tlc/modules/startup_sequence.rb +22 -0
  87. data/lib/rsmp/tlc/modules/system.rb +140 -0
  88. data/lib/rsmp/tlc/modules/traffic_data.rb +49 -0
  89. data/lib/rsmp/tlc/signal_group.rb +37 -41
  90. data/lib/rsmp/tlc/signal_plan.rb +14 -11
  91. data/lib/rsmp/tlc/signal_priority.rb +39 -35
  92. data/lib/rsmp/tlc/startup_sequence.rb +59 -0
  93. data/lib/rsmp/tlc/traffic_controller.rb +39 -1006
  94. data/lib/rsmp/tlc/traffic_controller_site.rb +58 -57
  95. data/lib/rsmp/version.rb +1 -1
  96. data/lib/rsmp.rb +86 -49
  97. data/rsmp.gemspec +24 -30
  98. metadata +87 -130
  99. data/lib/rsmp/archive.rb +0 -76
  100. data/lib/rsmp/collect/message_matchers.rb +0 -0
  101. data/lib/rsmp/component_base.rb +0 -87
  102. data/lib/rsmp/component_proxy.rb +0 -57
  103. data/lib/rsmp/components.rb +0 -65
  104. data/lib/rsmp/error.rb +0 -71
  105. data/lib/rsmp/logger.rb +0 -216
  106. data/lib/rsmp/proxy.rb +0 -695
  107. data/lib/rsmp/site.rb +0 -188
  108. data/lib/rsmp/site_proxy.rb +0 -389
  109. data/lib/rsmp/supervisor.rb +0 -287
  110. data/lib/rsmp/supervisor_proxy.rb +0 -516
  111. data/lib/rsmp/tlc/inputs.rb +0 -134
@@ -0,0 +1,89 @@
1
+ module RSMP
2
+ # RSMP component base class.
3
+
4
+ class ComponentBase
5
+ include Inspect
6
+
7
+ attr_reader :c_id, :ntsoid, :xnid, :node, :alarms, :statuses,
8
+ :aggregated_status, :aggregated_status_bools, :grouped
9
+
10
+ AGGREGATED_STATUS_KEYS = %i[local_control
11
+ communication_distruption
12
+ high_priority_alarm
13
+ medium_priority_alarm
14
+ low_priority_alarm
15
+ normal
16
+ rest
17
+ not_connected].freeze
18
+
19
+ def initialize(node:, id:, ntsoid: nil, xnid: nil, grouped: false)
20
+ if grouped == false && (ntsoid || xnid)
21
+ raise RSMP::ConfigurationError,
22
+ 'ntsoid and xnid are only allowed for grouped objects'
23
+ end
24
+
25
+ @c_id = id
26
+ @ntsoid = ntsoid
27
+ @xnid = xnid
28
+ @node = node
29
+ @grouped = grouped
30
+ clear_aggregated_status
31
+ @alarms = {}
32
+ end
33
+
34
+ def now
35
+ node.now
36
+ end
37
+
38
+ def clear_alarm_timestamps
39
+ @alarms.each_value(&:clear_timestamp)
40
+ end
41
+
42
+ def get_alarm_state(alarm_code)
43
+ @alarms[alarm_code] ||= RSMP::AlarmState.new component: self, code: alarm_code
44
+ end
45
+
46
+ def clear_aggregated_status
47
+ @aggregated_status = []
48
+ @aggregated_status_bools = Array.new(8, false)
49
+ @aggregated_status_bools[5] = true
50
+ end
51
+
52
+ def log(str, options)
53
+ default = { component: c_id }
54
+ @node.log str, default.merge(options)
55
+ end
56
+
57
+ def set_aggregated_status(status, options = {})
58
+ status = [status] if status.is_a? Symbol
59
+ raise InvalidArgument unless status.is_a? Array
60
+
61
+ input = status & AGGREGATED_STATUS_KEYS
62
+ return unless input != @aggregated_status
63
+
64
+ AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
65
+ @aggregated_status_bools[index] = status.include?(key)
66
+ end
67
+ aggregated_status_changed options
68
+ end
69
+
70
+ def aggregated_status_bools=(status)
71
+ raise InvalidArgument unless status.is_a? Array
72
+ raise InvalidArgument unless status.size == 8
73
+
74
+ return unless status != @aggregated_status_bools
75
+
76
+ @aggregated_status = []
77
+ AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
78
+ on = status[index] == true
79
+ @aggregated_status_bools[index] = on
80
+ @aggregated_status << key if on
81
+ end
82
+ aggregated_status_changed
83
+ end
84
+
85
+ def aggregated_status_changed(options = {})
86
+ @node.aggregated_status_changed self, options
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,75 @@
1
+ module RSMP
2
+ # A proxy to a remote RSMP component.
3
+ class ComponentProxy < ComponentBase
4
+ def initialize(node:, id:, ntsoid: nil, xnid: nil, grouped: false)
5
+ super
6
+ @alarms = {}
7
+ @statuses = {}
8
+ @allow_repeat_updates = {}
9
+ end
10
+
11
+ # allow the next status update to be a repeat value
12
+ def allow_repeat_updates(subscribe_list)
13
+ subscribe_list.each do |item|
14
+ sci = item['sCI']
15
+ n = item['n']
16
+ @allow_repeat_updates[sci] ||= Set.new # Set is like an array, but with no duplicates
17
+ @allow_repeat_updates[sci] << n
18
+ end
19
+ end
20
+
21
+ # Check that were not receiving repeated update values.
22
+ # The check is not performed for item with an update interval.
23
+ def check_repeat_values(message, subscription_list)
24
+ message.attribute('sS').each do |item|
25
+ check_status_item_for_repeats(item, subscription_list)
26
+ end
27
+ end
28
+
29
+ def check_status_item_for_repeats(item, subscription_list)
30
+ status_code = item['sCI']
31
+ status_name = item['n']
32
+ return if update_rate_set?(subscription_list, status_code, status_name)
33
+ return unless should_check_repeats?(subscription_list, status_code, status_name)
34
+
35
+ new_values = { 's' => item['s'], 'q' => item['q'] }
36
+ old_values = @statuses.dig(status_code, status_name)
37
+ raise RSMP::RepeatedStatusError, "no change for #{status_code} '#{status_name}'" if new_values == old_values
38
+ end
39
+
40
+ def update_rate_set?(subscription_list, status_code, status_name)
41
+ urt = subscription_list.dig(c_id, status_code, status_name, 'uRt')
42
+ urt.to_i.positive?
43
+ end
44
+
45
+ def should_check_repeats?(subscription_list, status_code, status_name)
46
+ soc = subscription_list.dig(c_id, status_code, status_name, 'sOc')
47
+ return false if soc == false
48
+ return false if @allow_repeat_updates[status_code]&.include?(status_name)
49
+
50
+ true
51
+ end
52
+
53
+ # Store the latest status update values
54
+ def store_status(message)
55
+ message.attribute('sS').each do |item|
56
+ sci = item['sCI']
57
+ n = item['n']
58
+ s = item['s']
59
+ q = item['q']
60
+ @statuses[sci] ||= {}
61
+ @statuses[sci][n] = { 's' => s, 'q' => q }
62
+
63
+ # once a value is received, don't allow the value to be a repeat
64
+ @allow_repeat_updates[sci]&.delete(n)
65
+ end
66
+ end
67
+
68
+ # handle incoming alarm
69
+ def handle_alarm(message)
70
+ code = message.attribute('aCId')
71
+ alarm = get_alarm_state code
72
+ alarm.update_from_message message
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,63 @@
1
+ # Things shared between sites and site proxies
2
+
3
+ module RSMP
4
+ module Components
5
+ attr_reader :components, :main
6
+
7
+ def initialize_components
8
+ @components = {}
9
+ @main = nil
10
+ end
11
+
12
+ def aggregated_status_changed(component, options = {}); end
13
+
14
+ def setup_components(settings)
15
+ return unless settings
16
+
17
+ check_main_component settings
18
+ settings.each_pair do |type, components_by_type|
19
+ next unless components_by_type
20
+
21
+ components_by_type.each_pair do |id, component_settings|
22
+ component_settings ||= {}
23
+ @components[id] = build_component(id: id, type: type, settings: component_settings)
24
+ @main = @components[id] if type == 'main'
25
+ end
26
+ end
27
+ end
28
+
29
+ def check_main_component(settings)
30
+ raise ConfigurationError, 'main component must be defined' unless settings['main'] && settings['main'].size >= 1
31
+ return unless settings['main'].size > 1
32
+
33
+ raise ConfigurationError, "only one main component can be defined, found #{settings['main'].keys.join(', ')}"
34
+ end
35
+
36
+ def add_component(component)
37
+ @components[component.c_id] = component
38
+ end
39
+
40
+ def infer_component_type(component_id)
41
+ raise UnknownComponent, "Component #{component_id} mising and cannot infer type"
42
+ end
43
+
44
+ def find_component(component_id, build: true)
45
+ component = @components[component_id]
46
+ return component if component
47
+
48
+ return unless build
49
+
50
+ inferred_type = infer_component_type component_id
51
+ component = inferred_type.new node: self, id: component_id
52
+ @components[component_id] = component
53
+ class_name = component.class.name.split('::').last
54
+ class_name << ' component' unless %w[Component ComponentProxy].include?(class_name)
55
+ log "Added component #{component_id} with the inferred type #{class_name}", level: :debug
56
+ component
57
+ end
58
+
59
+ def clear_alarm_timestamps
60
+ @components.each_value(&:clear_alarm_timestamps)
61
+ end
62
+ end
63
+ end
@@ -8,179 +8,187 @@ module RSMP
8
8
  module Convert
9
9
  module Export
10
10
  module JSONSchema
11
-
12
- @@json_options = {
11
+ JSON_OPTIONS = {
13
12
  array_nl: "\n",
14
13
  object_nl: "\n",
15
14
  indent: ' ',
16
15
  space_before: ' ',
17
16
  space: ' '
18
- }
17
+ }.freeze
19
18
 
20
- def self.output_json item
21
- JSON.generate(item,@@json_options)
19
+ def self.output_json(item)
20
+ JSON.generate(item, JSON_OPTIONS)
22
21
  end
23
22
 
24
- def self.build_value item
23
+ def self.build_value(item)
25
24
  out = {}
26
-
27
- if item['description']
28
- out["description"] = item['description']
29
- end
25
+ out['description'] = item['description'] if item['description']
30
26
 
31
27
  if item['list']
32
- case item['type']
33
- when "boolean"
34
- out["$ref"] = "../../../core/3.1.1/definitions.json#/boolean_list"
35
- when "integer", "ordinal", "unit", "scale", "long"
36
- out["$ref"] = "../../../core/3.1.1/definitions.json#/integer_list"
37
- else
38
- raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
39
- end
28
+ build_list_value(out, item)
29
+ else
30
+ build_single_value(out, item)
31
+ end
40
32
 
41
- if item["values"]
42
- value_list = item["values"].keys.join('|')
43
- out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
44
- end
33
+ out
34
+ end
45
35
 
46
- if item["pattern"]
47
- puts "Warning: Pattern not support for lists: #{item.inspect}"
48
- end
36
+ def self.build_list_value(out, item)
37
+ case item['type']
38
+ when 'boolean'
39
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/boolean_list'
40
+ when 'integer', 'ordinal', 'unit', 'scale', 'long'
41
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/integer_list'
49
42
  else
50
- case item['type']
51
- when "string", "base64"
52
- out["type"] = "string"
53
- when "boolean"
54
- out["$ref"] = "../../../core/3.1.1/definitions.json#/boolean"
55
- when "timestamp"
56
- out["$ref"] = "../../../core/3.1.1/definitions.json#/timestamp"
57
- when "integer", "ordinal", "unit", "scale", "long"
58
- out["$ref"] = "../../../core/3.1.1/definitions.json#/integer"
59
- else
60
- out["type"] = "string"
61
- end
62
-
63
- if item["values"]
64
- out["enum"] = item["values"].keys.sort
65
- end
43
+ raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
44
+ end
66
45
 
67
- if item["pattern"]
68
- out["pattern"] = item["pattern"]
69
- end
46
+ if item['values']
47
+ value_list = item['values'].keys.join('|')
48
+ out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
70
49
  end
71
50
 
51
+ puts "Warning: Pattern not support for lists: #{item.inspect}" if item['pattern']
52
+ end
53
+
54
+ def self.build_single_value(out, item)
55
+ case item['type']
56
+ when 'boolean'
57
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/boolean'
58
+ when 'timestamp'
59
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/timestamp'
60
+ when 'integer', 'ordinal', 'unit', 'scale', 'long'
61
+ out['$ref'] = '../../../core/3.1.1/definitions.json#/integer'
62
+ else # includes 'string', 'base64' and any other types
63
+ out['type'] = 'string'
64
+ end
72
65
 
73
- out
66
+ out['enum'] = item['values'].keys.sort if item['values']
67
+ out['pattern'] = item['pattern'] if item['pattern']
74
68
  end
75
69
 
76
- def self.build_item item, property_key: 'v'
77
- json = { "allOf" => [ { "description" => item['description'] } ] }
70
+ def self.build_item(item, property_key: 'v')
71
+ json = { 'allOf' => [{ 'description' => item['description'] }] }
78
72
  if item['arguments']
79
- json["allOf"].first["properties"] = { "n" => { "enum" => item['arguments'].keys.sort } }
80
- item['arguments'].each_pair do |key,argument|
81
- json["allOf"] << {
82
- "if" => { "required" => ["n"], "properties" => { "n" => { "const" => key }}},
83
- "then" => { "properties" => { property_key => build_value(argument) } }
73
+ json['allOf'].first['properties'] = { 'n' => { 'enum' => item['arguments'].keys.sort } }
74
+ item['arguments'].each_pair do |key, argument|
75
+ json['allOf'] << {
76
+ 'if' => { 'required' => ['n'], 'properties' => { 'n' => { 'const' => key } } },
77
+ 'then' => { 'properties' => { property_key => build_value(argument) } }
84
78
  }
85
79
  end
86
80
  end
87
81
  json
88
82
  end
89
83
 
90
- def self.output_alarms out, items
84
+ def self.output_alarms(out, items)
91
85
  list = items.keys.sort.map do |key|
92
86
  {
93
- "if" => { "required" => ["aCId"], "properties" => { "aCId" => { "const" => key }}},
94
- "then" => { "$ref" => "#{key}.json" }
87
+ 'if' => { 'required' => ['aCId'], 'properties' => { 'aCId' => { 'const' => key } } },
88
+ 'then' => { '$ref' => "#{key}.json" }
95
89
  }
96
90
  end
97
91
  json = {
98
- "properties" => {
99
- "aCId" => { "enum" => items.keys.sort },
100
- "rvs" => { "items" => { "allOf" => list } }
92
+ 'properties' => {
93
+ 'aCId' => { 'enum' => items.keys.sort },
94
+ 'rvs' => { 'items' => { 'allOf' => list } }
101
95
  }
102
96
  }
103
97
  out['alarms/alarms.json'] = output_json json
104
- items.each_pair { |key,item| output_alarm out, key, item }
98
+ items.each_pair { |key, item| output_alarm out, key, item }
105
99
  end
106
100
 
107
- def self.output_alarm out, key, item
101
+ def self.output_alarm(out, key, item)
108
102
  json = build_item item
109
103
  out["alarms/#{key}.json"] = output_json json
110
104
  end
111
105
 
112
- def self.output_statuses out, items
113
- list = [ { "properties" => { "sCI" => { "enum"=> items.keys.sort }}} ]
106
+ def self.output_statuses(out, items)
107
+ list = [{ 'properties' => { 'sCI' => { 'enum' => items.keys.sort } } }]
114
108
  items.keys.sort.each do |key|
115
109
  list << {
116
- "if"=> { "required" => ["sCI"], "properties" => { "sCI"=> { "const"=> key }}},
117
- "then" => { "$ref" => "#{key}.json" }
110
+ 'if' => { 'required' => ['sCI'], 'properties' => { 'sCI' => { 'const' => key } } },
111
+ 'then' => { '$ref' => "#{key}.json" }
118
112
  }
119
113
  end
120
- json = { "properties" => { "sS" => { "items" => { "allOf" => list }}}}
114
+ json = { 'properties' => { 'sS' => { 'items' => { 'allOf' => list } } } }
121
115
  out['statuses/statuses.json'] = output_json json
122
- items.each_pair { |key,item| output_status out, key, item }
116
+ items.each_pair { |key, item| output_status out, key, item }
123
117
  end
124
118
 
125
- def self.output_status out, key, item
126
- json = build_item item, property_key:'s'
119
+ def self.output_status(out, key, item)
120
+ json = build_item item, property_key: 's'
127
121
  out["statuses/#{key}.json"] = output_json json
128
122
  end
129
123
 
130
- def self.output_commands out, items
131
- list = [ { "properties" => { "cCI" => { "enum"=> items.keys.sort }}} ]
124
+ def self.output_commands(out, items)
125
+ list = [{ 'properties' => { 'cCI' => { 'enum' => items.keys.sort } } }]
132
126
  items.keys.sort.each do |key|
133
127
  list << {
134
- "if" => { "required" => ["cCI"], "properties" => { "cCI"=> { "const"=> key }}},
135
- "then" => { "$ref" => "#{key}.json" }
128
+ 'if' => { 'required' => ['cCI'], 'properties' => { 'cCI' => { 'const' => key } } },
129
+ 'then' => { '$ref' => "#{key}.json" }
136
130
  }
137
131
  end
138
- json = { "items" => { "allOf" => list }}
132
+ json = { 'items' => { 'allOf' => list } }
139
133
  out['commands/commands.json'] = output_json json
140
134
 
141
- json = { "properties" => { "arg" => { "$ref" => "commands.json" }}}
135
+ json = { 'properties' => { 'arg' => { '$ref' => 'commands.json' } } }
142
136
  out['commands/command_requests.json'] = output_json json
143
137
 
144
- json = { "properties" => { "rvs" => { "$ref" => "commands.json" }}}
138
+ json = { 'properties' => { 'rvs' => { '$ref' => 'commands.json' } } }
145
139
  out['commands/command_responses.json'] = output_json json
146
140
 
147
- items.each_pair { |key,item| output_command out, key, item }
141
+ items.each_pair { |key, item| output_command out, key, item }
148
142
  end
149
143
 
150
- def self.output_command out, key, item
144
+ def self.output_command(out, key, item)
151
145
  json = build_item item
152
- json["allOf"].first["properties"]['cO'] = { "const" => item['command'] }
146
+ json['allOf'].first['properties']['cO'] = { 'const' => item['command'] }
153
147
  out["commands/#{key}.json"] = output_json json
154
148
  end
155
149
 
156
- def self.output_root out, meta
157
- json = {
158
- "name"=> meta['name'],
159
- "description"=> meta['description'],
160
- "version"=> meta['version'],
161
- "allOf" => [
162
- {
163
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandRequest" }}},
164
- "then" => { "$ref" => "commands/command_requests.json" }
165
- },
166
- {
167
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandResponse" }}},
168
- "then" => { "$ref" => "commands/command_responses.json" }
169
- },
170
- {
171
- "if" => { "required" => ["type"], "properties" => { "type" => { "enum" => ["StatusRequest","StatusResponse","StatusSubscribe","StatusUnsubscribe","StatusUpdate"] }}},
172
- "then" => { "$ref" => "statuses/statuses.json" }
173
- },
174
- {
175
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "Alarm" }}},
176
- "then" => { "$ref" => "alarms/alarms.json" }
177
- }
178
- ]
150
+ def self.build_root_schema(meta)
151
+ {
152
+ 'name' => meta['name'],
153
+ 'description' => meta['description'],
154
+ 'version' => meta['version'],
155
+ 'allOf' => build_root_refs
179
156
  }
180
- out["sxl.json"] = output_json json
181
157
  end
182
158
 
183
- def self.generate sxl
159
+ def self.build_root_refs
160
+ [
161
+ {
162
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandRequest' } } },
163
+ 'then' => { '$ref' => 'commands/command_requests.json' }
164
+ },
165
+ {
166
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandResponse' } } },
167
+ 'then' => { '$ref' => 'commands/command_responses.json' }
168
+ },
169
+ {
170
+ 'if' => { 'required' => ['type'],
171
+ 'properties' => {
172
+ 'type' => {
173
+ 'enum' => %w[StatusRequest StatusResponse StatusSubscribe
174
+ StatusUnsubscribe StatusUpdate]
175
+ }
176
+ } },
177
+ 'then' => { '$ref' => 'statuses/statuses.json' }
178
+ },
179
+ {
180
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'Alarm' } } },
181
+ 'then' => { '$ref' => 'alarms/alarms.json' }
182
+ }
183
+ ]
184
+ end
185
+
186
+ def self.output_root(out, meta)
187
+ json = build_root_schema(meta)
188
+ out['sxl.json'] = output_json json
189
+ end
190
+
191
+ def self.generate(sxl)
184
192
  out = {}
185
193
  output_root out, sxl[:meta]
186
194
  output_alarms out, sxl[:alarms]
@@ -189,18 +197,16 @@ module RSMP
189
197
  out
190
198
  end
191
199
 
192
- def self.write sxl, folder
200
+ def self.write(sxl, folder)
193
201
  out = generate sxl
194
- out.each_pair do |relative_path,str|
202
+ out.each_pair do |relative_path, str|
195
203
  path = File.join(folder, relative_path)
196
- FileUtils.mkdir_p File.dirname(path) # create folders if needed
197
- file = File.open(path, 'w+') # w+ means truncate or create new file
204
+ FileUtils.mkdir_p File.dirname(path) # create folders if needed
205
+ file = File.open(path, 'w+') # w+ means truncate or create new file
198
206
  file.puts str
199
207
  end
200
208
  end
201
-
202
209
  end
203
-
204
210
  end
205
211
  end
206
- end
212
+ end
@@ -8,34 +8,37 @@ module RSMP
8
8
  module Convert
9
9
  module Import
10
10
  module YAML
11
-
12
- def self.read path
11
+ def self.read(path)
13
12
  convert ::YAML.load_file(path)
14
13
  end
15
14
 
16
- def self.parse str
15
+ def self.parse(str)
17
16
  convert ::YAML.load(str)
18
17
  end
19
18
 
20
- def self.convert yaml
21
- sxl = {
22
- meta: {},
23
- alarms: {},
24
- statuses: {},
25
- commands: {}
26
- }
27
-
19
+ def self.convert(yaml)
20
+ sxl = build_empty_sxl
28
21
  sxl[:meta] = yaml['meta']
22
+ merge_objects(sxl, yaml['objects'])
23
+ sxl
24
+ end
25
+
26
+ def self.build_empty_sxl
27
+ { meta: {}, alarms: {}, statuses: {}, commands: {} }
28
+ end
29
29
 
30
- yaml['objects'].each_pair do |type,object|
31
- object["alarms"].each { |id,item| sxl[:alarms][id] = item } if object["alarms"]
32
- object["statuses"].each { |id,item| sxl[:statuses][id] = item } if object["statuses"]
33
- object["commands"].each { |id,item| sxl[:commands][id] = item } if object["commands"]
30
+ def self.merge_objects(sxl, objects)
31
+ objects.each_pair do |_type, object|
32
+ merge_object_items(sxl, object)
34
33
  end
35
- sxl
36
34
  end
37
- end
38
35
 
36
+ def self.merge_object_items(sxl, object)
37
+ object['alarms']&.each { |id, item| sxl[:alarms][id] = item }
38
+ object['statuses']&.each { |id, item| sxl[:statuses][id] = item }
39
+ object['commands']&.each { |id, item| sxl[:commands][id] = item }
40
+ end
41
+ end
39
42
  end
40
43
  end
41
- end
44
+ end
@@ -6,7 +6,6 @@
6
6
  require 'time'
7
7
 
8
8
  module RSMP
9
-
10
9
  class Clock
11
10
  attr_reader :adjustment
12
11
 
@@ -14,7 +13,7 @@ module RSMP
14
13
  @adjustment = 0
15
14
  end
16
15
 
17
- def set target
16
+ def set(target)
18
17
  @adjustment = target - Time.now
19
18
  end
20
19
 
@@ -34,12 +33,12 @@ module RSMP
34
33
  Time.now.utc
35
34
  end
36
35
 
37
- def self.to_s time=nil
38
- (time || now).strftime("%FT%T.%3NZ")
36
+ def self.to_s(time = nil)
37
+ (time || now).strftime('%FT%T.%3NZ')
39
38
  end
40
39
 
41
- def self.parse str
40
+ def self.parse(str)
42
41
  Time.parse(str)
43
42
  end
44
43
  end
45
- end
44
+ end
@@ -1,7 +1,8 @@
1
1
  class Hash
2
2
  def deep_merge(other_hash)
3
3
  return self unless other_hash
4
- self.merge(other_hash) do |key, old, fresh|
4
+
5
+ merge(other_hash) do |_key, old, fresh|
5
6
  if old.is_a?(Hash) && fresh.is_a?(Hash)
6
7
  old.deep_merge(fresh)
7
8
  else