rsmp 0.7.3 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9f08828abc4896fcc6a40da66ed96db50f1c7ba8d2b819ae7f7771930920bd4
4
- data.tar.gz: 618100d9e2bf8ace347274a791740b094cf690be3e00e12d35f491e7a717b58e
3
+ metadata.gz: 5316c50ad6cd09d30e7d1baab78e717fac4656c06178272ef4adc1c6647fb544
4
+ data.tar.gz: 6ef4d635b42f3eac19b94b68bbcb05c59ce94de96e0e27f1f5c7a6deefd4e470
5
5
  SHA512:
6
- metadata.gz: fe9ce63eae79b1145e0bce329a5c2ed723075293d1811402d65349bc368f9246bfbe4823dddb33d3dead0e74bf113937f8aac1b1c39680af81cd313637c05d71
7
- data.tar.gz: e2451b14a8a9889de965a1e67b5e42e0dcfb6d28cbeb1d9bbfd962b324c8087502a93b1095edaf80fdae1c0721c38912d78d48154a8d359b2fd3846bd69063f7
6
+ metadata.gz: 1947906505e532d0d47d93f1d9fe2c8c3199ff58d378b9be3df18865339e966a6ad5f74512aa168ee4a5d641624cf7b0f170436d2c11e4f495bd7d6b88be502f
7
+ data.tar.gz: ee0ecbb229637276d6cec21ccacc6414199ad7f0392324633231cb4a8632a8b64b1aa2ba5abd979a7699ed6721a5a40b9d9c531f45dc0f59345149662e608b12
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.7.3)
4
+ rsmp (0.8.1)
5
5
  async (~> 1.29.1)
6
6
  async-io (~> 1.32.1)
7
7
  colorize (~> 0.8.1)
@@ -34,7 +34,7 @@ GEM
34
34
  childprocess (4.1.0)
35
35
  colorize (0.8.1)
36
36
  concurrent-ruby (1.1.9)
37
- console (1.13.1)
37
+ console (1.14.0)
38
38
  fiber-local
39
39
  contracts (0.17)
40
40
  cucumber (6.1.0)
@@ -95,7 +95,7 @@ GEM
95
95
  thor
96
96
  thread_safe
97
97
  rake (13.0.3)
98
- regexp_parser (2.1.1)
98
+ regexp_parser (2.2.0)
99
99
  rsmp_schemer (0.3.2)
100
100
  json_schemer (~> 0.2.18)
101
101
  rspec (3.10.0)
@@ -124,6 +124,7 @@ GEM
124
124
 
125
125
  PLATFORMS
126
126
  x86_64-darwin-20
127
+ x86_64-darwin-21
127
128
 
128
129
  DEPENDENCIES
129
130
  aruba (~> 1.1.2)
@@ -136,4 +137,4 @@ DEPENDENCIES
136
137
  timecop (~> 0.9.4)
137
138
 
138
139
  BUNDLED WITH
139
- 2.2.21
140
+ 2.2.32
@@ -19,9 +19,6 @@ Handle logging.
19
19
  ### Wait
20
20
  Handles waiting for an async condition and block.
21
21
 
22
- ### SiteProxyWait
23
- Handles waiting for different types of messages and responses from a remote site.
24
-
25
22
  ### Components
26
23
  Component handling.
27
24
 
@@ -38,7 +35,7 @@ A Site has one or more SupervisorProxies (connections to supervisor).
38
35
 
39
36
  A site has one of more components.
40
37
 
41
- ### Supervisor
38
+ ### Supervisor
42
39
  A Supervisor represents an RSMP supervisor, typically a central supervisor system. An RSMP supervisor can handle connections one or more sites.
43
40
 
44
41
  A Supervisor has one or more SiteProxies (connections to sites).
@@ -0,0 +1,196 @@
1
+ # Collection
2
+ You often need to collect messages or responses. The collector classes are used to collect message asyncronously. Other tasks continue until the collection completes, time outs or is cancelled.
3
+
4
+ A collector can collect ingoing and/or outgoing messages.
5
+
6
+ An object that includes the Notifier module (or implements the same functionality) must be provided when you construct a Collected. The collector will attach itself to this notifier when it starts collecting, to receive messages. The SiteProxy and SupervisorProxy classes both include the Notifier module, and can therefore be used as message sources.
7
+
8
+ Messages that match the relevant criteria are stored by the collector.
9
+
10
+ When the collection is done, the collector detaches from the notifier, and returns the status.
11
+
12
+
13
+ ## Collector
14
+ Class uses for collecting messages filtered by message type, direction and/or component id. A block can be used for custom filtering.
15
+
16
+ You can choose to collect a specific number of message and/or for a specific duration.
17
+
18
+ A collector has a status, which is `:ready` initialialy. When you start collecting, it changes to `:collecting`. It will be `:ok` once collection completes successfully, or `:cancel` if it was cancelled to to some error or by a filter block.
19
+
20
+ ### Initialization
21
+ When you create a collector, you specify the messages types you want to collect.
22
+ You can also specify ingoing and/or outgoing direction and the RSMP component.
23
+
24
+ ```ruby
25
+ collector = MessageCollector.new notifier, num: 10, ingoing: true, outgoing: true
26
+ ```
27
+
28
+ num: The number of messages to collect. If not provided, a timeout must be set instead.
29
+ timeout: The number of seconds to collect
30
+ ingoing: Whether to collect ingoing messages. Defaults to true
31
+ outgoing: Whether to collect outgoing messages. Defaults to true
32
+ component: An RSMP component id.
33
+
34
+ ### Collecting
35
+ Use collect() to start collecting and wait for completion or timeout. The status will be returned.
36
+
37
+ ```ruby
38
+ result = collector.collect # => :ok, :timeout or :cancelled
39
+ collector.messages # => collected messages
40
+ ```
41
+
42
+ If you want start collection, but not wait for the result, use `start()`. You can then later use `wait()` if you want:
43
+
44
+ ```ruby
45
+ result = collector.start # => nil
46
+ # do other stuff
47
+ result = collector.wait
48
+ ```
49
+
50
+ ### Custom filtering
51
+ You can use a block to do extra filtering. The block will be callled for each messages that fulfils the correct message type, direction and component id.
52
+
53
+ The block must return nil or a list of symbols to indicate whether the message should be kept, and whether collection should be cancelled.
54
+
55
+ ```ruby
56
+ result = collector.collect do |message|
57
+ :keep, :cancel # example of how to keep the message and cancel collection
58
+ end
59
+ ```
60
+
61
+ `:keep` keeps (collect) this message
62
+ `:cancel` cancel collection
63
+
64
+ Note that you cannot use `return` in a block. You can either simply provide the values as the last expresssion in the block, or use next().
65
+
66
+ Exceptions in the block will cause the collector to abort. If the collect! or wait! variants are used, the exception is propagated to the caller.
67
+
68
+ ### Bang version
69
+ The method collect!() will raise exceptions in case of errors, and will return the collect message directly.
70
+
71
+ ```ruby
72
+ message = collector.collect # => collected message.
73
+ ```
74
+
75
+ Similar, `wait!()` will raise an exception in case of timeouts or errors:
76
+
77
+ ```ruby
78
+ message = collector.wait! # => collected message.
79
+ ```
80
+
81
+
82
+ ### Schema Errors and Disconnects
83
+ The collector can optionally cancel collection in special cases, controlled by the `:cancel` option provided when contructing the collector.
84
+
85
+ ```ruby
86
+ options = {
87
+ cancel: {
88
+ disconnect: true,
89
+ schema_error: true
90
+ }
91
+ }
92
+ result = collector.collect options
93
+ ```
94
+
95
+ disconnect: If the proxy which provides messages experience a disconnect, the collector will cancel collection.
96
+
97
+ schema_error: If the proxy receives a message with a schema error, the collector will cancel collection, if the the invalid message has the correct message type.
98
+
99
+ ### NotAck
100
+ A typical scenaria is that you send a command or status request, and want to collect the response. But if the original message is rejected by the site, you will received a NotAck instead of a reply. The collector classes can handle this, as long as you provide the message id of the original request in the `m_id` key of teh options when you construct the collector.
101
+
102
+ If a NotAck is received with a matching `oMId` (original message id), the collection is cancelled.
103
+
104
+ ## StatusCollector
105
+ Waits for a set of status criteria to be met.
106
+
107
+ Note that a single RSMP status message can contain multiple status items. Unlike MessageCollector, a StatusCollector therefore operates on items, rather than messages, and you can't specify a number of messages to collect.
108
+
109
+
110
+ ### Criteria
111
+ You construct a StatusCollector with set of criteria, specifying the status codes, names, and optionally values that must be met.
112
+
113
+ ### Collecting
114
+ When you start collection, it will complete once all criteria are all fulfilled, the timeout is reached or a custom filtering block aborts the collection.
115
+
116
+ ```ruby
117
+ collector = StatusCollector.new(options)
118
+ result = matcher.collect(timeout: 5)
119
+ ```
120
+
121
+ ### Custom filtering
122
+ You can use a block to do extra filtering. The block will be called for each individual status item that fulfils all criteria, like status code and name, component, etc.
123
+
124
+ Like with MessageCollector, the block must return a hash specifing whether to keep the message and whether to continue collection.
125
+
126
+ ```ruby
127
+ matcher = StatusCollector.new(options)
128
+ result = matcher.collect(options) do |message,item|
129
+ next(:keep) if good_item?(item) # keep item
130
+ end
131
+ ```
132
+
133
+ ## Subscribing to status updates
134
+ The method `subscribe_to_status` can be used to subscribe to one of more status messages.
135
+
136
+ ### Without collection
137
+ The simple form sends an RSMP status subscription message to the site and then returns immediatly. To collect incoming status messages, you need to manually use e.g. a Collector.
138
+
139
+ A hash is returned, with `:sent` containing the send subscription messages.
140
+
141
+ ```ruby
142
+ options = {
143
+ list: [{'sCI'=>'S0001','n'=>'signalgroupstatus'}],
144
+ }
145
+ result = subscribe_to_status(options)
146
+ result.keys => # [:sent]
147
+ ```
148
+
149
+ Note: If you want to use this simple form and manually collect responses, it's best to start collection in an asyncronous task _before_ you subscribe, to make sure you don't miss early responses:
150
+
151
+ ```ruby
152
+ task = async do
153
+ MessageCollector.new(options).collect(num: 5, timeout:10) # start listening for status messages
154
+ end
155
+ result = subscribe_to_status(options) # subscribe
156
+ task.wait # wait for collection task to complete (or time out)
157
+ ```
158
+
159
+ ### With collection
160
+ If you provide `:collect` options, it will be used to construct a StatusUpdateCollector for collecting the relevant status messages. When collection completes the collector is returned in the `:collector` key:
161
+
162
+ ```ruby
163
+ options = {
164
+ list: [{'sCI'=>'S0001','n'=>'signalgroupstatus'}],
165
+ collect: {timeout: 5}
166
+ }
167
+ result = subscribe_to_status(options)
168
+ result.keys => # [:sent, :collector]
169
+ result[:collector].messages # => list of collected messages
170
+ ```
171
+
172
+ You can pass you own collector which will give you more control of how to collect the incoming status messages:
173
+
174
+ ```ruby
175
+ collector = Collector.new(options)
176
+ options = {collect: collector}
177
+ result = subscribe_to_status(options)
178
+ result.keys => # [:sent, :collector]
179
+ result[:collector].messages # => list of collected messages
180
+ ```
181
+
182
+ ### Processing responses
183
+ If you pass a block, the block will be used to construct a collector. The block will be called for each matching status item received.
184
+ Collection will continue until the block returns :cancel, or it times.
185
+
186
+ ```ruby
187
+ options = {
188
+ list: [{'sCI'=>'S0001','n'=>'signalgroupstatus'}]
189
+ }
190
+ result = subscribe_to_status(options) do |message|
191
+ # do something with message
192
+ :keep # or not
193
+ end
194
+ result.keys => # [:sent, :collector]
195
+ ```
196
+
data/lib/rsmp/cli.rb CHANGED
@@ -3,6 +3,10 @@ require 'rsmp'
3
3
 
4
4
  module RSMP
5
5
  class CLI < Thor
6
+ desc "version", "Show version"
7
+ def version
8
+ puts RSMP::VERSION
9
+ end
6
10
 
7
11
  desc "site", "Run RSMP site"
8
12
  method_option :config, :type => :string, :aliases => "-c", banner: 'Path to .yaml config file'
@@ -0,0 +1,9 @@
1
+ module RSMP
2
+ # Class for waiting for an aggregated status response
3
+ class AggregatedStatusCollector < Collector
4
+ def initialize proxy, options={}
5
+ required = { type: ['AggregatedStatus','MessageNotAck'], title: 'aggregated status' }
6
+ super proxy, options.merge(required)
7
+ end
8
+ end
9
+ end
@@ -1,9 +1,10 @@
1
1
  module RSMP
2
2
 
3
- # Collects ingoing and/or outgoing messages from a notifier.
4
- # Can filter by message type and wakes up the client once the desired number of messages has been collected.
3
+ # Collects messages from a notifier.
4
+ # Can filter by message type, componet and direction.
5
+ # Wakes up the once the desired number of messages has been collected.
5
6
  class Collector < Listener
6
- attr_reader :condition, :messages, :done
7
+ attr_reader :condition, :messages, :status, :error, :task
7
8
 
8
9
  def initialize proxy, options={}
9
10
  super proxy, options
@@ -17,16 +18,51 @@ module RSMP
17
18
  @outgoing = options[:outgoing] == nil ? false : options[:outgoing]
18
19
  @condition = Async::Notification.new
19
20
  @title = options[:title] || [@options[:type]].flatten.join('/')
20
- @options[:timeout] ||= 1
21
- @options[:num] ||= 1
21
+ @task = options[:task]
22
22
  reset
23
23
  end
24
24
 
25
+ def use_task task
26
+ @task = task
27
+ end
28
+
29
+ # Clear all query results
30
+ def reset
31
+ @messages = []
32
+ @error = nil
33
+ @status = :ready
34
+ end
35
+
25
36
  # Inspect formatter that shows the message we have collected
26
37
  def inspect
27
38
  "#<#{self.class.name}:#{self.object_id}, #{inspector(:@messages)}>"
28
39
  end
29
40
 
41
+ # Is collection active?
42
+ def collecting?
43
+ @status == :collecting
44
+ end
45
+
46
+ # Is collection active?
47
+ def ok?
48
+ @status == :ok
49
+ end
50
+
51
+ # Has collection time out?
52
+ def timeout?
53
+ @status == :timeout
54
+ end
55
+
56
+ # Is collection ready to start?
57
+ def ready?
58
+ @status == :ready
59
+ end
60
+
61
+ # Has collection been cancelled?
62
+ def cancelled?
63
+ @status == :cancelled
64
+ end
65
+
30
66
  # Want ingoing messages?
31
67
  def ingoing?
32
68
  @ingoing == true
@@ -37,90 +73,105 @@ module RSMP
37
73
  @outgoing == true
38
74
  end
39
75
 
40
- # Block until all messages have been collected
41
- def wait
42
- @condition.wait
76
+ # Collect message
77
+ # Will return once all messages have been collected, or timeout is reached
78
+ def collect &block
79
+ start &block
80
+ wait
81
+ @status
82
+ ensure
83
+ @notifier.remove_listener self
43
84
  end
44
85
 
45
86
  # Collect message
46
- # Will block until all messages have been collected,
47
- # or we time out
48
- def collect task, options={}, &block
49
- @options.merge! options
50
- @block = block
51
- unless @done
52
- listen do
53
- task.with_timeout(@options[:timeout]) do
54
- @condition.wait
55
- end
56
- end
87
+ # Returns the collected messages, or raise an exception in case of a time out.
88
+ def collect! &block
89
+ if collect(&block) == :timeout
90
+ raise RSMP::TimeoutError.new describe_progress
57
91
  end
58
- return @error if @error
59
- self
60
- rescue Async::TimeoutError
61
- str = "#{@title.capitalize} collection"
62
- str << " in response to #{options[:m_id]}" if options[:m_id]
63
- str << " didn't complete within #{@options[:timeout]}s"
64
- reached = progress
65
- str << ", reached #{progress[:reached]}/#{progress[:need]}"
66
- raise RSMP::TimeoutError.new str
92
+ @messages
67
93
  end
68
94
 
69
- # Return progress as collected vs. number requested
70
- def progress
71
- need = @options[:num]
72
- reached = @messages.size
73
- { need: need, got: reached }
95
+ # If collection is not active, return status immeditatly. Otherwise wait until
96
+ # the desired messages have been collected, or timeout is reached.
97
+ def wait
98
+ if collecting?
99
+ if @options[:timeout]
100
+ @task.with_timeout(@options[:timeout]) { @condition.wait }
101
+ else
102
+ @condition.wait
103
+ end
104
+ end
105
+ @status
106
+ rescue Async::TimeoutError
107
+ @status = :timeout
74
108
  end
75
109
 
76
- # Get the collected message.
77
- def message
78
- @messages.first
110
+ # If collection is not active, raise an error. Otherwise wait until
111
+ # the desired messages have been collected.
112
+ # If timeout is reached, an exceptioin is raised.
113
+ def wait!
114
+ wait
115
+ raise RSMP::TimeoutError.new(describe_progress) if timeout?
116
+ @messages
79
117
  end
80
118
 
81
- # Get the collected messages.
82
- def messages
83
- @messages
119
+ # Start collection and return immediately
120
+ # You can later use wait() to wait for completion
121
+ def start &block
122
+ raise RuntimeError.new("Can't begin unless ready (currenty #{@status})") unless ready?
123
+ @block = block
124
+ raise ArgumentError.new("Num, timeout or block must be provided") unless @options[:num] || @options[:timeout] || @block
125
+ reset
126
+ @status = :collecting
127
+ @notifier.add_listener self if @notifier
84
128
  end
85
129
 
86
- # Clear all query results
87
- def reset
88
- @messages = []
89
- @error = nil
90
- @done = false
130
+ # Build a string describing how how progress reached before timeout
131
+ def describe_progress
132
+ str = "#{@title.capitalize} collection "
133
+ str << "in response to #{@options[:m_id]} " if @options[:m_id]
134
+ str << "didn't complete within #{@options[:timeout]}s, "
135
+ str << "reached #{@messages.size}/#{@options[:num]}"
136
+ str
91
137
  end
92
138
 
93
139
  # Check if we receive a NotAck related to initiating request, identified by @m_id.
94
- def check_not_ack message
140
+ def reject_not_ack message
95
141
  return unless @options[:m_id]
96
142
  if message.is_a?(MessageNotAck)
97
143
  if message.attribute('oMId') == @options[:m_id]
98
144
  m_id_short = RSMP::Message.shorten_m_id @options[:m_id], 8
99
- @error = RSMP::MessageRejected.new("#{@title} #{m_id_short} was rejected with '#{message.attribute('rea')}'")
100
- complete
145
+ cancel RSMP::MessageRejected.new("#{@title} #{m_id_short} was rejected with '#{message.attribute('rea')}'")
146
+ true
101
147
  end
102
- false
103
148
  end
104
149
  end
105
150
 
106
151
  # Handle message. and return true when we're done collecting
107
152
  def notify message
108
153
  raise ArgumentError unless message
109
- raise RuntimeError.new("can't process message when already done") if @done
110
- check_not_ack(message)
111
- return true if @done
112
- check_match message
113
- complete if done?
114
- @done
154
+ raise RuntimeError.new("can't process message when done") unless ready? || collecting?
155
+ unless reject_not_ack(message)
156
+ perform_match message
157
+ end
158
+ @status
115
159
  end
116
160
 
117
161
  # Match message against our collection criteria
118
- def check_match message
119
- matched = match? message
120
- if matched == true
162
+ def perform_match message
163
+ return unless type_match?(message)
164
+ if @block
165
+ status = [@block.call(message)].flatten
166
+ keep message if status.include?(:keep)
167
+ if status.include?(:cancel)
168
+ cancel('Cancelled by block')
169
+ else
170
+ complete if done?
171
+ end
172
+ else
121
173
  keep message
122
- elsif matched == false
123
- forget message
174
+ complete if done?
124
175
  end
125
176
  end
126
177
 
@@ -132,8 +183,14 @@ module RSMP
132
183
  # Called when we're done collecting. Remove ourself as a listener,
133
184
  # se we don't receive message notifications anymore
134
185
  def complete
135
- @done = true
136
- @proxy.remove_listener self
186
+ @status = :ok
187
+ do_stop
188
+ end
189
+
190
+ # Remove ourself as a listener, so we don't receive message notifications anymore,
191
+ # and wake up the async condition
192
+ def do_stop
193
+ @notifier.remove_listener self
137
194
  @condition.signal
138
195
  end
139
196
 
@@ -143,7 +200,7 @@ module RSMP
143
200
  case error
144
201
  when RSMP::SchemaError
145
202
  notify_schema_error error, options
146
- when RSMP::ConnectionError
203
+ when RSMP::DisconnectError
147
204
  notify_disconnect error, options
148
205
  end
149
206
  end
@@ -154,24 +211,23 @@ module RSMP
154
211
  message = options[:message]
155
212
  return unless message
156
213
  klass = message.class.name.split('::').last
157
- return unless [@options[:type]].flatten.include? klass
158
- @proxy.log "Collect cancelled due to schema error in #{klass} #{message.m_id_short}", level: :debug
214
+ return unless @options[:type] == nil || [@options[:type]].flatten.include?(klass)
215
+ @notifier.log "Collection cancelled due to schema error in #{klass} #{message.m_id_short}", level: :debug
159
216
  cancel error
160
217
  end
161
218
 
162
219
  # Cancel if we received e notificaiton about a disconnect
163
220
  def notify_disconnect error, options
164
221
  return unless @options.dig(:cancel,:disconnect)
165
- @proxy.log "Collect cancelled due to a connection error: #{error.to_s}", level: :debug
222
+ @notifier.log "Collection cancelled due to a connection error: #{error.to_s}", level: :debug
166
223
  cancel error
167
224
  end
168
225
 
169
226
  # Abort collection
170
227
  def cancel error
171
- @error = error if error
172
- @done = false
173
- @proxy.remove_listener self
174
- @condition.signal
228
+ @error = error
229
+ @status = :cancelled
230
+ do_stop
175
231
  end
176
232
 
177
233
  # Store a message in the result array
@@ -179,30 +235,20 @@ module RSMP
179
235
  @messages << message
180
236
  end
181
237
 
182
- # Remove a message from the result array
183
- def forget message
184
- @messages.delete message
185
- end
186
-
187
238
  # Check a message against our match criteria
188
- # Return true if there's a match
189
- def match? message
190
- raise ArgumentError unless message
191
- return if message.direction == :in && @ingoing == false
192
- return if message.direction == :out && @outgoing == false
239
+ # Return true if there's a match, false if not
240
+ def type_match? message
241
+ return false if message.direction == :in && @ingoing == false
242
+ return false if message.direction == :out && @outgoing == false
193
243
  if @options[:type]
194
- return if message == nil
195
244
  if @options[:type].is_a? Array
196
- return unless @options[:type].include? message.type
245
+ return false unless @options[:type].include? message.type
197
246
  else
198
- return unless message.type == @options[:type]
247
+ return false unless message.type == @options[:type]
199
248
  end
200
249
  end
201
250
  if @options[:component]
202
- return if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
203
- end
204
- if @block
205
- return if @block.call(message) == false
251
+ return false if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
206
252
  end
207
253
  true
208
254
  end
@@ -0,0 +1,16 @@
1
+ module RSMP
2
+ # Match a specific command responses
3
+ class CommandQuery < Query
4
+ # Match a return value item against a query
5
+ def match? item
6
+ return nil if @want['cCI'] && @want['cCI'] != item['cCI']
7
+ return nil if @want['n'] && @want['n'] != item['n']
8
+ if @want['v'].is_a? Regexp
9
+ return false if @want['v'] && item['v'] !~ @want['v']
10
+ else
11
+ return false if @want['v'] && item['v'] != @want['v']
12
+ end
13
+ true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ module RSMP
2
+ # Class for waiting for specific command responses
3
+ class CommandResponseCollector < StateCollector
4
+ def initialize proxy, want, options={}
5
+ super proxy, want, options.merge(
6
+ type: ['CommandResponse','MessageNotAck'],
7
+ title:'command response'
8
+ )
9
+ end
10
+
11
+ def build_query want
12
+ CommandQuery.new want
13
+ end
14
+
15
+ # Get items, in our case the return values
16
+ def get_items message
17
+ message.attributes['rvs']
18
+ end
19
+ end
20
+ end
@@ -5,8 +5,13 @@ module RSMP
5
5
  class Listener
6
6
  include Inspect
7
7
 
8
- def initialize proxy, options={}
9
- @proxy = proxy
8
+ def initialize notifier, options={}
9
+ @notifier = notifier
10
+ end
11
+
12
+ def change_notifier notifier
13
+ @notifier.remove_listener self if @notifier
14
+ @notifier = notifier
10
15
  end
11
16
 
12
17
  def notify message
@@ -16,10 +21,10 @@ module RSMP
16
21
  end
17
22
 
18
23
  def listen &block
19
- @proxy.add_listener self
24
+ @notifier.add_listener self
20
25
  yield
21
26
  ensure
22
- @proxy.remove_listener self
27
+ @notifier.remove_listener self
23
28
  end
24
29
 
25
30
  end
@@ -1,64 +0,0 @@
1
- module RSMP
2
- # Class for waiting for specific command responses
3
- class CommandResponseMatcher < Matcher
4
- def initialize proxy, want, options={}
5
- super proxy, want, options.merge(
6
- type: ['CommandResponse','MessageNotAck'],
7
- title:'command response'
8
- )
9
- end
10
-
11
- def build_query want
12
- CommandQuery.new want
13
- end
14
-
15
- # Get items, in our case the return values
16
- def get_items message
17
- message.attributes['rvs']
18
- end
19
- end
20
-
21
- # Base class for waiting for status updates or responses
22
- class StatusUpdateOrResponseMatcher < Matcher
23
- def initialize proxy, want, options={}
24
- super proxy, want, options.merge
25
- end
26
-
27
- def build_query want
28
- StatusQuery.new want
29
- end
30
-
31
- # Get items, in our case status values
32
- def get_items message
33
- message.attributes['sS']
34
- end
35
- end
36
-
37
- # Class for waiting for specific status responses
38
- class StatusResponseMatcher < StatusUpdateOrResponseMatcher
39
- def initialize proxy, want, options={}
40
- super proxy, want, options.merge(
41
- type: ['StatusResponse','MessageNotAck'],
42
- title: 'status response'
43
- )
44
- end
45
- end
46
-
47
- # Class for waiting for specific status responses
48
- class StatusUpdateMatcher < StatusUpdateOrResponseMatcher
49
- def initialize proxy, want, options={}
50
- super proxy, want, options.merge(
51
- type: ['StatusUpdate','MessageNotAck'],
52
- title:'status update'
53
- )
54
- end
55
- end
56
-
57
- # Class for waiting for an aggregated status response
58
- class AggregatedStatusMatcher < Collector
59
- def initialize proxy, options={}
60
- required = { type: ['AggregatedStatus','MessageNotAck'], title: 'aggregated status' }
61
- super proxy, options.merge(required)
62
- end
63
- end
64
- end
@@ -1,5 +1,5 @@
1
1
  module RSMP
2
-
2
+
3
3
  # Class that matches a single status or command item
4
4
  class Query
5
5
  attr_reader :want, :got, :message
@@ -18,7 +18,7 @@ module RSMP
18
18
 
19
19
  # Check an item and set @done to true if it matches
20
20
  # Store the item and corresponding message if there's a positive or negative match
21
- def check_match item, message
21
+ def perform_match item, message
22
22
  matched = match? item
23
23
  if matched != nil
24
24
  @message = message
@@ -31,15 +31,14 @@ module RSMP
31
31
  # {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>/\d+/} =>
32
32
  # { <StatusResponse message>, {"cCI"=>"M0104", "cO"=>"setDate", "n"=>"month", "v"=>"9"} }
33
33
  # }
34
-
35
- class Matcher < Collector
34
+ class StateCollector < Collector
36
35
  attr_reader :queries
37
36
 
38
37
  # Initialize with a list of wanted statuses
39
38
  def initialize proxy, want, options={}
39
+ raise ArgumentError.new("num option cannot be used") if options[:num]
40
40
  super proxy, options.merge( ingoing: true, outgoing: false)
41
41
  @queries = want.map { |item| build_query item }
42
- @want = want
43
42
  end
44
43
 
45
44
  # Build a query object.
@@ -60,11 +59,6 @@ module RSMP
60
59
  @queries.map { |query| query.got }.compact
61
60
  end
62
61
 
63
- # get the first message. Useful when you only collect one mesage
64
- def message
65
- @queries.first.message
66
- end
67
-
68
62
  # Get messages from results
69
63
  def messages
70
64
  @queries.map { |query| query.message }.uniq
@@ -77,14 +71,14 @@ module RSMP
77
71
  { need: need, reached: reached }
78
72
  end
79
73
 
80
- # Are there queries left to match?
74
+ # Are there queries left to type_match?
81
75
  def done?
82
76
  @queries.all? { |query| query.done? }
83
77
  end
84
78
 
85
79
  # Get a simplified hash of queries, with values set to either true or false,
86
80
  # indicating which queries have been matched.
87
- def status
81
+ def query_status
88
82
  @queries.map { |query| [query.want, query.done?] }.to_h
89
83
  end
90
84
 
@@ -95,19 +89,23 @@ module RSMP
95
89
 
96
90
  # Check if a messages matches our criteria.
97
91
  # Match each query against each item in the message
98
- def check_match message
99
- return unless match?(message)
92
+ def perform_match message
93
+ return unless type_match?(message)
100
94
  @queries.each do |query| # look through queries
101
95
  get_items(message).each do |item| # look through items in message
102
- matched = query.check_match(item,message)
96
+ matched = query.perform_match(item,message)
97
+ if matched == true
98
+ matched = @block.call(message,item) if @block
99
+ end
103
100
  if matched != nil
104
101
  type = {true=>'match',false=>'mismatch'}[matched]
105
- @proxy.log "#{@title.capitalize} #{message.m_id_short} collect #{type} #{query.want}, item #{item}", level: :debug
102
+ @notifier.log "#{@title.capitalize} #{message.m_id_short} collect #{type} #{query.want}, item #{item}", level: :debug
106
103
  break
107
104
  end
108
105
  end
109
106
  end
110
- @proxy.log "#{@title.capitalize} collect reached #{summary}", level: :debug
107
+ complete if done?
108
+ @notifier.log "#{@title.capitalize} collect reached #{summary}", level: :debug
111
109
  end
112
110
  end
113
111
  end
@@ -1,20 +1,5 @@
1
1
  module RSMP
2
- # Match a specific command responses
3
- class CommandQuery < Query
4
- # Match a return value item against a query
5
- def match? item
6
- return nil if @want['cCI'] && @want['cCI'] != item['cCI']
7
- return nil if @want['n'] && @want['n'] != item['n']
8
- if @want['v'].is_a? Regexp
9
- return false if @want['v'] && item['v'] !~ @want['v']
10
- else
11
- return false if @want['v'] && item['v'] != @want['v']
12
- end
13
- true
14
- end
15
- end
16
-
17
- # Match a specific status response or update
2
+ # Match a specific status response or update
18
3
  class StatusQuery < Query
19
4
  # Match a status value against a query
20
5
  def match? item
@@ -0,0 +1,11 @@
1
+ module RSMP
2
+ # Class for waiting for specific status responses
3
+ class StatusResponseCollector < StatusUpdateOrResponseCollector
4
+ def initialize proxy, want, options={}
5
+ super proxy, want, options.merge(
6
+ type: ['StatusResponse','MessageNotAck'],
7
+ title: 'status response'
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module RSMP
2
+ # Class for waiting for specific status responses
3
+ class StatusUpdateCollector < StatusUpdateOrResponseCollector
4
+ def initialize proxy, want, options={}
5
+ super proxy, want, options.merge(
6
+ type: ['StatusUpdate','MessageNotAck'],
7
+ title:'status update'
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module RSMP
2
+ # Base class for waiting for status updates or responses
3
+ class StatusUpdateOrResponseCollector < StateCollector
4
+ def initialize proxy, want, options={}
5
+ super proxy, want, options.merge
6
+ end
7
+
8
+ def build_query want
9
+ RSMP::StatusQuery.new want
10
+ end
11
+
12
+ # Get items, in our case status values
13
+ def get_items message
14
+ message.attributes['sS']
15
+ end
16
+ end
17
+ end
data/lib/rsmp/error.rb CHANGED
@@ -41,7 +41,7 @@ module RSMP
41
41
  class TimeoutError < Error
42
42
  end
43
43
 
44
- class DisonnectError < Error
44
+ class DisconnectError < Error
45
45
  end
46
46
 
47
47
  class ConnectionError < Error
data/lib/rsmp/node.rb CHANGED
@@ -6,7 +6,7 @@ module RSMP
6
6
  include Wait
7
7
  include Inspect
8
8
 
9
- attr_reader :archive, :logger, :task, :deferred, :error_queue, :clock
9
+ attr_reader :archive, :logger, :task, :deferred, :error_queue, :clock, :collector
10
10
 
11
11
  def initialize options
12
12
  initialize_logging options
@@ -15,6 +15,8 @@ module RSMP
15
15
  @clock = Clock.new
16
16
  @error_queue = Async::Queue.new
17
17
  @ignore_errors = []
18
+ options[:collector]
19
+ @collect = options[:collect]
18
20
  end
19
21
 
20
22
  def ignore_errors classes, &block
data/lib/rsmp/proxy.rb CHANGED
@@ -15,9 +15,8 @@ module RSMP
15
15
 
16
16
  def initialize options
17
17
  initialize_logging options
18
- setup options
19
18
  initialize_distributor
20
- prepare_collection @settings['collect']
19
+ setup options
21
20
  clear
22
21
  end
23
22
 
@@ -37,6 +36,10 @@ module RSMP
37
36
  @sxl = nil
38
37
  @site_settings = nil # can't pick until we know the site id
39
38
  @state = :stopped
39
+ if options[:collect]
40
+ @collector = RSMP::Collector.new self, options[:collect]
41
+ @collector.start
42
+ end
40
43
  end
41
44
 
42
45
  def inspect
@@ -49,16 +52,9 @@ module RSMP
49
52
  node.clock
50
53
  end
51
54
 
52
- def prepare_collection num
53
- if num
54
- @collector = RSMP::Collector.new self, num: num, ingoing: true, outgoing: true
55
- add_listener @collector
56
- end
57
- end
58
-
59
- def collect task, options, &block
60
- collector = RSMP::Collector.new self, options
61
- collector.collect task, &block
55
+ def collect options, &block
56
+ collector = RSMP::Collector.new self, options.merge(task: @task)
57
+ collector.collect &block
62
58
  collector
63
59
  end
64
60
 
@@ -86,7 +82,7 @@ module RSMP
86
82
  return if @state == :stopped
87
83
  set_state :stopping
88
84
  stop_tasks
89
- notify_error DisonnectError.new("Connection was closed")
85
+ notify_error DisconnectError.new("Connection was closed")
90
86
  ensure
91
87
  close_socket
92
88
  clear
@@ -101,15 +101,8 @@ module RSMP
101
101
  "cId" => component,
102
102
  "mId" => m_id
103
103
  })
104
- if options[:collect]
105
- task = @task.async do |task|
106
- collect_aggregated_status task, options[:collect].merge(m_id: m_id)
107
- end
108
- send_message message, validate: options[:validate]
109
- return message, task.wait
110
- else
111
- send_message message, validate: options[:validate]
112
- message
104
+ send_and_collect_if_needed message, options do |task|
105
+ collect_aggregated_status task, options[:collect].merge(m_id: m_id, num:1)
113
106
  end
114
107
  end
115
108
 
@@ -181,7 +174,7 @@ module RSMP
181
174
  "sS" => request_list,
182
175
  "mId" => m_id
183
176
  })
184
- send_while_collecting message, options do |task|
177
+ send_and_collect_if_needed message, options do |task|
185
178
  collect_status_responses task, status_list, options[:collect].merge(m_id: m_id)
186
179
  end
187
180
  end
@@ -193,11 +186,15 @@ module RSMP
193
186
  acknowledge message
194
187
  end
195
188
 
196
- def send_while_collecting message, options, &block
197
- task = @task.async { |task| yield task } if options[:collect]
198
- send_message message, validate: options[:validate]
199
- return message, task.wait if task
200
- message
189
+ def send_and_collect_if_needed message, options, &block
190
+ if options[:collect]
191
+ task = @task.async { |task| yield task }
192
+ send_message message, validate: options[:validate]
193
+ { sent: message, collector: task.wait }
194
+ else
195
+ send_message message, validate: options[:validate]
196
+ return { sent: message }
197
+ end
201
198
  end
202
199
 
203
200
  def subscribe_to_status component_id, status_list, options={}
@@ -218,7 +215,7 @@ module RSMP
218
215
  "sS" => subscribe_list,
219
216
  'mId' => m_id
220
217
  })
221
- send_while_collecting message, options do |task|
218
+ send_and_collect_if_needed message, options do |task|
222
219
  collect_status_updates task, status_list, options[:collect].merge(m_id: m_id)
223
220
  end
224
221
  end
@@ -264,7 +261,7 @@ module RSMP
264
261
  "arg" => command_list,
265
262
  "mId" => m_id
266
263
  })
267
- send_while_collecting message, options do |task|
264
+ send_and_collect_if_needed message, options do |task|
268
265
  collect_command_responses task, command_list, options[:collect].merge(m_id: m_id)
269
266
  end
270
267
  end
@@ -352,19 +349,27 @@ module RSMP
352
349
  end
353
350
 
354
351
  def collect_status_updates task, status_list, options
355
- StatusUpdateMatcher.new(self, status_list, options).collect task
352
+ collector = StatusUpdateCollector.new(self, status_list, options.merge(task:task))
353
+ collector.collect
354
+ collector
356
355
  end
357
356
 
358
357
  def collect_status_responses task, status_list, options
359
- StatusResponseMatcher.new(self, status_list, options).collect task
358
+ collector = StatusResponseCollector.new(self, status_list, options.merge(task:task))
359
+ collector.collect
360
+ collector
360
361
  end
361
362
 
362
363
  def collect_command_responses task, command_list, options
363
- CommandResponseMatcher.new(self, command_list, options).collect task
364
+ collector = CommandResponseCollector.new(self, command_list, options.merge(task:task))
365
+ collector.collect
366
+ collector
364
367
  end
365
368
 
366
369
  def collect_aggregated_status task, options
367
- AggregatedStatusMatcher.new(self, options).collect task
370
+ collector = AggregatedStatusCollector.new(self, options.merge(task:task))
371
+ collector.collect
372
+ collector
368
373
  end
369
374
  end
370
375
  end
@@ -7,7 +7,6 @@ module RSMP
7
7
  attr_reader :rsmp_versions, :site_id, :supervisor_settings, :proxies, :logger
8
8
 
9
9
  def initialize options={}
10
-
11
10
  handle_supervisor_settings( options[:supervisor_settings] || {} )
12
11
  super options
13
12
  @proxies = []
@@ -161,7 +160,7 @@ module RSMP
161
160
  ip: info[:ip],
162
161
  port: info[:port],
163
162
  task: @task,
164
- settings: {'collect'=>@supervisor_settings['collect']},
163
+ collect: @collect,
165
164
  socket: socket,
166
165
  stream: stream,
167
166
  protocol: protocol,
@@ -244,10 +244,13 @@ module RSMP
244
244
  @status_subscriptions[component] ||= {}
245
245
  update_list[component] ||= {}
246
246
  now = Time.now # internal timestamp
247
+ subs = @status_subscriptions[component]
247
248
 
248
249
  message.attributes["sS"].each do |arg|
249
250
  sCI = arg["sCI"]
250
251
  subcription = {interval: arg["uRt"].to_i, last_sent_at: now}
252
+ subs[sCI] ||= {}
253
+ subs[sCI][arg["n"]] = subcription
251
254
  update_list[component][sCI] ||= []
252
255
  update_list[component][sCI] << arg["n"]
253
256
  end
@@ -279,11 +282,7 @@ module RSMP
279
282
  end
280
283
 
281
284
  def fetch_last_sent_status component, code, name
282
- if @last_status_sent && @last_status_sent[component] && @last_status_sent[component][code]
283
- @last_status_sent[component][code][name]
284
- else
285
- nil
286
- end
285
+ @last_status_sent.dig component, code, name
287
286
  end
288
287
 
289
288
  def store_last_sent_status message
@@ -307,10 +306,12 @@ module RSMP
307
306
  by_code.each_pair do |code,by_name|
308
307
  by_name.each_pair do |name,subscription|
309
308
  current = nil
309
+ should_send = false
310
310
  if subscription[:interval] == 0
311
311
  # send as soon as the data changes
312
312
  if component_object
313
313
  current, age = *(component_object.get_status code, name)
314
+ current = current.to_s
314
315
  end
315
316
  last_sent = fetch_last_sent_status component, code, name
316
317
  if current != last_sent
@@ -16,7 +16,7 @@ module RSMP
16
16
  return default unless plan.states
17
17
  states = plan.states[c_id]
18
18
  return default unless states
19
- state =states[pos]
19
+ state = states[pos]
20
20
  return default unless state =~ /[a-hA-G0-9N-P]/ # valid signal group states
21
21
  state
22
22
  end
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = "0.7.3"
2
+ VERSION = "0.8.1"
3
3
  end
data/lib/rsmp.rb CHANGED
@@ -20,10 +20,15 @@ require 'rsmp/components'
20
20
  require 'rsmp/collect/notifier'
21
21
  require 'rsmp/collect/listener'
22
22
  require 'rsmp/collect/collector'
23
+ require 'rsmp/collect/state_collector'
23
24
  require 'rsmp/collect/query'
24
- require 'rsmp/collect/matcher'
25
- require 'rsmp/collect/message_queries'
26
- require 'rsmp/collect/message_matchers'
25
+ require 'rsmp/collect/status_query'
26
+ require 'rsmp/collect/command_query'
27
+ require 'rsmp/collect/status_update_or_response_collector'
28
+ require 'rsmp/collect/status_response_collector'
29
+ require 'rsmp/collect/status_update_collector'
30
+ require 'rsmp/collect/command_response_collector'
31
+ require 'rsmp/collect/aggregated_status_collector'
27
32
  require 'rsmp/component'
28
33
  require 'rsmp/site'
29
34
  require 'rsmp/proxy'
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.7.3
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-18 00:00:00.000000000 Z
11
+ date: 2022-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -189,6 +189,7 @@ files:
189
189
  - ".gitignore"
190
190
  - ".gitmodules"
191
191
  - ".rspec"
192
+ - ".ruby-version"
192
193
  - CHANGELOG.md
193
194
  - Gemfile
194
195
  - Gemfile.lock
@@ -200,18 +201,25 @@ files:
200
201
  - config/supervisor.yaml
201
202
  - config/tlc.yaml
202
203
  - documentation/classes_and_modules.md
204
+ - documentation/collecting_message.md
203
205
  - documentation/message_distribution.md
204
206
  - exe/rsmp
205
207
  - lib/rsmp.rb
206
208
  - lib/rsmp/archive.rb
207
209
  - lib/rsmp/cli.rb
210
+ - lib/rsmp/collect/aggregated_status_collector.rb
208
211
  - lib/rsmp/collect/collector.rb
212
+ - lib/rsmp/collect/command_query.rb
213
+ - lib/rsmp/collect/command_response_collector.rb
209
214
  - lib/rsmp/collect/listener.rb
210
- - lib/rsmp/collect/matcher.rb
211
215
  - lib/rsmp/collect/message_matchers.rb
212
- - lib/rsmp/collect/message_queries.rb
213
216
  - lib/rsmp/collect/notifier.rb
214
217
  - lib/rsmp/collect/query.rb
218
+ - lib/rsmp/collect/state_collector.rb
219
+ - lib/rsmp/collect/status_query.rb
220
+ - lib/rsmp/collect/status_response_collector.rb
221
+ - lib/rsmp/collect/status_update_collector.rb
222
+ - lib/rsmp/collect/status_update_or_response_collector.rb
215
223
  - lib/rsmp/component.rb
216
224
  - lib/rsmp/components.rb
217
225
  - lib/rsmp/convert/export/json_schema.rb
@@ -262,7 +270,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
262
270
  - !ruby/object:Gem::Version
263
271
  version: '0'
264
272
  requirements: []
265
- rubygems_version: 3.2.15
273
+ rubygems_version: 3.2.32
266
274
  signing_key:
267
275
  specification_version: 4
268
276
  summary: RoadSide Message Protocol (RSMP) library.