rsmp 0.8.4 → 0.9.1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yaml +21 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile.lock +51 -67
  5. data/README.md +2 -12
  6. data/bin/console +1 -1
  7. data/cucumber.yml +1 -0
  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 +32 -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/deep_merge.rb +1 -0
  22. data/lib/rsmp/error.rb +0 -3
  23. data/lib/rsmp/inspect.rb +1 -1
  24. data/lib/rsmp/logger.rb +5 -5
  25. data/lib/rsmp/logging.rb +1 -1
  26. data/lib/rsmp/message.rb +1 -1
  27. data/lib/rsmp/node.rb +10 -45
  28. data/lib/rsmp/proxy.rb +184 -134
  29. data/lib/rsmp/rsmp.rb +1 -1
  30. data/lib/rsmp/site.rb +23 -60
  31. data/lib/rsmp/site_proxy.rb +33 -37
  32. data/lib/rsmp/supervisor.rb +25 -21
  33. data/lib/rsmp/supervisor_proxy.rb +58 -29
  34. data/lib/rsmp/task.rb +84 -0
  35. data/lib/rsmp/tlc/signal_group.rb +5 -3
  36. data/lib/rsmp/tlc/signal_plan.rb +2 -2
  37. data/lib/rsmp/tlc/traffic_controller.rb +97 -29
  38. data/lib/rsmp/tlc/traffic_controller_site.rb +43 -36
  39. data/lib/rsmp/version.rb +1 -1
  40. data/lib/rsmp.rb +1 -1
  41. data/rsmp.gemspec +7 -7
  42. metadata +21 -20
  43. data/lib/rsmp/site_proxy_wait.rb +0 -0
  44. data/lib/rsmp/wait.rb +0 -16
  45. data/test.rb +0 -27
@@ -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 => e
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,70 @@ 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 see issue #22
75
+ @task.async do |task|
76
+ task.annotate 'socket task'
77
+ # this timeout is a workaround for #connect hanging on windows if the other side is not present yet
78
+ task.with_timeout 1.1 do
79
+ @socket = @endpoint.connect
80
+ end
81
+ end.wait
82
+
58
83
  @stream = Async::IO::Stream.new(@socket)
59
84
  @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
85
+ set_state :connected
60
86
  end
61
87
 
62
- def connection_complete
88
+ def handshake_complete
63
89
  super
64
90
  sanitized_sxl_version = RSMP::Schemer.sanitize_version(sxl_version)
65
91
  log "Connection to supervisor established, using core #{@rsmp_version}, #{sxl} #{sanitized_sxl_version}", level: :info
@@ -114,16 +140,19 @@ module RSMP
114
140
  end
115
141
 
116
142
  def reconnect_delay
143
+ return false if @site_settings['intervals']['reconnect'] == :no
117
144
  interval = @site_settings['intervals']['reconnect']
118
- log "Waiting #{interval} seconds before trying to reconnect", level: :info
145
+ log "Will try to reconnect again every #{interval} seconds...", level: :info
146
+ @logger.mute @ip, @port
119
147
  @task.sleep interval
148
+ true
120
149
  end
121
150
 
122
151
  def version_accepted message
123
152
  log "Received Version message, using RSMP #{@rsmp_version}", message: message, level: :log
124
153
  start_timer
125
154
  acknowledge message
126
- connection_complete
155
+ handshake_complete
127
156
  @version_determined = true
128
157
  end
129
158
 
@@ -138,8 +167,8 @@ module RSMP
138
167
  "mId" => m_id,
139
168
  })
140
169
 
141
- send_and_optionally_collect message, options do |task|
142
- wait_for_acknowledgement task, options[:collect], m_id
170
+ send_and_optionally_collect message, options do |collect_options|
171
+ Collector.new self, collect_options.merge(task:@task, type: 'MessageAck')
143
172
  end
144
173
  end
145
174
 
@@ -234,7 +263,7 @@ module RSMP
234
263
  update_list = {}
235
264
  component = message.attributes["cId"]
236
265
  @status_subscriptions[component] ||= {}
237
- update_list[component] ||= {}
266
+ update_list[component] ||= {}
238
267
  now = Time.now # internal timestamp
239
268
  subs = @status_subscriptions[component]
240
269
 
@@ -299,7 +328,7 @@ module RSMP
299
328
  by_name.each_pair do |name,subscription|
300
329
  current = nil
301
330
  should_send = false
302
- if subscription[:interval] == 0
331
+ if subscription[:interval] == 0
303
332
  # send as soon as the data changes
304
333
  if component_object
305
334
  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 task_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
@@ -9,25 +9,27 @@ module RSMP
9
9
  end
10
10
 
11
11
  def timer
12
- @state = get_state
12
+ @state = compute_state
13
13
  end
14
14
 
15
- def get_state
15
+ def compute_state
16
16
  return 'a' if node.main.dark_mode
17
17
  return 'c' if node.main.yellow_flash
18
18
 
19
19
  cycle_counter = node.main.cycle_counter
20
20
 
21
21
  if node.main.startup_sequence_active
22
- @state = node.main.startup_state || 'a'
22
+ return node.main.startup_state || 'a'
23
23
  end
24
24
 
25
25
  default = 'a' # phase a means disabled/dark
26
26
  plan = node.main.current_plan
27
27
  return default unless plan
28
28
  return default unless plan.states
29
+
29
30
  states = plan.states[c_id]
30
31
  return default unless states
32
+
31
33
  state = states[cycle_counter]
32
34
  return default unless state =~ /[a-hA-G0-9N-P]/ # valid signal group states
33
35
  state
@@ -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: