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.
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
- close
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 || g.done? || now_ms - t0 > 1500
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). Idempotent.
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
- close
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 and follows who is alive. false once the connection is closed.
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
- return false unless @session
306
- pump
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? || g.done? || ::Asterism.now_ms >= @deadline || !::Asterism.connected?
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
data/mrblib/ros.rb CHANGED
@@ -11,8 +11,11 @@
11
11
  # add = Asterism::ROS.require_type("example_interfaces/srv/AddTwoInts")
12
12
  # node.service("/add_two_ints_fmrb", add) { |req| { sum: req.a + req.b } }
13
13
  # cli = node.client("/add_two_ints", add)
14
+ # node.subscribe("/cmd", "std_msgs/msg/String") { |msg, info| puts msg.data }
15
+ # node.every(1.0) { pub << { data: "tick" } }
14
16
  # loop do
15
- # node.poll # session.poll + answer services
17
+ # node.poll # session.poll, services, then the
18
+ # # subscribe and every blocks
16
19
  # pub << { data: "hello" } # or pub.publish(str.new(data: "hello"))
17
20
  # sub.each_pending { |msg, info| puts msg.data } # info: Attachment or nil
18
21
  # end
@@ -55,7 +58,7 @@ module Asterism
55
58
  def self.mangle(name)
56
59
  s = name.to_s
57
60
  return "%" if s.empty? || s == "/"
58
- out = ""
61
+ out = "".dup
59
62
  i = 0
60
63
  n = s.bytesize
61
64
  while i < n
@@ -101,7 +104,7 @@ module Asterism
101
104
  # rmw_zenoh hashes the entity's liveliness key the same way (with XXH3);
102
105
  # receivers only use the GID to tell publishers apart.
103
106
  def self.gid_for(key)
104
- out = ""
107
+ out = "".dup
105
108
  lane = 0
106
109
  while lane < 4
107
110
  h = 0x811c9dc5 ^ (lane * 0x9e3779b9 & 0xffffffff)
@@ -140,6 +143,11 @@ module Asterism
140
143
  @gid = gid
141
144
  end
142
145
 
146
+ # For pattern matching: case info in {sequence:, gid:}.
147
+ def deconstruct_keys(_keys)
148
+ { sequence: @sequence, stamp_ns: @stamp_ns, gid: @gid }
149
+ end
150
+
143
151
  def encode
144
152
  g = @gid.to_s
145
153
  ::Asterism::CDR.le_bytes(@sequence, 8) + ::Asterism::CDR.le_bytes(@stamp_ns, 8) +
@@ -217,7 +225,7 @@ module Asterism
217
225
 
218
226
  # Asterism::ROS::<Pkg>::<Name>, or nil.
219
227
  def self.type_constant(pkg, name)
220
- mod_name = ""
228
+ mod_name = "".dup # a fresh String: literals may be frozen (Ruby 4)
221
229
  pkg.split("_").each do |w|
222
230
  mod_name << w.byteslice(0, 1).upcase << w.byteslice(1, w.bytesize - 1) if w.bytesize > 0
223
231
  end
@@ -290,6 +298,20 @@ module Asterism
290
298
  end
291
299
  end
292
300
 
301
+ # For pattern matching: case msg in {linear: {x:}, angular: {z:}}.
302
+ # Nested messages stay messages (they match the same way).
303
+ def deconstruct_keys(keys)
304
+ h = {}
305
+ fs = self.class::FIELDS
306
+ i = 0
307
+ while i < fs.size
308
+ f = fs[i]
309
+ h[f] = __send__(f) if keys.nil? || keys.include?(f)
310
+ i += 1
311
+ end
312
+ h
313
+ end
314
+
293
315
  def ==(other)
294
316
  other.class == self.class && to_h == other.to_h
295
317
  end
@@ -318,6 +340,8 @@ module Asterism
318
340
  @next_id = 1
319
341
  @entities = []
320
342
  @services = []
343
+ @handled = [] # subscriptions with a block
344
+ @timers = []
321
345
  @key = "#{LIVELINESS_ROOT}/#{@domain}/#{@zid}/#{@nid}/#{@nid}/NN/" \
322
346
  "#{::Asterism::ROS.mangle(@enclave)}/#{::Asterism::ROS.mangle(@namespace)}/#{@name}"
323
347
  @token = session.liveliness(@key)
@@ -353,6 +377,35 @@ module Asterism
353
377
  e
354
378
  end
355
379
 
380
+ # Subscribes and gives each message (and its Attachment, or nil) to
381
+ # the block, from node.poll. Returns the Subscription (close ends it).
382
+ # The block runs on the update loop's stack: do not wait in it (no
383
+ # service call, no proxy call); start a call_async and look at it
384
+ # later instead.
385
+ def subscribe(topic, type, qos: DEFAULT_QOS, depth: 16, &handler)
386
+ raise ArgumentError, "subscribe needs a block (or use subscription)" unless handler
387
+ e = subscription(topic, type, qos: qos, depth: depth)
388
+ e.handler = handler
389
+ @handled << e
390
+ e
391
+ end
392
+
393
+ # Calls the block every `seconds` (the first time one period from now),
394
+ # from node.poll. Returns a Timer (cancel). It keeps the period; after
395
+ # a long stall it starts again from now instead of firing the missed
396
+ # times at once. As for subscribe: do not wait in the block.
397
+ def every(seconds, &blk)
398
+ raise ArgumentError, "every needs a block" unless blk
399
+ t = ::Asterism::ROS::Timer.new(self, seconds, blk)
400
+ @timers << t
401
+ t
402
+ end
403
+
404
+ def cancel_timer(timer)
405
+ @timers.delete(timer)
406
+ nil
407
+ end
408
+
356
409
  # node.call("/add_two_ints", AddTwoInts, a: 1, b: 2) -> response.
357
410
  # A client per service name is made on first use and kept.
358
411
  def call(service, type, request = nil, timeout_ms: ::Asterism::ROS::Client::DEFAULT_TIMEOUT_MS, **fields)
@@ -365,21 +418,85 @@ module Asterism
365
418
  c.call(request, timeout_ms: timeout_ms, **fields)
366
419
  end
367
420
 
368
- # Polls the session (Asterism::Zenoh::Session#poll) and answers the
369
- # requests that came in for this node's services. Returns what
370
- # session.poll returns (false once the session is closed).
421
+ # Polls the session (Asterism::Zenoh::Session#poll), answers the
422
+ # requests that came in for this node's services, then gives the
423
+ # messages that came in to the subscribe blocks and fires the timers
424
+ # that are due. Returns what session.poll returns (false once the
425
+ # session is closed).
371
426
  def poll(steps = 8)
427
+ ok = pump(steps)
428
+ deliver
429
+ fire_timers
430
+ ok
431
+ end
432
+
433
+ # What a waiting service call polls: the session and the services, not
434
+ # the subscribe / every blocks (those run from node.poll only).
435
+ def pump(steps = 8)
372
436
  ok = @session.poll(steps)
373
- @services.each { |sv| sv.handle_pending }
437
+ i = 0
438
+ while i < @services.size
439
+ @services[i].handle_pending
440
+ i += 1
441
+ end
374
442
  ok
375
443
  end
376
444
 
445
+ # While loops, not each with a block: the application's blocks are
446
+ # called from here, and nothing else should stand between them and
447
+ # the update loop on the stack.
448
+ def deliver
449
+ i = 0
450
+ while i < @handled.size
451
+ sub = @handled[i]
452
+ if sub.closed?
453
+ @handled.delete_at(i)
454
+ next
455
+ end
456
+ got = sub.each_pending
457
+ j = 0
458
+ while j < got.size
459
+ handle(sub, got[j][0], got[j][1])
460
+ j += 1
461
+ end
462
+ i += 1
463
+ end
464
+ end
465
+
466
+ def fire_timers
467
+ return if @timers.empty?
468
+ now = ::Asterism::ROS.now_ms
469
+ due = []
470
+ i = 0
471
+ while i < @timers.size
472
+ due << @timers[i] if @timers[i].due?(now)
473
+ i += 1
474
+ end
475
+ i = 0
476
+ while i < due.size
477
+ fire(due[i], now)
478
+ i += 1
479
+ end
480
+ end
481
+
482
+ # One message for one subscribe block, one timer firing (CRuby routes
483
+ # what they raise to on_error).
484
+ def handle(sub, msg, info)
485
+ sub.handler.call(msg, info)
486
+ end
487
+
488
+ def fire(timer, now)
489
+ timer.fire(now)
490
+ end
491
+
377
492
  # Withdraws the node and its publishers, subscriptions, services and
378
493
  # clients. Idempotent.
379
494
  def close
380
495
  @entities.each { |e| e.close }
381
496
  @entities = []
382
497
  @services = []
498
+ @handled = []
499
+ @timers = []
383
500
  @clients = {}
384
501
  @token.close if @token
385
502
  @token = nil
@@ -419,8 +536,10 @@ module Asterism
419
536
  # take their defaults).
420
537
  def publish(msg)
421
538
  raise ::Asterism::Zenoh::Error, "publisher closed" if @token.nil?
422
- @sequence += 1
423
- att = ::Asterism::ROS::Attachment.new(@sequence, ::Asterism::ROS.now_ns, @gid)
539
+ # Read once (a second thread may publish meanwhile on CRuby).
540
+ seq = @sequence + 1
541
+ @sequence = seq
542
+ att = ::Asterism::ROS::Attachment.new(seq, ::Asterism::ROS.now_ns, @gid)
424
543
  @session.put(@topic_key, @type.encode(msg), attachment: att.encode)
425
544
  nil
426
545
  end
@@ -440,6 +559,8 @@ module Asterism
440
559
 
441
560
  class Subscription
442
561
  attr_reader :topic_key, :token_key, :errors
562
+ # The block of node.subscribe (nil for node.subscription).
563
+ attr_accessor :handler
443
564
 
444
565
  def initialize(node, session, topic, type, qos, depth)
445
566
  @type = type
@@ -480,6 +601,47 @@ module Asterism
480
601
  @token = nil
481
602
  nil
482
603
  end
604
+
605
+ def closed?
606
+ @token.nil?
607
+ end
608
+ end
609
+
610
+ # node.every: a block called every period, from node.poll.
611
+ class Timer
612
+ # period: seconds as given; fired: how many times it ran.
613
+ attr_reader :period, :fired
614
+
615
+ def initialize(node, seconds, blk)
616
+ @period_ms = (seconds * 1000).to_i
617
+ raise ArgumentError, "the period must be positive" unless @period_ms > 0
618
+ @node = node
619
+ @period = seconds
620
+ @blk = blk
621
+ @next = ::Asterism::ROS.now_ms + @period_ms
622
+ @fired = 0
623
+ end
624
+
625
+ def due?(now_ms)
626
+ now_ms >= @next
627
+ end
628
+
629
+ # Keeps the period without drifting; after a long stall it starts
630
+ # again from now instead of firing the missed times at once.
631
+ def fire(now_ms)
632
+ @next += @period_ms
633
+ @next = now_ms + @period_ms if @next <= now_ms
634
+ @fired += 1
635
+ @blk.call
636
+ end
637
+
638
+ def cancel
639
+ @node.cancel_timer(self)
640
+ end
641
+
642
+ def inspect
643
+ "#<Asterism::ROS::Timer every #{@period} s>"
644
+ end
483
645
  end
484
646
  end
485
647
  end
@@ -600,6 +762,8 @@ module Asterism
600
762
  def collect
601
763
  return if @finished
602
764
  g = @get
765
+ # done? before taking the replies, as in Asterism::Future#collect.
766
+ ended = g.done?
603
767
  # The Array form, as in Asterism::Future (no block called from C).
604
768
  g.each_reply.each do |r|
605
769
  next unless @response.nil?
@@ -613,7 +777,7 @@ module Asterism
613
777
  end
614
778
  end
615
779
  now = ::Asterism::ROS.now_ms
616
- if !@response.nil? || g.done? || now >= @deadline
780
+ if !@response.nil? || ended || now >= @deadline
617
781
  @nobody = @response.nil? && now < @deadline
618
782
  @finished = true
619
783
  @took_ms = now - @started
@@ -647,11 +811,15 @@ module Asterism
647
811
  raise ::Asterism::Zenoh::Error, "client closed" if @token.nil?
648
812
  req = request.nil? ? fields : request
649
813
  payload = @type::Request.encode(req)
650
- @sequence += 1
651
- att = ::Asterism::ROS::Attachment.new(@sequence, ::Asterism::ROS.now_ns, @gid)
814
+ # Read once into a local: the request and its Call carry the same
815
+ # number even when another thread (CRuby) calls through this client
816
+ # meanwhile.
817
+ seq = @sequence + 1
818
+ @sequence = seq
819
+ att = ::Asterism::ROS::Attachment.new(seq, ::Asterism::ROS.now_ns, @gid)
652
820
  g = @session.get(@service_key, timeout_ms, nil, payload, attachment: att.encode,
653
821
  target: :all_complete, consolidation: :none)
654
- ::Asterism::ROS::Call.new(self, g, @sequence, timeout_ms)
822
+ ::Asterism::ROS::Call.new(self, g, seq, timeout_ms)
655
823
  end
656
824
 
657
825
  # Sends the request and waits for the response (polling the node, so
@@ -661,9 +829,11 @@ module Asterism
661
829
  call_async(request, timeout_ms: timeout_ms, **fields).value
662
830
  end
663
831
 
832
+ # Polls the node's session and services (not its subscribe / every
833
+ # blocks) until the call is done.
664
834
  def wait_for(c)
665
835
  until c.done?
666
- break unless @node.poll
836
+ break unless @node.pump
667
837
  break if c.done?
668
838
  ::Asterism::ROS.pause(WAIT_STEP_MS)
669
839
  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.1.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.1.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.1.0
26
+ version: 0.3.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: msgpack
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +115,10 @@ files:
115
115
  - data/msgs/std_msgs/msg/UInt8MultiArray.rb
116
116
  - lib/asterism.rb
117
117
  - lib/asterism/cruby.rb
118
+ - lib/asterism/cruby/objects.rb
119
+ - lib/asterism/cruby/ros.rb
120
+ - lib/asterism/cruby/runner.rb
121
+ - lib/asterism/cruby/zenoh.rb
118
122
  - lib/asterism/version.rb
119
123
  - mrblib/asterism.rb
120
124
  - mrblib/cdr.rb