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.
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Asterism
2
2
  # The gem's version (the CRuby gem asterism; it depends on asterism-zenoh
3
3
  # of the same version).
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
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