rsmp 0.8.6 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
 
@@ -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
@@ -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:
@@ -23,24 +23,29 @@ module RSMP
23
23
  reset
24
24
  end
25
25
 
26
- def reset
27
- @cycle_counter = 0
28
- @plan = 1
26
+ def reset_modes
29
27
  @dark_mode = true
30
28
  @yellow_flash = false
31
29
  @booting = false
32
- @control_mode = 'control'
33
- @police_key = 0
34
- @intersection = 0
35
30
  @is_starting = false
36
- @emergency_route = false
37
- @emergency_route_number = 0
38
- @traffic_situation = 0
31
+ @control_mode = 'control'
39
32
  @manual_control = false
40
33
  @fixed_time_control = false
41
34
  @isolated_control = false
42
35
  @yellow_flash = false
43
36
  @all_red = false
37
+ @police_key = 0
38
+ end
39
+
40
+ def reset
41
+ reset_modes
42
+
43
+ @cycle_counter = 0
44
+ @plan = 1
45
+ @intersection = 0
46
+ @emergency_route = false
47
+ @emergency_route_number = 0
48
+ @traffic_situation = 0
44
49
 
45
50
  @inputs = '0'*@num_inputs
46
51
  @input_activations = '0'*@num_inputs
@@ -76,12 +81,14 @@ module RSMP
76
81
 
77
82
  def timer now
78
83
  # TODO use monotone timer, to avoid jumps in case the user sets the system time
79
- @signal_groups.each { |group| group.timer }
80
84
  time = Time.now.to_i
81
85
  return if time == @time_int
82
86
  @time_int = time
83
87
  move_cycle_counter
84
88
  move_startup_sequence if @startup_sequence_active
89
+
90
+ @signal_groups.each { |group| group.timer }
91
+
85
92
  output_states
86
93
  end
87
94
 
@@ -98,6 +105,8 @@ module RSMP
98
105
 
99
106
  def initiate_startup_sequence
100
107
  log "Initiating startup sequence", level: :info
108
+ reset_modes
109
+ @dark_mode = false
101
110
  @startup_sequence_active = true
102
111
  @startup_sequence_initiated_at = nil
103
112
  @startup_sequence_pos = nil
@@ -107,7 +116,6 @@ module RSMP
107
116
  @startup_sequence_active = false
108
117
  @startup_sequence_initiated_at = nil
109
118
  @startup_sequence_pos = nil
110
-
111
119
  @yellow_flash = false
112
120
  @dark_mode = false
113
121
  end
@@ -127,26 +135,40 @@ module RSMP
127
135
 
128
136
  def output_states
129
137
  return unless @live_output
138
+
130
139
  str = @signal_groups.map do |group|
131
- s = "#{group.c_id}:#{group.state}"
132
- if group.state =~ /^[1-9]$/
140
+ state = group.state
141
+ s = "#{group.c_id}:#{state}"
142
+ if state =~ /^[1-9]$/
133
143
  s.colorize(:green)
134
- elsif group.state =~ /^[NOP]$/
144
+ elsif state =~ /^[NOP]$/
135
145
  s.colorize(:yellow)
136
- elsif group.state =~ /^[ae]$/
137
- s.colorize(:black)
138
- elsif group.state =~ /^[f]$/
146
+ elsif state =~ /^[ae]$/
147
+ s.colorize(:light_black)
148
+ elsif state =~ /^[f]$/
139
149
  s.colorize(:yellow)
140
- elsif group.state =~ /^[g]$/
150
+ elsif state =~ /^[g]$/
141
151
  s.colorize(:red)
142
152
  else
143
153
  s.colorize(:red)
144
154
  end
145
155
  end.join ' '
156
+
157
+ modes = '.'*9
158
+ modes[0] = 'B' if @booting
159
+ modes[1] = 'S' if @startup_sequence_active
160
+ modes[2] = 'D' if @dark_mode
161
+ modes[3] = 'Y' if @yellow_flash
162
+ modes[4] = 'M' if @manual_control
163
+ modes[5] = 'F' if @fixed_time_control
164
+ modes[6] = 'R' if @all_red
165
+ modes[7] = 'I' if @isolated_control
166
+ modes[8] = 'P' if @police_key != 0
167
+
146
168
  plan = "P#{@plan}"
147
169
 
148
170
  File.open @live_output, 'w' do |file|
149
- file.puts "#{plan.rjust(4)} #{pos.to_s.rjust(4)} #{str}\r"
171
+ file.puts "#{modes} #{plan.rjust(2)} #{@cycle_counter.to_s.rjust(3)} #{str}\r"
150
172
  end
151
173
  end
152
174
 
@@ -316,7 +338,7 @@ module RSMP
316
338
  return unless i>=0 && i<@num_inputs
317
339
  @inputs[i] = (arg['value'] ? '1' : '0')
318
340
  end
319
-
341
+
320
342
  def set_fixed_time_control status
321
343
  @fixed_time_control = status
322
344
  end
@@ -18,6 +18,18 @@ module RSMP
18
18
  unless @main
19
19
  raise ConfigurationError.new "TLC must have a main component"
20
20
  end
21
+
22
+ end
23
+
24
+ def start
25
+ super
26
+ start_tlc_timer
27
+ @main.initiate_startup_sequence
28
+ end
29
+
30
+ def stop_subtasks
31
+ stop_tlc_timer
32
+ super
21
33
  end
22
34
 
23
35
  def build_plans signal_plans
@@ -56,49 +68,45 @@ module RSMP
56
68
  end
57
69
  end
58
70
 
59
- def start_action
60
- super
61
- start_timer
62
- @main.initiate_startup_sequence
63
- end
64
-
65
- def start_timer
71
+ def start_tlc_timer
66
72
  task_name = "tlc timer"
67
73
  log "Starting #{task_name} with interval #{@interval} seconds", level: :debug
68
74
 
69
75
  @timer = @task.async do |task|
70
- task.annotate task_name
71
- next_time = Time.now.to_f
72
- loop do
73
- begin
74
- timer(@clock.now)
75
- rescue EOFError => e
76
- log "Connection closed: #{e}", level: :warning
77
- rescue IOError => e
78
- log "IOError", level: :warning
79
- rescue Errno::ECONNRESET
80
- log "Connection reset by peer", level: :warning
81
- rescue Errno::EPIPE => e
82
- log "Broken pipe", level: :warning
83
- rescue StandardError => e
84
- notify_error e, level: :internal
85
- ensure
86
- # adjust sleep duration to avoid drift. so wake up always happens on the
87
- # same fractional second.
88
- # note that Time.now is not monotonic. If the clock is changed,
89
- # either manaully or via NTP, the sleep interval might jump.
90
- # an alternative is to use ::Process.clock_gettime(::Process::CLOCK_MONOTONIC),
91
- # to get the current time. this ensures a constant interval, but
92
- # if the clock is changed, the wake up would then happen on a different
93
- # fractional second
94
- next_time += @interval
95
- duration = next_time - Time.now.to_f
96
- task.sleep duration
97
- end
76
+ task.annotate task_name
77
+ run_tlc_timer task
78
+ end
79
+ end
80
+
81
+ def run_tlc_timer task
82
+ next_time = Time.now.to_f
83
+ loop do
84
+ begin
85
+ timer(@clock.now)
86
+ rescue StandardError => e
87
+ notify_error e, level: :internal
88
+ ensure
89
+ # adjust sleep duration to avoid drift. so wake up always happens on the
90
+ # same fractional second.
91
+ # note that Time.now is not monotonic. If the clock is changed,
92
+ # either manaully or via NTP, the sleep interval might jump.
93
+ # an alternative is to use ::Process.clock_gettime(::Process::CLOCK_MONOTONIC),
94
+ # to get the current time. this ensures a constant interval, but
95
+ # if the clock is changed, the wake up would then happen on a different
96
+ # fractional second
97
+ next_time += @interval
98
+ duration = next_time - Time.now.to_f
99
+ task.sleep duration
98
100
  end
99
101
  end
100
102
  end
101
103
 
104
+ def stop_tlc_timer
105
+ return unless @timer
106
+ @timer.stop
107
+ @timer = nil
108
+ end
109
+
102
110
  def timer now
103
111
  return unless @main
104
112
  @main.timer now
@@ -142,7 +150,6 @@ module RSMP
142
150
  when :restart
143
151
  log "Restarting TLC", level: :info
144
152
  restart
145
- initiate_startup_sequence
146
153
  end
147
154
  end
148
155
  end
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = "0.8.6"
2
+ VERSION = "0.9.0"
3
3
  end
data/lib/rsmp.rb CHANGED
@@ -10,10 +10,10 @@ require 'json_schemer'
10
10
  require 'async/queue'
11
11
 
12
12
  require 'rsmp/rsmp'
13
+ require 'rsmp/task'
13
14
  require 'rsmp/deep_merge'
14
15
  require 'rsmp/inspect'
15
16
  require 'rsmp/logging'
16
- require 'rsmp/wait'
17
17
  require 'rsmp/node'
18
18
  require 'rsmp/supervisor'
19
19
  require 'rsmp/components'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsmp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-26 00:00:00.000000000 Z
11
+ date: 2022-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -186,6 +186,7 @@ executables:
186
186
  extensions: []
187
187
  extra_rdoc_files: []
188
188
  files:
189
+ - ".github/workflows/rspec.yaml"
189
190
  - ".gitignore"
190
191
  - ".gitmodules"
191
192
  - ".rspec"
@@ -203,6 +204,7 @@ files:
203
204
  - documentation/classes_and_modules.md
204
205
  - documentation/collecting_message.md
205
206
  - documentation/message_distribution.md
207
+ - documentation/tasks.md
206
208
  - exe/rsmp
207
209
  - lib/rsmp.rb
208
210
  - lib/rsmp/archive.rb
@@ -234,18 +236,16 @@ files:
234
236
  - lib/rsmp/rsmp.rb
235
237
  - lib/rsmp/site.rb
236
238
  - lib/rsmp/site_proxy.rb
237
- - lib/rsmp/site_proxy_wait.rb
238
239
  - lib/rsmp/supervisor.rb
239
240
  - lib/rsmp/supervisor_proxy.rb
241
+ - lib/rsmp/task.rb
240
242
  - lib/rsmp/tlc/detector_logic.rb
241
243
  - lib/rsmp/tlc/signal_group.rb
242
244
  - lib/rsmp/tlc/signal_plan.rb
243
245
  - lib/rsmp/tlc/traffic_controller.rb
244
246
  - lib/rsmp/tlc/traffic_controller_site.rb
245
247
  - lib/rsmp/version.rb
246
- - lib/rsmp/wait.rb
247
248
  - rsmp.gemspec
248
- - test.rb
249
249
  homepage: https://github.com/rsmp-nordic/rsmp
250
250
  licenses:
251
251
  - MIT