rsmp 0.48.4 → 0.51.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Gemfile.lock +19 -20
  4. data/README.md +27 -3
  5. data/Rakefile +3 -1
  6. data/documentation/cli.md +79 -4
  7. data/documentation/collecting_message.md +90 -170
  8. data/documentation/configuration.md +8 -0
  9. data/documentation/message_distribution.md +17 -3
  10. data/documentation/tasks.md +65 -117
  11. data/lib/rsmp/cli.rb +93 -5
  12. data/lib/rsmp/collect/collector.rb +117 -134
  13. data/lib/rsmp/collect/distributor.rb +16 -3
  14. data/lib/rsmp/collect/queue.rb +14 -6
  15. data/lib/rsmp/collect/receiver.rb +1 -1
  16. data/lib/rsmp/collect/state_collector.rb +11 -2
  17. data/lib/rsmp/collection.rb +12 -0
  18. data/lib/rsmp/completion.rb +49 -0
  19. data/lib/rsmp/component/component_base.rb +3 -3
  20. data/lib/rsmp/convert/export/json_schema/index.rb +8 -4
  21. data/lib/rsmp/convert/import/yaml.rb +12 -8
  22. data/lib/rsmp/delivery.rb +18 -0
  23. data/lib/rsmp/event.rb +19 -0
  24. data/lib/rsmp/event_source.rb +36 -0
  25. data/lib/rsmp/exchange.rb +10 -0
  26. data/lib/rsmp/failure.rb +23 -0
  27. data/lib/rsmp/helpers/error.rb +14 -23
  28. data/lib/rsmp/message.rb +10 -9
  29. data/lib/rsmp/node/node.rb +3 -21
  30. data/lib/rsmp/node/site/connections.rb +4 -9
  31. data/lib/rsmp/node/site/site.rb +33 -17
  32. data/lib/rsmp/node/supervisor/modules/configuration.rb +24 -11
  33. data/lib/rsmp/node/supervisor/modules/connection.rb +19 -5
  34. data/lib/rsmp/node/supervisor/modules/sites.rb +16 -5
  35. data/lib/rsmp/node/supervisor/supervisor.rb +13 -14
  36. data/lib/rsmp/node/task.rb +59 -53
  37. data/lib/rsmp/proxy/modules/acknowledgements.rb +22 -9
  38. data/lib/rsmp/proxy/modules/lifecycle.rb +134 -0
  39. data/lib/rsmp/proxy/modules/receive.rb +109 -66
  40. data/lib/rsmp/proxy/modules/send.rb +109 -51
  41. data/lib/rsmp/proxy/modules/state.rb +15 -5
  42. data/lib/rsmp/proxy/modules/tasks.rb +96 -70
  43. data/lib/rsmp/proxy/modules/versions.rb +69 -19
  44. data/lib/rsmp/proxy/modules/watchdogs.rb +15 -2
  45. data/lib/rsmp/proxy/proxy.rb +7 -69
  46. data/lib/rsmp/proxy/site/connection.rb +27 -22
  47. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +18 -8
  48. data/lib/rsmp/proxy/site/modules/alarms.rb +45 -38
  49. data/lib/rsmp/proxy/site/modules/commands.rb +18 -8
  50. data/lib/rsmp/proxy/site/modules/status.rb +44 -19
  51. data/lib/rsmp/proxy/site/site_proxy.rb +12 -10
  52. data/lib/rsmp/proxy/site/sxl_selection.rb +7 -2
  53. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +1 -1
  54. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +1 -1
  55. data/lib/rsmp/proxy/supervisor/modules/commands.rb +1 -1
  56. data/lib/rsmp/proxy/supervisor/modules/message_buffer.rb +8 -9
  57. data/lib/rsmp/proxy/supervisor/modules/status.rb +1 -1
  58. data/lib/rsmp/proxy/supervisor/modules/status_updates.rb +1 -1
  59. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +36 -31
  60. data/lib/rsmp/result.rb +95 -0
  61. data/lib/rsmp/schema/validation.rb +5 -3
  62. data/lib/rsmp/schema/version.rb +27 -0
  63. data/lib/rsmp/schema.rb +1 -0
  64. data/lib/rsmp/sxl/interface.rb +8 -1
  65. data/lib/rsmp/sxl/processing/catalogue.rb +61 -0
  66. data/lib/rsmp/sxl/processing/conflict_checker.rb +30 -0
  67. data/lib/rsmp/sxl/processing/dependency_graph.rb +37 -0
  68. data/lib/rsmp/sxl/processing/document.rb +158 -0
  69. data/lib/rsmp/sxl/processing/error.rb +8 -0
  70. data/lib/rsmp/sxl/processing/manifest.rb +144 -0
  71. data/lib/rsmp/sxl/processing/natural_sort.rb +22 -0
  72. data/lib/rsmp/sxl/processing/resolver.rb +94 -0
  73. data/lib/rsmp/sxl/processing/version_requirement.rb +86 -0
  74. data/lib/rsmp/sxl/processing.rb +18 -0
  75. data/lib/rsmp/sxl/supervisor_interface.rb +8 -1
  76. data/lib/rsmp/termination.rb +10 -0
  77. data/lib/rsmp/tlc/modules/plans.rb +0 -2
  78. data/lib/rsmp/tlc/modules/system.rb +2 -2
  79. data/lib/rsmp/tlc/proxy/control.rb +50 -16
  80. data/lib/rsmp/tlc/proxy/detectors.rb +15 -4
  81. data/lib/rsmp/tlc/proxy/io.rb +46 -31
  82. data/lib/rsmp/tlc/proxy/plans.rb +36 -19
  83. data/lib/rsmp/tlc/proxy/plans_bang.rb +44 -0
  84. data/lib/rsmp/tlc/proxy/status.rb +47 -23
  85. data/lib/rsmp/tlc/proxy/system.rb +16 -4
  86. data/lib/rsmp/tlc/supervisor_interface.rb +7 -1
  87. data/lib/rsmp/tlc/traffic_controller_site.rb +6 -9
  88. data/lib/rsmp/validation.rb +22 -0
  89. data/lib/rsmp/version.rb +1 -1
  90. data/lib/rsmp.rb +19 -4
  91. data/rsmp.gemspec +1 -1
  92. metadata +26 -4
  93. data/lib/rsmp/collect/collector/status.rb +0 -34
@@ -0,0 +1,144 @@
1
+ require 'date'
2
+ require 'psych'
3
+ require 'time'
4
+
5
+ module RSMP
6
+ module SXL
7
+ module Processing
8
+ # Creates and verifies deterministic SXL manifests.
9
+ module Manifest
10
+ TOP_LEVEL_KEYS = %w[meta sxls].freeze
11
+ META_KEYS = %w[created_at created_by format].freeze
12
+
13
+ def self.create(documents, format:, now: Time.now)
14
+ validate_documents!(documents, format)
15
+ {
16
+ 'meta' => {
17
+ 'created_at' => now.utc.iso8601,
18
+ 'created_by' => "rsmp v#{RSMP::VERSION}",
19
+ 'format' => format.to_s
20
+ },
21
+ 'sxls' => NaturalSort.sort(documents.map(&:name)).to_h do |name|
22
+ document = documents.find { |candidate| candidate.name == name }
23
+ [name, document.version_string]
24
+ end
25
+ }
26
+ end
27
+
28
+ def self.dump(manifest)
29
+ Psych.safe_dump(manifest, permitted_classes: [], aliases: false, line_width: -1)
30
+ end
31
+
32
+ def self.load(path)
33
+ raise Error, "Manifest #{path} not found" unless File.exist?(path)
34
+ raise Error, "Manifest #{path} is not a file" unless File.file?(path)
35
+
36
+ parse(File.read(path, encoding: 'UTF-8'), path: path)
37
+ rescue EncodingError => e
38
+ raise Error, "Cannot read manifest #{path}: #{e.message}"
39
+ end
40
+
41
+ def self.parse(source, path: '(manifest)')
42
+ Psych.safe_load(source, permitted_classes: [Date, Time], permitted_symbols: [], aliases: false)
43
+ rescue Psych::Exception => e
44
+ raise Error, "Cannot read manifest #{path}: #{e.message}"
45
+ end
46
+
47
+ def self.verify!(manifest, catalogue)
48
+ meta, sxls = validate_shape!(manifest)
49
+ documents = load_documents!(sxls, catalogue)
50
+ validate_dependency_closure!(documents, sxls)
51
+ validate_documents!(documents, meta['format'])
52
+ documents
53
+ end
54
+
55
+ def self.validate_documents!(documents, format)
56
+ format_version = VersionRequirement.exact_version(format, label: 'manifest format')
57
+ documents.each do |document|
58
+ next unless document.minimum_core_version && document.minimum_core_version > format_version
59
+
60
+ raise Error,
61
+ "SXL #{document.name} #{document.version_string} requires Core " \
62
+ "#{document.minimum_core_version}, newer than manifest format #{format}"
63
+ end
64
+ cycle = DependencyGraph.cycle(documents)
65
+ raise Error, "Cyclic dependency #{cycle.join(' -> ')}" if cycle
66
+
67
+ ConflictChecker.check!(documents)
68
+ end
69
+
70
+ def self.validate_shape!(manifest)
71
+ raise Error, 'Invalid manifest; expected a mapping' unless manifest.is_a?(Hash)
72
+
73
+ reject_unknown_keys!(manifest, TOP_LEVEL_KEYS, 'manifest')
74
+
75
+ meta = manifest['meta']
76
+ sxls = manifest['sxls']
77
+ raise Error, 'Invalid manifest meta; expected a mapping' unless meta.is_a?(Hash)
78
+ raise Error, 'Invalid manifest sxls; expected a non-empty mapping' unless sxls.is_a?(Hash) && !sxls.empty?
79
+
80
+ reject_unknown_keys!(meta, META_KEYS, 'manifest meta')
81
+ validate_metadata!(meta)
82
+ validate_sxls!(sxls)
83
+ [meta, sxls]
84
+ end
85
+
86
+ def self.validate_metadata!(meta)
87
+ missing = META_KEYS.reject { |key| meta.key?(key) }
88
+ raise Error, "Manifest meta is missing #{missing.join(', ')}" unless missing.empty?
89
+
90
+ Time.iso8601(meta['created_at'].to_s)
91
+ unless meta['created_by'].is_a?(String) && !meta['created_by'].empty?
92
+ raise Error, 'Manifest created_by must be a non-empty string'
93
+ end
94
+
95
+ VersionRequirement.exact_version(meta['format'], label: 'manifest format')
96
+ rescue ArgumentError
97
+ raise Error, 'Manifest created_at must be an ISO 8601 timestamp'
98
+ end
99
+
100
+ def self.validate_sxls!(sxls)
101
+ sxls.each_pair do |name, version|
102
+ Document.validate_name!(name)
103
+ VersionRequirement.exact_version(version, label: "version for #{name}")
104
+ end
105
+ expected_order = NaturalSort.sort(sxls.keys)
106
+ return if sxls.keys == expected_order
107
+
108
+ raise Error, 'Manifest SXLs are not in natural name order'
109
+ end
110
+
111
+ def self.load_documents!(sxls, catalogue)
112
+ sxls.map do |name, version|
113
+ document = catalogue.fetch(name, version)
114
+ raise Error, "SXL #{name} #{version} is not available from the supplied sources" unless document
115
+
116
+ document
117
+ end
118
+ end
119
+
120
+ def self.validate_dependency_closure!(documents, sxls)
121
+ documents.each do |document|
122
+ document.dependencies.each_pair do |name, requirement|
123
+ version = sxls[name]
124
+ raise Error, "Manifest is missing dependency #{name} required by #{document.name}" unless version
125
+ next if requirement.satisfied_by?(version)
126
+
127
+ raise Error,
128
+ "Manifest dependency #{name} #{version} does not satisfy #{requirement.source} " \
129
+ "required by #{document.name}"
130
+ end
131
+ end
132
+ end
133
+
134
+ def self.reject_unknown_keys!(hash, expected, label)
135
+ unknown = hash.keys - expected
136
+ raise Error, "Unknown #{label} key(s): #{unknown.join(', ')}" unless unknown.empty?
137
+ end
138
+
139
+ private_class_method :validate_shape!, :validate_metadata!, :validate_sxls!, :load_documents!,
140
+ :validate_dependency_closure!, :reject_unknown_keys!
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,22 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Natural sorting for identifiers containing decimal digit sequences.
5
+ module NaturalSort
6
+ def self.key(value)
7
+ value.scan(/\d+|\D+/).map do |part|
8
+ if part.match?(/\A\d+\z/)
9
+ [0, part.to_i, part.length]
10
+ else
11
+ [1, part]
12
+ end
13
+ end
14
+ end
15
+
16
+ def self.sort(values)
17
+ values.sort_by { |value| key(value) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,94 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Resolves root requirements to a deterministic set of exact SXL versions.
5
+ class Resolver
6
+ def initialize(catalogue)
7
+ @catalogue = catalogue
8
+ @diagnostics = []
9
+ end
10
+
11
+ def resolve(root_requirements)
12
+ constraints = Hash.new { |hash, key| hash[key] = [] }
13
+ root_requirements.each do |name, requirement|
14
+ Document.validate_name!(name)
15
+ constraints[name] << requirement
16
+ end
17
+ raise Error, 'At least one root SXL is required' if constraints.empty?
18
+
19
+ solution = search({}, constraints)
20
+ return sorted(solution.values) if solution
21
+
22
+ detail = @diagnostics.uniq.first(3).join('; ')
23
+ raise Error, "Cannot resolve SXL dependencies#{": #{detail}" unless detail.empty?}"
24
+ end
25
+
26
+ private
27
+
28
+ def search(selected, constraints)
29
+ return nil unless selected_versions_valid?(selected, constraints)
30
+
31
+ unresolved = NaturalSort.sort(constraints.keys.reject { |name| selected.key?(name) })
32
+ return selected if unresolved.empty?
33
+
34
+ name = unresolved.first
35
+ candidates = @catalogue.candidates(name, constraints[name])
36
+ record_missing_candidate(name, constraints[name]) if candidates.empty?
37
+
38
+ candidates.each do |document|
39
+ solution = search_candidate(name, document, selected, constraints)
40
+ return solution if solution
41
+ end
42
+ nil
43
+ end
44
+
45
+ def search_candidate(name, document, selected, constraints)
46
+ next_selected = selected.merge(name => document)
47
+ next_constraints = copy_constraints(constraints)
48
+ document.dependencies.each_pair do |dependency_name, requirement|
49
+ next_constraints[dependency_name] << requirement
50
+ end
51
+
52
+ cycle = DependencyGraph.cycle(next_selected.values)
53
+ if cycle
54
+ @diagnostics << "cyclic dependency #{cycle.join(' -> ')}"
55
+ return nil
56
+ end
57
+
58
+ search(next_selected, next_constraints)
59
+ end
60
+
61
+ def selected_versions_valid?(selected, constraints)
62
+ selected.each_pair do |name, document|
63
+ next if constraints[name].all? { |requirement| requirement.satisfied_by?(document.version) }
64
+
65
+ requirements = constraints[name].map(&:source).join(', ')
66
+ @diagnostics << "#{name} #{document.version_string} does not satisfy #{requirements}"
67
+ return false
68
+ end
69
+ true
70
+ end
71
+
72
+ def record_missing_candidate(name, requirements)
73
+ available = @catalogue.versions(name)
74
+ if available.empty?
75
+ @diagnostics << "no versions available for #{name}"
76
+ else
77
+ expected = requirements.map(&:source).join(', ')
78
+ @diagnostics << "no version of #{name} satisfies #{expected} (available: #{available.join(', ')})"
79
+ end
80
+ end
81
+
82
+ def copy_constraints(constraints)
83
+ constraints.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(name, requirements), copy|
84
+ copy[name] = requirements.dup
85
+ end
86
+ end
87
+
88
+ def sorted(documents)
89
+ documents.sort_by { |document| NaturalSort.key(document.name) }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,86 @@
1
+ require 'rubygems'
2
+
3
+ module RSMP
4
+ module SXL
5
+ module Processing
6
+ # Parses the version requirement syntax defined by RSMP Core.
7
+ class VersionRequirement
8
+ EXACT_VERSION = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\z/
9
+ PARTIAL_VERSION = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?\z/
10
+ CLAUSE = /\A(~|>=|<=|>|<)?\s*(\d+(?:\.\d+){1,2})\z/
11
+
12
+ attr_reader :source
13
+
14
+ def self.exact_version(value, label: 'version')
15
+ string = value.to_s
16
+ unless EXACT_VERSION.match?(string)
17
+ raise Error, "Invalid #{label} #{value.inspect}; expected MAJOR.MINOR.PATCH"
18
+ end
19
+
20
+ Gem::Version.new(string)
21
+ end
22
+
23
+ def initialize(source)
24
+ @source = source.to_s
25
+ @predicates = parse(@source)
26
+ end
27
+
28
+ def satisfied_by?(version)
29
+ version = self.class.exact_version(version) unless version.is_a?(Gem::Version)
30
+ @predicates.all? { |predicate| predicate.call(version) }
31
+ end
32
+
33
+ private
34
+
35
+ def parse(source)
36
+ raise Error, 'Version requirement cannot be empty' if source.strip.empty?
37
+
38
+ source.split(/\s+and\s+/).map { |clause| parse_clause(clause.strip) }
39
+ end
40
+
41
+ def parse_clause(clause)
42
+ match = CLAUSE.match(clause)
43
+ raise Error, "Invalid version requirement #{source.inspect}" unless match
44
+
45
+ operator = match[1]
46
+ version_string = match[2]
47
+ return compatible_predicate(version_string) if operator == '~'
48
+
49
+ version = self.class.exact_version(version_string, label: 'requirement version')
50
+ comparison_predicate(operator || '=', version)
51
+ end
52
+
53
+ def compatible_predicate(version_string)
54
+ match = PARTIAL_VERSION.match(version_string)
55
+ raise Error, "Invalid compatibility requirement #{source.inspect}" unless match
56
+
57
+ major, minor, patch = match.captures.map { |part| part&.to_i }
58
+ return major_zero_predicate(major, minor, patch) if major.zero?
59
+
60
+ lower = Gem::Version.new("#{major}.#{minor}.#{patch || 0}")
61
+ upper = Gem::Version.new("#{major + 1}.0.0")
62
+ ->(version) { version >= lower && version < upper }
63
+ end
64
+
65
+ def major_zero_predicate(major, minor, patch)
66
+ unless patch
67
+ raise Error, "Major-zero compatibility requirement #{source.inspect} must include an exact patch version"
68
+ end
69
+
70
+ exact = Gem::Version.new("#{major}.#{minor}.#{patch}")
71
+ ->(version) { version == exact }
72
+ end
73
+
74
+ def comparison_predicate(operator, expected)
75
+ case operator
76
+ when '=' then ->(version) { version == expected }
77
+ when '>' then ->(version) { version > expected }
78
+ when '>=' then ->(version) { version >= expected }
79
+ when '<' then ->(version) { version < expected }
80
+ when '<=' then ->(version) { version <= expected }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../version'
2
+ require_relative 'processing/error'
3
+ require_relative 'processing/natural_sort'
4
+ require_relative 'processing/version_requirement'
5
+ require_relative 'processing/document'
6
+ require_relative 'processing/catalogue'
7
+ require_relative 'processing/dependency_graph'
8
+ require_relative 'processing/resolver'
9
+ require_relative 'processing/conflict_checker'
10
+ require_relative 'processing/manifest'
11
+
12
+ module RSMP
13
+ module SXL
14
+ # Dependency resolution, conflict checks, and manifests for SXL definitions.
15
+ module Processing
16
+ end
17
+ end
18
+ end
@@ -8,12 +8,19 @@ module RSMP
8
8
 
9
9
  def_delegators :proxy,
10
10
  :request_status,
11
+ :request_status!,
11
12
  :request_status_and_collect,
13
+ :request_status_and_collect!,
12
14
  :subscribe_to_status,
15
+ :subscribe_to_status!,
13
16
  :subscribe_to_status_and_collect,
17
+ :subscribe_to_status_and_collect!,
14
18
  :unsubscribe_to_status,
19
+ :unsubscribe_to_status!,
15
20
  :send_command,
16
- :send_command_and_collect
21
+ :send_command!,
22
+ :send_command_and_collect,
23
+ :send_command_and_collect!
17
24
 
18
25
  def process_message(_message); end
19
26
  end
@@ -0,0 +1,10 @@
1
+ module RSMP
2
+ # An explicit reason for a long-running service task to finish normally.
3
+ Termination = Data.define(:reason, :source) do
4
+ def initialize(reason:, source:)
5
+ raise ArgumentError, 'termination reason must be a Symbol' unless reason.is_a?(Symbol)
6
+
7
+ super
8
+ end
9
+ end
10
+ end
@@ -213,8 +213,6 @@ module RSMP
213
213
  end.join(',')
214
214
  TrafficControllerSite.make_status times
215
215
  end
216
- rescue StandardError => e
217
- puts e
218
216
  end
219
217
  end
220
218
  end
@@ -64,7 +64,7 @@ module RSMP
64
64
 
65
65
  # S0091 - Operator logged in/out OP-panel
66
66
  def handle_s0091(_status_code, status_name = nil, options = {})
67
- if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1')
67
+ if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1.0')
68
68
  case status_name
69
69
  when 'user'
70
70
  TrafficControllerSite.make_status 0
@@ -81,7 +81,7 @@ module RSMP
81
81
 
82
82
  # S0092 - Operator logged in/out web-interface
83
83
  def handle_s0092(_status_code, status_name = nil, options = {})
84
- if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1')
84
+ if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1.0')
85
85
  case status_name
86
86
  when 'user'
87
87
  TrafficControllerSite.make_status 0
@@ -6,20 +6,29 @@ module RSMP
6
6
  module Control
7
7
  # M0001 - Set functional position (NormalControl, YellowFlash, Dark).
8
8
  def set_functional_position(status, within:, timeout_minutes: 0)
9
- validate_ready 'set functional position'
9
+ readiness = validate_ready 'set functional position'
10
+ return readiness if readiness.failure?
11
+
10
12
  raise 'TLC main component not found' unless main
11
13
 
12
14
  command_list = functional_position_command_list(status, timeout_minutes)
13
15
  confirm_status = functional_position_confirm_status(status)
14
- collector = send_command_and_collect(command_list, within: within)
15
- collector.ok!
16
- wait_for_status "functional position #{status}", confirm_status, timeout: within unless confirm_status.empty?
17
- { collector: collector }
16
+ send_command_and_collect(command_list, within: within).and_then do |exchange|
17
+ next Result.success(exchange) if confirm_status.empty?
18
+
19
+ wait_for_status("functional position #{status}", confirm_status, timeout: within).map { exchange }
20
+ end
21
+ end
22
+
23
+ def set_functional_position!(...)
24
+ set_functional_position(...).value!
18
25
  end
19
26
 
20
27
  # M0005 - Set or clear an emergency route.
21
28
  def set_emergency_route(route:, active:, within:)
22
- validate_ready 'set emergency route'
29
+ readiness = validate_ready 'set emergency route'
30
+ return readiness if readiness.failure?
31
+
23
32
  raise 'TLC main component not found' unless main
24
33
 
25
34
  security_code = security_code_for(2)
@@ -41,12 +50,18 @@ module RSMP
41
50
  'v' => command_value('M0005', 'emergencyroute', route)
42
51
  }]
43
52
 
44
- send_command_and_collect(command_list, within: within).ok!
53
+ send_command_and_collect(command_list, within: within)
54
+ end
55
+
56
+ def set_emergency_route!(...)
57
+ set_emergency_route(...).value!
45
58
  end
46
59
 
47
60
  # M0007 - Enable or disable fixed-time control.
48
61
  def set_fixed_time(status, within:)
49
- validate_ready 'set fixed time'
62
+ readiness = validate_ready 'set fixed time'
63
+ return readiness if readiness.failure?
64
+
50
65
  raise 'TLC main component not found' unless main
51
66
 
52
67
  security_code = security_code_for(2)
@@ -64,13 +79,20 @@ module RSMP
64
79
  }]
65
80
 
66
81
  confirm_status = [{ 'sCI' => 'S0009', 'n' => 'status', 's' => [boolean_value(status)] }]
67
- send_command_and_collect(command_list, within: within).ok!
68
- wait_for_status "fixed time #{status}", confirm_status, timeout: within
82
+ send_command_and_collect(command_list, within: within).and_then do |exchange|
83
+ wait_for_status("fixed time #{status}", confirm_status, timeout: within).map { exchange }
84
+ end
85
+ end
86
+
87
+ def set_fixed_time!(...)
88
+ set_fixed_time(...).value!
69
89
  end
70
90
 
71
91
  # M0003 - Set traffic situation (activate a specific situation number).
72
92
  def set_traffic_situation(situation, within:)
73
- validate_ready 'set traffic situation'
93
+ readiness = validate_ready 'set traffic situation'
94
+ return readiness if readiness.failure?
95
+
74
96
  raise 'TLC main component not found' unless main
75
97
 
76
98
  security_code = security_code_for(2)
@@ -93,13 +115,20 @@ module RSMP
93
115
  }]
94
116
 
95
117
  confirm_status = [{ 'sCI' => 'S0015', 'n' => 'status', 's' => integer_value(situation) }]
96
- send_command_and_collect(command_list, within: within).ok!
97
- wait_for_status "traffic situation #{situation}", confirm_status, timeout: within
118
+ send_command_and_collect(command_list, within: within).and_then do |exchange|
119
+ wait_for_status("traffic situation #{situation}", confirm_status, timeout: within).map { exchange }
120
+ end
121
+ end
122
+
123
+ def set_traffic_situation!(...)
124
+ set_traffic_situation(...).value!
98
125
  end
99
126
 
100
127
  # M0003 - Clear the active traffic situation.
101
128
  def unset_traffic_situation(within:)
102
- validate_ready 'unset traffic situation'
129
+ readiness = validate_ready 'unset traffic situation'
130
+ return readiness if readiness.failure?
131
+
103
132
  raise 'TLC main component not found' unless main
104
133
 
105
134
  security_code = security_code_for(2)
@@ -122,8 +151,13 @@ module RSMP
122
151
  }]
123
152
 
124
153
  confirm_status = [{ 'sCI' => 'S0015', 'n' => 'status', 's' => 1 }]
125
- send_command_and_collect(command_list, within: within).ok!
126
- wait_for_status 'traffic situation unset', confirm_status, timeout: within
154
+ send_command_and_collect(command_list, within: within).and_then do |exchange|
155
+ wait_for_status('traffic situation unset', confirm_status, timeout: within).map { exchange }
156
+ end
157
+ end
158
+
159
+ def unset_traffic_situation!(...)
160
+ unset_traffic_situation(...).value!
127
161
  end
128
162
 
129
163
  private
@@ -7,7 +7,8 @@ module RSMP
7
7
  # M0008 - Force detector logic to a given mode and status.
8
8
  # component_id must refer to the detector logic component, not main.
9
9
  def force_detector_logic(component_id, status:, mode:, within:)
10
- validate_ready 'force detector logic'
10
+ readiness = validate_ready 'force detector logic'
11
+ return readiness if readiness.failure?
11
12
 
12
13
  security_code = security_code_for(2)
13
14
 
@@ -27,12 +28,18 @@ module RSMP
27
28
  'n' => 'mode',
28
29
  'v' => command_value('M0008', 'mode', mode)
29
30
  }]
30
- send_command_and_collect(command_list, component: component_id, within: within).ok!
31
+ send_command_and_collect(command_list, component: component_id, within: within)
32
+ end
33
+
34
+ def force_detector_logic!(...)
35
+ force_detector_logic(...).value!
31
36
  end
32
37
 
33
38
  # M0021 - Set the trigger level for traffic counting.
34
39
  def set_trigger_level(status, within:)
35
- validate_ready 'set trigger level'
40
+ readiness = validate_ready 'set trigger level'
41
+ return readiness if readiness.failure?
42
+
36
43
  raise 'TLC main component not found' unless main
37
44
 
38
45
  security_code = security_code_for(2)
@@ -48,7 +55,11 @@ module RSMP
48
55
  'n' => 'securityCode',
49
56
  'v' => security_code.to_s
50
57
  }]
51
- send_command_and_collect(command_list, within: within).ok!
58
+ send_command_and_collect(command_list, within: within)
59
+ end
60
+
61
+ def set_trigger_level!(...)
62
+ set_trigger_level(...).value!
52
63
  end
53
64
  end
54
65
  end