asterism 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +222 -13
- data/lib/asterism/cruby/objects.rb +224 -0
- data/lib/asterism/cruby/ros.rb +305 -0
- data/lib/asterism/cruby/runner.rb +253 -0
- data/lib/asterism/cruby/zenoh.rb +648 -0
- data/lib/asterism/cruby.rb +6 -0
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +5 -0
- data/mrblib/asterism.rb +81 -9
- data/mrblib/future.rb +6 -1
- data/mrblib/ros.rb +185 -15
- metadata +7 -3
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
# Asterism::Zenoh with blocks, threads and Enumerators (CRuby only). The
|
|
2
|
+
# polled API of asterism-zenoh (Session, each_pending, each_reply) is the
|
|
3
|
+
# same on the boards and stays as it is; this is a layer on top of it.
|
|
4
|
+
#
|
|
5
|
+
# Asterism::Zenoh.open("tcp/192.0.2.2:7447") do |s|
|
|
6
|
+
# s.subscribe("home/**") { |sample| puts "#{sample.key} = #{sample.payload}" }
|
|
7
|
+
# s.queryable("home/pc/status") { |q| q.reply("ok") }
|
|
8
|
+
# s.put("home/pc/hello", "hi")
|
|
9
|
+
# s.get("home/**/status").each { |reply| p reply }
|
|
10
|
+
# s.subscribe("sensor/temp").each.lazy.map { _1.payload.to_f }.first(3)
|
|
11
|
+
# pub = s.publisher("home/pc/temp", encoding: "text/plain")
|
|
12
|
+
# pub.on_matching { |listening| puts "someone listens: #{listening}" }
|
|
13
|
+
# s.on_transport { |ev| puts "#{ev.zid} #{ev.kind}" }
|
|
14
|
+
# s.run # until Ctrl-C (or s.start / s.stop)
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Blocks given to subscribe, queryable and liveliness_watch run on the
|
|
18
|
+
# session's receiving thread (start) or inside run; Enumerators (each, get)
|
|
19
|
+
# wait on the thread that iterates them. See Asterism::Runner.
|
|
20
|
+
module Asterism
|
|
21
|
+
module Zenoh
|
|
22
|
+
# Pause between looks while an Enumerator waits for data (seconds).
|
|
23
|
+
ENUM_STEP = 0.002
|
|
24
|
+
|
|
25
|
+
# Sample and Reply (the received values, with kind, encoding,
|
|
26
|
+
# timestamp and the rest) are asterism-zenoh's (asterism/zenoh/values).
|
|
27
|
+
|
|
28
|
+
# A liveliness change: key of the token, and whether it appeared.
|
|
29
|
+
Liveliness = Data.define(:key, :alive) do
|
|
30
|
+
alias_method :alive?, :alive
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Added to the queries a Connection's queryable hands out: reply with
|
|
34
|
+
# only a payload answers on the queryable's own key (when it has no
|
|
35
|
+
# wildcard) instead of the query's, which may be a pattern; payloads
|
|
36
|
+
# that are not Strings are sent as to_s.
|
|
37
|
+
module QueryReply
|
|
38
|
+
def reply(*args, **kw)
|
|
39
|
+
args[-1] = args[-1].to_s unless args.empty? || args[-1].is_a?(String)
|
|
40
|
+
args.unshift(@asterism_key) if args.size == 1 && @asterism_key
|
|
41
|
+
super(*args, **kw)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# reply_del without a key: the queryable's own key, as reply.
|
|
45
|
+
def reply_del(key = nil, **kw)
|
|
46
|
+
super(key || @asterism_key, **kw)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reply_err(payload, **kw)
|
|
50
|
+
super(payload.is_a?(String) ? payload : payload.to_s, **kw)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A payload as a String (anything else is sent as to_s).
|
|
55
|
+
def self.bytes(payload)
|
|
56
|
+
payload.is_a?(String) ? payload : payload.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The queryable's key when it names one key (no wildcard).
|
|
60
|
+
def self.plain_key(key)
|
|
61
|
+
key.include?("*") || key.include?("$") ? nil : key
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.prepare_query(q, key)
|
|
65
|
+
q.extend(QueryReply)
|
|
66
|
+
q.instance_variable_set(:@asterism_key, key)
|
|
67
|
+
q
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Opens a session (the arguments of Session.open) and wraps it in a
|
|
71
|
+
# Connection. With a block: yields it, closes it when the block ends
|
|
72
|
+
# (also on an exception) and returns the block's value.
|
|
73
|
+
def self.open(locator = nil, interval: Runner::DEFAULT_INTERVAL, **opts)
|
|
74
|
+
conn = Connection.new(Session.open(locator, **opts), interval: interval)
|
|
75
|
+
return conn unless block_given?
|
|
76
|
+
begin
|
|
77
|
+
yield conn
|
|
78
|
+
ensure
|
|
79
|
+
conn.close
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Seconds (Float) or nil -> milliseconds for the polled API.
|
|
84
|
+
def self.ms(seconds, default_ms)
|
|
85
|
+
return default_ms if seconds.nil?
|
|
86
|
+
(seconds * 1000).round
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Waits on the calling thread until a sample comes into sub (an
|
|
90
|
+
# Asterism::Zenoh::Subscriber or LivelinessWatch), yielding each.
|
|
91
|
+
# Ends when sub or the session closes, or after timeout seconds.
|
|
92
|
+
def self.drain(sub, session, timeout, take: :each_pending)
|
|
93
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
94
|
+
loop do
|
|
95
|
+
got = sub.public_send(take)
|
|
96
|
+
got.each { |e| yield e }
|
|
97
|
+
break if sub.closed? || session.closed?
|
|
98
|
+
break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
99
|
+
sleep(ENUM_STEP) if got.empty?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# A session with the block API. The polled calls of the session are
|
|
104
|
+
# still there (session, or the methods without a block).
|
|
105
|
+
class Connection
|
|
106
|
+
attr_reader :session, :runner
|
|
107
|
+
|
|
108
|
+
def initialize(session, interval: Runner::DEFAULT_INTERVAL)
|
|
109
|
+
@session = session
|
|
110
|
+
@runner = Runner.new(session, interval: interval, name: "zenoh")
|
|
111
|
+
# The first task: false (the runner stops) once the session closed.
|
|
112
|
+
@runner.add { @session.poll ? 0 : false }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# opts: those of Session#put (attachment:, encoding:, priority:,
|
|
116
|
+
# congestion_control:, express:, reliability:, timestamp:,
|
|
117
|
+
# allowed_destination:). A payload that is not a String is sent as
|
|
118
|
+
# to_s.
|
|
119
|
+
def put(key, payload, **opts)
|
|
120
|
+
@session.put(key, Zenoh.bytes(payload), **opts)
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Deletes key (subscribers get a Sample of kind :delete). opts: those
|
|
125
|
+
# of Session#delete.
|
|
126
|
+
def delete(key, **opts)
|
|
127
|
+
@session.delete(key, **opts)
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A declared publisher (Connection::Publisher): put / delete,
|
|
132
|
+
# matching?, on_matching { |listening| }. opts: those of
|
|
133
|
+
# Session#publisher (encoding:, priority:, ...).
|
|
134
|
+
def publisher(key, **opts)
|
|
135
|
+
Publisher.new(self, @session.publisher(key, **opts), key)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# An advanced publisher (cache:, sample_miss_detection:,
|
|
139
|
+
# publisher_detection: and the publisher options): late subscribers
|
|
140
|
+
# with history get what it kept. Same object as publisher.
|
|
141
|
+
def advanced_publisher(key, **opts)
|
|
142
|
+
Publisher.new(self, @session.advanced_publisher(key, **opts), key)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# An advanced subscriber (history:, recovery:, subscriber_detection:,
|
|
146
|
+
# query_timeout_ms:): like subscribe, plus on_publisher and on_miss.
|
|
147
|
+
def advanced_subscriber(key, depth: 16, **opts, &blk)
|
|
148
|
+
sub = AdvancedSubscription.new(self, @session.advanced_subscriber(key, depth, **opts), key)
|
|
149
|
+
sub.handle(&blk) if blk
|
|
150
|
+
sub
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A declared get (Connection::Querier): get { |reply| }, matching?,
|
|
154
|
+
# on_matching. timeout in seconds; the rest as Session#querier.
|
|
155
|
+
def querier(key, timeout: 2.0, **opts)
|
|
156
|
+
Querier.new(self, @session.querier(key, timeout_ms: Zenoh.ms(timeout, 2000), **opts), key)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# on_transport(history: false) { |event| }: a TransportEvent (kind
|
|
160
|
+
# :added / :removed, zid, whatami) on the receiving thread each time a
|
|
161
|
+
# peer or router connects or goes. Returns an Events (close it).
|
|
162
|
+
def on_transport(history: false, depth: 16, &blk)
|
|
163
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
164
|
+
Events.new(self, @session.transport_events(depth, history: history), "on_transport", blk)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# on_link(history: false) { |event| }: a LinkEvent each time a link
|
|
168
|
+
# opens or closes.
|
|
169
|
+
def on_link(history: false, depth: 16, &blk)
|
|
170
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
171
|
+
Events.new(self, @session.link_events(depth, history: history), "on_link", blk)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# With a block: every sample is given to it (as a Sample) on the
|
|
175
|
+
# receiving thread; returns the Subscription (close it to stop).
|
|
176
|
+
# Without: a Subscription to take from (each, each_pending).
|
|
177
|
+
def subscribe(key, depth: 16, &blk)
|
|
178
|
+
sub = Subscription.new(self, @session.subscribe(key, depth), key)
|
|
179
|
+
sub.handle(&blk) if blk
|
|
180
|
+
sub
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# With a block: each query is given to it on the receiving thread and
|
|
184
|
+
# finished when it returns (reply in the block with q.reply). Without:
|
|
185
|
+
# a Queryable whose each yields the queries (finished after each).
|
|
186
|
+
def queryable(key, depth: 16, complete: false, &blk)
|
|
187
|
+
qa = Queryable.new(self, @session.queryable(key, depth, complete: complete), key)
|
|
188
|
+
qa.handle(&blk) if blk
|
|
189
|
+
qa
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The replies to a get: yields each Reply until every answer came or
|
|
193
|
+
# timeout (seconds) ran out, and returns their number. Without a
|
|
194
|
+
# block, an Enumerator (each iteration sends the get again).
|
|
195
|
+
# errors: true also yields the error replies (Reply#error?); opts:
|
|
196
|
+
# encoding:, priority:, congestion_control:, express:,
|
|
197
|
+
# accept_replies: (Session#get).
|
|
198
|
+
def get(key, timeout: 2.0, params: nil, payload: nil, attachment: nil, target: :all,
|
|
199
|
+
consolidation: :none, errors: false, **opts, &blk)
|
|
200
|
+
start = lambda do
|
|
201
|
+
@session.get(key, Zenoh.ms(timeout, 2000), params, payload,
|
|
202
|
+
attachment: attachment, target: target, consolidation: consolidation, **opts)
|
|
203
|
+
end
|
|
204
|
+
return enum_for(:get_each, start, errors) unless blk
|
|
205
|
+
get_each(start, errors, &blk)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# A liveliness token (close it to withdraw).
|
|
209
|
+
def liveliness(key)
|
|
210
|
+
@session.liveliness(key)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# With a block: yields key and alive (true when the token appeared) on
|
|
214
|
+
# the receiving thread for each change; the tokens alive now come
|
|
215
|
+
# first. Without: a Watch whose each yields Liveliness values.
|
|
216
|
+
def liveliness_watch(key, depth: 16, &blk)
|
|
217
|
+
w = Watch.new(self, @session.liveliness_watch(key, depth), key)
|
|
218
|
+
w.handle(&blk) if blk
|
|
219
|
+
w
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The keys of the liveliness tokens alive now (an Array; waits for
|
|
223
|
+
# the answers, at most timeout seconds).
|
|
224
|
+
def liveliness_get(key, timeout: 2.0)
|
|
225
|
+
g = @session.liveliness_get(key, Zenoh.ms(timeout, 2000))
|
|
226
|
+
keys = []
|
|
227
|
+
loop do
|
|
228
|
+
g.each_reply.each { |r| keys << r[0] }
|
|
229
|
+
break if g.done?
|
|
230
|
+
sleep(ENUM_STEP)
|
|
231
|
+
end
|
|
232
|
+
g.each_reply.each { |r| keys << r[0] }
|
|
233
|
+
keys
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Receives on a thread of its own (blocks run there). Returns self.
|
|
237
|
+
def start
|
|
238
|
+
@runner.start
|
|
239
|
+
self
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Stops the receiving thread (or ends run).
|
|
243
|
+
def stop
|
|
244
|
+
@runner.stop
|
|
245
|
+
self
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Receives on this thread until stop, the connection closing, or
|
|
249
|
+
# Ctrl-C. Returns nil.
|
|
250
|
+
def run
|
|
251
|
+
@runner.run
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def running?
|
|
255
|
+
@runner.running?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# on_error { |error, where| }: what the blocks raise. Without a
|
|
259
|
+
# handler it is printed (warn); the receiving goes on either way.
|
|
260
|
+
def on_error(&blk)
|
|
261
|
+
@runner.on_error(&blk)
|
|
262
|
+
self
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def poll
|
|
266
|
+
@session.poll
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def closed?
|
|
270
|
+
@session.closed?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def zid
|
|
274
|
+
@session.zid
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def peers
|
|
278
|
+
@session.peers
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# The Zenoh IDs of the peers / routers connected now.
|
|
282
|
+
def peer_zids = @session.peer_zids
|
|
283
|
+
def router_zids = @session.router_zids
|
|
284
|
+
|
|
285
|
+
# What is connected now: Arrays of Transport / Link.
|
|
286
|
+
def transports = @session.transports
|
|
287
|
+
def links = @session.links
|
|
288
|
+
|
|
289
|
+
# A new Timestamp from the session's clock (put(timestamp:)).
|
|
290
|
+
def new_timestamp = @session.new_timestamp
|
|
291
|
+
|
|
292
|
+
# A KeyExpr declared on the session (use it as the key afterwards).
|
|
293
|
+
def declare_keyexpr(key) = @session.declare_keyexpr(key)
|
|
294
|
+
|
|
295
|
+
# Stops receiving and closes the session (with everything declared on
|
|
296
|
+
# it). Idempotent.
|
|
297
|
+
def close
|
|
298
|
+
@runner.close
|
|
299
|
+
@session.close
|
|
300
|
+
nil
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def inspect
|
|
304
|
+
"#<Asterism::Zenoh::Connection #{closed? ? 'closed' : zid}#{running? ? ' running' : ''}>"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
private
|
|
308
|
+
|
|
309
|
+
# Sends the get (start) and yields each Reply; error replies only
|
|
310
|
+
# with errors. The done? before taking the replies: once the get is
|
|
311
|
+
# done, every reply it had is already in the queue.
|
|
312
|
+
def get_each(start, errors)
|
|
313
|
+
g = start.call
|
|
314
|
+
n = 0
|
|
315
|
+
loop do
|
|
316
|
+
done = g.done?
|
|
317
|
+
got = g.each_result
|
|
318
|
+
got.each do |r|
|
|
319
|
+
next if r.error? && !errors
|
|
320
|
+
n += 1
|
|
321
|
+
yield r
|
|
322
|
+
end
|
|
323
|
+
break if done && g.pending == 0
|
|
324
|
+
sleep(ENUM_STEP) if got.empty?
|
|
325
|
+
end
|
|
326
|
+
n
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# A subscription of a Connection. Either its block gets the samples
|
|
330
|
+
# (on the receiving thread), or the application takes them: each (an
|
|
331
|
+
# Enumerator that waits for samples) or each_pending (what is there
|
|
332
|
+
# now, as the polled API).
|
|
333
|
+
class Subscription
|
|
334
|
+
include Enumerable
|
|
335
|
+
|
|
336
|
+
attr_reader :key, :subscriber
|
|
337
|
+
|
|
338
|
+
def initialize(conn, subscriber, key)
|
|
339
|
+
@conn = conn
|
|
340
|
+
@subscriber = subscriber
|
|
341
|
+
@key = key
|
|
342
|
+
@task = nil
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def handle(&blk)
|
|
346
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
347
|
+
where = "subscribe #{@key}"
|
|
348
|
+
r = @conn.runner
|
|
349
|
+
@task = r.add do
|
|
350
|
+
got = @subscriber.each_sample
|
|
351
|
+
got.each { |s| r.guard(where) { blk.call(s) } }
|
|
352
|
+
got.size
|
|
353
|
+
end
|
|
354
|
+
self
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Yields each Sample as it comes, waiting for the next; ends when the
|
|
358
|
+
# subscription or the session closes, or after timeout seconds.
|
|
359
|
+
# Without a block, an Enumerator (each.lazy.map { ... }.first(3)).
|
|
360
|
+
def each(timeout: nil, &blk)
|
|
361
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
362
|
+
raise ::Asterism::Error, "this subscription has a block; its samples go there" if @task
|
|
363
|
+
Zenoh.drain(@subscriber, @conn.session, timeout, take: :each_sample) { |s| blk.call(s) }
|
|
364
|
+
self
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# The samples there now as Samples (an Array), or yields them.
|
|
368
|
+
def each_pending
|
|
369
|
+
got = @subscriber.each_sample
|
|
370
|
+
return got unless block_given?
|
|
371
|
+
got.each { |s| yield s }
|
|
372
|
+
got.size
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def pending = @subscriber.pending
|
|
376
|
+
def received = @subscriber.received
|
|
377
|
+
def dropped = @subscriber.dropped
|
|
378
|
+
def closed? = @subscriber.closed?
|
|
379
|
+
|
|
380
|
+
def close
|
|
381
|
+
@conn.runner.remove(@task) if @task
|
|
382
|
+
@task = nil
|
|
383
|
+
@subscriber.close
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def inspect
|
|
388
|
+
"#<Asterism::Zenoh::Connection::Subscription #{@key}#{@task ? ' (block)' : ''}>"
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# A queryable of a Connection: its block answers on the receiving
|
|
393
|
+
# thread, or each yields the queries (each finished after the block).
|
|
394
|
+
class Queryable
|
|
395
|
+
include Enumerable
|
|
396
|
+
|
|
397
|
+
attr_reader :key, :queryable
|
|
398
|
+
|
|
399
|
+
def initialize(conn, queryable, key)
|
|
400
|
+
@conn = conn
|
|
401
|
+
@queryable = queryable
|
|
402
|
+
@key = key
|
|
403
|
+
@task = nil
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def handle(&blk)
|
|
407
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
408
|
+
where = "queryable #{@key}"
|
|
409
|
+
own = Zenoh.plain_key(@key)
|
|
410
|
+
r = @conn.runner
|
|
411
|
+
@task = r.add do
|
|
412
|
+
qs = @queryable.each_pending
|
|
413
|
+
qs.each do |q|
|
|
414
|
+
r.guard(where) { blk.call(Zenoh.prepare_query(q, own)) }
|
|
415
|
+
ensure
|
|
416
|
+
q.finish
|
|
417
|
+
end
|
|
418
|
+
qs.size
|
|
419
|
+
end
|
|
420
|
+
self
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Yields each query (Asterism::Zenoh::Query: key, params, payload,
|
|
424
|
+
# attachment, reply; reply(payload) answers on the queryable's own
|
|
425
|
+
# key) as it comes and finishes it after the block.
|
|
426
|
+
# Ends when the queryable or the session closes, or after timeout.
|
|
427
|
+
def each(timeout: nil, &blk)
|
|
428
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
429
|
+
raise ::Asterism::Error, "this queryable has a block; its queries go there" if @task
|
|
430
|
+
own = Zenoh.plain_key(@key)
|
|
431
|
+
Zenoh.drain(@queryable, @conn.session, timeout) do |q|
|
|
432
|
+
blk.call(Zenoh.prepare_query(q, own))
|
|
433
|
+
ensure
|
|
434
|
+
q.finish
|
|
435
|
+
end
|
|
436
|
+
self
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def pending = @queryable.pending
|
|
440
|
+
def received = @queryable.received
|
|
441
|
+
def dropped = @queryable.dropped
|
|
442
|
+
def closed? = @queryable.closed?
|
|
443
|
+
|
|
444
|
+
def close
|
|
445
|
+
@conn.runner.remove(@task) if @task
|
|
446
|
+
@task = nil
|
|
447
|
+
@queryable.close
|
|
448
|
+
nil
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# A liveliness watch of a Connection.
|
|
453
|
+
class Watch
|
|
454
|
+
include Enumerable
|
|
455
|
+
|
|
456
|
+
attr_reader :key, :watch
|
|
457
|
+
|
|
458
|
+
def initialize(conn, watch, key)
|
|
459
|
+
@conn = conn
|
|
460
|
+
@watch = watch
|
|
461
|
+
@key = key
|
|
462
|
+
@task = nil
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def handle(&blk)
|
|
466
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
467
|
+
where = "liveliness_watch #{@key}"
|
|
468
|
+
r = @conn.runner
|
|
469
|
+
@task = r.add do
|
|
470
|
+
got = @watch.each_pending
|
|
471
|
+
got.each { |e| r.guard(where) { blk.call(e[0], e[1]) } }
|
|
472
|
+
got.size
|
|
473
|
+
end
|
|
474
|
+
self
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Yields each change as a Liveliness (key, alive), waiting for the
|
|
478
|
+
# next; the tokens alive now come first.
|
|
479
|
+
def each(timeout: nil, &blk)
|
|
480
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
481
|
+
raise ::Asterism::Error, "this watch has a block; its changes go there" if @task
|
|
482
|
+
Zenoh.drain(@watch, @conn.session, timeout) { |e| blk.call(Liveliness.new(e[0], e[1])) }
|
|
483
|
+
self
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def closed? = @watch.closed?
|
|
487
|
+
|
|
488
|
+
def close
|
|
489
|
+
@conn.runner.remove(@task) if @task
|
|
490
|
+
@task = nil
|
|
491
|
+
@watch.close
|
|
492
|
+
nil
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# A publisher (or advanced publisher) of a Connection.
|
|
497
|
+
class Publisher
|
|
498
|
+
attr_reader :key, :publisher
|
|
499
|
+
|
|
500
|
+
def initialize(conn, publisher, key)
|
|
501
|
+
@conn = conn
|
|
502
|
+
@publisher = publisher
|
|
503
|
+
@key = key.to_s
|
|
504
|
+
@tasks = []
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# opts: attachment:, encoding:, timestamp:. A payload that is not a
|
|
508
|
+
# String is sent as to_s.
|
|
509
|
+
def put(payload, **opts)
|
|
510
|
+
@publisher.put(Zenoh.bytes(payload), **opts)
|
|
511
|
+
nil
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def delete(**opts)
|
|
515
|
+
@publisher.delete(**opts)
|
|
516
|
+
nil
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# True when a subscriber matches the key now.
|
|
520
|
+
def matching? = @publisher.matching?
|
|
521
|
+
|
|
522
|
+
# on_matching { |listening| }: true when the first subscriber
|
|
523
|
+
# appears, false when the last one goes, on the receiving thread.
|
|
524
|
+
def on_matching(depth: 16, &blk)
|
|
525
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
526
|
+
@tasks << Events.new(@conn, @publisher.matching_listener(depth), "on_matching #{@key}", blk)
|
|
527
|
+
self
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def closed? = @publisher.closed?
|
|
531
|
+
|
|
532
|
+
def close
|
|
533
|
+
@tasks.each(&:close)
|
|
534
|
+
@tasks.clear
|
|
535
|
+
@publisher.close
|
|
536
|
+
nil
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def inspect
|
|
540
|
+
"#<Asterism::Zenoh::Connection::Publisher #{@key}>"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# A querier of a Connection: the same get, sent again and again.
|
|
545
|
+
class Querier
|
|
546
|
+
attr_reader :key, :querier
|
|
547
|
+
|
|
548
|
+
def initialize(conn, querier, key)
|
|
549
|
+
@conn = conn
|
|
550
|
+
@querier = querier
|
|
551
|
+
@key = key.to_s
|
|
552
|
+
@tasks = []
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Yields each Reply until every answer came or the querier's time
|
|
556
|
+
# ran out; returns their number. errors: true also yields the error
|
|
557
|
+
# replies. Without a block, an Enumerator.
|
|
558
|
+
def get(params: nil, payload: nil, attachment: nil, encoding: nil, errors: false, &blk)
|
|
559
|
+
start = -> { @querier.get(params, payload, attachment: attachment, encoding: encoding) }
|
|
560
|
+
return @conn.send(:enum_for, :get_each, start, errors) unless blk
|
|
561
|
+
@conn.send(:get_each, start, errors, &blk)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def matching? = @querier.matching?
|
|
565
|
+
|
|
566
|
+
# on_matching { |answered| }: true when a queryable matches, false
|
|
567
|
+
# when none does any more.
|
|
568
|
+
def on_matching(depth: 16, &blk)
|
|
569
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
570
|
+
@tasks << Events.new(@conn, @querier.matching_listener(depth), "on_matching #{@key}", blk)
|
|
571
|
+
self
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def closed? = @querier.closed?
|
|
575
|
+
|
|
576
|
+
def close
|
|
577
|
+
@tasks.each(&:close)
|
|
578
|
+
@tasks.clear
|
|
579
|
+
@querier.close
|
|
580
|
+
nil
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# A subscription with history and recovery (advanced_subscriber).
|
|
585
|
+
class AdvancedSubscription < Subscription
|
|
586
|
+
def initialize(conn, subscriber, key)
|
|
587
|
+
super
|
|
588
|
+
@extra = []
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# on_publisher { |key, alive| }: the advanced publishers on the key
|
|
592
|
+
# that declared publisher_detection, as they come and go (the ones
|
|
593
|
+
# there now first).
|
|
594
|
+
def on_publisher(depth: 16, history: true, &blk)
|
|
595
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
596
|
+
w = Watch.new(@conn, @subscriber.detect_publishers(depth, history: history), @key)
|
|
597
|
+
w.handle(&blk)
|
|
598
|
+
@extra << w
|
|
599
|
+
self
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# on_miss { |miss| }: samples that never came (Miss: source_zid,
|
|
603
|
+
# source_eid, count); needs sample_miss_detection on the publisher.
|
|
604
|
+
def on_miss(depth: 16, &blk)
|
|
605
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
606
|
+
@extra << Events.new(@conn, @subscriber.miss_listener(depth), "on_miss #{@key}", blk)
|
|
607
|
+
self
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def close
|
|
611
|
+
@extra.each(&:close)
|
|
612
|
+
@extra.clear
|
|
613
|
+
super
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def inspect
|
|
617
|
+
"#<Asterism::Zenoh::Connection::AdvancedSubscription #{@key}#{@task ? ' (block)' : ''}>"
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# The values of a listener (MatchingListener or EventListener) given
|
|
622
|
+
# to a block on the receiving thread. close stops it.
|
|
623
|
+
class Events
|
|
624
|
+
attr_reader :listener
|
|
625
|
+
|
|
626
|
+
def initialize(conn, listener, where, blk)
|
|
627
|
+
@conn = conn
|
|
628
|
+
@listener = listener
|
|
629
|
+
r = conn.runner
|
|
630
|
+
@task = r.add do
|
|
631
|
+
got = @listener.each_pending
|
|
632
|
+
got.each { |v| r.guard(where) { blk.call(v) } }
|
|
633
|
+
got.size
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def closed? = @listener.closed?
|
|
638
|
+
|
|
639
|
+
def close
|
|
640
|
+
@conn.runner.remove(@task) if @task
|
|
641
|
+
@task = nil
|
|
642
|
+
@listener.close
|
|
643
|
+
nil
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
end
|
data/lib/asterism/cruby.rb
CHANGED
|
@@ -18,6 +18,12 @@ module Asterism
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# A monotonic clock (deadlines and node.every must not jump with the
|
|
22
|
+
# wall clock).
|
|
23
|
+
def self.now_ms
|
|
24
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
25
|
+
end
|
|
26
|
+
|
|
21
27
|
def self.error_reply(klass, message)
|
|
22
28
|
::MessagePack.pack(["error", klass.to_s, message.to_s], compatibility_mode: true)
|
|
23
29
|
end
|
data/lib/asterism/version.rb
CHANGED
data/lib/asterism.rb
CHANGED
|
@@ -16,3 +16,8 @@ require_relative "asterism/version"
|
|
|
16
16
|
require_relative "../mrblib/#{f}"
|
|
17
17
|
end
|
|
18
18
|
require_relative "asterism/cruby"
|
|
19
|
+
# The Ruby-like API on top (blocks, a receiving thread, Enumerators,
|
|
20
|
+
# pattern matching): CRuby only, see README "The Ruby-like API".
|
|
21
|
+
%w[runner zenoh objects ros].each do |f|
|
|
22
|
+
require_relative "asterism/cruby/#{f}"
|
|
23
|
+
end
|