rsmp 0.51.4 → 0.53.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 +9 -0
- data/Gemfile.lock +1 -1
- data/documentation/collecting_message.md +112 -0
- data/documentation/configuration.md +53 -2
- data/lib/rsmp/collect/collector.rb +6 -0
- data/lib/rsmp/collect/queue.rb +4 -0
- data/lib/rsmp/collect/status_collector.rb +2 -2
- data/lib/rsmp/component/component_base.rb +10 -1
- data/lib/rsmp/component/component_proxy.rb +1 -1
- data/lib/rsmp/component/components.rb +24 -2
- data/lib/rsmp/node/site/site.rb +21 -7
- data/lib/rsmp/node/site/status.rb +39 -0
- data/lib/rsmp/node/supervisor/modules/connection.rb +20 -4
- data/lib/rsmp/node/supervisor/modules/sites.rb +2 -0
- data/lib/rsmp/options/schemas/required_settings.json +20 -12
- data/lib/rsmp/options/schemas/supervisor_site.json +1 -1
- data/lib/rsmp/options/site_options.rb +1 -0
- data/lib/rsmp/proxy/modules/acknowledgements.rb +2 -0
- data/lib/rsmp/proxy/modules/receive.rb +32 -2
- data/lib/rsmp/proxy/modules/send.rb +23 -3
- data/lib/rsmp/proxy/modules/version_validation.rb +76 -0
- data/lib/rsmp/proxy/modules/versions.rb +21 -22
- data/lib/rsmp/proxy/proxy.rb +29 -5
- data/lib/rsmp/proxy/site/modules/aggregated_status.rb +22 -2
- data/lib/rsmp/proxy/site/modules/commands.rb +1 -1
- data/lib/rsmp/proxy/site/site_proxy.rb +22 -13
- data/lib/rsmp/proxy/site/sxl_selection.rb +13 -22
- data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +56 -1
- data/lib/rsmp/proxy/supervisor/modules/alarms.rb +11 -4
- data/lib/rsmp/proxy/supervisor/modules/commands.rb +4 -3
- data/lib/rsmp/proxy/supervisor/modules/message_buffer.rb +6 -1
- data/lib/rsmp/proxy/supervisor/modules/status.rb +46 -1
- data/lib/rsmp/proxy/supervisor/modules/status_updates.rb +17 -1
- data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +12 -16
- data/lib/rsmp/schema/validation.rb +2 -1
- data/lib/rsmp/version.rb +1 -1
- data/schemas/core/3.1.2/definitions.json +5 -0
- data/schemas/core/3.1.2/status_subscribe.json +1 -1
- data/schemas/core/3.1.3/definitions.json +5 -0
- data/schemas/core/3.1.3/status_subscribe.json +1 -1
- data/schemas/core/3.1.4/definitions.json +5 -0
- data/schemas/core/3.1.4/status_subscribe.json +1 -1
- data/schemas/core/3.1.5/definitions.json +5 -0
- data/schemas/core/3.1.5/status_subscribe.json +1 -1
- data/schemas/core/3.2.0/definitions.json +5 -0
- data/schemas/core/3.2.0/status_subscribe.json +1 -1
- data/schemas/core/3.2.1/definitions.json +5 -0
- data/schemas/core/3.2.1/status_subscribe.json +1 -1
- data/schemas/core/3.2.2/definitions.json +5 -0
- data/schemas/core/3.2.2/status_subscribe.json +1 -1
- data/schemas/core/3.3.0/aggregated_status.json +17 -11
- data/schemas/core/3.3.0/aggregated_status_request.json +4 -4
- data/schemas/core/3.3.0/component_list.json +1 -1
- data/schemas/core/3.3.0/definitions.json +23 -2
- data/schemas/core/3.3.0/status_subscribe.json +14 -4
- data/schemas/core/3.3.0/version.json +46 -26
- data/schemas/tlc/1.0.10/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.13/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.14/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.15/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.7/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.8/defs/definitions.json +5 -0
- data/schemas/tlc/1.0.9/defs/definitions.json +5 -0
- data/schemas/tlc/1.1.0/defs/definitions.json +5 -0
- data/schemas/tlc/1.2.0/defs/definitions.json +5 -0
- data/schemas/tlc/1.2.1/defs/definitions.json +5 -0
- data/schemas/tlc/1.3.0/defs/definitions.json +7 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66bd0079bb76c72ad04ce15f191ac3f33a06bcaf4f941c8d23f10b8239c3f308
|
|
4
|
+
data.tar.gz: 34f6fb37b13637ffe1bdf3f92064e103fc3b8b798fce11e2764d0ab810f23190
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 140f8cd7c0f9f5b542d4b4f47fd4d99095ff7eecaaf784bc6b6ae92e5dee1014b4f0a6338159c21e3bdda86e3ef06764f17b9d0055c3880b213a2c18e0e18708
|
|
7
|
+
data.tar.gz: ab5da12c73a27485b2e94d73cc2090adce96b69aa97dbb4530b5d5819aad20ecf0fa466d2b3b3f5068e678cbe41fcfefdf9808155c25656ad4fb52e78432ce7c
|
data/CHANGELOG.md
CHANGED
|
@@ -729,3 +729,12 @@ Initial release.
|
|
|
729
729
|
## 0.51.4
|
|
730
730
|
- don't update subscription list until unsubscribe is acknowledged
|
|
731
731
|
|
|
732
|
+
## 0.52.0
|
|
733
|
+
- allow the proxy's `collect:` option to accept a Proc that builds a collector or queue before the handshake
|
|
734
|
+
- add optional `predicate:` filtering to collectors, including status collectors
|
|
735
|
+
- add `start` and `stop` aliases to queues for use with proxy collection factories
|
|
736
|
+
- fix status collectors to honor `component:` and `responses:`; the misspelled `reponses:` option is no longer used
|
|
737
|
+
|
|
738
|
+
## 0.53.0
|
|
739
|
+
- update core 3.3.0 support to sync with latest core 3.3.0 draft
|
|
740
|
+
- allow fractional status subscription intervals for all older core versions to align with their specs.
|
data/Gemfile.lock
CHANGED
|
@@ -112,6 +112,118 @@ result = collector.collect do |message|
|
|
|
112
112
|
end
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
### Message predicates
|
|
116
|
+
|
|
117
|
+
Use `predicate:` for conditions beyond the message type, direction, and component
|
|
118
|
+
handled by `RSMP::Filter`. The Proc receives a whole message after the filter
|
|
119
|
+
accepts it. Returning `false` or `nil` skips the message before collection
|
|
120
|
+
callbacks or state-item matching; earlier item matches remain intact.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
collector = RSMP::Collector.new(
|
|
124
|
+
proxy,
|
|
125
|
+
filter: RSMP::Filter.new(type: 'StatusUpdate', ingoing: true, outgoing: false),
|
|
126
|
+
predicate: ->(message) { message.is_a?(RSMP::StatusUpdate) && message.attribute('sS').any? },
|
|
127
|
+
num: 1,
|
|
128
|
+
timeout: 5
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Predicates also work with `StatusCollector`. They do not suppress failures such
|
|
133
|
+
as a rejection of the initiating request or an invalid peer message. `Filter`
|
|
134
|
+
admits `MessageNotAck` even when a different message type is requested, so a
|
|
135
|
+
predicate that accesses type-specific attributes should check the message type.
|
|
136
|
+
|
|
137
|
+
### Status collection
|
|
138
|
+
|
|
139
|
+
`StatusCollector` matches wanted status items within incoming messages. Both
|
|
140
|
+
`StatusUpdate` and `StatusResponse` are accepted by default. Set `responses: false`
|
|
141
|
+
to accept updates only, or `updates: false` to accept responses only.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
want = [{ 'sCI' => 'S0001', 'n' => 'signalgroupstatus' }]
|
|
145
|
+
collector = RSMP::StatusCollector.new(
|
|
146
|
+
proxy, want, component: 'C1', responses: false, timeout: 5
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`component:` filters the message's `cId`; the wanted items describe entries in
|
|
151
|
+
`sS`, which do not have their own component ID. Collection completes when all
|
|
152
|
+
wanted items match, rather than after a fixed number of messages.
|
|
153
|
+
|
|
154
|
+
### Collection during proxy setup
|
|
155
|
+
|
|
156
|
+
The proxy's `collect:` option accepts the existing collector options hash:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
proxy_options = {
|
|
160
|
+
collect: {
|
|
161
|
+
filter: RSMP::Filter.new(type: 'StatusUpdate', ingoing: true, outgoing: false),
|
|
162
|
+
num: 1,
|
|
163
|
+
timeout: 5
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
It also accepts a Proc that receives the new proxy and returns a collector or
|
|
169
|
+
queue. The proxy calls `start` on the returned object and exposes it through
|
|
170
|
+
`proxy.collector`. This starts receiving before the handshake can deliver
|
|
171
|
+
messages. Return an unstarted object; the factory runs before version negotiation
|
|
172
|
+
and should not rely on the proxy being fully initialized or connected.
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
proxy_options = {
|
|
176
|
+
collect: proc do |proxy|
|
|
177
|
+
RSMP::StatusCollector.new(
|
|
178
|
+
proxy,
|
|
179
|
+
[{ 'sCI' => 'S0001', 'n' => 'signalgroupstatus' }],
|
|
180
|
+
component: 'C1', responses: false, timeout: 5
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Pass these options when creating the proxy. Use `proxy.collector.wait` or
|
|
187
|
+
`wait!` for a collector, or `wait_for_message` or `wait_for_message!` for a queue.
|
|
188
|
+
|
|
189
|
+
## Queues
|
|
190
|
+
|
|
191
|
+
`RSMP::Queue` retains accepted messages until consumed, including messages that
|
|
192
|
+
arrive before the caller starts waiting. Each wait returns one message, rather
|
|
193
|
+
than a collection. `start` and `stop` are aliases for `start_receiving` and
|
|
194
|
+
`stop_receiving`, so a queue can also be returned by a proxy's `collect:` factory.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
queue = RSMP::Queue.new(proxy, filter: RSMP::Filter.new(type: 'StatusUpdate'))
|
|
198
|
+
queue.start
|
|
199
|
+
begin
|
|
200
|
+
result = queue.wait_for_message(timeout: 5)
|
|
201
|
+
puts result.value if result.success?
|
|
202
|
+
ensure
|
|
203
|
+
queue.stop
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
A queue stays registered until stopped. Its `timeout:` applies to an individual
|
|
208
|
+
wait: expiry returns a failure with code `:timeout`, or raises
|
|
209
|
+
`RSMP::OperationError` through `wait_for_message!`.
|
|
210
|
+
|
|
211
|
+
An enclosing Async timeout raised while waiting in the queue is converted in
|
|
212
|
+
the same way, even when no queue timeout is supplied:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
begin
|
|
216
|
+
message = Async::Task.current.with_timeout(5) { queue.wait_for_message! }
|
|
217
|
+
rescue RSMP::OperationError => error
|
|
218
|
+
raise unless error.failure.code == :timeout
|
|
219
|
+
|
|
220
|
+
warn 'The caller deadline expired'
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This example assumes the queue is already started. Both timeout sources use the
|
|
225
|
+
same failure code; `failure.cause` retains the original Async exception.
|
|
226
|
+
|
|
115
227
|
## Sending and collecting atomically
|
|
116
228
|
|
|
117
229
|
High-level send methods follow the same non-bang/bang convention:
|
|
@@ -21,8 +21,11 @@ and `required_supervisor_settings.json` schemas then check required settings aft
|
|
|
21
21
|
supervisor site inheritance are resolved:
|
|
22
22
|
|
|
23
23
|
- Every configured signal plan must have a `cycle_time`.
|
|
24
|
-
-
|
|
25
|
-
|
|
24
|
+
- Before Core 3.3.0, exactly one `main` component is required. Sites supply a default main component
|
|
25
|
+
when omitted; supervisor site settings can inherit it from `default`. From Core 3.3.0, a main
|
|
26
|
+
component is not used. With `core_version: "3.3.0"`, no default components are added, even if
|
|
27
|
+
`components` is omitted. Configurations that allow older Core versions (including an omitted
|
|
28
|
+
`core_version` or `"all"`) retain the default main component.
|
|
26
29
|
- A site in `server` role must have a `port`, either explicitly or from its first supervisor endpoint.
|
|
27
30
|
With `supervisors: []`, the port must be explicit.
|
|
28
31
|
|
|
@@ -36,6 +39,54 @@ Earlier Core versions retain their historical wire formats for compatibility. In
|
|
|
36
39
|
|
|
37
40
|
Legacy SXL strings such as `1.1` and `1.2` remain supported for connections using a Core version before 3.3.0. Their three-part equivalents are used only for internal schema lookup.
|
|
38
41
|
|
|
42
|
+
## Core 3.3.0
|
|
43
|
+
|
|
44
|
+
Preliminary Core 3.3.0 support follows `rsmp_core` branch `3.3.0` at commit
|
|
45
|
+
`1aa02faf82da4f47fee57c43c546c092420aa576`, including PR #318. These rules apply only
|
|
46
|
+
when Core 3.3.0 is selected. Earlier Core versions retain their existing behavior.
|
|
47
|
+
|
|
48
|
+
SXL responses use `ok`, `unsupported`, `mismatch` and `expected`. Only `ok` enables
|
|
49
|
+
an SXL, at the exact offered version. `supported` is diagnostic and does not trigger
|
|
50
|
+
fallback. A connection can become ready even if no SXL is accepted.
|
|
51
|
+
|
|
52
|
+
Set `use_alarms: false` in the supervisor's `default` or per-site configuration to
|
|
53
|
+
disable every Alarm message in both directions on a 3.3.0 connection. The default
|
|
54
|
+
is true.
|
|
55
|
+
|
|
56
|
+
The 3.3.0 handshake ends with ComponentList followed by a site-wide AggregatedStatus.
|
|
57
|
+
Readiness follows acknowledgement of that status; either MessageAck or MessageNotAck
|
|
58
|
+
completes this step. The initial status is mandatory even with `send_after_connect: false`.
|
|
59
|
+
|
|
60
|
+
AggregatedStatus has no `cId`, `fP` or `fS`. All false bits mean normal operation;
|
|
61
|
+
bits 2, 6 and 8 are always false in site messages. Site-wide conditions include all
|
|
62
|
+
components and active alarm priorities, including SXLs rejected on a connection.
|
|
63
|
+
Read received bits through `SiteProxy#site_aggregated_status` and override
|
|
64
|
+
`Supervisor#site_aggregated_status_changed(site_proxy)` for notifications.
|
|
65
|
+
For 3.3.0, call `request_aggregated_status` or `request_aggregated_status_and_collect`
|
|
66
|
+
without a component. A supplied component argument is ignored on the wire. Legacy
|
|
67
|
+
requests remain component-addressed.
|
|
68
|
+
|
|
69
|
+
Use `add_component`, `remove_component` and `ComponentBase#update_metadata` to update
|
|
70
|
+
the inventory. Ready 3.3.0 connections receive a complete ComponentList after a change;
|
|
71
|
+
disconnected connections receive the current list in their next handshake. The
|
|
72
|
+
supervisor preserves received ordering and retains unchanged component objects.
|
|
73
|
+
Component names are optional on the wire but must be nonempty strings when supplied.
|
|
74
|
+
The supervisor uses the component ID when a list entry omits its name, including
|
|
75
|
+
when a later list removes a previously supplied name.
|
|
76
|
+
Removing the last component is prohibited because the Core 3.3.0 schema requires a
|
|
77
|
+
nonempty ComponentList.
|
|
78
|
+
|
|
79
|
+
Subscription intervals use `uRt`, an integer or decimal
|
|
80
|
+
number represented as a string, e.g. `"2"` or `"0.5"`. Negative intervals are invalid.
|
|
81
|
+
Zero disables periodic updates and is valid only with `sOc: true`.
|
|
82
|
+
The Core source and vendored 3.3.0 schemas include this correction to the pinned baseline.
|
|
83
|
+
The isolated main-component requirement in `site_configuration.rst` is treated as
|
|
84
|
+
obsolete for 3.3.0 in favor of the component model and site-wide status definitions.
|
|
85
|
+
|
|
86
|
+
Validator follow-up should cover the new Version statuses and failures, both alarm
|
|
87
|
+
directions, site-wide status bits and fields, final-handshake ACK/NotAck, component
|
|
88
|
+
changes, and accepted-SXL filtering during initial synchronization and buffer replay.
|
|
89
|
+
|
|
39
90
|
## Loading a configuration file
|
|
40
91
|
|
|
41
92
|
Use the option classes to load a YAML configuration file:
|
|
@@ -7,6 +7,9 @@ module RSMP
|
|
|
7
7
|
|
|
8
8
|
attr_reader :messages, :m_id, :initiator
|
|
9
9
|
|
|
10
|
+
# :predicate optionally receives each message accepted by the filter. A falsey
|
|
11
|
+
# result skips the message before collection callbacks or item matching.
|
|
12
|
+
# Rejection and invalid-message failures are handled independently of it.
|
|
10
13
|
def initialize(distributor, options = {})
|
|
11
14
|
initialize_receiver distributor, filter: options[:filter]
|
|
12
15
|
@options = {
|
|
@@ -17,6 +20,7 @@ module RSMP
|
|
|
17
20
|
}.deep_merge(options)
|
|
18
21
|
@timeout = options[:timeout]
|
|
19
22
|
@num = options[:num]
|
|
23
|
+
@predicate = options[:predicate]
|
|
20
24
|
@initiator = options[:initiator]
|
|
21
25
|
@m_id = options[:m_id] || @initiator&.attributes&.dig('mId')
|
|
22
26
|
make_title(options[:title])
|
|
@@ -131,6 +135,8 @@ module RSMP
|
|
|
131
135
|
def perform_match?(message)
|
|
132
136
|
return false if reject_not_ack?(message)
|
|
133
137
|
return false unless acceptable?(message)
|
|
138
|
+
return false if @predicate && !@predicate.call(message)
|
|
139
|
+
return false unless active?
|
|
134
140
|
|
|
135
141
|
if @block
|
|
136
142
|
status = Array(@block.call(message))
|
data/lib/rsmp/collect/queue.rb
CHANGED
|
@@ -7,6 +7,10 @@ module RSMP
|
|
|
7
7
|
|
|
8
8
|
attr_reader :messages
|
|
9
9
|
|
|
10
|
+
# Allow queues to be started by a proxy's collect factory, like collectors.
|
|
11
|
+
alias start start_receiving
|
|
12
|
+
alias stop stop_receiving
|
|
13
|
+
|
|
10
14
|
def initialize(distributor, filter: nil)
|
|
11
15
|
initialize_receiver distributor, filter: filter
|
|
12
16
|
@condition = Async::Notification.new
|
|
@@ -4,11 +4,11 @@ module RSMP
|
|
|
4
4
|
def initialize(proxy, want, options = {})
|
|
5
5
|
type = []
|
|
6
6
|
type << 'StatusUpdate' unless options[:updates] == false
|
|
7
|
-
type << 'StatusResponse' unless options[:
|
|
7
|
+
type << 'StatusResponse' unless options[:responses] == false
|
|
8
8
|
|
|
9
9
|
super(proxy, want, options.merge(
|
|
10
10
|
title: 'status response',
|
|
11
|
-
filter: RSMP::Filter.new(ingoing: true, outgoing: false, type: type)
|
|
11
|
+
filter: RSMP::Filter.new(ingoing: true, outgoing: false, type: type, component: options[:component])
|
|
12
12
|
))
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -41,8 +41,10 @@ module RSMP
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def update_metadata(type: nil, name: nil)
|
|
44
|
+
changed = (type && type != @component_type) || (name && name != @name)
|
|
44
45
|
@component_type = type if type
|
|
45
46
|
@name = name if name
|
|
47
|
+
@node.component_list_changed if changed
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def now
|
|
@@ -73,7 +75,14 @@ module RSMP
|
|
|
73
75
|
raise ArgumentError, 'aggregated status must be an Array' unless status.is_a? Array
|
|
74
76
|
|
|
75
77
|
input = status & AGGREGATED_STATUS_KEYS
|
|
76
|
-
|
|
78
|
+
if @node.respond_to?(:site_wide_aggregated_status?) && @node.site_wide_aggregated_status?
|
|
79
|
+
bits = AGGREGATED_STATUS_KEYS.map { |key| input.include?(key) }
|
|
80
|
+
return if bits == @aggregated_status_bools
|
|
81
|
+
|
|
82
|
+
@aggregated_status = input
|
|
83
|
+
else
|
|
84
|
+
return unless input != @aggregated_status
|
|
85
|
+
end
|
|
77
86
|
|
|
78
87
|
AGGREGATED_STATUS_KEYS.each_with_index do |key, index|
|
|
79
88
|
@aggregated_status_bools[index] = status.include?(key)
|
|
@@ -39,7 +39,7 @@ module RSMP
|
|
|
39
39
|
|
|
40
40
|
def update_rate_set?(subscription_list, status_code, status_name)
|
|
41
41
|
urt = subscription_list.dig(c_id, status_code, status_name, 'uRt')
|
|
42
|
-
urt.
|
|
42
|
+
urt.to_f.positive?
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def should_check_repeats?(subscription_list, status_code, status_name)
|
|
@@ -13,7 +13,7 @@ module RSMP
|
|
|
13
13
|
def setup_components(settings)
|
|
14
14
|
return unless settings
|
|
15
15
|
|
|
16
|
-
check_main_component settings
|
|
16
|
+
check_main_component settings unless core_3_3_components?
|
|
17
17
|
settings.each_pair do |type, components_by_type|
|
|
18
18
|
next unless components_by_type
|
|
19
19
|
|
|
@@ -30,6 +30,10 @@ module RSMP
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def core_3_3_components?
|
|
34
|
+
@site_settings && @site_settings['core_version'] == '3.3.0'
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def check_main_component(settings)
|
|
34
38
|
raise ConfigurationError, 'main component must be defined' unless settings['main'] && settings['main'].size >= 1
|
|
35
39
|
return unless settings['main'].size > 1
|
|
@@ -39,11 +43,29 @@ module RSMP
|
|
|
39
43
|
|
|
40
44
|
def add_component(component)
|
|
41
45
|
@components[component.c_id] = component
|
|
46
|
+
component_list_changed
|
|
47
|
+
component
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def component_list_changed; end
|
|
51
|
+
|
|
52
|
+
def remove_component(id)
|
|
53
|
+
raise ConfigurationError, 'Cannot remove the legacy main component' if @main&.c_id == id &&
|
|
54
|
+
@site_settings['core_version'] != '3.3.0'
|
|
55
|
+
|
|
56
|
+
if core_3_3_components? && @components.size == 1 && @components.key?(id)
|
|
57
|
+
raise ConfigurationError, 'Core 3.3 ComponentList must not be empty'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
removed = @components.delete(id)
|
|
61
|
+
@main = nil if removed == @main
|
|
62
|
+
component_list_changed if removed
|
|
63
|
+
removed
|
|
42
64
|
end
|
|
43
65
|
|
|
44
66
|
def natural_sort_key(value)
|
|
45
67
|
value.to_s.split(/(\d+)/).map do |part|
|
|
46
|
-
part.match?(/\A\d+\z/) ? part.to_i : part
|
|
68
|
+
part.match?(/\A\d+\z/) ? [0, part.to_i, part.length] : [1, part]
|
|
47
69
|
end
|
|
48
70
|
end
|
|
49
71
|
|
data/lib/rsmp/node/site/site.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
require_relative 'status'
|
|
2
|
+
|
|
1
3
|
module RSMP
|
|
2
4
|
# RSMP site implementation that manages proxies and components.
|
|
3
5
|
class Site < Node
|
|
4
6
|
include Components
|
|
7
|
+
include SiteStatus
|
|
5
8
|
include SiteConnections
|
|
6
9
|
|
|
7
10
|
attr_reader :core_version, :site_settings, :logger, :proxies, :ready_condition
|
|
@@ -127,12 +130,6 @@ module RSMP
|
|
|
127
130
|
start_status_timer(parent: parent)
|
|
128
131
|
end
|
|
129
132
|
|
|
130
|
-
def aggregated_status_changed(component, _options = {})
|
|
131
|
-
@proxies.each do |proxy|
|
|
132
|
-
proxy.send_aggregated_status component
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
133
|
def alarm_acknowledged(alarm_state)
|
|
137
134
|
send_alarm AlarmAcknowledged.new(alarm_state.to_hash)
|
|
138
135
|
end
|
|
@@ -143,11 +140,28 @@ module RSMP
|
|
|
143
140
|
|
|
144
141
|
def alarm_activated_or_deactivated(alarm_state)
|
|
145
142
|
send_alarm AlarmIssue.new(alarm_state.to_hash)
|
|
143
|
+
notify_site_status_change
|
|
146
144
|
end
|
|
147
145
|
|
|
148
146
|
def send_alarm(alarm)
|
|
147
|
+
# Snapshot before any connection encodes the message for its SXL version.
|
|
148
|
+
independent = @proxies.any? { |proxy| proxy.respond_to?(:core_3_3_0_or_newer?) && proxy.core_3_3_0_or_newer? }
|
|
149
|
+
attributes = JSON.parse(JSON.generate(alarm.attributes)) if independent
|
|
149
150
|
@proxies.each do |proxy|
|
|
150
|
-
|
|
151
|
+
next unless proxy.use_alarms?
|
|
152
|
+
|
|
153
|
+
send_connection_alarm(proxy, alarm, attributes)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def send_connection_alarm(proxy, alarm, attributes)
|
|
158
|
+
return if proxy.core_3_3_0_or_newer? && !proxy.sxl_message_allowed?(alarm)
|
|
159
|
+
|
|
160
|
+
alarm = alarm.class.new(JSON.parse(JSON.generate(attributes)).merge('mId' => Message.make_m_id)) if attributes
|
|
161
|
+
if proxy.core_3_3_0_or_newer? && !proxy.ready?
|
|
162
|
+
proxy.buffer_message alarm
|
|
163
|
+
else
|
|
164
|
+
proxy.send_generated_message alarm
|
|
151
165
|
end
|
|
152
166
|
end
|
|
153
167
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module RSMP
|
|
2
|
+
# Site-wide status and component change notifications for Core 3.3.
|
|
3
|
+
module SiteStatus
|
|
4
|
+
def aggregated_status_changed(component, _options = {})
|
|
5
|
+
@proxies.each do |proxy|
|
|
6
|
+
if proxy.core_3_3_0_or_newer?
|
|
7
|
+
proxy.send_changed_site_status
|
|
8
|
+
else
|
|
9
|
+
proxy.send_aggregated_status component
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def site_wide_aggregated_status?
|
|
15
|
+
@site_settings['core_version'] == '3.3.0' || @proxies&.any?(&:core_3_3_0_or_newer?)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Core 3.3 status includes every SXL, independently of connection negotiation.
|
|
19
|
+
def site_aggregated_status
|
|
20
|
+
bits = Array.new(8, false)
|
|
21
|
+
@components.each_value do |component|
|
|
22
|
+
[0, 2, 3, 4, 6].each { |index| bits[index] ||= component.aggregated_status_bools[index] }
|
|
23
|
+
component.alarms.each_value do |alarm|
|
|
24
|
+
bits[alarm.priority + 1] = true if alarm.active && (1..3).cover?(alarm.priority)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
bits
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def notify_site_status_change
|
|
31
|
+
@proxies.each { |proxy| proxy.send_changed_site_status if proxy.core_3_3_0_or_newer? }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def component_list_changed
|
|
35
|
+
@proxies&.each { |proxy| proxy.send_component_list if proxy.core_3_3_0_or_newer? && proxy.ready? }
|
|
36
|
+
notify_site_status_change if @proxies
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -92,11 +92,28 @@ module RSMP
|
|
|
92
92
|
site_id: site_id,
|
|
93
93
|
site_settings: site_settings
|
|
94
94
|
}
|
|
95
|
+
rescue HandshakeError => e
|
|
96
|
+
reject_initial_version(protocol, e)
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def reject_initial_version(protocol, error)
|
|
101
|
+
attributes = Message.parse_attributes(protocol.peek_line)
|
|
102
|
+
id = attributes['mId']
|
|
103
|
+
if id.is_a?(String) && /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i.match?(id)
|
|
104
|
+
protocol.write_lines(JSON.generate('mType' => 'rSMsg', 'type' => 'MessageNotAck',
|
|
105
|
+
'oMId' => id, 'rea' => error.message))
|
|
106
|
+
end
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def retrieve_site_id(protocol)
|
|
98
110
|
version_message = peek_version_message protocol
|
|
99
|
-
version_message.
|
|
111
|
+
ids = version_message.attributes['siteId']
|
|
112
|
+
unless ids.is_a?(Array) && ids.first.is_a?(Hash) && ids.first['sId'].is_a?(String)
|
|
113
|
+
raise HandshakeError, 'Invalid siteId in Version request'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
ids.first['sId']
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
def setup_proxy(proxy, settings, id)
|
|
@@ -112,10 +129,9 @@ module RSMP
|
|
|
112
129
|
proxy
|
|
113
130
|
end
|
|
114
131
|
|
|
115
|
-
def validate_and_start_proxy(proxy,
|
|
132
|
+
def validate_and_start_proxy(proxy, _protocol)
|
|
116
133
|
proxy.setup_site_settings
|
|
117
|
-
|
|
118
|
-
log "Validating using core version #{proxy.core_version}", level: :debug
|
|
134
|
+
|
|
119
135
|
proxy.start
|
|
120
136
|
proxy.wait
|
|
121
137
|
ensure
|
|
@@ -3,17 +3,7 @@
|
|
|
3
3
|
"$comment": "Validate after applying defaults and inherited site settings; structural validation uses the other config schemas.",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
6
|
-
"components": {
|
|
7
|
-
"type": "object",
|
|
8
|
-
"required": ["main"],
|
|
9
|
-
"properties": {
|
|
10
|
-
"main": {
|
|
11
|
-
"type": "object",
|
|
12
|
-
"minProperties": 1,
|
|
13
|
-
"maxProperties": 1
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
},
|
|
6
|
+
"components": { "type": "object" },
|
|
17
7
|
"signal_plans": {
|
|
18
8
|
"type": "object",
|
|
19
9
|
"additionalProperties": {
|
|
@@ -26,5 +16,23 @@
|
|
|
26
16
|
"required": ["connection_role"],
|
|
27
17
|
"properties": { "connection_role": { "const": "server" } }
|
|
28
18
|
},
|
|
29
|
-
"then": { "required": ["port"] }
|
|
19
|
+
"then": { "required": ["port"] },
|
|
20
|
+
"allOf": [
|
|
21
|
+
{
|
|
22
|
+
"if": {
|
|
23
|
+
"required": ["core_version"],
|
|
24
|
+
"properties": { "core_version": { "const": "3.3.0" } }
|
|
25
|
+
},
|
|
26
|
+
"else": {
|
|
27
|
+
"properties": {
|
|
28
|
+
"components": {
|
|
29
|
+
"required": ["main"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"main": { "type": "object", "minProperties": 1, "maxProperties": 1 }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
30
38
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"intervals": { "$ref": "config_common.json#/$defs/intervals" },
|
|
25
25
|
"timeouts": { "$ref": "config_common.json#/$defs/timeouts" },
|
|
26
26
|
"send_after_connect": { "type": "boolean" },
|
|
27
|
-
"
|
|
27
|
+
"use_alarms": { "type": "boolean" },
|
|
28
28
|
"message_buffer": { "$ref": "config_common.json#/$defs/message_buffer" },
|
|
29
29
|
"skip_validation": { "type": "array", "items": { "type": "string" } },
|
|
30
30
|
"security_codes": { "$ref": "config_common.json#/$defs/security_codes" },
|
|
@@ -56,6 +56,7 @@ module RSMP
|
|
|
56
56
|
|
|
57
57
|
def apply_defaults(options)
|
|
58
58
|
defaults = defaults()
|
|
59
|
+
defaults['components'] = {} if options['core_version'] == '3.3.0'
|
|
59
60
|
defaults['components']['main'] = options['components']['main'] if options.dig('components', 'main')
|
|
60
61
|
data = defaults.deep_merge(options)
|
|
61
62
|
port = data.dig('supervisors', 0, 'port')
|
|
@@ -111,6 +111,7 @@ module RSMP
|
|
|
111
111
|
|
|
112
112
|
@acknowledgements[original.m_id] = message
|
|
113
113
|
@acknowledgement_condition.signal message
|
|
114
|
+
initial_status_acknowledged(original) if respond_to?(:initial_status_acknowledged)
|
|
114
115
|
else
|
|
115
116
|
log_acknowledgement_for_unknown message
|
|
116
117
|
end
|
|
@@ -124,6 +125,7 @@ module RSMP
|
|
|
124
125
|
log_acknowledgement_for_original message, original
|
|
125
126
|
@acknowledgements[original.m_id] = message
|
|
126
127
|
@acknowledgement_condition.signal message
|
|
128
|
+
initial_status_acknowledged(original) if respond_to?(:initial_status_acknowledged)
|
|
127
129
|
else
|
|
128
130
|
log_acknowledgement_for_unknown message
|
|
129
131
|
end
|
|
@@ -26,11 +26,12 @@ module RSMP
|
|
|
26
26
|
def process_packet(json)
|
|
27
27
|
attributes = Message.parse_attributes(json)
|
|
28
28
|
message = Message.build(attributes, json)
|
|
29
|
+
message.attributes['mId'] = attributes['mId'] if message.is_a?(Version)
|
|
29
30
|
process_incoming_message(message)
|
|
30
31
|
rescue InvalidPacket => e
|
|
31
32
|
reject_invalid_packet(json, e)
|
|
32
33
|
rescue MalformedMessage => e
|
|
33
|
-
|
|
34
|
+
reject_malformed_version_or_packet(json, attributes, e)
|
|
34
35
|
rescue PeerMessageError => e
|
|
35
36
|
reject_processed_message(message, e)
|
|
36
37
|
rescue HandshakeError, FatalError => e
|
|
@@ -39,7 +40,28 @@ module RSMP
|
|
|
39
40
|
@node&.clear_deferred
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
def reject_malformed_version_or_packet(json, attributes, error)
|
|
44
|
+
if attributes.is_a?(Hash) && attributes['type'] == 'Version' && core_3_3_0_or_newer?
|
|
45
|
+
message = Version.new(attributes)
|
|
46
|
+
message.attributes['mId'] = attributes['mId']
|
|
47
|
+
reject_fatal_message(message, error)
|
|
48
|
+
else
|
|
49
|
+
reject_malformed_packet(json, attributes, error)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_core_3_3_permissions!(message)
|
|
54
|
+
if core_3_3_0_or_newer? && message.is_a?(Alarm) && !use_alarms?
|
|
55
|
+
raise MessageRejected,
|
|
56
|
+
'Alarm exchange is disabled'
|
|
57
|
+
end
|
|
58
|
+
return unless core_3_3_0_or_newer? && @version_determined && !ready? && application_message?(message)
|
|
59
|
+
|
|
60
|
+
raise MessageRejected, 'Communication establishment is incomplete'
|
|
61
|
+
end
|
|
62
|
+
|
|
42
63
|
def process_incoming_message(message)
|
|
64
|
+
check_core_3_3_permissions!(message)
|
|
43
65
|
validate = should_validate_ingoing_message?(message)
|
|
44
66
|
if validate
|
|
45
67
|
validation = message.validate(schemas)
|
|
@@ -86,6 +108,7 @@ module RSMP
|
|
|
86
108
|
publish_peer_failure(failure, message: message)
|
|
87
109
|
log(text, message: message, level: :warning)
|
|
88
110
|
dont_acknowledge(message, text, reason)
|
|
111
|
+
complete_initial_status(message) if respond_to?(:complete_initial_status)
|
|
89
112
|
Result.failure(failure: failure)
|
|
90
113
|
end
|
|
91
114
|
|
|
@@ -109,11 +132,18 @@ module RSMP
|
|
|
109
132
|
text = "Rejected #{message&.type || 'message'}: #{error.message}"
|
|
110
133
|
failure = peer_failure(:protocol_failure, text, message: message)
|
|
111
134
|
publish_peer_failure(failure, message: message)
|
|
112
|
-
|
|
135
|
+
if message && (!message.is_a?(Version) || valid_received_message_id?(message))
|
|
136
|
+
dont_acknowledge(message, "Rejected #{message.type}", error.message.to_s)
|
|
137
|
+
end
|
|
113
138
|
close(reason: :protocol_failure, failure: failure)
|
|
114
139
|
Result.failure(failure: failure)
|
|
115
140
|
end
|
|
116
141
|
|
|
142
|
+
def valid_received_message_id?(message)
|
|
143
|
+
id = message.m_id
|
|
144
|
+
id.is_a?(String) && /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i.match?(id)
|
|
145
|
+
end
|
|
146
|
+
|
|
117
147
|
def peer_failure(code, text, message: nil, **context)
|
|
118
148
|
Failure.new(
|
|
119
149
|
code: code,
|