rsmp 0.40.0 → 0.41.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.
@@ -0,0 +1,143 @@
1
+ # Proxy for handling communication with a remote Traffic Light Controller (TLC).
2
+ # Provides high-level methods for interacting with TLC functionality.
3
+ # Acts as a mirror of the remote TLC by automatically subscribing to status updates.
4
+
5
+ module RSMP
6
+ module TLC
7
+ # Proxy for handling communication with a remote traffic light controller.
8
+ class TrafficControllerProxy < SiteProxy
9
+ include Proxy::Control
10
+ include Proxy::IO
11
+ include Proxy::Plans
12
+ include Proxy::Status
13
+ include Proxy::Detectors
14
+ include Proxy::System
15
+
16
+ attr_reader :timeplan_source, :timeplan, :timeouts,
17
+ :functional_position, :yellow_flash, :traffic_situation
18
+
19
+ # Backwards-compatible accessors expected by tests and callers
20
+ def current_plan
21
+ @timeplan
22
+ end
23
+
24
+ def plan_source
25
+ @timeplan_source
26
+ end
27
+
28
+ def initialize(options)
29
+ super
30
+ @timeplan_source = nil
31
+ @timeplan = nil
32
+ @functional_position = nil
33
+ @yellow_flash = nil
34
+ @traffic_situation = nil
35
+ @timeouts = node.supervisor_settings.dig('default', 'timeouts') || {}
36
+ end
37
+
38
+ def subscribe_to_timeplan(options: {})
39
+ validate_ready 'subscribe to timeplan'
40
+
41
+ status_list = [
42
+ { 'sCI' => 'S0014', 'n' => 'status', 'uRt' => '0' },
43
+ { 'sCI' => 'S0014', 'n' => 'source', 'uRt' => '0' }
44
+ ]
45
+ status_list.each { |item| item['sOc'] = true } if use_soc?
46
+
47
+ merged_options = @timeouts.merge(options)
48
+
49
+ raise 'TLC main component not found' unless main
50
+
51
+ subscribe_to_status main.c_id, status_list, merged_options
52
+ end
53
+
54
+ # Override status update processing to automatically store cached status values.
55
+ def process_status_update(message)
56
+ super
57
+
58
+ status_values = message.attribute('sS')
59
+ return unless status_values
60
+
61
+ status_values.each { |item| cache_status_item(item) }
62
+ end
63
+
64
+ # Get all timeplan attributes stored in the main ComponentProxy.
65
+ def timeplan_attributes
66
+ main&.statuses&.dig('S0014') || {}
67
+ end
68
+
69
+ # Returns true if sOc (send on change) should be used.
70
+ # sOc is supported in RSMP core version 3.1.5 and later.
71
+ def use_soc?
72
+ return false unless core_version
73
+
74
+ RSMP::Proxy.version_meets_requirement?(core_version, '>=3.1.5')
75
+ end
76
+
77
+ private
78
+
79
+ # Automatically subscribe to key TLC statuses to keep proxy in sync.
80
+ def auto_subscribe_to_statuses
81
+ return unless main
82
+
83
+ subscribe_to_timeplan
84
+ subscribe_to_key_statuses
85
+ end
86
+
87
+ # Look up security code for a given level from site settings.
88
+ # Expects @site_settings['security_codes'] = { 1 => 'code1', 2 => 'code2' }
89
+ def security_code_for(level)
90
+ codes = @site_settings&.dig('security_codes') || {}
91
+ code = codes[level] || codes[level.to_s]
92
+ raise ArgumentError, "Security code for level #{level} is not configured" unless code
93
+
94
+ code
95
+ end
96
+
97
+ # Send a command and, if confirm: or confirm!: is present in options, wait for
98
+ # confirming status updates afterwards.
99
+ #
100
+ # confirm_description - human-readable label used in log output
101
+ # confirm_status_list - status items to wait for (passed to wait_for_status)
102
+ # component_id - component to wait on (defaults to main)
103
+ #
104
+ # If options[:confirm] is set, timeout errors are silently swallowed.
105
+ # If options[:confirm!] is set, timeout errors are raised.
106
+ def send_command_with_confirm(component_id, command_list, options, confirm_description, confirm_status_list)
107
+ result = send_command component_id, command_list, @timeouts.merge(options.except(:confirm, :confirm!))
108
+
109
+ confirm_opts = options[:confirm] || options[:confirm!]
110
+ return result unless confirm_opts
111
+ return result if confirm_status_list.nil? || confirm_status_list.empty?
112
+
113
+ timeout = confirm_opts.is_a?(Hash) ? confirm_opts[:timeout] : nil
114
+ wait_kwargs = { timeout: timeout }.compact
115
+ begin
116
+ wait_for_status confirm_description, confirm_status_list, **wait_kwargs
117
+ rescue RSMP::TimeoutError
118
+ raise if options[:confirm!]
119
+ end
120
+
121
+ result
122
+ end
123
+
124
+ # Process a single status item and update the corresponding cached value.
125
+ def cache_status_item(item)
126
+ case item['sCI']
127
+ when 'S0007' then @functional_position = item['s'] if item['n'] == 'status'
128
+ when 'S0011' then @yellow_flash = item['s'] if item['n'] == 'status'
129
+ when 'S0014' then cache_s0014_attribute(item)
130
+ when 'S0015' then @traffic_situation = item['s'] if item['n'] == 'status'
131
+ end
132
+ end
133
+
134
+ # Update cached values for S0014 (timeplan) status attributes.
135
+ def cache_s0014_attribute(item)
136
+ case item['n']
137
+ when 'status' then @timeplan = item['s'].to_i
138
+ when 'source' then @timeplan_source = item['s']
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -12,7 +12,7 @@ module RSMP
12
12
  # setup options before calling super initializer,
13
13
  # since build of components depend on options
14
14
  @sxl = 'traffic_light_controller'
15
- @security_codes = options[:site_settings]['security_codes']
15
+ @security_codes = normalize_security_codes(options.dig(:site_settings, 'security_codes'))
16
16
  @interval = options[:site_settings].dig('intervals', 'timer') || 1
17
17
  @startup_sequence = options[:site_settings]['startup_sequence'] || 'efg'
18
18
  build_plans options[:site_settings]['signal_plans']
@@ -131,6 +131,15 @@ module RSMP
131
131
  raise MessageRejected, "Wrong security code for level #{level}"
132
132
  end
133
133
 
134
+ def normalize_security_codes(codes)
135
+ return {} unless codes.is_a?(Hash)
136
+
137
+ codes.each_with_object({}) do |(key, value), memo|
138
+ int_key = key.is_a?(String) && key.match?(/^\d+$/) ? key.to_i : key
139
+ memo[int_key] = value
140
+ end
141
+ end
142
+
134
143
  def change_security_code(level, old_code, new_code)
135
144
  verify_security_code level, old_code
136
145
  @security_codes[level] = new_code
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = '0.40.0'.freeze
2
+ VERSION = '0.41.0'.freeze
3
3
  end
data/lib/rsmp.rb CHANGED
@@ -88,6 +88,13 @@ require_relative 'rsmp/tlc/modules/display'
88
88
  require_relative 'rsmp/tlc/modules/helpers'
89
89
  require_relative 'rsmp/tlc/startup_sequence'
90
90
  require_relative 'rsmp/tlc/traffic_controller_site'
91
+ require_relative 'rsmp/tlc/proxy/control'
92
+ require_relative 'rsmp/tlc/proxy/detectors'
93
+ require_relative 'rsmp/tlc/proxy/io'
94
+ require_relative 'rsmp/tlc/proxy/plans'
95
+ require_relative 'rsmp/tlc/proxy/status'
96
+ require_relative 'rsmp/tlc/proxy/system'
97
+ require_relative 'rsmp/tlc/traffic_controller_proxy'
91
98
  require_relative 'rsmp/tlc/traffic_controller'
92
99
  require_relative 'rsmp/tlc/detector_logic'
93
100
  require_relative 'rsmp/tlc/signal_group'
@@ -95,5 +102,4 @@ require_relative 'rsmp/tlc/signal_plan'
95
102
  require_relative 'rsmp/tlc/input_states'
96
103
  require_relative 'rsmp/tlc/signal_priority'
97
104
  require_relative 'rsmp/convert/import/yaml'
98
- require_relative 'rsmp/convert/export/json_schema'
99
105
  require_relative 'rsmp/version'
data/rsmp.gemspec CHANGED
@@ -37,5 +37,5 @@ Gem::Specification.new do |spec|
37
37
  spec.add_dependency 'io-stream', '~> 0.10'
38
38
  spec.add_dependency 'logger', '~> 1.6'
39
39
  spec.add_dependency 'ostruct', '~> 0.6'
40
- spec.add_dependency 'rsmp_schema', '~> 0.8'
40
+ spec.add_dependency 'rsmp_schema', '~> 0.10'
41
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsmp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.0
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
@@ -99,14 +99,14 @@ dependencies:
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '0.8'
102
+ version: '0.10'
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '0.8'
109
+ version: '0.10'
110
110
  description: Easy RSMP site and supervisor communication.
111
111
  email:
112
112
  - zf0f@kk.dk
@@ -167,7 +167,6 @@ files:
167
167
  - lib/rsmp/component/component_base.rb
168
168
  - lib/rsmp/component/component_proxy.rb
169
169
  - lib/rsmp/component/components.rb
170
- - lib/rsmp/convert/export/json_schema.rb
171
170
  - lib/rsmp/convert/import/yaml.rb
172
171
  - lib/rsmp/helpers/clock.rb
173
172
  - lib/rsmp/helpers/deep_merge.rb
@@ -190,6 +189,7 @@ files:
190
189
  - lib/rsmp/options/options.rb
191
190
  - lib/rsmp/options/schemas/site.json
192
191
  - lib/rsmp/options/schemas/supervisor.json
192
+ - lib/rsmp/options/schemas/supervisor_site.json
193
193
  - lib/rsmp/options/schemas/traffic_controller_site.json
194
194
  - lib/rsmp/options/site_options.rb
195
195
  - lib/rsmp/options/supervisor_options.rb
@@ -225,11 +225,18 @@ files:
225
225
  - lib/rsmp/tlc/modules/startup_sequence.rb
226
226
  - lib/rsmp/tlc/modules/system.rb
227
227
  - lib/rsmp/tlc/modules/traffic_data.rb
228
+ - lib/rsmp/tlc/proxy/control.rb
229
+ - lib/rsmp/tlc/proxy/detectors.rb
230
+ - lib/rsmp/tlc/proxy/io.rb
231
+ - lib/rsmp/tlc/proxy/plans.rb
232
+ - lib/rsmp/tlc/proxy/status.rb
233
+ - lib/rsmp/tlc/proxy/system.rb
228
234
  - lib/rsmp/tlc/signal_group.rb
229
235
  - lib/rsmp/tlc/signal_plan.rb
230
236
  - lib/rsmp/tlc/signal_priority.rb
231
237
  - lib/rsmp/tlc/startup_sequence.rb
232
238
  - lib/rsmp/tlc/traffic_controller.rb
239
+ - lib/rsmp/tlc/traffic_controller_proxy.rb
233
240
  - lib/rsmp/tlc/traffic_controller_site.rb
234
241
  - lib/rsmp/version.rb
235
242
  - rsmp.gemspec
@@ -1,214 +0,0 @@
1
- # Import SXL from YAML format
2
-
3
- require 'yaml'
4
- require 'json'
5
- require 'fileutils'
6
-
7
- module RSMP
8
- module Convert
9
- module Export
10
- # Converts SXL (YAML) structures into JSON Schema files.
11
- # Converts SXL (YAML) structures into JSON Schema files.
12
- module JSONSchema
13
- JSON_OPTIONS = {
14
- array_nl: "\n",
15
- object_nl: "\n",
16
- indent: ' ',
17
- space_before: ' ',
18
- space: ' '
19
- }.freeze
20
-
21
- def self.output_json(item)
22
- JSON.generate(item, JSON_OPTIONS)
23
- end
24
-
25
- def self.build_value(item)
26
- out = {}
27
- out['description'] = item['description'] if item['description']
28
-
29
- if item['list']
30
- build_list_value(out, item)
31
- else
32
- build_single_value(out, item)
33
- end
34
-
35
- out
36
- end
37
-
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'
44
- else
45
- raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
46
- end
47
-
48
- if item['values']
49
- value_list = item['values'].keys.join('|')
50
- out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
51
- end
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
67
-
68
- out['enum'] = item['values'].keys.sort if item['values']
69
- out['pattern'] = item['pattern'] if item['pattern']
70
- end
71
-
72
- def self.build_item(item, property_key: 'v')
73
- json = { 'allOf' => [{ 'description' => item['description'] }] }
74
- if item['arguments']
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) } }
80
- }
81
- end
82
- end
83
- json
84
- end
85
-
86
- def self.output_alarms(out, items)
87
- list = items.keys.sort.map do |key|
88
- {
89
- 'if' => { 'required' => ['aCId'], 'properties' => { 'aCId' => { 'const' => key } } },
90
- 'then' => { '$ref' => "#{key}.json" }
91
- }
92
- end
93
- json = {
94
- 'properties' => {
95
- 'aCId' => { 'enum' => items.keys.sort },
96
- 'rvs' => { 'items' => { 'allOf' => list } }
97
- }
98
- }
99
- out['alarms/alarms.json'] = output_json json
100
- items.each_pair { |key, item| output_alarm out, key, item }
101
- end
102
-
103
- def self.output_alarm(out, key, item)
104
- json = build_item item
105
- out["alarms/#{key}.json"] = output_json json
106
- end
107
-
108
- def self.output_statuses(out, items)
109
- list = [{ 'properties' => { 'sCI' => { 'enum' => items.keys.sort } } }]
110
- items.keys.sort.each do |key|
111
- list << {
112
- 'if' => { 'required' => ['sCI'], 'properties' => { 'sCI' => { 'const' => key } } },
113
- 'then' => { '$ref' => "#{key}.json" }
114
- }
115
- end
116
- json = { 'properties' => { 'sS' => { 'items' => { 'allOf' => list } } } }
117
- out['statuses/statuses.json'] = output_json json
118
- items.each_pair { |key, item| output_status out, key, item }
119
- end
120
-
121
- def self.output_status(out, key, item)
122
- json = build_item item, property_key: 's'
123
- out["statuses/#{key}.json"] = output_json json
124
- end
125
-
126
- def self.output_commands(out, items)
127
- list = [{ 'properties' => { 'cCI' => { 'enum' => items.keys.sort } } }]
128
- items.keys.sort.each do |key|
129
- list << {
130
- 'if' => { 'required' => ['cCI'], 'properties' => { 'cCI' => { 'const' => key } } },
131
- 'then' => { '$ref' => "#{key}.json" }
132
- }
133
- end
134
- json = { 'items' => { 'allOf' => list } }
135
- out['commands/commands.json'] = output_json json
136
-
137
- json = { 'properties' => { 'arg' => { '$ref' => 'commands.json' } } }
138
- out['commands/command_requests.json'] = output_json json
139
-
140
- json = { 'properties' => { 'rvs' => { '$ref' => 'commands.json' } } }
141
- out['commands/command_responses.json'] = output_json json
142
-
143
- items.each_pair { |key, item| output_command out, key, item }
144
- end
145
-
146
- def self.output_command(out, key, item)
147
- json = build_item item
148
- json['allOf'].first['properties']['cO'] = { 'const' => item['command'] }
149
- out["commands/#{key}.json"] = output_json json
150
- end
151
-
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
158
- }
159
- end
160
-
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)
194
- out = {}
195
- output_root out, sxl[:meta]
196
- output_alarms out, sxl[:alarms]
197
- output_statuses out, sxl[:statuses]
198
- output_commands out, sxl[:commands]
199
- out
200
- end
201
-
202
- def self.write(sxl, folder)
203
- out = generate sxl
204
- out.each_pair do |relative_path, str|
205
- path = File.join(folder, relative_path)
206
- FileUtils.mkdir_p File.dirname(path) # create folders if needed
207
- file = File.open(path, 'w+') # w+ means truncate or create new file
208
- file.puts str
209
- end
210
- end
211
- end
212
- end
213
- end
214
- end