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.
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
@@ -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) +
@@ -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.2.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.2.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.2.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