rsmp 0.8.3 → 0.9.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yaml +14 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile.lock +46 -67
  5. data/README.md +2 -2
  6. data/bin/console +1 -1
  7. data/config/tlc.yaml +8 -6
  8. data/documentation/classes_and_modules.md +4 -4
  9. data/documentation/collecting_message.md +2 -2
  10. data/documentation/tasks.md +149 -0
  11. data/lib/rsmp/archive.rb +3 -3
  12. data/lib/rsmp/cli.rb +27 -4
  13. data/lib/rsmp/collect/aggregated_status_collector.rb +1 -1
  14. data/lib/rsmp/collect/collector.rb +13 -6
  15. data/lib/rsmp/collect/command_response_collector.rb +1 -1
  16. data/lib/rsmp/collect/state_collector.rb +1 -1
  17. data/lib/rsmp/collect/status_collector.rb +2 -1
  18. data/lib/rsmp/components.rb +3 -3
  19. data/lib/rsmp/convert/export/json_schema.rb +4 -4
  20. data/lib/rsmp/convert/import/yaml.rb +1 -1
  21. data/lib/rsmp/error.rb +0 -3
  22. data/lib/rsmp/inspect.rb +1 -1
  23. data/lib/rsmp/logger.rb +5 -5
  24. data/lib/rsmp/logging.rb +1 -1
  25. data/lib/rsmp/message.rb +1 -1
  26. data/lib/rsmp/node.rb +10 -45
  27. data/lib/rsmp/proxy.rb +184 -134
  28. data/lib/rsmp/rsmp.rb +1 -1
  29. data/lib/rsmp/site.rb +24 -61
  30. data/lib/rsmp/site_proxy.rb +33 -37
  31. data/lib/rsmp/supervisor.rb +25 -21
  32. data/lib/rsmp/supervisor_proxy.rb +55 -29
  33. data/lib/rsmp/task.rb +84 -0
  34. data/lib/rsmp/tlc/signal_group.rb +17 -7
  35. data/lib/rsmp/tlc/signal_plan.rb +2 -2
  36. data/lib/rsmp/tlc/traffic_controller.rb +125 -39
  37. data/lib/rsmp/tlc/traffic_controller_site.rb +51 -35
  38. data/lib/rsmp/version.rb +1 -1
  39. data/lib/rsmp.rb +1 -1
  40. data/rsmp.gemspec +7 -7
  41. metadata +20 -20
  42. data/lib/rsmp/site_proxy_wait.rb +0 -0
  43. data/lib/rsmp/wait.rb +0 -16
  44. data/test.rb +0 -27
data/lib/rsmp/site.rb CHANGED
@@ -9,12 +9,14 @@ module RSMP
9
9
  attr_reader :rsmp_versions, :site_settings, :logger, :proxies
10
10
 
11
11
  def initialize options={}
12
+ super options
12
13
  initialize_components
13
14
  handle_site_settings options
14
- super options
15
15
  @proxies = []
16
16
  @sleep_condition = Async::Notification.new
17
17
  @proxies_condition = Async::Notification.new
18
+
19
+ build_proxies
18
20
  end
19
21
 
20
22
  def site_id
@@ -62,25 +64,29 @@ module RSMP
62
64
  RSMP::Schemer::find_schema! sxl, version, lenient: true
63
65
  end
64
66
 
65
- def reconnect
66
- @sleep_condition.signal
67
+ def run
68
+ log "Starting site #{@site_settings["site_id"]}",
69
+ level: :info,
70
+ timestamp: @clock.now
71
+ @proxies.each { |proxy| proxy.start }
72
+ @proxies.each { |proxy| proxy.wait }
67
73
  end
68
74
 
69
- def start_action
75
+ def build_proxies
70
76
  @site_settings["supervisors"].each do |supervisor_settings|
71
- @task.async do |task|
72
- task.annotate "site proxy"
73
- connect_to_supervisor task, supervisor_settings
74
- rescue StandardError => e
75
- notify_error e, level: :internal
76
- end
77
+ @proxies << SupervisorProxy.new({
78
+ site: self,
79
+ task: @task,
80
+ settings: @site_settings,
81
+ ip: supervisor_settings['ip'],
82
+ port: supervisor_settings['port'],
83
+ logger: @logger,
84
+ archive: @archive,
85
+ collect: @collect
86
+ })
77
87
  end
78
88
  end
79
89
 
80
- def build_proxy settings
81
- SupervisorProxy.new settings
82
- end
83
-
84
90
  def aggregated_status_changed component, options={}
85
91
  @proxies.each do |proxy|
86
92
  proxy.send_aggregated_status component, options if proxy.ready?
@@ -91,7 +97,7 @@ module RSMP
91
97
  proxy = build_proxy({
92
98
  site: self,
93
99
  task: @task,
94
- settings: @site_settings,
100
+ settings: @site_settings,
95
101
  ip: supervisor_settings['ip'],
96
102
  port: supervisor_settings['port'],
97
103
  logger: @logger,
@@ -99,63 +105,20 @@ module RSMP
99
105
  collect: @collect
100
106
  })
101
107
  @proxies << proxy
108
+ proxy.start
102
109
  @proxies_condition.signal
103
- run_site_proxy task, proxy
104
- ensure
105
- @proxies.delete proxy
106
- @proxies_condition.signal
107
- end
108
-
109
- def run_site_proxy task, proxy
110
- loop do
111
- proxy.run # run until disconnected
112
- rescue IOError => e
113
- log "Stream error: #{e}", level: :warning
114
- rescue StandardError => e
115
- notify_error e, level: :internal
116
- ensure
117
- begin
118
- if @site_settings['intervals']['watchdog'] != :no
119
- # sleep until waken by reconnect() or the reconnect interval passed
120
- proxy.set_state :wait_for_reconnect
121
- task.with_timeout(@site_settings['intervals']['watchdog']) do
122
- @sleep_condition.wait
123
- end
124
- else
125
- proxy.set_state :cannot_connect
126
- break
127
- end
128
- rescue Async::TimeoutError
129
- # ignore
130
- end
131
- end
132
110
  end
133
111
 
112
+ # stop
134
113
  def stop
135
114
  log "Stopping site #{@site_settings["site_id"]}", level: :info
136
- @proxies.each do |proxy|
137
- proxy.stop
138
- end
139
- @proxies.clear
140
115
  super
141
116
  end
142
-
143
- def starting
144
- log "Starting site #{@site_settings["site_id"]}",
145
- level: :info,
146
- timestamp: @clock.now
147
- end
148
-
149
- def alarm
150
- @proxies.each do |proxy|
151
- proxy.stop
152
- end
153
- end
154
117
 
155
118
  def wait_for_supervisor ip, timeout
156
119
  supervisor = find_supervisor ip
157
120
  return supervisor if supervisor
158
- wait_for(@proxy_condition,timeout) { find_supervisor ip }
121
+ wait_for_condition(@proxy_condition,timeout:timeout) { find_supervisor ip }
159
122
  rescue Async::TimeoutError
160
123
  raise RSMP::TimeoutError.new "Supervisor '#{ip}' did not connect within #{timeout}s"
161
124
  end
@@ -1,6 +1,6 @@
1
1
  # Handles a supervisor connection to a remote client
2
2
 
3
- module RSMP
3
+ module RSMP
4
4
  class SiteProxy < Proxy
5
5
  include Components
6
6
 
@@ -14,33 +14,37 @@ module RSMP
14
14
  @site_id = options[:site_id]
15
15
  end
16
16
 
17
+ # handle communication
18
+ # when we're created, the socket is already open
19
+ def run
20
+ set_state :connected
21
+ start_reader
22
+ wait_for_reader # run until disconnected
23
+ rescue RSMP::ConnectionError => e
24
+ log e, level: :error
25
+ rescue StandardError => e
26
+ notify_error e, level: :internal
27
+ ensure
28
+ close
29
+ end
30
+
17
31
  def revive options
18
32
  super options
19
33
  @supervisor = options[:supervisor]
20
34
  @settings = @supervisor.supervisor_settings.clone
21
35
  end
22
36
 
23
-
24
37
  def inspect
25
38
  "#<#{self.class.name}:#{self.object_id}, #{inspector(
26
39
  :@acknowledgements,:@settings,:@site_settings,:@components
27
40
  )}>"
28
41
  end
42
+
29
43
  def node
30
44
  supervisor
31
45
  end
32
46
 
33
- def start
34
- super
35
- start_reader
36
- end
37
-
38
- def stop
39
- log "Closing connection to site", level: :info
40
- super
41
- end
42
-
43
- def connection_complete
47
+ def handshake_complete
44
48
  super
45
49
  sanitized_sxl_version = RSMP::Schemer.sanitize_version(@site_sxl_version)
46
50
  log "Connection to site #{@site_id} established, using core #{@rsmp_version}, #{@sxl} #{sanitized_sxl_version}", level: :info
@@ -68,7 +72,7 @@ module RSMP
68
72
  else
69
73
  super message
70
74
  end
71
- rescue RSMP::RepeatedAlarmError, RSMP::RepeatedStatusError, TimestampError => e
75
+ rescue RSMP::RepeatedAlarmError, RSMP::RepeatedStatusError
72
76
  str = "Rejected #{message.type} message,"
73
77
  dont_acknowledge message, str, "#{e}"
74
78
  notify_error e.exception("#{str}#{e.message} #{message.json}")
@@ -101,13 +105,11 @@ module RSMP
101
105
  "cId" => component,
102
106
  "mId" => m_id
103
107
  })
104
- send_and_optionally_collect message, options do |task|
105
- collector = AggregatedStatusCollector.new(
108
+ send_and_optionally_collect message, options do |collect_options|
109
+ AggregatedStatusCollector.new(
106
110
  self,
107
- options[:collect].merge(task:@task,m_id: m_id, num:1)
111
+ collect_options.merge(task:@task,m_id: m_id, num:1)
108
112
  )
109
- collector.collect
110
- collector
111
113
  end
112
114
  end
113
115
 
@@ -150,7 +152,7 @@ module RSMP
150
152
  end
151
153
 
152
154
  def version_acknowledged
153
- connection_complete
155
+ handshake_complete
154
156
  end
155
157
 
156
158
  def process_watchdog message
@@ -179,14 +181,12 @@ module RSMP
179
181
  "sS" => request_list,
180
182
  "mId" => m_id
181
183
  })
182
- send_and_optionally_collect message, options do |task|
183
- collector = StatusCollector.new(
184
+ send_and_optionally_collect message, options do |collect_options|
185
+ StatusCollector.new(
184
186
  self,
185
187
  status_list,
186
- options[:collect].merge(task:@task,m_id: m_id)
188
+ collect_options.merge(task:@task,m_id: m_id)
187
189
  )
188
- collector.collect
189
- collector
190
190
  end
191
191
  end
192
192
 
@@ -200,7 +200,7 @@ module RSMP
200
200
  def subscribe_to_status component_id, status_list, options={}
201
201
  validate_ready 'subscribe to status'
202
202
  m_id = options[:m_id] || RSMP::Message.make_m_id
203
-
203
+
204
204
  # additional items can be used when verifying the response,
205
205
  # but must to remove from the subscribe message
206
206
  subscribe_list = status_list.map { |item| item.slice('sCI','n','uRt') }
@@ -215,14 +215,12 @@ module RSMP
215
215
  "sS" => subscribe_list,
216
216
  'mId' => m_id
217
217
  })
218
- send_and_optionally_collect message, options do |task|
219
- collector = StatusCollector.new(
218
+ send_and_optionally_collect message, options do |collect_options|
219
+ StatusCollector.new(
220
220
  self,
221
221
  status_list,
222
- options[:collect].merge(task:@task,m_id: m_id)
222
+ collect_options.merge(task:@task,m_id: m_id)
223
223
  )
224
- collector.collect
225
- collector
226
224
  end
227
225
  end
228
226
 
@@ -267,14 +265,12 @@ module RSMP
267
265
  "arg" => command_list,
268
266
  "mId" => m_id
269
267
  })
270
- send_and_optionally_collect message, options do |task|
271
- collector = CommandResponseCollector.new(
268
+ send_and_optionally_collect message, options do |collect_options|
269
+ CommandResponseCollector.new(
272
270
  self,
273
271
  command_list,
274
- options[:collect].merge(task:@task,m_id: m_id)
272
+ collect_options.merge(task:@task,m_id: m_id)
275
273
  )
276
- collector.collect
277
- collector
278
274
  end
279
275
  end
280
276
 
@@ -332,7 +328,7 @@ module RSMP
332
328
  log "Using site settings for guest", level: :debug
333
329
  return @settings['guest']
334
330
  end
335
-
331
+
336
332
  nil
337
333
  end
338
334
 
@@ -58,26 +58,31 @@ module RSMP
58
58
  end
59
59
  end
60
60
 
61
- def start_action
61
+ # listen for connections
62
+ # Async::IO::Endpoint#accept createa an async task that we will wait for
63
+ def run
64
+ log "Starting supervisor on port #{@supervisor_settings["port"]}",
65
+ level: :info,
66
+ timestamp: @clock.now
67
+
62
68
  @endpoint = Async::IO::Endpoint.tcp('0.0.0.0', @supervisor_settings["port"])
63
- @endpoint.accept do |socket| # creates async tasks
69
+ tasks = @endpoint.accept do |socket| # creates async tasks
64
70
  handle_connection(socket)
65
71
  rescue StandardError => e
66
72
  notify_error e, level: :internal
67
73
  end
74
+ tasks.each { |task| task.wait }
68
75
  rescue StandardError => e
69
76
  notify_error e, level: :internal
70
77
  end
71
78
 
79
+ # stop
72
80
  def stop
73
81
  log "Stopping supervisor #{@supervisor_settings["site_id"]}", level: :info
74
- @proxies.each { |proxy| proxy.stop }
75
- @proxies.clear
76
82
  super
77
- @tcp_server.close if @tcp_server
78
- @tcp_server = nil
79
83
  end
80
84
 
85
+ # handle an incoming connction by either accepting of rejecting it
81
86
  def handle_connection socket
82
87
  remote_port = socket.remote_address.ip_port
83
88
  remote_hostname = socket.remote_address.ip_address
@@ -85,9 +90,9 @@ module RSMP
85
90
 
86
91
  info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:Clock.now}
87
92
  if accept? socket, info
88
- connect socket, info
93
+ accept_connection socket, info
89
94
  else
90
- reject socket, info
95
+ reject_connection socket, info
91
96
  end
92
97
  rescue ConnectionError => e
93
98
  log "Rejected connection from #{remote_ip}:#{remote_port}, #{e.to_s}", level: :warning
@@ -99,12 +104,6 @@ module RSMP
99
104
  close socket, info
100
105
  end
101
106
 
102
- def starting
103
- log "Starting supervisor on port #{@supervisor_settings["port"]}",
104
- level: :info,
105
- timestamp: @clock.now
106
- end
107
-
108
107
  def accept? socket, info
109
108
  true
110
109
  end
@@ -143,7 +142,8 @@ module RSMP
143
142
  message.attribute('siteId').first['sId']
144
143
  end
145
144
 
146
- def connect socket, info
145
+ # accept an incoming connecting by creating and starting a proxy
146
+ def accept_connection socket, info
147
147
  log "Site connected from #{format_ip_and_port(info)}",
148
148
  ip: info[:ip],
149
149
  port: info[:port],
@@ -182,7 +182,8 @@ module RSMP
182
182
  proxy = build_proxy settings.merge(site_id:id) # keep the id learned by peeking above
183
183
  @proxies.push proxy
184
184
  end
185
- proxy.run # will run until the site disconnects
185
+ proxy.start # will run until the site disconnects
186
+ proxy.wait
186
187
  ensure
187
188
  site_ids_changed
188
189
  stop if @supervisor_settings['one_shot']
@@ -192,7 +193,7 @@ module RSMP
192
193
  @site_id_condition.signal
193
194
  end
194
195
 
195
- def reject socket, info
196
+ def reject_connection socket, info
196
197
  log "Site rejected", ip: info[:ip], level: :info
197
198
  end
198
199
 
@@ -224,10 +225,13 @@ module RSMP
224
225
  nil
225
226
  end
226
227
 
227
- def wait_for_site site_id, timeout
228
+ def wait_for_site site_id, timeout:
228
229
  site = find_site site_id
229
230
  return site if site
230
- wait_for(@site_id_condition,timeout) { find_site site_id }
231
+ wait_for_condition(@site_id_condition,timeout:timeout) do
232
+ find_site site_id
233
+ end
234
+
231
235
  rescue Async::TimeoutError
232
236
  if site_id == :any
233
237
  str = "No site connected"
@@ -237,8 +241,8 @@ module RSMP
237
241
  raise RSMP::TimeoutError.new "#{str} within #{timeout}s"
238
242
  end
239
243
 
240
- def wait_for_site_disconnect site_id, timeout
241
- wait_for(@site_id_condition,timeout) { true unless find_site site_id }
244
+ def wait_for_site_disconnect site_id, timeout:
245
+ wait_for_condition(@site_id_condition,timeout:timeout) { true unless find_site site_id }
242
246
  rescue Async::TimeoutError
243
247
  raise RSMP::TimeoutError.new "Site '#{site_id}' did not disconnect within #{timeout}s"
244
248
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'digest'
4
4
 
5
- module RSMP
5
+ module RSMP
6
6
  class SupervisorProxy < Proxy
7
7
 
8
8
  attr_reader :supervisor_id, :site
@@ -22,44 +22,67 @@ module RSMP
22
22
  site
23
23
  end
24
24
 
25
- def start
25
+ # handle communication
26
+ # if disconnected, then try to reconnect
27
+ def run
28
+ loop do
29
+ connect
30
+ start_reader
31
+ start_handshake
32
+ wait_for_reader # run until disconnected
33
+ break if reconnect_delay == false
34
+ rescue Restart
35
+ @logger.mute @ip, @port
36
+ raise
37
+ rescue RSMP::ConnectionError => e
38
+ log e, level: :error
39
+ break if reconnect_delay == false
40
+ rescue StandardError => e
41
+ notify_error e, level: :internal
42
+ break if reconnect_delay == false
43
+ ensure
44
+ close
45
+ stop_subtasks
46
+ end
47
+ end
48
+
49
+ def start_handshake
50
+ send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
51
+ end
52
+
53
+ # connect to the supervisor and initiate handshake supervisor
54
+ def connect
26
55
  log "Connecting to supervisor at #{@ip}:#{@port}", level: :info
27
- super
28
- connect
56
+ set_state :connecting
57
+ connect_tcp
29
58
  @logger.unmute @ip, @port
30
59
  log "Connected to supervisor at #{@ip}:#{@port}", level: :info
31
- start_reader
32
- send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
33
60
  rescue SystemCallError => e
34
- log "Could not connect to supervisor at #{@ip}:#{@port}: Errno #{e.errno} #{e}", level: :error
35
- retry_notice
61
+ raise ConnectionError.new "Could not connect to supervisor at #{@ip}:#{@port}: Errno #{e.errno} #{e}"
36
62
  rescue StandardError => e
37
- log "Error while connecting to supervisor at #{@ip}:#{@port}: #{e}", level: :error
38
- retry_notice
39
- end
40
-
41
- def retry_notice
42
- unless @site.site_settings['intervals']['reconnect'] == :no
43
- log "Will try to reconnect again every #{@site.site_settings['intervals']['reconnect']} seconds..", level: :info
44
- @logger.mute @ip, @port
45
- end
63
+ raise ConnectionError.new "Error while connecting to supervisor at #{@ip}:#{@port}: #{e}"
46
64
  end
47
65
 
48
- def stop
49
- log "Closing connection to supervisor", level: :info
66
+ def stop_task
50
67
  super
51
68
  @last_status_sent = nil
52
69
  end
53
70
 
54
- def connect
55
- return if @socket
71
+ def connect_tcp
56
72
  @endpoint = Async::IO::Endpoint.tcp(@ip, @port)
57
- @socket = @endpoint.connect
73
+
74
+ # Async::IO::Endpoint#connect renames the current task. run in a subtask to avoid this
75
+ @task.async do |task|
76
+ task.annotate 'socket task'
77
+ @socket = @endpoint.connect
78
+ end.wait
79
+
58
80
  @stream = Async::IO::Stream.new(@socket)
59
81
  @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
82
+ set_state :connected
60
83
  end
61
84
 
62
- def connection_complete
85
+ def handshake_complete
63
86
  super
64
87
  sanitized_sxl_version = RSMP::Schemer.sanitize_version(sxl_version)
65
88
  log "Connection to supervisor established, using core #{@rsmp_version}, #{sxl} #{sanitized_sxl_version}", level: :info
@@ -114,16 +137,19 @@ module RSMP
114
137
  end
115
138
 
116
139
  def reconnect_delay
140
+ return false if @site_settings['intervals']['reconnect'] == :no
117
141
  interval = @site_settings['intervals']['reconnect']
118
- log "Waiting #{interval} seconds before trying to reconnect", level: :info
142
+ log "Will try to reconnect again every #{interval} seconds...", level: :info
143
+ @logger.mute @ip, @port
119
144
  @task.sleep interval
145
+ true
120
146
  end
121
147
 
122
148
  def version_accepted message
123
149
  log "Received Version message, using RSMP #{@rsmp_version}", message: message, level: :log
124
150
  start_timer
125
151
  acknowledge message
126
- connection_complete
152
+ handshake_complete
127
153
  @version_determined = true
128
154
  end
129
155
 
@@ -138,8 +164,8 @@ module RSMP
138
164
  "mId" => m_id,
139
165
  })
140
166
 
141
- send_and_optionally_collect message, options do |task|
142
- wait_for_acknowledgement task, options[:collect], m_id
167
+ send_and_optionally_collect message, options do |collect_options|
168
+ Collector.new self, collect_options.merge(task:@task, type: 'MessageAck')
143
169
  end
144
170
  end
145
171
 
@@ -234,7 +260,7 @@ module RSMP
234
260
  update_list = {}
235
261
  component = message.attributes["cId"]
236
262
  @status_subscriptions[component] ||= {}
237
- update_list[component] ||= {}
263
+ update_list[component] ||= {}
238
264
  now = Time.now # internal timestamp
239
265
  subs = @status_subscriptions[component]
240
266
 
@@ -299,7 +325,7 @@ module RSMP
299
325
  by_name.each_pair do |name,subscription|
300
326
  current = nil
301
327
  should_send = false
302
- if subscription[:interval] == 0
328
+ if subscription[:interval] == 0
303
329
  # send as soon as the data changes
304
330
  if component_object
305
331
  current, age = *(component_object.get_status code, name)
data/lib/rsmp/task.rb ADDED
@@ -0,0 +1,84 @@
1
+ module RSMP
2
+ class Restart < StandardError
3
+ end
4
+
5
+ module Task
6
+ attr_reader :task
7
+
8
+ def initialize_task
9
+ @task = nil
10
+ end
11
+
12
+ # start our async tasks and return immediately
13
+ # run() will be called inside the task to perform actual long-running work
14
+ def start
15
+ return if @task
16
+ Async do |task|
17
+ task.annotate "#{self.class.name} main task"
18
+ @task = task
19
+ run
20
+ stop_subtasks
21
+ @task = nil
22
+ end
23
+ self
24
+ end
25
+
26
+ # initiate restart by raising a Restart exception
27
+ def restart
28
+ raise Restart.new "restart initiated by #{self.class.name}:#{object_id}"
29
+ end
30
+
31
+ # get the status of our task, or nil of no task
32
+ def status
33
+ @task.status if @task
34
+ end
35
+
36
+ # perform any long-running work
37
+ # the method will be called from an async task, and should not return
38
+ # if subtasks are needed, the method should call wait() on each of them
39
+ # once running, ready() must be called
40
+ def run
41
+ start_subtasks
42
+ end
43
+
44
+ # wait for our task to complete
45
+ def wait
46
+ @task.wait if @task
47
+ end
48
+
49
+ # stop our task
50
+ def stop
51
+ stop_subtasks
52
+ stop_task if @task
53
+ end
54
+
55
+ def stop_subtasks
56
+ end
57
+
58
+ # stop our task and any subtask
59
+ def stop_task
60
+ @task.stop
61
+ @task = nil
62
+ end
63
+
64
+ # wait for an async condition to signal, then yield to block
65
+ # if block returns true we're done. otherwise, wait again
66
+ def wait_for_condition condition, timeout:, task:Async::Task.current, &block
67
+ unless task
68
+ raise RuntimeError.new("Can't wait without a task")
69
+ end
70
+ task.with_timeout(timeout) do
71
+ while task.running?
72
+ value = condition.wait
73
+ return value unless block
74
+ result = yield value
75
+ return result if result
76
+ end
77
+ raise RuntimeError.new("Can't wait for condition because task #{task.object_id} #{task.annotation} is not running")
78
+ end
79
+ rescue Async::TimeoutError
80
+ raise RSMP::TimeoutError.new
81
+ end
82
+
83
+ end
84
+ end
@@ -6,25 +6,35 @@ module RSMP
6
6
  # plan is a string, with each character representing a signal phase at a particular second in the cycle
7
7
  def initialize node:, id:
8
8
  super node: node, id: id, grouped: false
9
- move 0
10
9
  end
11
10
 
12
- def get_state pos
11
+ def timer
12
+ @state = compute_state
13
+ end
14
+
15
+ def compute_state
16
+ return 'a' if node.main.dark_mode
17
+ return 'c' if node.main.yellow_flash
18
+
19
+ cycle_counter = node.main.cycle_counter
20
+
21
+ if node.main.startup_sequence_active
22
+ return node.main.startup_state || 'a'
23
+ end
24
+
13
25
  default = 'a' # phase a means disabled/dark
14
26
  plan = node.main.current_plan
15
27
  return default unless plan
16
28
  return default unless plan.states
29
+
17
30
  states = plan.states[c_id]
18
31
  return default unless states
19
- state = states[pos]
32
+
33
+ state = states[cycle_counter]
20
34
  return default unless state =~ /[a-hA-G0-9N-P]/ # valid signal group states
21
35
  state
22
36
  end
23
37
 
24
- def move pos
25
- @state = get_state pos
26
- end
27
-
28
38
  def handle_command command_code, arg
29
39
  case command_code
30
40
  when 'M0010', 'M0011'
@@ -1,8 +1,8 @@
1
1
  module RSMP
2
2
  module TLC
3
3
  # A Traffic Light Controller Signal Plan.
4
- # A signal plan is a description of how all signal groups should change
5
- # state over time.
4
+ # A signal plan is a description of how all signal groups should change
5
+ # state over time.
6
6
  class SignalPlan
7
7
  attr_reader :nr, :states, :dynamic_bands
8
8
  def initialize nr:, states:, dynamic_bands: