asterism 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdb7bf2126361f3b9ae4097467832d950440b9a65c419a92394f230b4d7b0277
4
- data.tar.gz: ba8621f58ee2d5fb22f5eeb3b68917cb6ce9f9420d07230d5d69e246b9c53564
3
+ metadata.gz: f03c6be1c5461da008a067c55b609a009b31b7552810836b7272bcdd35443ba7
4
+ data.tar.gz: df8c8641c8039408dcb3ca803b7fdcfe0a24e3eef3199da0b1277e980977f936
5
5
  SHA512:
6
- metadata.gz: cc43c0ab1f0076a148bcd65a648b0220eb8559912b296a7fdbb7b4810f108ab8f66d439e951e3c1a3fe24329ac6fb5a76593214c7571e3f18b55d833a917276d
7
- data.tar.gz: 48114f986885d604142a4fb369b1a31f4a7cb6d1dbff95ffc0b0566e97801bbd646278cb498b4c0e1003c8a3b425a526262ffb384557eeb0cdc4a7e450b6bc63
6
+ metadata.gz: 663fbb3e79d9b824128b541b899028809d899916af70a28fe761581fa2e28285f9841ef78dfafd088c2583f2c9df705328566b1f465ec31886df277c013c90a2
7
+ data.tar.gz: d447307b7bba1387d35316044c483f974a814ab1b0a4c7f5d32e17e8fc0b954ed2388ee53aaef9bf780bb7ccb98a93669fdf30f0efb7357d007535cae26a33ec
data/README.md CHANGED
@@ -23,7 +23,15 @@ each Ruby:
23
23
  - mruby / PicoRuby: [picoruby-asterism-zenoh](https://github.com/ruby-asterism/picoruby-asterism-zenoh)
24
24
  (over zenoh-pico)
25
25
 
26
- Neither gem is on rubygems.org yet (version 0.2.0 builds from these
26
+ Which zenoh-c features the CRuby binding exposes (configuration and TLS,
27
+ scouting, publishers, queriers, the advanced publisher / subscriber,
28
+ events, key expressions, timestamps) and which zenoh-pico could offer is
29
+ in asterism-zenoh's
30
+ [feature coverage](https://github.com/ruby-asterism/asterism-zenoh/blob/main/docs/feature_coverage.md).
31
+ The ones added in 0.3.0 are CRuby only: the shared layer (`mrblib/`) does
32
+ not use them.
33
+
34
+ Neither gem is on rubygems.org yet (version 0.3.0 builds from these
27
35
  repositories with `rake gem`).
28
36
 
29
37
  ## Using it
@@ -31,7 +39,7 @@ repositories with `rake gem`).
31
39
  CRuby (needs CRuby 3.2+ and a C compiler):
32
40
 
33
41
  ```
34
- gem install asterism # also installs asterism-zenoh (~> 0.2.0) and msgpack
42
+ gem install asterism # also installs asterism-zenoh (~> 0.3.0) and msgpack
35
43
  ruby -e 'require "asterism"'
36
44
  ```
37
45
 
@@ -44,7 +52,7 @@ build but not yet tested. To install from the repositories instead:
44
52
 
45
53
  ```
46
54
  (cd ../asterism-zenoh && rake gem) && rake gem
47
- gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.2.0.gem pkg/asterism-0.2.0.gem
55
+ gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.3.0.gem pkg/asterism-0.3.0.gem
48
56
  ```
49
57
 
50
58
  From the working trees, without installing:
@@ -115,17 +123,41 @@ Asterism::Zenoh.open("tcp/192.0.2.2:7447") do |s| # closed when the block ends
115
123
  s.liveliness_watch("asterism/**") { |key, alive| puts "#{key} #{alive}" }
116
124
  s.run # receive on this thread until Ctrl-C
117
125
  end
126
+
127
+ # asterism-zenoh 0.3.0 (CRuby only)
128
+ Asterism::Zenoh.open("tls/192.0.2.2:7447",
129
+ config: { "transport/link/tls/root_ca_certificate" => "ca.pem" }) do |s|
130
+ pub = s.publisher("home/pc/temp", encoding: "text/plain", priority: :data_high)
131
+ pub.on_matching { |listening| puts "listened to: #{listening}" }
132
+ pub.put(21.5)
133
+ s.delete("home/pc/old")
134
+ s.subscribe("home/**") { |sm| p [sm.kind, sm.encoding, sm.timestamp&.to_time] }
135
+ q = s.querier("home/**/status", timeout: 1.0)
136
+ q.get(errors: true).each { |r| puts r.error? ? "error: #{r.payload}" : r.payload }
137
+ latched = s.advanced_publisher("home/pc/mode", cache: 1, sample_miss_detection: true)
138
+ latched.put("eco") # late subscribers with history: get it
139
+ s.advanced_subscriber("home/**/mode", history: true) { |sm| puts sm.payload }
140
+ s.on_transport { |ev| puts "#{ev.zid} #{ev.kind}" }
141
+ s.run
142
+ end
118
143
  ```
119
144
 
120
145
  | Call | Notes |
121
146
  |---|---|
122
- | `Asterism::Zenoh.open(locator = nil, mode:, listen:, interval: 0.002)` | `Session.open` wrapped in a `Connection`. With a block: yields it, closes it after (also on an exception), returns the block's value |
147
+ | `Asterism::Zenoh.open(locator = nil, mode:, listen:, scouting:, timestamping:, config:, config_file:, interval: 0.002)` | `Session.open` wrapped in a `Connection`. With a block: yields it, closes it after (also on an exception), returns the block's value |
123
148
  | `Connection.new(session)` | wraps a session opened with the polled API |
124
- | `s.put(key, payload, attachment: nil)` | a payload that is not a String is sent as `to_s` |
149
+ | `s.put(key, payload, attachment: nil, **opts)` | a payload that is not a String is sent as `to_s`. opts: `encoding:`, `priority:`, `congestion_control:`, `express:`, `reliability:`, `timestamp:`, `allowed_destination:` (asterism-zenoh's `Session#put`) |
150
+ | `s.delete(key, **opts)` | subscribers get a `Sample` of kind `:delete` |
151
+ | `s.publisher(key, **opts)` | a `Publisher`: `put(payload, attachment:, encoding:, timestamp:)`, `delete`, `matching?`, `on_matching { \|listening\| }` (on the receiving thread), `close` |
152
+ | `s.advanced_publisher(key, cache:, sample_miss_detection:, publisher_detection:, **opts)` | the same `Publisher`, keeping the last samples for late subscribers (ROS 2's transient local) |
153
+ | `s.advanced_subscriber(key, depth: 16, history:, recovery:, **opts) { \|sample\| }` | a `Subscription` that also gets the publishers' history; `on_publisher { \|key, alive\| }`, `on_miss { \|miss\| }` |
154
+ | `s.querier(key, timeout: 2.0, **opts)` | a `Querier`: `get(params:, payload:, attachment:, encoding:, errors: false)` like `s.get`, `matching?`, `on_matching`, `close` |
155
+ | `s.on_transport(history: false) { \|ev\| }` / `s.on_link { \|ev\| }` | a `TransportEvent` / `LinkEvent` (`added?` / `removed?`, `zid`, ...) on the receiving thread; returns an `Events` (`close`) |
156
+ | `s.peer_zids`, `router_zids`, `transports`, `links`, `new_timestamp`, `declare_keyexpr(key)` | as in asterism-zenoh |
125
157
  | `s.subscribe(key, depth: 16) { \|sample\| }` | the block gets each `Sample` on the receiving thread. Returns a `Subscription` (`close` ends it) |
126
158
  | `s.subscribe(key, depth: 16)` | a `Subscription` to take from: `each(timeout: nil)` waits for samples (an Enumerator without a block; Enumerable), `each_pending` gives what is there now as Samples, `pending` / `received` / `dropped` / `closed?` / `close` |
127
159
  | `s.queryable(key, depth: 16, complete: false) { \|q\| }` | each query on the receiving thread, finished when the block returns. `q.reply(payload)` answers on the queryable's key (when it has no wildcard), `q.reply(key, payload, attachment:)` as before. Without a block, `each(timeout:)` yields the queries |
128
- | `s.get(key, timeout: 2.0, params:, payload:, attachment:, target:, consolidation:)` | an Enumerator of `Reply`; each iteration sends the get again. With a block: yields each, returns their number |
160
+ | `s.get(key, timeout: 2.0, params:, payload:, attachment:, target:, consolidation:, errors: false, **opts)` | an Enumerator of `Reply`; each iteration sends the get again. With a block: yields each, returns their number. `errors: true` also yields the error replies (`reply.error?`); opts: `encoding:`, `priority:`, `congestion_control:`, `express:`, `accept_replies:` |
129
161
  | `s.liveliness(key)` | a token (`close`) |
130
162
  | `s.liveliness_watch(key) { \|key, alive\| }` | on the receiving thread; the tokens alive now come first. Without a block, `each` yields `Liveliness` (`key`, `alive?`) |
131
163
  | `s.liveliness_get(key, timeout: 2.0)` | the keys alive now (an Array) |
@@ -134,8 +166,9 @@ end
134
166
  | `s.on_error { \|error, where\| }` | what the blocks raise (`StandardError`); without a handler it is printed with `warn`. Receiving goes on |
135
167
  | `s.session`, `zid`, `peers`, `poll`, `closed?`, `close` | |
136
168
 
137
- `Sample`, `Reply` (`key`, `payload`, `attachment`; `text` is the payload as
138
- UTF-8) and `Liveliness` are `Data` values, so they work with pattern
169
+ `Sample`, `Reply` (`key`, `payload`, `attachment`, and since 0.3.0 `kind`,
170
+ `encoding`, `timestamp` and the rest; `text` is the payload as UTF-8; both
171
+ are asterism-zenoh's) and `Liveliness` are `Data` values, so they work with pattern
139
172
  matching: `case sample in {key: %r{/temp\z}, payload:}`. Times are seconds
140
173
  here (the polled API counts milliseconds).
141
174
 
@@ -8,6 +8,9 @@
8
8
  # s.put("home/pc/hello", "hi")
9
9
  # s.get("home/**/status").each { |reply| p reply }
10
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}" }
11
14
  # s.run # until Ctrl-C (or s.start / s.stop)
12
15
  # end
13
16
  #
@@ -19,28 +22,8 @@ module Asterism
19
22
  # Pause between looks while an Enumerator waits for data (seconds).
20
23
  ENUM_STEP = 0.002
21
24
 
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
25
+ # Sample and Reply (the received values, with kind, encoding,
26
+ # timestamp and the rest) are asterism-zenoh's (asterism/zenoh/values).
44
27
 
45
28
  # A liveliness change: key of the token, and whether it appeared.
46
29
  Liveliness = Data.define(:key, :alive) do
@@ -57,6 +40,20 @@ module Asterism
57
40
  args.unshift(@asterism_key) if args.size == 1 && @asterism_key
58
41
  super(*args, **kw)
59
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
60
57
  end
61
58
 
62
59
  # The queryable's key when it names one key (no wildcard).
@@ -92,10 +89,10 @@ module Asterism
92
89
  # Waits on the calling thread until a sample comes into sub (an
93
90
  # Asterism::Zenoh::Subscriber or LivelinessWatch), yielding each.
94
91
  # Ends when sub or the session closes, or after timeout seconds.
95
- def self.drain(sub, session, timeout)
92
+ def self.drain(sub, session, timeout, take: :each_pending)
96
93
  deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
97
94
  loop do
98
- got = sub.each_pending
95
+ got = sub.public_send(take)
99
96
  got.each { |e| yield e }
100
97
  break if sub.closed? || session.closed?
101
98
  break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
@@ -115,11 +112,65 @@ module Asterism
115
112
  @runner.add { @session.poll ? 0 : false }
116
113
  end
117
114
 
118
- def put(key, payload, attachment: nil)
119
- @session.put(key, payload.is_a?(String) ? payload : payload.to_s, attachment: attachment)
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)
120
128
  nil
121
129
  end
122
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
+
123
174
  # With a block: every sample is given to it (as a Sample) on the
124
175
  # receiving thread; returns the Subscription (close it to stop).
125
176
  # Without: a Subscription to take from (each, each_pending).
@@ -141,11 +192,17 @@ module Asterism
141
192
  # The replies to a get: yields each Reply until every answer came or
142
193
  # timeout (seconds) ran out, and returns their number. Without a
143
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).
144
198
  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)
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)
149
206
  end
150
207
 
151
208
  # A liveliness token (close it to withdraw).
@@ -221,6 +278,20 @@ module Asterism
221
278
  @session.peers
222
279
  end
223
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
+
224
295
  # Stops receiving and closes the session (with everything declared on
225
296
  # it). Idempotent.
226
297
  def close
@@ -235,17 +306,21 @@ module Asterism
235
306
 
236
307
  private
237
308
 
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)
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
241
314
  n = 0
242
315
  loop do
243
- got = g.each_reply
316
+ done = g.done?
317
+ got = g.each_result
244
318
  got.each do |r|
319
+ next if r.error? && !errors
245
320
  n += 1
246
- yield Reply.new(r[0], r[1], r[2])
321
+ yield r
247
322
  end
248
- break if g.done? && g.pending == 0
323
+ break if done && g.pending == 0
249
324
  sleep(ENUM_STEP) if got.empty?
250
325
  end
251
326
  n
@@ -272,8 +347,8 @@ module Asterism
272
347
  where = "subscribe #{@key}"
273
348
  r = @conn.runner
274
349
  @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])) } }
350
+ got = @subscriber.each_sample
351
+ got.each { |s| r.guard(where) { blk.call(s) } }
277
352
  got.size
278
353
  end
279
354
  self
@@ -285,13 +360,13 @@ module Asterism
285
360
  def each(timeout: nil, &blk)
286
361
  return enum_for(:each, timeout: timeout) unless blk
287
362
  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])) }
363
+ Zenoh.drain(@subscriber, @conn.session, timeout, take: :each_sample) { |s| blk.call(s) }
289
364
  self
290
365
  end
291
366
 
292
367
  # The samples there now as Samples (an Array), or yields them.
293
368
  def each_pending
294
- got = @subscriber.each_pending.map { |e| Sample.new(e[0], e[1], e[2]) }
369
+ got = @subscriber.each_sample
295
370
  return got unless block_given?
296
371
  got.each { |s| yield s }
297
372
  got.size
@@ -417,6 +492,157 @@ module Asterism
417
492
  nil
418
493
  end
419
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
420
646
  end
421
647
  end
422
648
  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.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/mrblib/ros.rb CHANGED
@@ -58,7 +58,7 @@ module Asterism
58
58
  def self.mangle(name)
59
59
  s = name.to_s
60
60
  return "%" if s.empty? || s == "/"
61
- out = ""
61
+ out = "".dup
62
62
  i = 0
63
63
  n = s.bytesize
64
64
  while i < n
@@ -104,7 +104,7 @@ module Asterism
104
104
  # rmw_zenoh hashes the entity's liveliness key the same way (with XXH3);
105
105
  # receivers only use the GID to tell publishers apart.
106
106
  def self.gid_for(key)
107
- out = ""
107
+ out = "".dup
108
108
  lane = 0
109
109
  while lane < 4
110
110
  h = 0x811c9dc5 ^ (lane * 0x9e3779b9 & 0xffffffff)
@@ -225,7 +225,7 @@ module Asterism
225
225
 
226
226
  # Asterism::ROS::<Pkg>::<Name>, or nil.
227
227
  def self.type_constant(pkg, name)
228
- mod_name = ""
228
+ mod_name = "".dup # a fresh String: literals may be frozen (Ruby 4)
229
229
  pkg.split("_").each do |w|
230
230
  mod_name << w.byteslice(0, 1).upcase << w.byteslice(1, w.bytesize - 1) if w.bytesize > 0
231
231
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asterism
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katsuhiko Kageyama
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.0
19
+ version: 0.3.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.0
26
+ version: 0.3.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: msgpack
29
29
  requirement: !ruby/object:Gem::Requirement