asterism 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -0
- data/README.md +277 -62
- data/lib/asterism/cruby/objects.rb +25 -10
- data/lib/asterism/cruby/ros.rb +6 -4
- data/lib/asterism/cruby/zenoh.rb +428 -72
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +2 -2
- data/mrblib/asterism.rb +112 -19
- data/mrblib/cdr.rb +5 -1
- data/mrblib/future.rb +11 -2
- data/mrblib/proxy.rb +13 -3
- data/mrblib/ros.rb +121 -41
- metadata +5 -4
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,
|
|
@@ -58,7 +64,7 @@ module Asterism
|
|
|
58
64
|
def self.mangle(name)
|
|
59
65
|
s = name.to_s
|
|
60
66
|
return "%" if s.empty? || s == "/"
|
|
61
|
-
out = ""
|
|
67
|
+
out = "".dup
|
|
62
68
|
i = 0
|
|
63
69
|
n = s.bytesize
|
|
64
70
|
while i < n
|
|
@@ -104,7 +110,7 @@ module Asterism
|
|
|
104
110
|
# rmw_zenoh hashes the entity's liveliness key the same way (with XXH3);
|
|
105
111
|
# receivers only use the GID to tell publishers apart.
|
|
106
112
|
def self.gid_for(key)
|
|
107
|
-
out = ""
|
|
113
|
+
out = "".dup
|
|
108
114
|
lane = 0
|
|
109
115
|
while lane < 4
|
|
110
116
|
h = 0x811c9dc5 ^ (lane * 0x9e3779b9 & 0xffffffff)
|
|
@@ -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
|
-
|
|
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 < ::
|
|
201
|
+
class UnknownType < ::Asterism::Error; end
|
|
184
202
|
|
|
185
203
|
def self.type_path
|
|
186
204
|
TYPE_PATH
|
|
@@ -225,7 +243,7 @@ module Asterism
|
|
|
225
243
|
|
|
226
244
|
# Asterism::ROS::<Pkg>::<Name>, or nil.
|
|
227
245
|
def self.type_constant(pkg, name)
|
|
228
|
-
mod_name = ""
|
|
246
|
+
mod_name = "".dup # a fresh String: literals may be frozen (Ruby 4)
|
|
229
247
|
pkg.split("_").each do |w|
|
|
230
248
|
mod_name << w.byteslice(0, 1).upcase << w.byteslice(1, w.bytesize - 1) if w.bytesize > 0
|
|
231
249
|
end
|
|
@@ -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
|
-
|
|
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` (
|
|
394
|
-
# from node.poll. Returns a Timer (cancel).
|
|
395
|
-
# a long stall it starts again from now
|
|
396
|
-
# times at once. As for subscribe: do not
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
411
|
-
|
|
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::
|
|
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;
|
|
613
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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::
|
|
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::
|
|
799
|
+
raise ::Asterism::Zenoh::ClosedError, "session is closed" if @client.session_closed?
|
|
754
800
|
if @nobody
|
|
755
|
-
raise ::Asterism::ROS::
|
|
801
|
+
raise ::Asterism::ROS::TimeoutError, "no answer from #{@client.service_name} (nobody serves it)"
|
|
756
802
|
end
|
|
757
|
-
raise ::Asterism::ROS::
|
|
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
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
|
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::
|
|
828
|
-
def call(
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|