asterism 0.3.0 → 0.4.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
@@ -20,6 +20,7 @@
20
20
  # sub.each_pending { |msg, info| puts msg.data } # info: Attachment or nil
21
21
  # end
22
22
  # cli.call(a: 2, b: 3).sum # waits (polling), or
23
+ # cli.call(request: {a: 2, b: 3}, timeout: 5.0) # the request as a Hash
23
24
  # c = cli.call_async(a: 2, b: 3); c.done?; c.value
24
25
  # node.close
25
26
  #
@@ -47,6 +48,11 @@
47
48
  # Nothing here depends on the host: it uses an Asterism::Zenoh::Session
48
49
  # (put with attachment:, subscribe, liveliness, zid) and Asterism::CDR.
49
50
  module Asterism
51
+ # (asterism.rb defines both; this file is also loaded on its own, with
52
+ # cdr.rb, by the type tools.)
53
+ class Error < StandardError; end
54
+ class TimeoutError < Error; end
55
+
50
56
  module ROS
51
57
  LIVELINESS_ROOT = "@ros2_lv"
52
58
  # rmw_zenoh's QoS chunk for the default profile (reliable, volatile,
@@ -120,8 +126,20 @@ module Asterism
120
126
  out
121
127
  end
122
128
 
123
- # A service call got no answer in time (or nobody serves that name).
124
- class Timeout < ::StandardError; end
129
+ # A service call got no answer in time (or nobody serves that name). An
130
+ # Asterism::TimeoutError (so an Asterism::Error); its old name
131
+ # Asterism::ROS::Timeout still works, with a deprecation warning.
132
+ class TimeoutError < ::Asterism::TimeoutError; end
133
+
134
+ # The old name Asterism::ROS::Timeout (deprecated): see
135
+ # Asterism.const_missing.
136
+ def self.const_missing(name)
137
+ if name == :Timeout
138
+ ::Asterism.deprecated("Asterism::ROS::Timeout", "Asterism::ROS::TimeoutError")
139
+ return TimeoutError
140
+ end
141
+ super
142
+ end
125
143
 
126
144
  # Milliseconds from an arbitrary origin (the board's clock when there is
127
145
  # one), and a short pause; the same helpers Asterism's calls use.
@@ -180,7 +198,7 @@ module Asterism
180
198
  TYPE_PATH = ["/usr/share/asterism/msgs"]
181
199
 
182
200
  # The type has no generated file in TYPE_PATH.
183
- class UnknownType < ::StandardError; end
201
+ class UnknownType < ::Asterism::Error; end
184
202
 
185
203
  def self.type_path
186
204
  TYPE_PATH
@@ -381,22 +399,26 @@ module Asterism
381
399
  # the block, from node.poll. Returns the Subscription (close ends it).
382
400
  # The block runs on the update loop's stack: do not wait in it (no
383
401
  # service call, no proxy call); start a call_async and look at it
384
- # later instead.
402
+ # later instead. Without a block, the same as subscription: a
403
+ # Subscription to take from with each_pending.
385
404
  def subscribe(topic, type, qos: DEFAULT_QOS, depth: 16, &handler)
386
- raise ArgumentError, "subscribe needs a block (or use subscription)" unless handler
405
+ return subscription(topic, type, qos: qos, depth: depth) unless handler
387
406
  e = subscription(topic, type, qos: qos, depth: depth)
388
407
  e.handler = handler
389
408
  @handled << e
390
409
  e
391
410
  end
392
411
 
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)
412
+ # Calls the block every `seconds` (or every ms: milliseconds; the first
413
+ # time one period from now), from node.poll. Returns a Timer (cancel).
414
+ # It keeps the period; after a long stall it starts again from now
415
+ # instead of firing the missed times at once. As for subscribe: do not
416
+ # wait in the block.
417
+ def every(seconds = nil, ms: nil, &blk)
398
418
  raise ArgumentError, "every needs a block" unless blk
399
- t = ::Asterism::ROS::Timer.new(self, seconds, blk)
419
+ raise ArgumentError, "every: give the period once (seconds, or ms:)" if !seconds.nil? && !ms.nil?
420
+ raise ArgumentError, "every: a period is needed (seconds, or ms:)" if seconds.nil? && ms.nil?
421
+ t = ::Asterism::ROS::Timer.new(self, seconds, blk, ms)
400
422
  @timers << t
401
423
  t
402
424
  end
@@ -407,15 +429,17 @@ module Asterism
407
429
  end
408
430
 
409
431
  # node.call("/add_two_ints", AddTwoInts, a: 1, b: 2) -> response.
410
- # A client per service name is made on first use and kept.
411
- def call(service, type, request = nil, timeout_ms: ::Asterism::ROS::Client::DEFAULT_TIMEOUT_MS, **fields)
432
+ # The request as keywords, as a Hash or message (positional or
433
+ # request:); the time limit as timeout: (seconds) or timeout_ms:. A
434
+ # client per service name is made on first use and kept.
435
+ def call(service, type, req = nil, request: nil, timeout: nil, timeout_ms: nil, **fields)
412
436
  @clients ||= {}
413
437
  c = @clients[service]
414
438
  if c.nil? || c.closed?
415
439
  c = client(service, type)
416
440
  @clients[service] = c
417
441
  end
418
- c.call(request, timeout_ms: timeout_ms, **fields)
442
+ c.call(req, request: request, timeout: timeout, timeout_ms: timeout_ms, **fields)
419
443
  end
420
444
 
421
445
  # Polls the session (Asterism::Zenoh::Session#poll), answers the
@@ -432,6 +456,7 @@ module Asterism
432
456
 
433
457
  # What a waiting service call polls: the session and the services, not
434
458
  # the subscribe / every blocks (those run from node.poll only).
459
+ # @api private
435
460
  def pump(steps = 8)
436
461
  ok = @session.poll(steps)
437
462
  i = 0
@@ -444,7 +469,7 @@ module Asterism
444
469
 
445
470
  # While loops, not each with a block: the application's blocks are
446
471
  # called from here, and nothing else should stand between them and
447
- # the update loop on the stack.
472
+ # the update loop on the stack. @api private
448
473
  def deliver
449
474
  i = 0
450
475
  while i < @handled.size
@@ -463,6 +488,7 @@ module Asterism
463
488
  end
464
489
  end
465
490
 
491
+ # @api private
466
492
  def fire_timers
467
493
  return if @timers.empty?
468
494
  now = ::Asterism::ROS.now_ms
@@ -480,7 +506,7 @@ module Asterism
480
506
  end
481
507
 
482
508
  # One message for one subscribe block, one timer firing (CRuby routes
483
- # what they raise to on_error).
509
+ # what they raise to on_error). @api private
484
510
  def handle(sub, msg, info)
485
511
  sub.handler.call(msg, info)
486
512
  end
@@ -508,6 +534,7 @@ module Asterism
508
534
  end
509
535
 
510
536
  # For the entities: [data key, liveliness token key] of a topic.
537
+ # @api private
511
538
  def entity_keys(kind, topic, type, qos)
512
539
  full = ::Asterism::ROS.resolve(topic, @namespace)
513
540
  data = "#{@domain}/#{::Asterism::ROS.strip_slashes(full)}/#{type::TYPE_NAME}/#{type::TYPE_HASH}"
@@ -535,7 +562,7 @@ module Asterism
535
562
  # msg: a message of the type, or a Hash of its fields (missing ones
536
563
  # take their defaults).
537
564
  def publish(msg)
538
- raise ::Asterism::Zenoh::Error, "publisher closed" if @token.nil?
565
+ raise ::Asterism::Zenoh::ClosedError, "publisher closed" if @token.nil?
539
566
  # Read once (a second thread may publish meanwhile on CRuby).
540
567
  seq = @sequence + 1
541
568
  @sequence = seq
@@ -566,7 +593,7 @@ module Asterism
566
593
  @type = type
567
594
  @topic_key, @token_key = node.entity_keys("MS", topic, type, qos)
568
595
  @errors = 0
569
- @sub = session.subscribe(@topic_key, depth)
596
+ @sub = session.subscribe(@topic_key, depth: depth)
570
597
  @token = session.liveliness(@token_key)
571
598
  end
572
599
 
@@ -591,6 +618,16 @@ module Asterism
591
618
  out.size
592
619
  end
593
620
 
621
+ # Messages waiting / received so far / dropped because the queue was
622
+ # full (the counters of the Zenoh subscriber underneath).
623
+ def pending
624
+ @sub.pending
625
+ end
626
+
627
+ def received
628
+ @sub.received
629
+ end
630
+
594
631
  def dropped
595
632
  @sub.dropped
596
633
  end
@@ -609,14 +646,16 @@ module Asterism
609
646
 
610
647
  # node.every: a block called every period, from node.poll.
611
648
  class Timer
612
- # period: seconds as given; fired: how many times it ran.
613
- attr_reader :period, :fired
649
+ # period: seconds (as given, or ms / 1000.0); period_ms: the same in
650
+ # milliseconds; fired: how many times it ran.
651
+ attr_reader :period, :period_ms, :fired
614
652
 
615
- def initialize(node, seconds, blk)
616
- @period_ms = (seconds * 1000).to_i
653
+ # @api private (node.every makes it)
654
+ def initialize(node, seconds, blk, ms = nil)
655
+ @period_ms = ms.nil? ? (seconds * 1000).round : ms.round
617
656
  raise ArgumentError, "the period must be positive" unless @period_ms > 0
618
657
  @node = node
619
- @period = seconds
658
+ @period = ms.nil? ? seconds : ms / 1000.0
620
659
  @blk = blk
621
660
  @next = ::Asterism::ROS.now_ms + @period_ms
622
661
  @fired = 0
@@ -659,7 +698,7 @@ module Asterism
659
698
  @service_key, @token_key = node.entity_keys("SS", service, type, qos)
660
699
  @handled = 0
661
700
  @errors = 0
662
- @queryable = session.queryable(@service_key, depth, complete: true)
701
+ @queryable = session.queryable(@service_key, depth: depth, complete: true)
663
702
  @token = session.liveliness(@token_key)
664
703
  end
665
704
 
@@ -739,6 +778,11 @@ module Asterism
739
778
  @nobody = false
740
779
  end
741
780
 
781
+ # took_ms in seconds (a Float; nil until done).
782
+ def took
783
+ @took_ms.nil? ? nil : @took_ms / 1000.0
784
+ end
785
+
742
786
  # True once the response came, the time ran out, or no server answered.
743
787
  def done?
744
788
  collect
@@ -746,19 +790,23 @@ module Asterism
746
790
  end
747
791
 
748
792
  # The response (type::Response); waits until done. Raises
749
- # Asterism::ROS::Timeout when there was none.
793
+ # Asterism::ROS::TimeoutError when there was none, and
794
+ # Asterism::Zenoh::ClosedError when the session closed (1.0:
795
+ # Asterism::Disconnected).
750
796
  def value
751
797
  @client.wait_for(self) unless done?
752
798
  if @response.nil?
753
- raise ::Asterism::Zenoh::Error, "session is closed" if @client.session_closed?
799
+ raise ::Asterism::Zenoh::ClosedError, "session is closed" if @client.session_closed?
754
800
  if @nobody
755
- raise ::Asterism::ROS::Timeout, "no answer from #{@client.service_name} (nobody serves it)"
801
+ raise ::Asterism::ROS::TimeoutError, "no answer from #{@client.service_name} (nobody serves it)"
756
802
  end
757
- raise ::Asterism::ROS::Timeout, "no answer from #{@client.service_name} within #{@timeout_ms} ms"
803
+ raise ::Asterism::ROS::TimeoutError,
804
+ "no answer from #{@client.service_name} within #{@timeout_ms / 1000.0} s (#{@timeout_ms} ms)"
758
805
  end
759
806
  @response
760
807
  end
761
808
 
809
+ # @api private
762
810
  def collect
763
811
  return if @finished
764
812
  g = @get
@@ -788,6 +836,8 @@ module Asterism
788
836
  # The client side of a service (node.client). Sends each request as a
789
837
  # get on the service key, plus the SC token.
790
838
  class Client
839
+ # The time limit of a call, in seconds and in milliseconds.
840
+ DEFAULT_TIMEOUT = 2.0
791
841
  DEFAULT_TIMEOUT_MS = 2000
792
842
  # Pause between polls while a call waits (ms).
793
843
  WAIT_STEP_MS = 2
@@ -805,11 +855,14 @@ module Asterism
805
855
  @token = session.liveliness(@token_key)
806
856
  end
807
857
 
808
- # Sends the request and returns a Call at once. request: a
809
- # type::Request, a Hash, or the fields as keywords.
810
- def call_async(request = nil, timeout_ms: DEFAULT_TIMEOUT_MS, **fields)
811
- raise ::Asterism::Zenoh::Error, "client closed" if @token.nil?
812
- req = request.nil? ? fields : request
858
+ # Sends the request and returns a Call at once. The request: a
859
+ # type::Request or a Hash (positional, or request:), or the fields as
860
+ # keywords. The time limit: timeout: (seconds) or timeout_ms: (2 s).
861
+ # A request with a field named timeout or timeout_ms is best given as
862
+ # request: { ... } (see README, Reserved keywords).
863
+ def call_async(req = nil, request: nil, timeout: nil, timeout_ms: nil, **fields)
864
+ raise ::Asterism::Zenoh::ClosedError, "client closed" if @token.nil?
865
+ req, timeout_ms = request_and_limit("Client#call", req, request, timeout, timeout_ms, fields)
813
866
  payload = @type::Request.encode(req)
814
867
  # Read once into a local: the request and its Call carry the same
815
868
  # number even when another thread (CRuby) calls through this client
@@ -817,20 +870,47 @@ module Asterism
817
870
  seq = @sequence + 1
818
871
  @sequence = seq
819
872
  att = ::Asterism::ROS::Attachment.new(seq, ::Asterism::ROS.now_ns, @gid)
820
- g = @session.get(@service_key, timeout_ms, nil, payload, attachment: att.encode,
873
+ g = @session.get(@service_key, timeout_ms: timeout_ms, payload: payload, attachment: att.encode,
821
874
  target: :all_complete, consolidation: :none)
822
875
  ::Asterism::ROS::Call.new(self, g, seq, timeout_ms)
823
876
  end
824
877
 
825
878
  # Sends the request and waits for the response (polling the node, so
826
879
  # this node's services keep answering meanwhile). Raises
827
- # Asterism::ROS::Timeout when no response came in time.
828
- def call(request = nil, timeout_ms: DEFAULT_TIMEOUT_MS, **fields)
829
- call_async(request, timeout_ms: timeout_ms, **fields).value
880
+ # Asterism::ROS::TimeoutError when no response came in time.
881
+ def call(req = nil, request: nil, timeout: nil, timeout_ms: nil, **fields)
882
+ call_async(req, request: request, timeout: timeout, timeout_ms: timeout_ms, **fields).value
883
+ end
884
+
885
+ # The request and the time limit (ms) from call's arguments. A
886
+ # keyword that is also a field of the request type is ambiguous: a
887
+ # field named timeout stays a field (as before 0.4.0), timeout_ms
888
+ # stays the time limit, and both warn (request: { ... } says which).
889
+ # @api private
890
+ def request_and_limit(where, req, request, timeout, timeout_ms, fields)
891
+ raise ArgumentError, "#{where}: give the request once (positional or request:)" if !req.nil? && !request.nil?
892
+ req = request if req.nil?
893
+ if req.nil?
894
+ names = @type::Request::FIELDS
895
+ if !timeout.nil? && names.include?(:timeout)
896
+ ::Asterism.deprecated("#{where} with a request field named timeout as a keyword",
897
+ "request: { timeout: ... } (timeout: is the time limit in seconds)",
898
+ "it is taken as the field here, as before 0.4.0")
899
+ fields[:timeout] = timeout
900
+ timeout = nil
901
+ end
902
+ if !timeout_ms.nil? && names.include?(:timeout_ms)
903
+ ::Asterism.deprecated("#{where} with a request field named timeout_ms as a keyword",
904
+ "request: { timeout_ms: ... } and timeout: for the time limit",
905
+ "timeout_ms: is taken as the time limit here")
906
+ end
907
+ req = fields
908
+ end
909
+ [req, ::Asterism.time_ms(where, timeout, timeout_ms, nil, DEFAULT_TIMEOUT_MS)]
830
910
  end
831
911
 
832
912
  # Polls the node's session and services (not its subscribe / every
833
- # blocks) until the call is done.
913
+ # blocks) until the call is done. @api private
834
914
  def wait_for(c)
835
915
  until c.done?
836
916
  break unless @node.pump
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asterism
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katsuhiko Kageyama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-08 00:00:00.000000000 Z
11
+ date: 2026-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asterism-zenoh
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.0
19
+ version: 0.4.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.3.0
26
+ version: 0.4.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: msgpack
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -46,6 +46,7 @@ executables: []
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
+ - CHANGELOG.md
49
50
  - LICENSE
50
51
  - NOTICE
51
52
  - README.md