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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +645 -3
- data/Gemfile.lock +12 -12
- data/config/supervisor.yaml +1 -1
- data/documentation/configuration.md +67 -1
- data/lib/rsmp/cli.rb +2 -2
- data/lib/rsmp/helpers/inspect.rb +1 -1
- data/lib/rsmp/node/supervisor/modules/configuration.rb +10 -8
- data/lib/rsmp/node/supervisor/modules/connection.rb +14 -5
- data/lib/rsmp/node/supervisor/supervisor.rb +7 -0
- data/lib/rsmp/options/schemas/supervisor.json +6 -3
- data/lib/rsmp/options/schemas/supervisor_site.json +46 -0
- data/lib/rsmp/options/schemas/traffic_controller_site.json +3 -2
- data/lib/rsmp/options/supervisor_options.rb +4 -2
- data/lib/rsmp/proxy/proxy.rb +1 -0
- data/lib/rsmp/proxy/site/modules/alarms.rb +58 -0
- data/lib/rsmp/proxy/site/modules/status.rb +9 -0
- data/lib/rsmp/proxy/site/site_proxy.rb +12 -7
- data/lib/rsmp/tlc/modules/inputs.rb +30 -1
- data/lib/rsmp/tlc/proxy/control.rb +158 -0
- data/lib/rsmp/tlc/proxy/detectors.rb +58 -0
- data/lib/rsmp/tlc/proxy/io.rb +119 -0
- data/lib/rsmp/tlc/proxy/plans.rb +226 -0
- data/lib/rsmp/tlc/proxy/status.rb +120 -0
- data/lib/rsmp/tlc/proxy/system.rb +58 -0
- data/lib/rsmp/tlc/traffic_controller_proxy.rb +143 -0
- data/lib/rsmp/tlc/traffic_controller_site.rb +10 -1
- data/lib/rsmp/version.rb +1 -1
- data/lib/rsmp.rb +7 -1
- data/rsmp.gemspec +1 -1
- metadata +11 -4
- data/lib/rsmp/convert/export/json_schema.rb +0 -214
data/config/supervisor.yaml
CHANGED
|
@@ -54,15 +54,81 @@ log:
|
|
|
54
54
|
|
|
55
55
|
```yaml
|
|
56
56
|
port: 12111
|
|
57
|
-
|
|
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['
|
|
167
|
-
settings['
|
|
166
|
+
settings['default'] ||= {}
|
|
167
|
+
settings['default']['core_version'] = options[:core]
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
def apply_log_options(log_settings)
|
data/lib/rsmp/helpers/inspect.rb
CHANGED
|
@@ -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
|
|
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('
|
|
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['
|
|
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
|
-
|
|
29
|
+
base = @supervisor_settings['default'] || {}
|
|
30
30
|
|
|
31
|
-
@supervisor_settings['sites']
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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']['
|
|
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, '
|
|
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}
|
|
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
|
|
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 =
|
|
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
|
-
"
|
|
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": {
|
|
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
|
-
'
|
|
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
|
}
|
data/lib/rsmp/proxy/proxy.rb
CHANGED
|
@@ -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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|