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.
@@ -1,5 +1,5 @@
1
1
  port: 12111
2
- guest:
2
+ default:
3
3
  sxl: tlc
4
4
  intervals:
5
5
  timer: 0.1
@@ -54,15 +54,81 @@ log:
54
54
 
55
55
  ```yaml
56
56
  port: 12111
57
- guest:
57
+ default:
58
58
  sxl: tlc
59
59
  intervals:
60
60
  timer: 0.1
61
61
  watchdog: 0.1
62
62
  log:
63
63
  json: true
64
+ sites:
65
+ TLC001:
66
+ sxl: tlc
67
+ sxl_version: "1.2.1"
68
+ intervals:
69
+ timer: 0.1
70
+ watchdog: 0.1
71
+ timeouts:
72
+ connect: 1
73
+ acknowledgement: 1
74
+ components:
75
+ main:
76
+ TC:
64
77
  ```
65
78
 
79
+ Per-site configuration follows the supervisor-side site schema (`lib/rsmp/options/schemas/supervisor_site.json`). Each site entry must include an `sxl` value; if `sxl` is missing the supervisor will raise a `RSMP::ConfigurationError` on startup.
80
+
81
+ ## Supervisor settings
82
+
83
+ The following lists the top-level supervisor settings and the keys available for per-site configuration under `sites`.
84
+
85
+ Top-level supervisor settings
86
+
87
+ - `port`: integer|string — TCP port the supervisor listens on (default: `12111`).
88
+ - `ip`: string — address to bind to.
89
+ - `ips`: string or array — `'all'` or a list of allowed IP addresses.
90
+ - `site_id`: string — optional site identifier for the supervisor itself.
91
+ - `max_sites`: integer — limit concurrent connected sites.
92
+ - `default`: object — default settings applied to sites that don't have a specific `sites` entry. Contains keys:
93
+ - `sxl`: string — default SXL type for default sites (e.g. `tlc`).
94
+ - `sxl_version`, `core_version`: strings for version hints.
95
+ - `intervals`: object with `timer`, `watchdog` (numbers, seconds).
96
+ - `timeouts`: object with `watchdog`, `acknowledgement` (numbers, seconds).
97
+ - `log`: object — log settings (see `log_settings` elsewhere in docs).
98
+ - `sites`: mapping — per-site settings (see below).
99
+
100
+ ## Per-site settings (`sites` mapping)
101
+
102
+ Each key under `sites` is a site id (for example `TLC001`) and the value is the supervisor-side configuration for that site. These settings tell the supervisor how to handle incoming connections from that specific site (which SXL/schema to use, per-site timeouts, component layout, etc.). Per-site configuration follows the supervisor-side schema at `lib/rsmp/options/schemas/supervisor_site.json`.
103
+
104
+ If a connecting site's id is not present under `sites`, the supervisor will fall back to the `default` settings. The runtime configuration check will raise `RSMP::ConfigurationError` if a site entry is present but missing the required `sxl` key.
105
+
106
+
107
+ Common per-site keys
108
+
109
+ - `sxl` (string, required): the SXL type to use for this site (for example `tlc`). The supervisor will attempt to load the corresponding schemas for this SXL.
110
+ - `sxl_version` (string): preferred SXL version (informational; runtime version comes from the site's Version message).
111
+ - `type` (string): optional human-readable type identifier.
112
+ - `site_id` (string): explicit site identifier (if different from the mapping key).
113
+ - `supervisors` (array): list of supervisor endpoints (objects with `ip` and `port`). Useful for reverse mappings or local-site configs.
114
+ - `components` (object): component definitions (same structure as site `components`), used by the supervisor-side proxies to set up component proxies.
115
+ - `intervals` (object): per-site timer settings — `timer`, `watchdog`, `reconnect`, `after_connect` (numbers, seconds).
116
+ - `timeouts` (object): per-site timeouts — `connect`, `watchdog`, `acknowledgement` (numbers, seconds).
117
+ - `send_after_connect` (boolean): whether to send messages after connect without waiting for additional events.
118
+ - `skip_validation` (array[string]): list of message types to skip JSON schema validation for this site.
119
+ - `security_codes` (object): map of security code levels to secrets.
120
+
121
+ ### TLC-specific settings
122
+
123
+ TLC-specific settings are used when a site uses the `tlc` SXL and include:
124
+
125
+ - `startup_sequence` (string): expected startup sequence for the traffic controller.
126
+ - `signal_plans` (object): signal plan definitions and timing information.
127
+ - `inputs` (object): input definitions for the controller.
128
+ - `live_output` (string|null): optional live output destination.
129
+
130
+ See `lib/rsmp/options/schemas/traffic_controller_site.json` for the full schema and examples.
131
+
66
132
  ## Validation
67
133
 
68
134
  Invalid configurations raise `RSMP::ConfigurationError` with details about the failing path. The CLI prints these errors when loading config files.
data/lib/rsmp/cli.rb CHANGED
@@ -163,8 +163,8 @@ module RSMP
163
163
  def apply_core_version_option(settings)
164
164
  return unless options[:core]
165
165
 
166
- settings['guest'] ||= {}
167
- settings['guest']['core_version'] = options[:core]
166
+ settings['default'] ||= {}
167
+ settings['default']['core_version'] = options[:core]
168
168
  end
169
169
 
170
170
  def apply_log_options(log_settings)
@@ -11,7 +11,7 @@ module RSMP
11
11
  # Array#to_s and Hash#to_s usually show items, but here we show just number
12
12
  # of items, when the short form is requested.
13
13
  module Inspect
14
- def inspector *short_items
14
+ def inspector(*short_items)
15
15
  instance_variables.map do |var_name|
16
16
  var = instance_variable_get(var_name)
17
17
  class_name = var.class.name
@@ -6,13 +6,13 @@ module RSMP
6
6
  def handle_supervisor_settings(supervisor_settings)
7
7
  options = RSMP::Supervisor::Options.new(supervisor_settings || {})
8
8
  @supervisor_settings = options.to_h
9
- @core_version = @supervisor_settings.dig('guest', 'core_version')
9
+ @core_version = @supervisor_settings.dig('default', 'core_version')
10
10
  check_site_sxl_types
11
11
  end
12
12
 
13
13
  def check_site_sxl_types
14
14
  sites = @supervisor_settings['sites'].clone || {}
15
- sites['guest'] = @supervisor_settings['guest']
15
+ sites['default'] = @supervisor_settings['default']
16
16
  sites.each do |site_id, settings|
17
17
  raise RSMP::ConfigurationError, "Configuration for site '#{site_id}' is empty" unless settings
18
18
 
@@ -26,16 +26,18 @@ module RSMP
26
26
  end
27
27
 
28
28
  def site_id_to_site_setting(site_id)
29
- return {} unless @supervisor_settings['sites']
29
+ base = @supervisor_settings['default'] || {}
30
30
 
31
- @supervisor_settings['sites'].each_pair do |id, settings|
32
- return settings if id == 'guest' || id == site_id
33
- end
34
- raise HandshakeError, "site id #{site_id} unknown"
31
+ return base unless @supervisor_settings['sites']
32
+
33
+ site_specific = @supervisor_settings['sites'][site_id] || @supervisor_settings['sites']['default']
34
+ return base unless site_specific
35
+
36
+ base.deep_merge(site_specific)
35
37
  end
36
38
 
37
39
  def ip_to_site_settings(ip)
38
- @supervisor_settings['sites'][ip] || @supervisor_settings['sites']['guest']
40
+ @supervisor_settings['sites'][ip] || @supervisor_settings['sites']['default']
39
41
  end
40
42
  end
41
43
  end
@@ -40,7 +40,7 @@ module RSMP
40
40
  return if @supervisor_settings['ips'] == 'all'
41
41
  return if @supervisor_settings['ips'].include? ip
42
42
 
43
- raise ConnectionError, 'guest ip not allowed'
43
+ raise ConnectionError, 'default ip not allowed'
44
44
  end
45
45
 
46
46
  def check_max_sites
@@ -60,6 +60,8 @@ module RSMP
60
60
  def build_proxy_settings(socket, info)
61
61
  stream = IO::Stream::Buffered.new(socket)
62
62
  protocol = RSMP::Protocol.new stream
63
+ site_id = retrieve_site_id(protocol)
64
+ site_settings = site_id_to_site_setting site_id
63
65
 
64
66
  {
65
67
  supervisor: self,
@@ -72,7 +74,9 @@ module RSMP
72
74
  protocol: protocol,
73
75
  info: info,
74
76
  logger: @logger,
75
- archive: @archive
77
+ archive: @archive,
78
+ site_id: site_id,
79
+ site_settings: site_settings
76
80
  }
77
81
  end
78
82
 
@@ -83,12 +87,12 @@ module RSMP
83
87
 
84
88
  def setup_proxy(proxy, settings, id)
85
89
  if proxy
86
- raise ConnectionError, "Site #{id} alredy connected from port #{proxy.port}" if proxy.connected?
90
+ raise ConnectionError, "Site #{id} already connected from port #{proxy.port}" if proxy.connected?
87
91
 
88
92
  proxy.revive settings
89
93
  else
90
94
  check_max_sites
91
- proxy = build_proxy settings.merge(site_id: id)
95
+ proxy = build_proxy settings
92
96
  @proxies.push proxy
93
97
  end
94
98
  proxy
@@ -100,6 +104,9 @@ module RSMP
100
104
  log "Validating using core version #{proxy.core_version}", level: :debug
101
105
  proxy.start
102
106
  proxy.wait
107
+ ensure
108
+ proxy_type = proxy ? proxy.class.name.split('::').last : 'SiteProxy'
109
+ log "Created #{proxy_type} for site #{proxy&.site_id}", level: :debug
103
110
  end
104
111
 
105
112
  def accept_connection(socket, info)
@@ -112,7 +119,7 @@ module RSMP
112
119
  authorize_ip info[:ip]
113
120
 
114
121
  settings = build_proxy_settings(socket, info)
115
- id = retrieve_site_id(settings[:protocol])
122
+ id = settings[:site_id]
116
123
  proxy = setup_proxy(find_site(id), settings, id)
117
124
 
118
125
  validate_and_start_proxy(proxy, settings[:protocol])
@@ -121,6 +128,8 @@ module RSMP
121
128
  stop if @supervisor_settings['one_shot']
122
129
  end
123
130
 
131
+ # Proxy type is now derived from `site_settings['sxl']` in Supervisor#build_proxy.
132
+
124
133
  def reject_connection(_socket, info)
125
134
  log 'Site rejected', ip: info[:ip], level: :info
126
135
  end
@@ -59,6 +59,13 @@ module RSMP
59
59
  end
60
60
 
61
61
  def build_proxy(settings)
62
+ # Determine proxy type from site settings (SXL). Fall back to supervisor
63
+ # default settings when site-specific settings are not present.
64
+ site_settings = settings[:site_settings] || @supervisor_settings['default']
65
+ sxl_type = site_settings && site_settings['sxl']
66
+
67
+ return RSMP::TLC::TrafficControllerProxy.new(settings) if sxl_type == 'tlc'
68
+
62
69
  SiteProxy.new settings
63
70
  end
64
71
 
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "site_id": { "type": "string" },
14
14
  "max_sites": { "type": "integer" },
15
- "guest": {
15
+ "default": {
16
16
  "type": "object",
17
17
  "properties": {
18
18
  "sxl": { "type": "string" },
@@ -37,7 +37,10 @@
37
37
  },
38
38
  "additionalProperties": true
39
39
  },
40
- "sites": { "type": "object" }
40
+ "sites": {
41
+ "type": "object",
42
+ "additionalProperties": { "$ref": "supervisor_site.json" }
43
+ }
41
44
  },
42
45
  "additionalProperties": true
43
- }
46
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "supervisor_site.json",
4
+ "type": "object",
5
+ "properties": {
6
+ "sxl": { "type": "string" },
7
+ "sxl_version": { "type": "string" },
8
+ "type": { "type": "string" },
9
+ "components": { "type": "object" },
10
+ "supervisors": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "properties": {
15
+ "ip": { "type": "string" },
16
+ "port": { "type": ["integer", "string"] }
17
+ },
18
+ "required": ["ip", "port"],
19
+ "additionalProperties": true
20
+ }
21
+ },
22
+ "intervals": {
23
+ "type": "object",
24
+ "properties": {
25
+ "timer": { "type": "number" },
26
+ "watchdog": { "type": "number" },
27
+ "reconnect": { "type": "number" },
28
+ "after_connect": { "type": "number" }
29
+ },
30
+ "additionalProperties": true
31
+ },
32
+ "timeouts": {
33
+ "type": "object",
34
+ "properties": {
35
+ "connect": { "type": "number" },
36
+ "watchdog": { "type": "number" },
37
+ "acknowledgement": { "type": "number" }
38
+ },
39
+ "additionalProperties": true
40
+ },
41
+ "send_after_connect": { "type": "boolean" },
42
+ "skip_validation": { "type": "array", "items": { "type": "string" } },
43
+ "security_codes": { "type": "object" }
44
+ },
45
+ "additionalProperties": true
46
+ }
@@ -10,9 +10,10 @@
10
10
  "startup_sequence": { "type": "string" },
11
11
  "signal_plans": { "type": "object" },
12
12
  "inputs": { "type": "object" },
13
- "live_output": { "type": ["string", "null"] }
13
+ "live_output": { "type": ["string", "null"] },
14
+ "type": { "type": "string" }
14
15
  },
15
16
  "additionalProperties": true
16
17
  }
17
18
  ]
18
- }
19
+ }
@@ -6,7 +6,7 @@ module RSMP
6
6
  {
7
7
  'port' => 12_111,
8
8
  'ips' => 'all',
9
- 'guest' => {
9
+ 'default' => {
10
10
  'sxl' => 'tlc',
11
11
  'intervals' => {
12
12
  'timer' => 1,
@@ -14,7 +14,9 @@ module RSMP
14
14
  },
15
15
  'timeouts' => {
16
16
  'watchdog' => 2,
17
- 'acknowledgement' => 2
17
+ 'acknowledgement' => 2,
18
+ 'command' => 10,
19
+ 'status_response' => 10
18
20
  }
19
21
  }
20
22
  }
@@ -22,6 +22,7 @@ module RSMP
22
22
 
23
23
  def initialize(options)
24
24
  @node = options[:node]
25
+ options[:logger] = @node&.logger unless options[:logger] # default to node logger
25
26
  initialize_logging options
26
27
  initialize_distributor
27
28
  initialize_task
@@ -21,6 +21,64 @@ module RSMP
21
21
  send_message message, validate: options[:validate]
22
22
  message
23
23
  end
24
+
25
+ # Send an AlarmSuspend message and optionally collect the confirming response.
26
+ # When collect: true, returns [message, response]; when collect: false, returns message.
27
+ def suspend_alarm(task, c_id:, a_c_id:, collect: false)
28
+ message = RSMP::AlarmSuspend.new(
29
+ 'mId' => RSMP::Message.make_m_id,
30
+ 'cId' => c_id,
31
+ 'aCId' => a_c_id
32
+ )
33
+ if collect
34
+ collect_task = task.async do
35
+ RSMP::AlarmCollector.new(self,
36
+ m_id: message.m_id,
37
+ num: 1,
38
+ matcher: {
39
+ 'cId' => c_id,
40
+ 'aCI' => a_c_id,
41
+ 'aSp' => 'Suspend',
42
+ 'sS' => /^Suspended/i
43
+ },
44
+ timeout: node.supervisor_settings.dig('default', 'timeouts', 'alarm')).collect!
45
+ end
46
+ send_message message
47
+ [message, collect_task.wait.first]
48
+ else
49
+ send_message message
50
+ message
51
+ end
52
+ end
53
+
54
+ # Send an AlarmResume message and optionally collect the confirming response.
55
+ # When collect: true, returns [message, response]; when collect: false, returns message.
56
+ def resume_alarm(task, c_id:, a_c_id:, collect: false)
57
+ message = RSMP::AlarmResume.new(
58
+ 'mId' => RSMP::Message.make_m_id,
59
+ 'cId' => c_id,
60
+ 'aCId' => a_c_id
61
+ )
62
+ if collect
63
+ collect_task = task.async do
64
+ RSMP::AlarmCollector.new(self,
65
+ m_id: message.m_id,
66
+ num: 1,
67
+ matcher: {
68
+ 'cId' => c_id,
69
+ 'aCI' => a_c_id,
70
+ 'aSp' => 'Suspend',
71
+ 'sS' => /^notSuspended/i
72
+ },
73
+ timeout: node.supervisor_settings.dig('default', 'timeouts', 'alarm')).collect!
74
+ end
75
+ send_message message
76
+ [message, collect_task.wait.first]
77
+ else
78
+ send_message message
79
+ message
80
+ end
81
+ end
24
82
  end
25
83
  end
26
84
  end
@@ -97,6 +97,15 @@ module RSMP
97
97
  message
98
98
  end
99
99
 
100
+ # unsubscribes to all statuses (with all attributes) defined in the used SXL
101
+ def unsubscribe_from_all(component_id)
102
+ catalogue = RSMP::Schema.status_catalogue(@sxl, sxl_version)
103
+ status_list = catalogue.flat_map do |status_code_id, names|
104
+ names.map { |name| { 'sCI' => status_code_id.to_s, 'n' => name.to_s } }
105
+ end
106
+ unsubscribe_to_status component_id, status_list
107
+ end
108
+
100
109
  def process_status_update(message)
101
110
  component = find_component message.attribute('cId')
102
111
  component.check_repeat_values message, @status_subscriptions
@@ -158,15 +158,20 @@ module RSMP
158
158
  end
159
159
 
160
160
  def find_site_settings(_site_id)
161
- if @settings['sites'] && @settings['sites'][@site_id]
162
- log "Using site settings for site id #{@site_id}", level: :debug
163
- return @settings['sites'][@site_id]
161
+ base = @settings['default'] || {}
162
+
163
+ if @settings['sites']
164
+ site_specific = @settings['sites'][@site_id] || @settings['sites']['default']
165
+ if site_specific
166
+ label = @settings['sites'][@site_id] ? "site id #{@site_id}" : 'default'
167
+ log "Using #{label} site settings", level: :debug
168
+ return base.deep_merge(site_specific)
169
+ end
164
170
  end
165
171
 
166
- @settings['guest']
167
- if @settings['guest']
168
- log 'Using site settings for guest', level: :debug
169
- return @settings['guest']
172
+ unless base.empty?
173
+ log 'Using default site settings', level: :debug
174
+ return base
170
175
  end
171
176
 
172
177
  nil
@@ -7,7 +7,7 @@ module RSMP
7
7
  def setup_inputs(inputs)
8
8
  if inputs
9
9
  num_inputs = inputs['total']
10
- @input_programming = inputs['programming']
10
+ @input_programming = normalize_input_programming(inputs['programming'])
11
11
  else
12
12
  @input_programming = nil
13
13
  end
@@ -167,6 +167,35 @@ module RSMP
167
167
  TrafficControllerSite.make_status @inputs.forced_string
168
168
  end
169
169
  end
170
+
171
+ private
172
+
173
+ def normalize_input_programming(programming)
174
+ return nil if programming.nil?
175
+ return programming if programming.is_a?(Array)
176
+ return programming unless programming.is_a?(Hash)
177
+
178
+ normalized = normalize_programming_keys(programming)
179
+ return normalized unless normalized.keys.all?(Integer)
180
+
181
+ programming_hash_to_array(normalized)
182
+ end
183
+
184
+ def normalize_programming_keys(programming)
185
+ programming.each_with_object({}) do |(key, value), memo|
186
+ int_key = key.is_a?(String) && key.match?(/^\d+$/) ? key.to_i : key
187
+ memo[int_key] = value
188
+ end
189
+ end
190
+
191
+ def programming_hash_to_array(normalized)
192
+ max_key = normalized.keys.max
193
+ program_array = Array.new(max_key + 1)
194
+ normalized.each do |index, value|
195
+ program_array[index] = value
196
+ end
197
+ program_array
198
+ end
170
199
  end
171
200
  end
172
201
  end
@@ -0,0 +1,158 @@
1
+ module RSMP
2
+ module TLC
3
+ module Proxy
4
+ # Command methods for operational control of a remote TLC.
5
+ # Covers functional position, emergency routes, I/O modes, signal group orders, and system settings.
6
+ module Control
7
+ # M0001 — Set functional position (NormalControl, YellowFlash, Dark).
8
+ def set_functional_position(status, timeout_minutes: 0, options: {})
9
+ validate_ready 'set functional position'
10
+ raise 'TLC main component not found' unless main
11
+
12
+ command_list = functional_position_command_list(status, timeout_minutes)
13
+ confirm_status = functional_position_confirm_status(status)
14
+ send_command_with_confirm main.c_id, command_list, options, "functional position #{status}", confirm_status
15
+ end
16
+
17
+ # M0005 — Set or clear an emergency route.
18
+ def set_emergency_route(route:, active:, options: {})
19
+ validate_ready 'set emergency route'
20
+ raise 'TLC main component not found' unless main
21
+
22
+ security_code = security_code_for(2)
23
+ active_str = active ? 'True' : 'False'
24
+
25
+ command_list = [{
26
+ 'cCI' => 'M0005',
27
+ 'cO' => 'setEmergency',
28
+ 'n' => 'status',
29
+ 'v' => active_str
30
+ }, {
31
+ 'cCI' => 'M0005',
32
+ 'cO' => 'setEmergency',
33
+ 'n' => 'securityCode',
34
+ 'v' => security_code.to_s
35
+ }, {
36
+ 'cCI' => 'M0005',
37
+ 'cO' => 'setEmergency',
38
+ 'n' => 'emergencyroute',
39
+ 'v' => route.to_s
40
+ }]
41
+
42
+ confirm_status = [{ 'sCI' => 'S0006', 'n' => 'status', 's' => active_str }]
43
+ send_command_with_confirm main.c_id, command_list, options,
44
+ "emergency route #{route} #{active ? 'active' : 'inactive'}", confirm_status
45
+ end
46
+
47
+ # M0007 — Enable or disable fixed-time control.
48
+ def set_fixed_time(status, options: {})
49
+ validate_ready 'set fixed time'
50
+ raise 'TLC main component not found' unless main
51
+
52
+ security_code = security_code_for(2)
53
+
54
+ command_list = [{
55
+ 'cCI' => 'M0007',
56
+ 'cO' => 'setFixedTime',
57
+ 'n' => 'status',
58
+ 'v' => status.to_s
59
+ }, {
60
+ 'cCI' => 'M0007',
61
+ 'cO' => 'setFixedTime',
62
+ 'n' => 'securityCode',
63
+ 'v' => security_code.to_s
64
+ }]
65
+
66
+ confirm_status = [{ 'sCI' => 'S0009', 'n' => 'status',
67
+ 's' => /^#{Regexp.escape(status.to_s)}(,#{Regexp.escape(status.to_s)})*$/ }]
68
+ send_command_with_confirm main.c_id, command_list, options, "fixed time #{status}", confirm_status
69
+ end
70
+
71
+ # M0003 — Set traffic situation (activate a specific situation number).
72
+ def set_traffic_situation(situation, options: {})
73
+ validate_ready 'set traffic situation'
74
+ raise 'TLC main component not found' unless main
75
+
76
+ security_code = security_code_for(2)
77
+
78
+ command_list = [{
79
+ 'cCI' => 'M0003',
80
+ 'cO' => 'setTrafficSituation',
81
+ 'n' => 'status',
82
+ 'v' => 'True'
83
+ }, {
84
+ 'cCI' => 'M0003',
85
+ 'cO' => 'setTrafficSituation',
86
+ 'n' => 'securityCode',
87
+ 'v' => security_code.to_s
88
+ }, {
89
+ 'cCI' => 'M0003',
90
+ 'cO' => 'setTrafficSituation',
91
+ 'n' => 'traficsituation',
92
+ 'v' => situation.to_s
93
+ }]
94
+
95
+ confirm_status = [{ 'sCI' => 'S0015', 'n' => 'status', 's' => situation.to_s }]
96
+ send_command_with_confirm main.c_id, command_list, options, "traffic situation #{situation}", confirm_status
97
+ end
98
+
99
+ # M0003 — Clear the active traffic situation.
100
+ def unset_traffic_situation(options: {})
101
+ validate_ready 'unset traffic situation'
102
+ raise 'TLC main component not found' unless main
103
+
104
+ security_code = security_code_for(2)
105
+
106
+ command_list = [{
107
+ 'cCI' => 'M0003',
108
+ 'cO' => 'setTrafficSituation',
109
+ 'n' => 'status',
110
+ 'v' => 'False'
111
+ }, {
112
+ 'cCI' => 'M0003',
113
+ 'cO' => 'setTrafficSituation',
114
+ 'n' => 'securityCode',
115
+ 'v' => security_code.to_s
116
+ }, {
117
+ 'cCI' => 'M0003',
118
+ 'cO' => 'setTrafficSituation',
119
+ 'n' => 'traficsituation',
120
+ 'v' => '1'
121
+ }]
122
+
123
+ confirm_status = [{ 'sCI' => 'S0015', 'n' => 'status', 's' => '1' }]
124
+ send_command_with_confirm main.c_id, command_list, options, 'traffic situation unset', confirm_status
125
+ end
126
+
127
+ private
128
+
129
+ def functional_position_command_list(status, timeout_minutes)
130
+ security_code = security_code_for(2)
131
+ [
132
+ { 'cCI' => 'M0001', 'cO' => 'setValue', 'n' => 'status', 'v' => status.to_s },
133
+ { 'cCI' => 'M0001', 'cO' => 'setValue', 'n' => 'securityCode', 'v' => security_code.to_s },
134
+ { 'cCI' => 'M0001', 'cO' => 'setValue', 'n' => 'timeout', 'v' => timeout_minutes.to_s },
135
+ { 'cCI' => 'M0001', 'cO' => 'setValue', 'n' => 'intersection', 'v' => '0' }
136
+ ]
137
+ end
138
+
139
+ def functional_position_confirm_status(status)
140
+ case status.to_s
141
+ when 'YellowFlash'
142
+ [{ 'sCI' => 'S0011', 'n' => 'status', 's' => /^True(,True)*$/ }]
143
+ when 'Dark'
144
+ [{ 'sCI' => 'S0007', 'n' => 'status', 's' => /^False(,False)*$/ }]
145
+ when 'NormalControl'
146
+ [
147
+ { 'sCI' => 'S0007', 'n' => 'status', 's' => /^True(,True)*$/ },
148
+ { 'sCI' => 'S0011', 'n' => 'status', 's' => /^False(,False)*$/ },
149
+ { 'sCI' => 'S0005', 'n' => 'status', 's' => 'False' }
150
+ ]
151
+ else
152
+ []
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end