rsmp 0.45.1 → 0.46.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a71c7f02cc5b94aa0b4d4f4b7ad87fbd0406c3b8cd1ceacfc7fa12aad6ccaba5
4
- data.tar.gz: 7de215575f1e79a7494c629ae9211f86b31077586f348c12041590d70f859605
3
+ metadata.gz: a5a5d66114ac44b96bc823b0366eddc9225abeb1686ec75cdd60d25373c8e53b
4
+ data.tar.gz: 49387a2b58f4aa7cfd0e36226240cbb1e93d2f49e5fd5636eb9cf3cabd0e3b53
5
5
  SHA512:
6
- metadata.gz: 5580aa1b51574cc1f81feb53040b44c8b540287e15f09a966c5dbb90ce16d54a67be73043a6882ae93075636163f0d7f72e181716fd95d691ade8d8e899b722e
7
- data.tar.gz: e7aa473295a9ed35350f5ae4b04e114959f68322d1e19a6ef88ec8743b46af5b448b2e4a9dd2f9b95f531b719c5952af0e1bd73af88ebb63c896492777817eb5
6
+ metadata.gz: f0cf699c114f7c61b03e42e199ecddee74270bb6a3fabe980c95f89951e7edc557cebc9b7c3eb1668614dc73a2c69a6c849be3010c6cdcb3210459a506ca04ef
7
+ data.tar.gz: 7b53bd11a26ad80c150862fbd28fe98b6a9fb0be238a613e2500ac0ffb439c34188b10f962f120c54fa491a371db05688a98d0f9ca27549eb9658a498f1d2d2a
data/AGENTS.md ADDED
@@ -0,0 +1,57 @@
1
+ # RSMP Agent Guide
2
+
3
+ This repository contains the Ruby implementation of RSMP, the `rsmp` CLI, and vendored RSMP Core and TLC SXL JSON Schemas.
4
+
5
+ ## Where Things Live
6
+
7
+ - `lib/rsmp/` contains the library, protocol logic, CLI implementation, schema handling, and converters.
8
+ - `exe/rsmp` is the executable entrypoint.
9
+ - `test/rsmp/` contains the internal `sus` test suite.
10
+ - `config/` contains example site and supervisor configuration files.
11
+ - `documentation/` contains project documentation.
12
+ - `schemas/` contains vendored Core and TLC schemas used at runtime. Do not edit these casually; update them from the source repos when possible.
13
+
14
+ ## Commands
15
+
16
+ - `bundle exec sus` runs the test suite.
17
+ - `bundle exec rake test` also runs the test suite.
18
+ - `bundle exec rubocop` checks Ruby style.
19
+ - `bundle exec exe/rsmp schema generate --in path/to/sxl.yaml --out path/to/output` generates JSON Schema files from an SXL YAML file.
20
+
21
+ Always run project executables through `bundle exec` so the correct bundle is used.
22
+
23
+ ## Schema Workflows
24
+
25
+ To generate JSON Schema files from an SXL YAML file:
26
+
27
+ ```sh
28
+ bundle exec exe/rsmp schema generate --in ../rsmp_sxl_traffic_lights/schema/sxl.yaml --out ../rsmp_sxl_traffic_lights/schema
29
+ ```
30
+
31
+ To refresh the vendored schemas in this repo from the sibling source repositories:
32
+
33
+ ```sh
34
+ bundle exec rake schemas:update
35
+ ```
36
+
37
+ The `schemas:update` task assumes sibling repos at `../rsmp_core` and `../rsmp_sxl_traffic_lights` by default. Pass explicit paths when needed:
38
+
39
+ ```sh
40
+ bundle exec rake schemas:update[/path/to/rsmp_core,/path/to/rsmp_sxl_traffic_lights]
41
+ ```
42
+
43
+ The task archives `schema/` from configured version branches in those repos, so make sure the local branches exist and are up to date before running it.
44
+
45
+ ## Editing Guidance
46
+
47
+ - Follow existing Ruby, `sus`, Thor CLI, and Async patterns.
48
+ - Tests may start local sites and supervisors; they use port `13111` to avoid the default RSMP port `12111`.
49
+ - CLI tests call the Thor class directly rather than spawning a separate process.
50
+ - Keep schema converter changes aligned with `RSMP::Schema` loading and validation behavior.
51
+ - If vendored schemas change, run the relevant schema and CLI tests.
52
+
53
+ ## Validation
54
+
55
+ - Run the narrowest relevant `bundle exec sus ...` command after edits, or the full `bundle exec sus` when behavior is shared.
56
+ - Run `bundle exec rubocop` for Ruby changes when practical.
57
+ - If schema source repos, branches, or dependencies are unavailable, mention which validation or schema update step was skipped and why.
data/CHANGELOG.md CHANGED
@@ -670,3 +670,18 @@ Initial release.
670
670
  - fix issues with config normalization
671
671
  - make collector cancellation raise pending errors
672
672
  - replace Cucumber/Aruba CLI tests with sus tests that call the Thor CLI directly
673
+
674
+ ## 0.45.1
675
+ - fix CLI execution by using relative requires
676
+ - update schema generation and generated schemas
677
+ - support patterns in list item types
678
+ - resolve SXL schema core definitions dynamically
679
+ - add CLI tests and split message tests
680
+
681
+ ## 0.45.2
682
+ - update gems
683
+ - add AGENTS.md
684
+ - replace em dashes to avoid UTF issues
685
+
686
+ ## 0.46.0
687
+ - add outgoing message buffering for sites
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.45.1)
4
+ rsmp (0.46.0)
5
5
  async (~> 2.39)
6
6
  colorize (~> 1.1)
7
7
  io-endpoint (~> 0.17)
@@ -35,7 +35,7 @@ GEM
35
35
  io-endpoint (0.17.2)
36
36
  io-event (1.16.1)
37
37
  io-stream (0.13.0)
38
- json (2.19.8)
38
+ json (2.19.9)
39
39
  json_schemer (2.5.0)
40
40
  bigdecimal
41
41
  hana (~> 1.3)
@@ -85,18 +85,18 @@ The following lists the top-level supervisor settings and the keys available for
85
85
 
86
86
  Top-level supervisor settings
87
87
 
88
- - `port`: integer|string TCP port the supervisor listens on (default: `12111`).
89
- - `ip`: string address to bind to.
90
- - `ips`: string or array `'all'` or a list of allowed IP addresses.
91
- - `site_id`: string optional site identifier for the supervisor itself.
92
- - `max_sites`: integer limit concurrent connected sites.
93
- - `default`: object default settings applied to sites that don't have a specific `sites` entry. Contains keys:
94
- - `sxls`: object default SXL versions for default sites, for example `{ "tlc": "1.3.0" }`.
88
+ - `port`: integer|string - TCP port the supervisor listens on (default: `12111`).
89
+ - `ip`: string - address to bind to.
90
+ - `ips`: string or array - `'all'` or a list of allowed IP addresses.
91
+ - `site_id`: string - optional site identifier for the supervisor itself.
92
+ - `max_sites`: integer - limit concurrent connected sites.
93
+ - `default`: object - default settings applied to sites that don't have a specific `sites` entry. Contains keys:
94
+ - `sxls`: object - default SXL versions for default sites, for example `{ "tlc": "1.3.0" }`.
95
95
  - `core_version`: string for the accepted RSMP Core version.
96
96
  - `intervals`: object with `timer`, `watchdog` (numbers, seconds).
97
97
  - `timeouts`: object with `watchdog`, `acknowledgement` (numbers, seconds).
98
- - `log`: object log settings (see `log_settings` elsewhere in docs).
99
- - `sites`: mapping per-site settings (see below).
98
+ - `log`: object - log settings (see `log_settings` elsewhere in docs).
99
+ - `sites`: mapping - per-site settings (see below).
100
100
 
101
101
  ## Per-site settings (`sites` mapping)
102
102
 
@@ -113,8 +113,8 @@ Common per-site keys
113
113
  - `site_id` (string): explicit site identifier (if different from the mapping key).
114
114
  - `supervisors` (array): list of supervisor endpoints (objects with `ip` and `port`). Useful for reverse mappings or local-site configs.
115
115
  - `components` (object): component definitions (same structure as site `components`), used by the supervisor-side proxies to set up component proxies.
116
- - `intervals` (object): per-site timer settings `timer`, `watchdog`, `reconnect`, `after_connect` (numbers, seconds).
117
- - `timeouts` (object): per-site timeouts `connect`, `watchdog`, `acknowledgement` (numbers, seconds).
116
+ - `intervals` (object): per-site timer settings - `timer`, `watchdog`, `reconnect`, `after_connect` (numbers, seconds).
117
+ - `timeouts` (object): per-site timeouts - `connect`, `watchdog`, `acknowledgement` (numbers, seconds).
118
118
  - `send_after_connect` (boolean): whether to send messages after connect without waiting for additional events.
119
119
  - `skip_validation` (array[string]): list of message types to skip JSON schema validation for this site.
120
120
  - `security_codes` (object): map of security code levels to secrets.
@@ -96,10 +96,9 @@ module RSMP
96
96
 
97
97
  def run
98
98
  log_site_starting
99
- @proxies.each do |proxy|
100
- proxy.start
101
- proxy.wait
102
- end
99
+ start_status_timer
100
+ @proxies.each(&:start)
101
+ @proxies.each(&:wait)
103
102
  end
104
103
 
105
104
  def build_proxies
@@ -119,7 +118,7 @@ module RSMP
119
118
 
120
119
  def aggregated_status_changed(component, _options = {})
121
120
  @proxies.each do |proxy|
122
- proxy.send_aggregated_status component if proxy.ready?
121
+ proxy.send_aggregated_status component
123
122
  end
124
123
  end
125
124
 
@@ -137,10 +136,50 @@ module RSMP
137
136
 
138
137
  def send_alarm(alarm)
139
138
  @proxies.each do |proxy|
140
- proxy.send_message alarm if proxy.ready? && proxy.receive_alarms?
139
+ proxy.send_message alarm if proxy.receive_alarms?
140
+ end
141
+ end
142
+
143
+ def start_status_timer
144
+ return if @status_timer
145
+
146
+ interval = @site_settings['intervals']['timer'] || 1
147
+ log "Starting site status timer with interval #{interval} seconds", level: :debug
148
+ @status_timer = @task.async do |task|
149
+ task.annotate 'site status timer'
150
+ run_status_timer task, interval
151
+ end
152
+ end
153
+
154
+ def run_status_timer(task, interval)
155
+ next_time = Time.now.to_f
156
+ loop do
157
+ now = Clock.now
158
+ tick_status_subscriptions now
159
+ rescue StandardError => e
160
+ distribute_error e, level: :internal
161
+ ensure
162
+ next_time += interval
163
+ duration = next_time - Time.now.to_f
164
+ task.sleep duration
141
165
  end
142
166
  end
143
167
 
168
+ def tick_status_subscriptions(now)
169
+ @proxies.each { |proxy| proxy.status_update_timer now }
170
+ end
171
+
172
+ def stop_status_timer
173
+ @status_timer&.stop
174
+ ensure
175
+ @status_timer = nil
176
+ end
177
+
178
+ def stop_subtasks
179
+ stop_status_timer
180
+ super
181
+ end
182
+
144
183
  def connect_to_supervisor(_task, supervisor_settings)
145
184
  proxy = build_proxy({
146
185
  site: self,
@@ -41,6 +41,32 @@
41
41
  "additionalProperties": true
42
42
  },
43
43
  "send_after_connect": { "type": "boolean" },
44
+ "message_buffer": {
45
+ "type": "object",
46
+ "properties": {
47
+ "enabled": { "type": "boolean" },
48
+ "max_messages": { "type": "integer", "minimum": 1 },
49
+ "statuses": {
50
+ "oneOf": [
51
+ { "type": "boolean" },
52
+ {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "properties": {
57
+ "cId": { "type": "string" },
58
+ "sCI": { "type": "string" },
59
+ "n": { "type": "string" }
60
+ },
61
+ "required": ["sCI"],
62
+ "additionalProperties": false
63
+ }
64
+ }
65
+ ]
66
+ }
67
+ },
68
+ "additionalProperties": false
69
+ },
44
70
  "components": { "type": "object" },
45
71
  "security_codes": { "type": "object" },
46
72
  "startup_sequence": { "type": "string" },
@@ -21,6 +21,7 @@ module RSMP
21
21
  'acknowledgement' => 2
22
22
  },
23
23
  'send_after_connect' => true,
24
+ 'message_buffer' => default_message_buffer,
24
25
  'components' => {
25
26
  'main' => {
26
27
  'C1' => {}
@@ -35,6 +36,14 @@ module RSMP
35
36
 
36
37
  private
37
38
 
39
+ def default_message_buffer
40
+ {
41
+ 'enabled' => true,
42
+ 'max_messages' => 10_000,
43
+ 'statuses' => []
44
+ }
45
+ end
46
+
38
47
  def apply_defaults(options)
39
48
  defaults = defaults()
40
49
  defaults['components']['main'] = options['components']['main'] if options.dig('components', 'main')
@@ -11,8 +11,13 @@ module RSMP
11
11
  distribute_error error.exception("#{str} #{message.json}")
12
12
  end
13
13
 
14
- def send_message(message, reason = nil, validate: true, force: false)
15
- raise NotReady if !force && !connected?
14
+ def send_message(message, reason = nil, validate: true, force: false, buffer: true)
15
+ unless force || connected?
16
+ error = NotReady.new
17
+ raise error unless buffer
18
+
19
+ return buffer_message(message, error)
20
+ end
16
21
  raise IOError unless @protocol
17
22
 
18
23
  message.direction = :out
@@ -22,15 +27,18 @@ module RSMP
22
27
  expect_acknowledgement message
23
28
  distribute message
24
29
  log_send message, reason
25
- rescue IOError
26
- buffer_message message
30
+ rescue NotReady, IOError => e
31
+ raise e unless buffer
32
+
33
+ buffer_message message, e
27
34
  rescue SchemaError, RSMP::Schema::Error => e
28
35
  handle_send_schema_error(message, e)
29
36
  end
30
37
 
31
- def buffer_message(message)
32
- # TODO
33
- # log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
38
+ def buffer_message(message, error = nil)
39
+ str = "Cannot send #{message.type} because the connection is closed."
40
+ log str, message: message, level: :error
41
+ raise error if error
34
42
  end
35
43
 
36
44
  def log_send(message, reason = nil)
@@ -125,13 +125,14 @@ module RSMP
125
125
  end
126
126
 
127
127
  def unsubscribe_to_status(status_list, component: nil, validate: nil)
128
- validate_ready 'unsubscribe to status'
129
128
  component ||= main.c_id
130
129
 
131
130
  status_list.each do |item|
132
131
  remove_subscription_item(component, item['sCI'], item['n'])
133
132
  end
134
133
 
134
+ return unless ready? # if the connection is don't we skip sending
135
+
135
136
  message = RSMP::StatusUnsubscribe.new({
136
137
  'cId' => component,
137
138
  'sS' => status_list
@@ -13,7 +13,7 @@ module RSMP
13
13
  def send_aggregated_status(component, m_id: nil)
14
14
  m_id ||= RSMP::Message.make_m_id
15
15
 
16
- se = if Proxy.version_meets_requirement?(core_version, '<=3.1.2')
16
+ se = if core_version && Proxy.version_meets_requirement?(core_version, '<=3.1.2')
17
17
  component.aggregated_status_bools.map { |bool| bool ? 'true' : 'false' }
18
18
  else
19
19
  component.aggregated_status_bools
@@ -0,0 +1,144 @@
1
+ module RSMP
2
+ class SupervisorProxy < Proxy
3
+ module Modules
4
+ # In-memory outgoing communication buffer for site-originated messages.
5
+ module MessageBuffer
6
+ def message_buffer_settings
7
+ @site_settings['message_buffer'] || {}
8
+ end
9
+
10
+ def message_buffer_enabled?
11
+ message_buffer_settings['enabled'] != false
12
+ end
13
+
14
+ def message_buffer_max_messages
15
+ message_buffer_settings['max_messages'] || 10_000
16
+ end
17
+
18
+ def status_buffer_selectors
19
+ return message_buffer_settings['statuses'] if message_buffer_settings.key? 'statuses'
20
+
21
+ []
22
+ end
23
+
24
+ def status_buffer_selector?(component_id, status)
25
+ selectors = status_buffer_selectors
26
+ return true if selectors == true
27
+ return false unless selectors.is_a?(Array)
28
+
29
+ selectors.any? { |selector| status_buffer_selector_matches?(selector, component_id, status) }
30
+ end
31
+
32
+ def status_buffer_selector_matches?(selector, component_id, status)
33
+ selector = selector.transform_keys(&:to_s)
34
+ component_matches = !selector['cId'] || selector['cId'] == component_id
35
+ code_matches = !selector['sCI'] || selector['sCI'] == status['sCI']
36
+ name_matches = !selector['n'] || selector['n'] == status['n']
37
+ component_matches && code_matches && name_matches
38
+ end
39
+
40
+ def clone_message(message, attributes = message.attributes)
41
+ message.class.new(JSON.parse(JSON.generate(attributes)))
42
+ end
43
+
44
+ def site_originated_buffer_candidate?(message)
45
+ message.is_a?(RSMP::AggregatedStatus) ||
46
+ message.is_a?(RSMP::AlarmIssue) ||
47
+ message.is_a?(RSMP::AlarmSuspended) ||
48
+ message.is_a?(RSMP::AlarmResumed) ||
49
+ message.is_a?(RSMP::AlarmAcknowledged) ||
50
+ message.is_a?(RSMP::StatusUpdate)
51
+ end
52
+
53
+ def prepare_status_update_for_buffer(message, core_version:, for_send:)
54
+ attributes = JSON.parse(JSON.generate(message.attributes))
55
+ component_id = attributes['cId']
56
+ attributes['sS'] = attributes['sS'].select { |status| status_buffer_selector?(component_id, status) }
57
+ return if attributes['sS'].empty?
58
+
59
+ if for_send && core_version && version_meets_requirement?(core_version, '>=3.2.0')
60
+ attributes['sS'].each { |status| status['q'] = 'old' }
61
+ end
62
+ clone_message message, attributes
63
+ end
64
+
65
+ def normalize_aggregated_status_buffer(states, core_version)
66
+ if version_meets_requirement?(core_version, '<=3.1.2')
67
+ states.map { |item| item == true || item.to_s == 'true' ? 'true' : 'false' }
68
+ else
69
+ states.map { |item| item == true || item.to_s == 'true' }
70
+ end
71
+ end
72
+
73
+ def prepare_aggregated_status_for_buffer(message, core_version:, for_send:)
74
+ attributes = JSON.parse(JSON.generate(message.attributes))
75
+ if for_send && core_version && attributes['se']
76
+ attributes['se'] = normalize_aggregated_status_buffer(attributes['se'], core_version)
77
+ end
78
+ clone_message message, attributes
79
+ end
80
+
81
+ def prepare_message_for_buffer(message, core_version: @core_version, for_send: false)
82
+ return unless message_buffer_enabled?
83
+ return unless site_originated_buffer_candidate? message
84
+ return false if message.is_a?(RSMP::StatusUpdate) && status_buffer_selectors == false
85
+ return false if message.is_a?(RSMP::Alarm) && !receive_alarms?
86
+
87
+ if message.is_a? RSMP::AggregatedStatus
88
+ prepare_aggregated_status_for_buffer message, core_version: core_version, for_send: for_send
89
+ elsif message.is_a? RSMP::StatusUpdate
90
+ prepare_status_update_for_buffer message, core_version: core_version, for_send: for_send
91
+ else
92
+ clone_message message
93
+ end
94
+ end
95
+
96
+ def buffer_message(message, error = nil)
97
+ prepared = prepare_message_for_buffer message, core_version: @core_version
98
+ if prepared
99
+ enqueue_buffered_message prepared
100
+ elsif site_originated_buffer_candidate? message
101
+ log "Discarded #{message.type}; it is not configured for buffering", message: message, level: :warning
102
+ else
103
+ super
104
+ end
105
+ rescue NotReady, IOError
106
+ raise error if error
107
+ end
108
+
109
+ def enqueue_buffered_message(message)
110
+ while @message_buffer.size >= message_buffer_max_messages
111
+ dropped = @message_buffer.shift
112
+ log "Dropped buffered #{dropped.type}; message buffer is full", message: dropped, level: :warning
113
+ end
114
+ @message_buffer << message
115
+ log "Buffered #{message.type}; #{message_buffer.size} message(s) queued", message: message, level: :warning
116
+ message
117
+ end
118
+
119
+ def flush_message_buffer
120
+ return if @message_buffer.empty?
121
+
122
+ queued = @message_buffer
123
+ @message_buffer = []
124
+ log "Sending #{queued.size} buffered message(s)", level: :info
125
+ queued.each_with_index do |message, index|
126
+ break unless flush_buffered_message(message, queued, index)
127
+ end
128
+ end
129
+
130
+ def flush_buffered_message(message, queued, index)
131
+ prepared = prepare_message_for_buffer message, core_version: @core_version, for_send: true
132
+ return true unless prepared
133
+
134
+ send_message prepared, 'from buffer', buffer: false
135
+ true
136
+ rescue NotReady, IOError
137
+ @message_buffer = queued[index..] + @message_buffer
138
+ log "Stopped sending buffered messages; #{message_buffer.size} message(s) remain queued", level: :warning
139
+ false
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -101,6 +101,21 @@ module RSMP
101
101
  acknowledge message
102
102
  end
103
103
 
104
+ def prune_unbuffered_status_subscriptions
105
+ @status_subscriptions.each_key.to_a.each do |component_id|
106
+ by_code = @status_subscriptions[component_id]
107
+ by_code.each_key.to_a.each do |code|
108
+ by_name = by_code[code]
109
+ by_name.delete_if do |name, _subscription|
110
+ status = { 'sCI' => code, 'n' => name }
111
+ !status_buffer_selector?(component_id, status)
112
+ end
113
+ by_code.delete(code) if by_name.empty?
114
+ end
115
+ @status_subscriptions.delete(component_id) if by_code.empty?
116
+ end
117
+ end
118
+
104
119
  def fetch_last_sent_status(component, code, name)
105
120
  @last_status_sent&.dig component, code, name
106
121
  end
@@ -7,8 +7,9 @@ module RSMP
7
7
  include Modules::Commands
8
8
  include Modules::Alarms
9
9
  include Modules::AggregatedStatus
10
+ include Modules::MessageBuffer
10
11
 
11
- attr_reader :supervisor_id, :site
12
+ attr_reader :supervisor_id, :site, :message_buffer
12
13
 
13
14
  def initialize(options)
14
15
  super(options.merge(node: options[:site]))
@@ -21,6 +22,7 @@ module RSMP
21
22
  @accepted_sxls = @sxls.dup
22
23
  @rejected_sxls = []
23
24
  @synthetic_id = Supervisor.build_id_from_ip_port @ip, @port
25
+ @message_buffer = []
24
26
  end
25
27
 
26
28
  # handle communication
@@ -51,6 +53,11 @@ module RSMP
51
53
  send_version_request @site_settings['site_id'], core_versions
52
54
  end
53
55
 
56
+ def close
57
+ prune_unbuffered_status_subscriptions
58
+ super
59
+ end
60
+
54
61
  # connect to the supervisor and initiate handshake supervisor
55
62
  def connect
56
63
  log "Connecting to supervisor at #{@ip}:#{@port}", level: :info
@@ -98,6 +105,7 @@ module RSMP
98
105
  send_all_aggregated_status
99
106
  send_active_alarms if receive_alarms?
100
107
  end
108
+ flush_message_buffer
101
109
  super
102
110
  end
103
111
 
@@ -170,11 +178,6 @@ module RSMP
170
178
  send_watchdog
171
179
  end
172
180
 
173
- def timer(now)
174
- super
175
- status_update_timer now if ready?
176
- end
177
-
178
181
  def process_version(message)
179
182
  return extraneous_version message if @version_determined
180
183
 
@@ -4,7 +4,7 @@ module RSMP
4
4
  # Command methods for operational control of a remote TLC.
5
5
  # Covers functional position, emergency routes, I/O modes, signal group orders, and system settings.
6
6
  module Control
7
- # M0001 Set functional position (NormalControl, YellowFlash, Dark).
7
+ # M0001 - Set functional position (NormalControl, YellowFlash, Dark).
8
8
  def set_functional_position(status, within:, timeout_minutes: 0)
9
9
  validate_ready 'set functional position'
10
10
  raise 'TLC main component not found' unless main
@@ -17,7 +17,7 @@ module RSMP
17
17
  { collector: collector }
18
18
  end
19
19
 
20
- # M0005 Set or clear an emergency route.
20
+ # M0005 - Set or clear an emergency route.
21
21
  def set_emergency_route(route:, active:, within:)
22
22
  validate_ready 'set emergency route'
23
23
  raise 'TLC main component not found' unless main
@@ -45,7 +45,7 @@ module RSMP
45
45
  send_command_and_collect(command_list, within: within).ok!
46
46
  end
47
47
 
48
- # M0007 Enable or disable fixed-time control.
48
+ # M0007 - Enable or disable fixed-time control.
49
49
  def set_fixed_time(status, within:)
50
50
  validate_ready 'set fixed time'
51
51
  raise 'TLC main component not found' unless main
@@ -70,7 +70,7 @@ module RSMP
70
70
  wait_for_status "fixed time #{status}", confirm_status, timeout: within
71
71
  end
72
72
 
73
- # M0003 Set traffic situation (activate a specific situation number).
73
+ # M0003 - Set traffic situation (activate a specific situation number).
74
74
  def set_traffic_situation(situation, within:)
75
75
  validate_ready 'set traffic situation'
76
76
  raise 'TLC main component not found' unless main
@@ -99,7 +99,7 @@ module RSMP
99
99
  wait_for_status "traffic situation #{situation}", confirm_status, timeout: within
100
100
  end
101
101
 
102
- # M0003 Clear the active traffic situation.
102
+ # M0003 - Clear the active traffic situation.
103
103
  def unset_traffic_situation(within:)
104
104
  validate_ready 'unset traffic situation'
105
105
  raise 'TLC main component not found' unless main
@@ -4,7 +4,7 @@ module RSMP
4
4
  # Command methods for operational control of a remote TLC.
5
5
  # Covers functional position, emergency routes, I/O modes, signal group orders, and system settings.
6
6
  module Detectors
7
- # M0008 Force detector logic to a given mode and status.
7
+ # M0008 - Force detector logic to a given mode and status.
8
8
  # component_id must refer to the detector logic component, not main.
9
9
  def force_detector_logic(component_id, status:, mode:, within:)
10
10
  validate_ready 'force detector logic'
@@ -30,7 +30,7 @@ module RSMP
30
30
  send_command_and_collect(command_list, component: component_id, within: within).ok!
31
31
  end
32
32
 
33
- # M0021 Set the trigger level for traffic counting.
33
+ # M0021 - Set the trigger level for traffic counting.
34
34
  def set_trigger_level(status, within:)
35
35
  validate_ready 'set trigger level'
36
36
  raise 'TLC main component not found' unless main
@@ -4,7 +4,7 @@ module RSMP
4
4
  # Command methods for I/O control of a remote TLC.
5
5
  # Covers detector logic, input/output forcing and setting.
6
6
  module IO
7
- # M0006 Set a single input to a given status.
7
+ # M0006 - Set a single input to a given status.
8
8
  def set_input(input:, status:, within:)
9
9
  validate_ready 'set input'
10
10
  raise 'TLC main component not found' unless main
@@ -30,7 +30,7 @@ module RSMP
30
30
  send_command_and_collect(command_list, within: within).ok!
31
31
  end
32
32
 
33
- # M0013 Set all inputs via a bit-pattern string.
33
+ # M0013 - Set all inputs via a bit-pattern string.
34
34
  def set_inputs(status, within:)
35
35
  validate_ready 'set inputs'
36
36
  raise 'TLC main component not found' unless main
@@ -51,7 +51,7 @@ module RSMP
51
51
  send_command_and_collect(command_list, within: within).ok!
52
52
  end
53
53
 
54
- # M0019 Force an input to a given value.
54
+ # M0019 - Force an input to a given value.
55
55
  def force_input(input:, status:, value:, within:)
56
56
  validate_ready 'force input'
57
57
  raise 'TLC main component not found' unless main
@@ -62,7 +62,7 @@ module RSMP
62
62
  wait_for_status "force input #{input}", confirm_status, timeout: within
63
63
  end
64
64
 
65
- # M0020 Force an output to a given value.
65
+ # M0020 - Force an output to a given value.
66
66
  def force_output(output:, status:, value:, within:)
67
67
  validate_ready 'force output'
68
68
  raise 'TLC main component not found' unless main
@@ -4,7 +4,7 @@ module RSMP
4
4
  # Command methods for signal plans.
5
5
  # Covers time plans, week/day tables, bands, offsets, and cycle times.
6
6
  module Plans
7
- # M0014 Set dynamic bands for a signal plan.
7
+ # M0014 - Set dynamic bands for a signal plan.
8
8
  def set_dynamic_bands(plan:, status:, within:)
9
9
  validate_ready 'set dynamic bands'
10
10
  raise 'TLC main component not found' unless main
@@ -30,7 +30,7 @@ module RSMP
30
30
  send_command_and_collect(command_list, within: within).ok!
31
31
  end
32
32
 
33
- # M0023 Set timeout for dynamic bands.
33
+ # M0023 - Set timeout for dynamic bands.
34
34
  def set_dynamic_bands_timeout(status, within:)
35
35
  validate_ready 'set dynamic bands timeout'
36
36
  raise 'TLC main component not found' unless main
@@ -51,7 +51,7 @@ module RSMP
51
51
  send_command_and_collect(command_list, within: within).ok!
52
52
  end
53
53
 
54
- # M0015 Set offset for a signal plan.
54
+ # M0015 - Set offset for a signal plan.
55
55
  def set_offset(plan:, offset:, within:)
56
56
  validate_ready 'set offset'
57
57
  raise 'TLC main component not found' unless main
@@ -105,7 +105,7 @@ module RSMP
105
105
  wait_for_status("timeplan #{plan_nr}", confirm_status, timeout: within)
106
106
  end
107
107
 
108
- # M0016 Set week table (mapping week days to traffic situations).
108
+ # M0016 - Set week table (mapping week days to traffic situations).
109
109
  def set_week_table(status, within:)
110
110
  validate_ready 'set week table'
111
111
  raise 'TLC main component not found' unless main
@@ -126,7 +126,7 @@ module RSMP
126
126
  send_command_and_collect(command_list, within:).ok!
127
127
  end
128
128
 
129
- # M0017 Set day table (mapping time periods to signal plans).
129
+ # M0017 - Set day table (mapping time periods to signal plans).
130
130
  def set_day_table(status, within:)
131
131
  validate_ready 'set day table'
132
132
  raise 'TLC main component not found' unless main
@@ -147,7 +147,7 @@ module RSMP
147
147
  send_command_and_collect(command_list, within:).ok!
148
148
  end
149
149
 
150
- # M0018 Set cycle time for a signal plan.
150
+ # M0018 - Set cycle time for a signal plan.
151
151
  def set_cycle_time(plan:, cycle_time:, within:)
152
152
  validate_ready 'set cycle time'
153
153
  raise 'TLC main component not found' unless main
@@ -173,7 +173,7 @@ module RSMP
173
173
  send_command_and_collect(command_list, within:).ok!
174
174
  end
175
175
 
176
- # M0010 Order signal start for a signal group component.
176
+ # M0010 - Order signal start for a signal group component.
177
177
  def order_signal_start(component_id, within:)
178
178
  validate_ready 'order signal start'
179
179
 
@@ -193,7 +193,7 @@ module RSMP
193
193
  send_command_and_collect(command_list, component: component_id, within:).ok!
194
194
  end
195
195
 
196
- # M0011 Order signal stop for a signal group component.
196
+ # M0011 - Order signal stop for a signal group component.
197
197
  def order_signal_stop(component_id, within:)
198
198
  validate_ready 'order signal stop'
199
199
 
@@ -4,7 +4,7 @@ module RSMP
4
4
  # Command methods for operational control of a remote TLC.
5
5
  # Covers functional position, emergency routes, I/O modes, signal group orders, and system settings.
6
6
  module System
7
- # M0103 Change security code for a given level.
7
+ # M0103 - Change security code for a given level.
8
8
  # Does not use security_code_for since the codes are passed explicitly.
9
9
  def set_security_code(level:, old_code:, new_code:, within:)
10
10
  validate_ready 'set security code'
@@ -29,7 +29,7 @@ module RSMP
29
29
  send_command_and_collect(command_list, within: within).ok!
30
30
  end
31
31
 
32
- # M0104 Set the clock on the remote TLC. clock must respond to year/month/day/hour/min/sec.
32
+ # M0104 - Set the clock on the remote TLC. clock must respond to year/month/day/hour/min/sec.
33
33
  def set_clock(clock, within:)
34
34
  validate_ready 'set clock'
35
35
  raise 'TLC main component not found' unless main
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = '0.45.1'.freeze
2
+ VERSION = '0.46.0'.freeze
3
3
  end
data/lib/rsmp.rb CHANGED
@@ -77,6 +77,7 @@ require_relative 'rsmp/proxy/supervisor/modules/status'
77
77
  require_relative 'rsmp/proxy/supervisor/modules/commands'
78
78
  require_relative 'rsmp/proxy/supervisor/modules/alarms'
79
79
  require_relative 'rsmp/proxy/supervisor/modules/aggregated_status'
80
+ require_relative 'rsmp/proxy/supervisor/modules/message_buffer'
80
81
  require_relative 'rsmp/proxy/supervisor/supervisor_proxy'
81
82
  require_relative 'rsmp/proxy/site/modules/status'
82
83
  require_relative 'rsmp/proxy/site/modules/aggregated_status'
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.45.1
4
+ version: 0.46.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
@@ -137,6 +137,7 @@ files:
137
137
  - ".gitmodules"
138
138
  - ".rubocop.yml"
139
139
  - ".tool-versions"
140
+ - AGENTS.md
140
141
  - CHANGELOG.md
141
142
  - Gemfile
142
143
  - Gemfile.lock
@@ -227,6 +228,7 @@ files:
227
228
  - lib/rsmp/proxy/supervisor/modules/aggregated_status.rb
228
229
  - lib/rsmp/proxy/supervisor/modules/alarms.rb
229
230
  - lib/rsmp/proxy/supervisor/modules/commands.rb
231
+ - lib/rsmp/proxy/supervisor/modules/message_buffer.rb
230
232
  - lib/rsmp/proxy/supervisor/modules/status.rb
231
233
  - lib/rsmp/proxy/supervisor/supervisor_proxy.rb
232
234
  - lib/rsmp/schema.rb