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