asterism 0.1.0 → 0.2.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 +189 -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 +422 -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 +182 -12
- metadata +7 -3
|
@@ -0,0 +1,422 @@
|
|
|
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
|
+
# s.run # until Ctrl-C (or s.start / s.stop)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Blocks given to subscribe, queryable and liveliness_watch run on the
|
|
15
|
+
# session's receiving thread (start) or inside run; Enumerators (each, get)
|
|
16
|
+
# wait on the thread that iterates them. See Asterism::Runner.
|
|
17
|
+
module Asterism
|
|
18
|
+
module Zenoh
|
|
19
|
+
# Pause between looks while an Enumerator waits for data (seconds).
|
|
20
|
+
ENUM_STEP = 0.002
|
|
21
|
+
|
|
22
|
+
# A received sample. payload and attachment are binary Strings (the
|
|
23
|
+
# attachment nil when there was none); text is the payload as UTF-8.
|
|
24
|
+
Sample = Data.define(:key, :payload, :attachment) do
|
|
25
|
+
def text
|
|
26
|
+
payload.dup.force_encoding(Encoding::UTF_8)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
"#{key}: #{payload}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A reply to a get: the key it was sent with, payload and attachment.
|
|
35
|
+
Reply = Data.define(:key, :payload, :attachment) do
|
|
36
|
+
def text
|
|
37
|
+
payload.dup.force_encoding(Encoding::UTF_8)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
"#{key}: #{payload}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A liveliness change: key of the token, and whether it appeared.
|
|
46
|
+
Liveliness = Data.define(:key, :alive) do
|
|
47
|
+
alias_method :alive?, :alive
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Added to the queries a Connection's queryable hands out: reply with
|
|
51
|
+
# only a payload answers on the queryable's own key (when it has no
|
|
52
|
+
# wildcard) instead of the query's, which may be a pattern; payloads
|
|
53
|
+
# that are not Strings are sent as to_s.
|
|
54
|
+
module QueryReply
|
|
55
|
+
def reply(*args, **kw)
|
|
56
|
+
args[-1] = args[-1].to_s unless args.empty? || args[-1].is_a?(String)
|
|
57
|
+
args.unshift(@asterism_key) if args.size == 1 && @asterism_key
|
|
58
|
+
super(*args, **kw)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The queryable's key when it names one key (no wildcard).
|
|
63
|
+
def self.plain_key(key)
|
|
64
|
+
key.include?("*") || key.include?("$") ? nil : key
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.prepare_query(q, key)
|
|
68
|
+
q.extend(QueryReply)
|
|
69
|
+
q.instance_variable_set(:@asterism_key, key)
|
|
70
|
+
q
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Opens a session (the arguments of Session.open) and wraps it in a
|
|
74
|
+
# Connection. With a block: yields it, closes it when the block ends
|
|
75
|
+
# (also on an exception) and returns the block's value.
|
|
76
|
+
def self.open(locator = nil, interval: Runner::DEFAULT_INTERVAL, **opts)
|
|
77
|
+
conn = Connection.new(Session.open(locator, **opts), interval: interval)
|
|
78
|
+
return conn unless block_given?
|
|
79
|
+
begin
|
|
80
|
+
yield conn
|
|
81
|
+
ensure
|
|
82
|
+
conn.close
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Seconds (Float) or nil -> milliseconds for the polled API.
|
|
87
|
+
def self.ms(seconds, default_ms)
|
|
88
|
+
return default_ms if seconds.nil?
|
|
89
|
+
(seconds * 1000).round
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Waits on the calling thread until a sample comes into sub (an
|
|
93
|
+
# Asterism::Zenoh::Subscriber or LivelinessWatch), yielding each.
|
|
94
|
+
# Ends when sub or the session closes, or after timeout seconds.
|
|
95
|
+
def self.drain(sub, session, timeout)
|
|
96
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
97
|
+
loop do
|
|
98
|
+
got = sub.each_pending
|
|
99
|
+
got.each { |e| yield e }
|
|
100
|
+
break if sub.closed? || session.closed?
|
|
101
|
+
break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
102
|
+
sleep(ENUM_STEP) if got.empty?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A session with the block API. The polled calls of the session are
|
|
107
|
+
# still there (session, or the methods without a block).
|
|
108
|
+
class Connection
|
|
109
|
+
attr_reader :session, :runner
|
|
110
|
+
|
|
111
|
+
def initialize(session, interval: Runner::DEFAULT_INTERVAL)
|
|
112
|
+
@session = session
|
|
113
|
+
@runner = Runner.new(session, interval: interval, name: "zenoh")
|
|
114
|
+
# The first task: false (the runner stops) once the session closed.
|
|
115
|
+
@runner.add { @session.poll ? 0 : false }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def put(key, payload, attachment: nil)
|
|
119
|
+
@session.put(key, payload.is_a?(String) ? payload : payload.to_s, attachment: attachment)
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# With a block: every sample is given to it (as a Sample) on the
|
|
124
|
+
# receiving thread; returns the Subscription (close it to stop).
|
|
125
|
+
# Without: a Subscription to take from (each, each_pending).
|
|
126
|
+
def subscribe(key, depth: 16, &blk)
|
|
127
|
+
sub = Subscription.new(self, @session.subscribe(key, depth), key)
|
|
128
|
+
sub.handle(&blk) if blk
|
|
129
|
+
sub
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# With a block: each query is given to it on the receiving thread and
|
|
133
|
+
# finished when it returns (reply in the block with q.reply). Without:
|
|
134
|
+
# a Queryable whose each yields the queries (finished after each).
|
|
135
|
+
def queryable(key, depth: 16, complete: false, &blk)
|
|
136
|
+
qa = Queryable.new(self, @session.queryable(key, depth, complete: complete), key)
|
|
137
|
+
qa.handle(&blk) if blk
|
|
138
|
+
qa
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The replies to a get: yields each Reply until every answer came or
|
|
142
|
+
# timeout (seconds) ran out, and returns their number. Without a
|
|
143
|
+
# block, an Enumerator (each iteration sends the get again).
|
|
144
|
+
def get(key, timeout: 2.0, params: nil, payload: nil, attachment: nil, target: :all,
|
|
145
|
+
consolidation: :none, &blk)
|
|
146
|
+
args = [key, timeout, params, payload, attachment, target, consolidation]
|
|
147
|
+
return enum_for(:get_each, *args) unless blk
|
|
148
|
+
get_each(*args, &blk)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A liveliness token (close it to withdraw).
|
|
152
|
+
def liveliness(key)
|
|
153
|
+
@session.liveliness(key)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# With a block: yields key and alive (true when the token appeared) on
|
|
157
|
+
# the receiving thread for each change; the tokens alive now come
|
|
158
|
+
# first. Without: a Watch whose each yields Liveliness values.
|
|
159
|
+
def liveliness_watch(key, depth: 16, &blk)
|
|
160
|
+
w = Watch.new(self, @session.liveliness_watch(key, depth), key)
|
|
161
|
+
w.handle(&blk) if blk
|
|
162
|
+
w
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The keys of the liveliness tokens alive now (an Array; waits for
|
|
166
|
+
# the answers, at most timeout seconds).
|
|
167
|
+
def liveliness_get(key, timeout: 2.0)
|
|
168
|
+
g = @session.liveliness_get(key, Zenoh.ms(timeout, 2000))
|
|
169
|
+
keys = []
|
|
170
|
+
loop do
|
|
171
|
+
g.each_reply.each { |r| keys << r[0] }
|
|
172
|
+
break if g.done?
|
|
173
|
+
sleep(ENUM_STEP)
|
|
174
|
+
end
|
|
175
|
+
g.each_reply.each { |r| keys << r[0] }
|
|
176
|
+
keys
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Receives on a thread of its own (blocks run there). Returns self.
|
|
180
|
+
def start
|
|
181
|
+
@runner.start
|
|
182
|
+
self
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Stops the receiving thread (or ends run).
|
|
186
|
+
def stop
|
|
187
|
+
@runner.stop
|
|
188
|
+
self
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Receives on this thread until stop, the connection closing, or
|
|
192
|
+
# Ctrl-C. Returns nil.
|
|
193
|
+
def run
|
|
194
|
+
@runner.run
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def running?
|
|
198
|
+
@runner.running?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# on_error { |error, where| }: what the blocks raise. Without a
|
|
202
|
+
# handler it is printed (warn); the receiving goes on either way.
|
|
203
|
+
def on_error(&blk)
|
|
204
|
+
@runner.on_error(&blk)
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def poll
|
|
209
|
+
@session.poll
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def closed?
|
|
213
|
+
@session.closed?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def zid
|
|
217
|
+
@session.zid
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def peers
|
|
221
|
+
@session.peers
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Stops receiving and closes the session (with everything declared on
|
|
225
|
+
# it). Idempotent.
|
|
226
|
+
def close
|
|
227
|
+
@runner.close
|
|
228
|
+
@session.close
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def inspect
|
|
233
|
+
"#<Asterism::Zenoh::Connection #{closed? ? 'closed' : zid}#{running? ? ' running' : ''}>"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def get_each(key, timeout, params, payload, attachment, target, consolidation)
|
|
239
|
+
g = @session.get(key, Zenoh.ms(timeout, 2000), params, payload,
|
|
240
|
+
attachment: attachment, target: target, consolidation: consolidation)
|
|
241
|
+
n = 0
|
|
242
|
+
loop do
|
|
243
|
+
got = g.each_reply
|
|
244
|
+
got.each do |r|
|
|
245
|
+
n += 1
|
|
246
|
+
yield Reply.new(r[0], r[1], r[2])
|
|
247
|
+
end
|
|
248
|
+
break if g.done? && g.pending == 0
|
|
249
|
+
sleep(ENUM_STEP) if got.empty?
|
|
250
|
+
end
|
|
251
|
+
n
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A subscription of a Connection. Either its block gets the samples
|
|
255
|
+
# (on the receiving thread), or the application takes them: each (an
|
|
256
|
+
# Enumerator that waits for samples) or each_pending (what is there
|
|
257
|
+
# now, as the polled API).
|
|
258
|
+
class Subscription
|
|
259
|
+
include Enumerable
|
|
260
|
+
|
|
261
|
+
attr_reader :key, :subscriber
|
|
262
|
+
|
|
263
|
+
def initialize(conn, subscriber, key)
|
|
264
|
+
@conn = conn
|
|
265
|
+
@subscriber = subscriber
|
|
266
|
+
@key = key
|
|
267
|
+
@task = nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def handle(&blk)
|
|
271
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
272
|
+
where = "subscribe #{@key}"
|
|
273
|
+
r = @conn.runner
|
|
274
|
+
@task = r.add do
|
|
275
|
+
got = @subscriber.each_pending
|
|
276
|
+
got.each { |e| r.guard(where) { blk.call(Sample.new(e[0], e[1], e[2])) } }
|
|
277
|
+
got.size
|
|
278
|
+
end
|
|
279
|
+
self
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Yields each Sample as it comes, waiting for the next; ends when the
|
|
283
|
+
# subscription or the session closes, or after timeout seconds.
|
|
284
|
+
# Without a block, an Enumerator (each.lazy.map { ... }.first(3)).
|
|
285
|
+
def each(timeout: nil, &blk)
|
|
286
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
287
|
+
raise ::Asterism::Error, "this subscription has a block; its samples go there" if @task
|
|
288
|
+
Zenoh.drain(@subscriber, @conn.session, timeout) { |e| blk.call(Sample.new(e[0], e[1], e[2])) }
|
|
289
|
+
self
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# The samples there now as Samples (an Array), or yields them.
|
|
293
|
+
def each_pending
|
|
294
|
+
got = @subscriber.each_pending.map { |e| Sample.new(e[0], e[1], e[2]) }
|
|
295
|
+
return got unless block_given?
|
|
296
|
+
got.each { |s| yield s }
|
|
297
|
+
got.size
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def pending = @subscriber.pending
|
|
301
|
+
def received = @subscriber.received
|
|
302
|
+
def dropped = @subscriber.dropped
|
|
303
|
+
def closed? = @subscriber.closed?
|
|
304
|
+
|
|
305
|
+
def close
|
|
306
|
+
@conn.runner.remove(@task) if @task
|
|
307
|
+
@task = nil
|
|
308
|
+
@subscriber.close
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def inspect
|
|
313
|
+
"#<Asterism::Zenoh::Connection::Subscription #{@key}#{@task ? ' (block)' : ''}>"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A queryable of a Connection: its block answers on the receiving
|
|
318
|
+
# thread, or each yields the queries (each finished after the block).
|
|
319
|
+
class Queryable
|
|
320
|
+
include Enumerable
|
|
321
|
+
|
|
322
|
+
attr_reader :key, :queryable
|
|
323
|
+
|
|
324
|
+
def initialize(conn, queryable, key)
|
|
325
|
+
@conn = conn
|
|
326
|
+
@queryable = queryable
|
|
327
|
+
@key = key
|
|
328
|
+
@task = nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def handle(&blk)
|
|
332
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
333
|
+
where = "queryable #{@key}"
|
|
334
|
+
own = Zenoh.plain_key(@key)
|
|
335
|
+
r = @conn.runner
|
|
336
|
+
@task = r.add do
|
|
337
|
+
qs = @queryable.each_pending
|
|
338
|
+
qs.each do |q|
|
|
339
|
+
r.guard(where) { blk.call(Zenoh.prepare_query(q, own)) }
|
|
340
|
+
ensure
|
|
341
|
+
q.finish
|
|
342
|
+
end
|
|
343
|
+
qs.size
|
|
344
|
+
end
|
|
345
|
+
self
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Yields each query (Asterism::Zenoh::Query: key, params, payload,
|
|
349
|
+
# attachment, reply; reply(payload) answers on the queryable's own
|
|
350
|
+
# key) as it comes and finishes it after the block.
|
|
351
|
+
# Ends when the queryable or the session closes, or after timeout.
|
|
352
|
+
def each(timeout: nil, &blk)
|
|
353
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
354
|
+
raise ::Asterism::Error, "this queryable has a block; its queries go there" if @task
|
|
355
|
+
own = Zenoh.plain_key(@key)
|
|
356
|
+
Zenoh.drain(@queryable, @conn.session, timeout) do |q|
|
|
357
|
+
blk.call(Zenoh.prepare_query(q, own))
|
|
358
|
+
ensure
|
|
359
|
+
q.finish
|
|
360
|
+
end
|
|
361
|
+
self
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def pending = @queryable.pending
|
|
365
|
+
def received = @queryable.received
|
|
366
|
+
def dropped = @queryable.dropped
|
|
367
|
+
def closed? = @queryable.closed?
|
|
368
|
+
|
|
369
|
+
def close
|
|
370
|
+
@conn.runner.remove(@task) if @task
|
|
371
|
+
@task = nil
|
|
372
|
+
@queryable.close
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# A liveliness watch of a Connection.
|
|
378
|
+
class Watch
|
|
379
|
+
include Enumerable
|
|
380
|
+
|
|
381
|
+
attr_reader :key, :watch
|
|
382
|
+
|
|
383
|
+
def initialize(conn, watch, key)
|
|
384
|
+
@conn = conn
|
|
385
|
+
@watch = watch
|
|
386
|
+
@key = key
|
|
387
|
+
@task = nil
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def handle(&blk)
|
|
391
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
392
|
+
where = "liveliness_watch #{@key}"
|
|
393
|
+
r = @conn.runner
|
|
394
|
+
@task = r.add do
|
|
395
|
+
got = @watch.each_pending
|
|
396
|
+
got.each { |e| r.guard(where) { blk.call(e[0], e[1]) } }
|
|
397
|
+
got.size
|
|
398
|
+
end
|
|
399
|
+
self
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Yields each change as a Liveliness (key, alive), waiting for the
|
|
403
|
+
# next; the tokens alive now come first.
|
|
404
|
+
def each(timeout: nil, &blk)
|
|
405
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
406
|
+
raise ::Asterism::Error, "this watch has a block; its changes go there" if @task
|
|
407
|
+
Zenoh.drain(@watch, @conn.session, timeout) { |e| blk.call(Liveliness.new(e[0], e[1])) }
|
|
408
|
+
self
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def closed? = @watch.closed?
|
|
412
|
+
|
|
413
|
+
def close
|
|
414
|
+
@conn.runner.remove(@task) if @task
|
|
415
|
+
@task = nil
|
|
416
|
+
@watch.close
|
|
417
|
+
nil
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
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
|
data/mrblib/asterism.rb
CHANGED
|
@@ -166,7 +166,7 @@ module Asterism
|
|
|
166
166
|
# and Error when the same <node>/<app> is already on the network.
|
|
167
167
|
def self.connect(locator, node:, app:, mode: nil, listen: nil)
|
|
168
168
|
raise Error, "already connected (Asterism.close first)" if connected?
|
|
169
|
-
|
|
169
|
+
release
|
|
170
170
|
node = check_name("node", node)
|
|
171
171
|
app = check_name("app", app)
|
|
172
172
|
begin
|
|
@@ -195,6 +195,7 @@ module Asterism
|
|
|
195
195
|
@proxies = {}
|
|
196
196
|
@depth = 0
|
|
197
197
|
@lost = nil
|
|
198
|
+
@known = [] # the other nodes told to on_join so far
|
|
198
199
|
self
|
|
199
200
|
end
|
|
200
201
|
|
|
@@ -206,8 +207,9 @@ module Asterism
|
|
|
206
207
|
found = false
|
|
207
208
|
while true
|
|
208
209
|
s.poll
|
|
210
|
+
ended = g.done? # before taking the replies (see Future#collect)
|
|
209
211
|
found = true if g.each_reply.size > 0
|
|
210
|
-
break if found ||
|
|
212
|
+
break if found || ended || now_ms - t0 > 1500
|
|
211
213
|
pause(WAIT_STEP_MS)
|
|
212
214
|
end
|
|
213
215
|
found
|
|
@@ -235,8 +237,17 @@ module Asterism
|
|
|
235
237
|
@lost
|
|
236
238
|
end
|
|
237
239
|
|
|
238
|
-
# Closes the connection (and with it every exposed object)
|
|
240
|
+
# Closes the connection (and with it every exposed object), and forgets
|
|
241
|
+
# the on_join / on_leave blocks. Idempotent.
|
|
239
242
|
def self.close
|
|
243
|
+
release
|
|
244
|
+
@known = nil
|
|
245
|
+
@on_join = nil
|
|
246
|
+
@on_leave = nil
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def self.release
|
|
240
251
|
s = @session
|
|
241
252
|
@session = nil
|
|
242
253
|
@objects = {}
|
|
@@ -247,7 +258,6 @@ module Asterism
|
|
|
247
258
|
@node_token = nil
|
|
248
259
|
@queryable = nil
|
|
249
260
|
@watch = nil
|
|
250
|
-
nil
|
|
251
261
|
end
|
|
252
262
|
|
|
253
263
|
def self.session!
|
|
@@ -255,10 +265,11 @@ module Asterism
|
|
|
255
265
|
@session
|
|
256
266
|
end
|
|
257
267
|
|
|
268
|
+
# The connection went away: closed, but the next Asterism.poll still tells
|
|
269
|
+
# on_leave about every node known until then.
|
|
258
270
|
def self.lost(reason)
|
|
259
271
|
@lost = reason
|
|
260
|
-
|
|
261
|
-
@lost = reason
|
|
272
|
+
release
|
|
262
273
|
end
|
|
263
274
|
|
|
264
275
|
# ------------------------------------------------------------ exposing
|
|
@@ -300,10 +311,71 @@ module Asterism
|
|
|
300
311
|
# ------------------------------------------------------------ polling
|
|
301
312
|
|
|
302
313
|
# Call from the application's update loop. Answers the calls that came
|
|
303
|
-
# in
|
|
314
|
+
# in, follows who is alive and calls the on_join / on_leave blocks. false
|
|
315
|
+
# once the connection is closed.
|
|
304
316
|
def self.poll
|
|
305
|
-
|
|
306
|
-
|
|
317
|
+
ok = @session ? pump : false
|
|
318
|
+
tell_nodes
|
|
319
|
+
ok
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# ------------------------------------------------------------ nodes coming and going
|
|
323
|
+
|
|
324
|
+
# on_join { |node| }: another node appeared (its node token or one of its
|
|
325
|
+
# objects). The nodes there already join on the first polls after
|
|
326
|
+
# connecting. on_leave { |node| }: a node is gone (no token and no object
|
|
327
|
+
# left); when the connection is lost, every node known until then leaves.
|
|
328
|
+
#
|
|
329
|
+
# The blocks run from Asterism.poll only, never from the polling inside a
|
|
330
|
+
# waiting call. What a block raises comes out of Asterism.poll (on CRuby
|
|
331
|
+
# with a receiving thread: to on_error). Asterism.close forgets them.
|
|
332
|
+
def self.on_join(&blk)
|
|
333
|
+
raise ArgumentError, "on_join needs a block" unless blk
|
|
334
|
+
(@on_join ||= []) << blk
|
|
335
|
+
self
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def self.on_leave(&blk)
|
|
339
|
+
raise ArgumentError, "on_leave needs a block" unless blk
|
|
340
|
+
(@on_leave ||= []) << blk
|
|
341
|
+
self
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Compares the nodes alive now with those told before and calls the
|
|
345
|
+
# blocks for the difference.
|
|
346
|
+
def self.tell_nodes
|
|
347
|
+
known = @known
|
|
348
|
+
return if known.nil?
|
|
349
|
+
now = @session ? nodes : []
|
|
350
|
+
now.shift if @session # this node
|
|
351
|
+
joined = []
|
|
352
|
+
left = []
|
|
353
|
+
now.each { |n| joined << n unless known.include?(n) }
|
|
354
|
+
known.each { |n| left << n unless now.include?(n) }
|
|
355
|
+
# After a loss, everything has been told: nothing more to compare.
|
|
356
|
+
@known = @session ? now : nil
|
|
357
|
+
tell_each("on_join", @on_join, joined)
|
|
358
|
+
tell_each("on_leave", @on_leave, left)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# While loops, not each with a block: the blocks are the application's,
|
|
362
|
+
# and nothing else stands between them and the update loop on the stack.
|
|
363
|
+
def self.tell_each(where, blocks, names)
|
|
364
|
+
return if blocks.nil? || names.empty?
|
|
365
|
+
i = 0
|
|
366
|
+
while i < names.size
|
|
367
|
+
j = 0
|
|
368
|
+
while j < blocks.size
|
|
369
|
+
tell(where, blocks[j], names[i])
|
|
370
|
+
j += 1
|
|
371
|
+
end
|
|
372
|
+
i += 1
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# One block for one node (CRuby routes what it raises to on_error).
|
|
377
|
+
def self.tell(_where, blk, node)
|
|
378
|
+
blk.call(node)
|
|
307
379
|
end
|
|
308
380
|
|
|
309
381
|
def self.pump
|
data/mrblib/future.rb
CHANGED
|
@@ -70,11 +70,16 @@ module Asterism
|
|
|
70
70
|
def collect
|
|
71
71
|
return if @finished
|
|
72
72
|
g = @get
|
|
73
|
+
# done? before taking the replies: with a binding whose replies come
|
|
74
|
+
# in on another thread (zenoh-c), a reply and the end of the get may
|
|
75
|
+
# both arrive between the two; read the other way round, the get
|
|
76
|
+
# would look finished with its reply still queued.
|
|
77
|
+
ended = g.done?
|
|
73
78
|
# The Array form, not a block: a block called from C costs another
|
|
74
79
|
# interpreter entry on the C stack, and this runs inside nested waits.
|
|
75
80
|
replies = g.each_reply
|
|
76
81
|
@reply = replies[0][1] if @reply.nil? && replies.size > 0
|
|
77
|
-
if !@reply.nil? ||
|
|
82
|
+
if !@reply.nil? || ended || ::Asterism.now_ms >= @deadline || !::Asterism.connected?
|
|
78
83
|
@finished = true
|
|
79
84
|
@took = ::Asterism.now_ms - @started
|
|
80
85
|
end
|