asterism 0.3.0 → 0.4.1

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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +149 -0
  3. data/NOTICE +12 -7
  4. data/README.md +305 -74
  5. data/data/msgs/LICENSE-BSD-3-Clause-tf2_msgs.txt +29 -0
  6. data/data/msgs/NOTICE +19 -13
  7. data/data/msgs/builtin_interfaces/msg/Duration.rb +4 -0
  8. data/data/msgs/builtin_interfaces/msg/Time.rb +4 -0
  9. data/data/msgs/diagnostic_msgs/msg/DiagnosticArray.rb +40 -0
  10. data/data/msgs/diagnostic_msgs/msg/DiagnosticStatus.rb +55 -0
  11. data/data/msgs/diagnostic_msgs/msg/KeyValue.rb +37 -0
  12. data/data/msgs/example_interfaces/srv/AddTwoInts.rb +7 -0
  13. data/data/msgs/geometry_msgs/msg/Accel.rb +4 -0
  14. data/data/msgs/geometry_msgs/msg/AccelStamped.rb +4 -0
  15. data/data/msgs/geometry_msgs/msg/Point.rb +5 -0
  16. data/data/msgs/geometry_msgs/msg/Point32.rb +5 -0
  17. data/data/msgs/geometry_msgs/msg/PointStamped.rb +4 -0
  18. data/data/msgs/geometry_msgs/msg/Pose.rb +4 -0
  19. data/data/msgs/geometry_msgs/msg/Pose2D.rb +5 -0
  20. data/data/msgs/geometry_msgs/msg/PoseStamped.rb +4 -0
  21. data/data/msgs/geometry_msgs/msg/PoseWithCovariance.rb +39 -0
  22. data/data/msgs/geometry_msgs/msg/Quaternion.rb +6 -0
  23. data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +4 -0
  24. data/data/msgs/geometry_msgs/msg/Transform.rb +4 -0
  25. data/data/msgs/geometry_msgs/msg/TransformStamped.rb +5 -0
  26. data/data/msgs/geometry_msgs/msg/Twist.rb +4 -0
  27. data/data/msgs/geometry_msgs/msg/TwistStamped.rb +4 -0
  28. data/data/msgs/geometry_msgs/msg/TwistWithCovariance.rb +39 -0
  29. data/data/msgs/geometry_msgs/msg/Vector3.rb +5 -0
  30. data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +4 -0
  31. data/data/msgs/geometry_msgs/msg/Wrench.rb +4 -0
  32. data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +4 -0
  33. data/data/msgs/nav_msgs/msg/MapMetaData.rb +52 -0
  34. data/data/msgs/nav_msgs/msg/OccupancyGrid.rb +44 -0
  35. data/data/msgs/nav_msgs/msg/Odometry.rb +49 -0
  36. data/data/msgs/nav_msgs/msg/Path.rb +40 -0
  37. data/data/msgs/rcl_interfaces/msg/Log.rb +64 -0
  38. data/data/msgs/rcl_interfaces/msg/Parameter.rb +39 -0
  39. data/data/msgs/rcl_interfaces/msg/ParameterEvent.rb +52 -0
  40. data/data/msgs/rcl_interfaces/msg/ParameterValue.rb +69 -0
  41. data/data/msgs/sensor_msgs/msg/BatteryState.rb +18 -0
  42. data/data/msgs/sensor_msgs/msg/CompressedImage.rb +43 -0
  43. data/data/msgs/sensor_msgs/msg/FluidPressure.rb +5 -0
  44. data/data/msgs/sensor_msgs/msg/Illuminance.rb +5 -0
  45. data/data/msgs/sensor_msgs/msg/Imu.rb +9 -0
  46. data/data/msgs/sensor_msgs/msg/JointState.rb +7 -0
  47. data/data/msgs/sensor_msgs/msg/LaserScan.rb +71 -0
  48. data/data/msgs/sensor_msgs/msg/MagneticField.rb +5 -0
  49. data/data/msgs/sensor_msgs/msg/NavSatFix.rb +9 -0
  50. data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +4 -0
  51. data/data/msgs/sensor_msgs/msg/PointCloud2.rb +68 -0
  52. data/data/msgs/sensor_msgs/msg/PointField.rb +53 -0
  53. data/data/msgs/sensor_msgs/msg/Range.rb +9 -0
  54. data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +5 -0
  55. data/data/msgs/sensor_msgs/msg/Temperature.rb +5 -0
  56. data/data/msgs/std_msgs/msg/Bool.rb +3 -0
  57. data/data/msgs/std_msgs/msg/Byte.rb +3 -0
  58. data/data/msgs/std_msgs/msg/ByteMultiArray.rb +4 -0
  59. data/data/msgs/std_msgs/msg/Char.rb +3 -0
  60. data/data/msgs/std_msgs/msg/ColorRGBA.rb +6 -0
  61. data/data/msgs/std_msgs/msg/Empty.rb +1 -0
  62. data/data/msgs/std_msgs/msg/Float32.rb +3 -0
  63. data/data/msgs/std_msgs/msg/Float32MultiArray.rb +4 -0
  64. data/data/msgs/std_msgs/msg/Float64.rb +3 -0
  65. data/data/msgs/std_msgs/msg/Float64MultiArray.rb +4 -0
  66. data/data/msgs/std_msgs/msg/Header.rb +4 -0
  67. data/data/msgs/std_msgs/msg/Int16.rb +3 -0
  68. data/data/msgs/std_msgs/msg/Int16MultiArray.rb +4 -0
  69. data/data/msgs/std_msgs/msg/Int32.rb +3 -0
  70. data/data/msgs/std_msgs/msg/Int32MultiArray.rb +4 -0
  71. data/data/msgs/std_msgs/msg/Int64.rb +3 -0
  72. data/data/msgs/std_msgs/msg/Int64MultiArray.rb +4 -0
  73. data/data/msgs/std_msgs/msg/Int8.rb +3 -0
  74. data/data/msgs/std_msgs/msg/Int8MultiArray.rb +4 -0
  75. data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +5 -0
  76. data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +4 -0
  77. data/data/msgs/std_msgs/msg/String.rb +3 -0
  78. data/data/msgs/std_msgs/msg/UInt16.rb +3 -0
  79. data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +4 -0
  80. data/data/msgs/std_msgs/msg/UInt32.rb +3 -0
  81. data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +4 -0
  82. data/data/msgs/std_msgs/msg/UInt64.rb +3 -0
  83. data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +4 -0
  84. data/data/msgs/std_msgs/msg/UInt8.rb +3 -0
  85. data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +4 -0
  86. data/data/msgs/tf2_msgs/msg/TFMessage.rb +35 -0
  87. data/data/msgs/visualization_msgs/msg/Marker.rb +132 -0
  88. data/data/msgs/visualization_msgs/msg/MarkerArray.rb +35 -0
  89. data/data/msgs/visualization_msgs/msg/MeshFile.rb +37 -0
  90. data/data/msgs/visualization_msgs/msg/UVCoordinate.rb +37 -0
  91. data/lib/asterism/cruby/objects.rb +25 -10
  92. data/lib/asterism/cruby/ros.rb +6 -4
  93. data/lib/asterism/cruby/zenoh.rb +190 -60
  94. data/lib/asterism/version.rb +1 -1
  95. data/lib/asterism.rb +2 -2
  96. data/mrblib/asterism.rb +112 -19
  97. data/mrblib/cdr.rb +5 -1
  98. data/mrblib/future.rb +11 -2
  99. data/mrblib/proxy.rb +13 -3
  100. data/mrblib/ros.rb +189 -44
  101. data/tools/asterism_msggen.rb +32 -5
  102. metadata +29 -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,
@@ -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
@@ -191,11 +209,14 @@ module Asterism
191
209
  # returns it (Asterism::ROS::GeometryMsgs::Twist). The file loads the
192
210
  # types it is made of first. A type already defined (loaded before, or
193
211
  # written in Ruby by the application) is returned without a file.
212
+ #
213
+ # The name is checked before it reaches the file system (it may come
214
+ # from the network): pkg/msg/Name or pkg/srv/Name, the package of
215
+ # a-z, 0-9 and _ starting with a letter, the type of letters and digits
216
+ # starting with a capital (the rules of ROS 2's interface names).
217
+ # Anything else raises ArgumentError.
194
218
  def self.require_type(name)
195
- parts = name.to_s.split("/")
196
- if parts.size != 3 || (parts[1] != "msg" && parts[1] != "srv")
197
- raise ArgumentError, "bad type name #{name.inspect} (want pkg/msg/Name or pkg/srv/Name)"
198
- end
219
+ parts = check_type_name(name)
199
220
  t = type_constant(parts[0], parts[2])
200
221
  return t if t
201
222
  found = nil
@@ -209,6 +230,68 @@ module Asterism
209
230
  t
210
231
  end
211
232
 
233
+ # "pkg/msg/Name" -> ["pkg", "msg", "Name"], or ArgumentError. By hand,
234
+ # byte by byte: the boards have no Regexp.
235
+ # @api private
236
+ def self.check_type_name(name)
237
+ s = name.to_s
238
+ parts = s.split("/")
239
+ # (split drops a trailing "/": the parts must give the name back)
240
+ ok = parts.size == 3 && parts.join("/") == s && (parts[1] == "msg" || parts[1] == "srv") &&
241
+ ros_name_part?(parts[0], false) && ros_name_part?(parts[2], true)
242
+ raise ArgumentError, "bad type name #{name.inspect} (want pkg/msg/Name or pkg/srv/Name)" unless ok
243
+ parts
244
+ end
245
+
246
+ # A package name ([a-z][a-z0-9_]*) or, with type true, a type name
247
+ # ([A-Z][A-Za-z0-9]*).
248
+ # @api private
249
+ def self.ros_name_part?(s, type)
250
+ n = s.bytesize
251
+ return false if n == 0
252
+ c = s.getbyte(0)
253
+ return false unless type ? (c >= 65 && c <= 90) : (c >= 97 && c <= 122)
254
+ i = 1
255
+ while i < n
256
+ c = s.getbyte(i)
257
+ ok = (c >= 97 && c <= 122) || (c >= 48 && c <= 57) ||
258
+ (type ? (c >= 65 && c <= 90) : c == 95)
259
+ return false unless ok
260
+ i += 1
261
+ end
262
+ true
263
+ end
264
+
265
+ # What the fields of a type and of every message type it is made of
266
+ # are: {ROS name => FIELD_TYPES}, the type first, each type once. The
267
+ # type is a generated type or its ROS name (the types are loaded with
268
+ # require_type); for a service, its Request and Response. Each entry of
269
+ # FIELD_TYPES is [name, base, kind, nested, capacity, string_capacity]
270
+ # (plain Strings, Integers and nil; see tools/asterism_msggen.rb):
271
+ # Asterism::ROS.field_types("sensor_msgs/msg/JointState")
272
+ # # => {"sensor_msgs/msg/JointState" => [["header", "std_msgs/msg/Header",
273
+ # # "scalar", "std_msgs/msg/Header", nil, nil],
274
+ # # ["name", "string", "sequence", nil, nil, nil], ...],
275
+ # # "std_msgs/msg/Header" => [...], "builtin_interfaces/msg/Time" => [...]}
276
+ # A type without FIELD_TYPES (written by hand, or generated before
277
+ # 0.4.1) raises UnknownType.
278
+ def self.field_types(type)
279
+ t = type_of(type)
280
+ queue = t.is_a?(::Class) ? [t] : [t::Request, t::Response]
281
+ out = {}
282
+ until queue.empty?
283
+ c = queue.shift
284
+ next if out.key?(c::ROS_NAME)
285
+ unless c.const_defined?(:FIELD_TYPES, false)
286
+ raise UnknownType, "#{c::ROS_NAME} has no FIELD_TYPES (generate it with asterism_msggen.rb of 0.4.1 or later)"
287
+ end
288
+ fts = c::FIELD_TYPES
289
+ out[c::ROS_NAME] = fts
290
+ fts.each { |f| queue << require_type(f[3]) if f[3] }
291
+ end
292
+ out
293
+ end
294
+
212
295
  # On mruby the file is evaluated in this VM (Kernel#eval): unlike
213
296
  # require, which runs each file in a Sandbox task of its own that stays
214
297
  # for the life of the VM, this keeps only the compiled code (measured in
@@ -252,8 +335,8 @@ module Asterism
252
335
  end
253
336
 
254
337
  # The base of the generated message types (Request / Response of a
255
- # service too). A type adds ROS_NAME, TYPE_NAME, TYPE_HASH, FIELDS, an
256
- # accessor per field, initialize(**fields) and the CDR steps
338
+ # service too). A type adds ROS_NAME, TYPE_NAME, TYPE_HASH, FIELDS,
339
+ # FIELD_TYPES (0.4.1 and later), an accessor per field, initialize(**fields) and the CDR steps
257
340
  # write(w, msg) / read(r); the rest is here.
258
341
  class Message
259
342
  # A message from nil (all defaults), a Hash of fields (Symbol or
@@ -381,22 +464,26 @@ module Asterism
381
464
  # the block, from node.poll. Returns the Subscription (close ends it).
382
465
  # The block runs on the update loop's stack: do not wait in it (no
383
466
  # service call, no proxy call); start a call_async and look at it
384
- # later instead.
467
+ # later instead. Without a block, the same as subscription: a
468
+ # Subscription to take from with each_pending.
385
469
  def subscribe(topic, type, qos: DEFAULT_QOS, depth: 16, &handler)
386
- raise ArgumentError, "subscribe needs a block (or use subscription)" unless handler
470
+ return subscription(topic, type, qos: qos, depth: depth) unless handler
387
471
  e = subscription(topic, type, qos: qos, depth: depth)
388
472
  e.handler = handler
389
473
  @handled << e
390
474
  e
391
475
  end
392
476
 
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)
477
+ # Calls the block every `seconds` (or every ms: milliseconds; the first
478
+ # time one period from now), from node.poll. Returns a Timer (cancel).
479
+ # It keeps the period; after a long stall it starts again from now
480
+ # instead of firing the missed times at once. As for subscribe: do not
481
+ # wait in the block.
482
+ def every(seconds = nil, ms: nil, &blk)
398
483
  raise ArgumentError, "every needs a block" unless blk
399
- t = ::Asterism::ROS::Timer.new(self, seconds, blk)
484
+ raise ArgumentError, "every: give the period once (seconds, or ms:)" if !seconds.nil? && !ms.nil?
485
+ raise ArgumentError, "every: a period is needed (seconds, or ms:)" if seconds.nil? && ms.nil?
486
+ t = ::Asterism::ROS::Timer.new(self, seconds, blk, ms)
400
487
  @timers << t
401
488
  t
402
489
  end
@@ -407,15 +494,17 @@ module Asterism
407
494
  end
408
495
 
409
496
  # 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)
497
+ # The request as keywords, as a Hash or message (positional or
498
+ # request:); the time limit as timeout: (seconds) or timeout_ms:. A
499
+ # client per service name is made on first use and kept.
500
+ def call(service, type, req = nil, request: nil, timeout: nil, timeout_ms: nil, **fields)
412
501
  @clients ||= {}
413
502
  c = @clients[service]
414
503
  if c.nil? || c.closed?
415
504
  c = client(service, type)
416
505
  @clients[service] = c
417
506
  end
418
- c.call(request, timeout_ms: timeout_ms, **fields)
507
+ c.call(req, request: request, timeout: timeout, timeout_ms: timeout_ms, **fields)
419
508
  end
420
509
 
421
510
  # Polls the session (Asterism::Zenoh::Session#poll), answers the
@@ -432,6 +521,7 @@ module Asterism
432
521
 
433
522
  # What a waiting service call polls: the session and the services, not
434
523
  # the subscribe / every blocks (those run from node.poll only).
524
+ # @api private
435
525
  def pump(steps = 8)
436
526
  ok = @session.poll(steps)
437
527
  i = 0
@@ -444,7 +534,7 @@ module Asterism
444
534
 
445
535
  # While loops, not each with a block: the application's blocks are
446
536
  # called from here, and nothing else should stand between them and
447
- # the update loop on the stack.
537
+ # the update loop on the stack. @api private
448
538
  def deliver
449
539
  i = 0
450
540
  while i < @handled.size
@@ -463,6 +553,7 @@ module Asterism
463
553
  end
464
554
  end
465
555
 
556
+ # @api private
466
557
  def fire_timers
467
558
  return if @timers.empty?
468
559
  now = ::Asterism::ROS.now_ms
@@ -480,7 +571,7 @@ module Asterism
480
571
  end
481
572
 
482
573
  # One message for one subscribe block, one timer firing (CRuby routes
483
- # what they raise to on_error).
574
+ # what they raise to on_error). @api private
484
575
  def handle(sub, msg, info)
485
576
  sub.handler.call(msg, info)
486
577
  end
@@ -508,6 +599,7 @@ module Asterism
508
599
  end
509
600
 
510
601
  # For the entities: [data key, liveliness token key] of a topic.
602
+ # @api private
511
603
  def entity_keys(kind, topic, type, qos)
512
604
  full = ::Asterism::ROS.resolve(topic, @namespace)
513
605
  data = "#{@domain}/#{::Asterism::ROS.strip_slashes(full)}/#{type::TYPE_NAME}/#{type::TYPE_HASH}"
@@ -535,7 +627,7 @@ module Asterism
535
627
  # msg: a message of the type, or a Hash of its fields (missing ones
536
628
  # take their defaults).
537
629
  def publish(msg)
538
- raise ::Asterism::Zenoh::Error, "publisher closed" if @token.nil?
630
+ raise ::Asterism::Zenoh::ClosedError, "publisher closed" if @token.nil?
539
631
  # Read once (a second thread may publish meanwhile on CRuby).
540
632
  seq = @sequence + 1
541
633
  @sequence = seq
@@ -566,7 +658,7 @@ module Asterism
566
658
  @type = type
567
659
  @topic_key, @token_key = node.entity_keys("MS", topic, type, qos)
568
660
  @errors = 0
569
- @sub = session.subscribe(@topic_key, depth)
661
+ @sub = session.subscribe(@topic_key, depth: depth)
570
662
  @token = session.liveliness(@token_key)
571
663
  end
572
664
 
@@ -591,6 +683,16 @@ module Asterism
591
683
  out.size
592
684
  end
593
685
 
686
+ # Messages waiting / received so far / dropped because the queue was
687
+ # full (the counters of the Zenoh subscriber underneath).
688
+ def pending
689
+ @sub.pending
690
+ end
691
+
692
+ def received
693
+ @sub.received
694
+ end
695
+
594
696
  def dropped
595
697
  @sub.dropped
596
698
  end
@@ -609,14 +711,16 @@ module Asterism
609
711
 
610
712
  # node.every: a block called every period, from node.poll.
611
713
  class Timer
612
- # period: seconds as given; fired: how many times it ran.
613
- attr_reader :period, :fired
714
+ # period: seconds (as given, or ms / 1000.0); period_ms: the same in
715
+ # milliseconds; fired: how many times it ran.
716
+ attr_reader :period, :period_ms, :fired
614
717
 
615
- def initialize(node, seconds, blk)
616
- @period_ms = (seconds * 1000).to_i
718
+ # @api private (node.every makes it)
719
+ def initialize(node, seconds, blk, ms = nil)
720
+ @period_ms = ms.nil? ? (seconds * 1000).round : ms.round
617
721
  raise ArgumentError, "the period must be positive" unless @period_ms > 0
618
722
  @node = node
619
- @period = seconds
723
+ @period = ms.nil? ? seconds : ms / 1000.0
620
724
  @blk = blk
621
725
  @next = ::Asterism::ROS.now_ms + @period_ms
622
726
  @fired = 0
@@ -659,7 +763,7 @@ module Asterism
659
763
  @service_key, @token_key = node.entity_keys("SS", service, type, qos)
660
764
  @handled = 0
661
765
  @errors = 0
662
- @queryable = session.queryable(@service_key, depth, complete: true)
766
+ @queryable = session.queryable(@service_key, depth: depth, complete: true)
663
767
  @token = session.liveliness(@token_key)
664
768
  end
665
769
 
@@ -739,6 +843,11 @@ module Asterism
739
843
  @nobody = false
740
844
  end
741
845
 
846
+ # took_ms in seconds (a Float; nil until done).
847
+ def took
848
+ @took_ms.nil? ? nil : @took_ms / 1000.0
849
+ end
850
+
742
851
  # True once the response came, the time ran out, or no server answered.
743
852
  def done?
744
853
  collect
@@ -746,19 +855,23 @@ module Asterism
746
855
  end
747
856
 
748
857
  # The response (type::Response); waits until done. Raises
749
- # Asterism::ROS::Timeout when there was none.
858
+ # Asterism::ROS::TimeoutError when there was none, and
859
+ # Asterism::Zenoh::ClosedError when the session closed (1.0:
860
+ # Asterism::Disconnected).
750
861
  def value
751
862
  @client.wait_for(self) unless done?
752
863
  if @response.nil?
753
- raise ::Asterism::Zenoh::Error, "session is closed" if @client.session_closed?
864
+ raise ::Asterism::Zenoh::ClosedError, "session is closed" if @client.session_closed?
754
865
  if @nobody
755
- raise ::Asterism::ROS::Timeout, "no answer from #{@client.service_name} (nobody serves it)"
866
+ raise ::Asterism::ROS::TimeoutError, "no answer from #{@client.service_name} (nobody serves it)"
756
867
  end
757
- raise ::Asterism::ROS::Timeout, "no answer from #{@client.service_name} within #{@timeout_ms} ms"
868
+ raise ::Asterism::ROS::TimeoutError,
869
+ "no answer from #{@client.service_name} within #{@timeout_ms / 1000.0} s (#{@timeout_ms} ms)"
758
870
  end
759
871
  @response
760
872
  end
761
873
 
874
+ # @api private
762
875
  def collect
763
876
  return if @finished
764
877
  g = @get
@@ -788,6 +901,8 @@ module Asterism
788
901
  # The client side of a service (node.client). Sends each request as a
789
902
  # get on the service key, plus the SC token.
790
903
  class Client
904
+ # The time limit of a call, in seconds and in milliseconds.
905
+ DEFAULT_TIMEOUT = 2.0
791
906
  DEFAULT_TIMEOUT_MS = 2000
792
907
  # Pause between polls while a call waits (ms).
793
908
  WAIT_STEP_MS = 2
@@ -805,11 +920,14 @@ module Asterism
805
920
  @token = session.liveliness(@token_key)
806
921
  end
807
922
 
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
923
+ # Sends the request and returns a Call at once. The request: a
924
+ # type::Request or a Hash (positional, or request:), or the fields as
925
+ # keywords. The time limit: timeout: (seconds) or timeout_ms: (2 s).
926
+ # A request with a field named timeout or timeout_ms is best given as
927
+ # request: { ... } (see README, Reserved keywords).
928
+ def call_async(req = nil, request: nil, timeout: nil, timeout_ms: nil, **fields)
929
+ raise ::Asterism::Zenoh::ClosedError, "client closed" if @token.nil?
930
+ req, timeout_ms = request_and_limit("Client#call", req, request, timeout, timeout_ms, fields)
813
931
  payload = @type::Request.encode(req)
814
932
  # Read once into a local: the request and its Call carry the same
815
933
  # number even when another thread (CRuby) calls through this client
@@ -817,20 +935,47 @@ module Asterism
817
935
  seq = @sequence + 1
818
936
  @sequence = seq
819
937
  att = ::Asterism::ROS::Attachment.new(seq, ::Asterism::ROS.now_ns, @gid)
820
- g = @session.get(@service_key, timeout_ms, nil, payload, attachment: att.encode,
938
+ g = @session.get(@service_key, timeout_ms: timeout_ms, payload: payload, attachment: att.encode,
821
939
  target: :all_complete, consolidation: :none)
822
940
  ::Asterism::ROS::Call.new(self, g, seq, timeout_ms)
823
941
  end
824
942
 
825
943
  # Sends the request and waits for the response (polling the node, so
826
944
  # 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
945
+ # Asterism::ROS::TimeoutError when no response came in time.
946
+ def call(req = nil, request: nil, timeout: nil, timeout_ms: nil, **fields)
947
+ call_async(req, request: request, timeout: timeout, timeout_ms: timeout_ms, **fields).value
948
+ end
949
+
950
+ # The request and the time limit (ms) from call's arguments. A
951
+ # keyword that is also a field of the request type is ambiguous: a
952
+ # field named timeout stays a field (as before 0.4.0), timeout_ms
953
+ # stays the time limit, and both warn (request: { ... } says which).
954
+ # @api private
955
+ def request_and_limit(where, req, request, timeout, timeout_ms, fields)
956
+ raise ArgumentError, "#{where}: give the request once (positional or request:)" if !req.nil? && !request.nil?
957
+ req = request if req.nil?
958
+ if req.nil?
959
+ names = @type::Request::FIELDS
960
+ if !timeout.nil? && names.include?(:timeout)
961
+ ::Asterism.deprecated("#{where} with a request field named timeout as a keyword",
962
+ "request: { timeout: ... } (timeout: is the time limit in seconds)",
963
+ "it is taken as the field here, as before 0.4.0")
964
+ fields[:timeout] = timeout
965
+ timeout = nil
966
+ end
967
+ if !timeout_ms.nil? && names.include?(:timeout_ms)
968
+ ::Asterism.deprecated("#{where} with a request field named timeout_ms as a keyword",
969
+ "request: { timeout_ms: ... } and timeout: for the time limit",
970
+ "timeout_ms: is taken as the time limit here")
971
+ end
972
+ req = fields
973
+ end
974
+ [req, ::Asterism.time_ms(where, timeout, timeout_ms, nil, DEFAULT_TIMEOUT_MS)]
830
975
  end
831
976
 
832
977
  # Polls the node's session and services (not its subscribe / every
833
- # blocks) until the call is done.
978
+ # blocks) until the call is done. @api private
834
979
  def wait_for(c)
835
980
  until c.done?
836
981
  break unless @node.pump
@@ -11,11 +11,12 @@
11
11
  #
12
12
  # Each generated file defines one type (a message, or a service with its
13
13
  # Request and Response) under Asterism::ROS::<PackageInCamelCase>, with its
14
- # fields and defaults, ROS_NAME, TYPE_NAME (the DDS name rmw_zenoh uses),
15
- # TYPE_HASH (RIHS01, computed here) and the CDR conversion in both directions
16
- # on top of Asterism::CDR. A file loads the types it is made of through
17
- # Asterism::ROS.require_type first. The output layout mirrors the ROS names:
18
- # <out>/<pkg>/msg/<Name>.rb and <out>/<pkg>/srv/<Name>.rb.
14
+ # fields and defaults, FIELD_TYPES (what each field is), ROS_NAME,
15
+ # TYPE_NAME (the DDS name rmw_zenoh uses), TYPE_HASH (RIHS01, computed here)
16
+ # and the CDR conversion in both directions on top of Asterism::CDR. A file
17
+ # loads the types it is made of through Asterism::ROS.require_type first.
18
+ # The output layout mirrors the ROS names: <out>/<pkg>/msg/<Name>.rb and
19
+ # <out>/<pkg>/srv/<Name>.rb.
19
20
  #
20
21
  # The type hash follows the type description rules of ROS 2 Jazzy
21
22
  # (rosidl_generator_type_description, REP 2011): the type and every type it
@@ -578,6 +579,7 @@ module AsterismMsgGen
578
579
  out << " TYPE_NAME = #{dds_name(m).inspect}\n"
579
580
  out << " TYPE_HASH = #{hash.inspect}\n"
580
581
  out << " FIELDS = [#{fs.map { |f| ":#{f.name}" }.join(', ')}]\n"
582
+ out << field_types_lit(fs)
581
583
  m.constants.each do |c|
582
584
  out << " #{c.name} = #{lit(c.value, c.base)}\n"
583
585
  end
@@ -616,6 +618,31 @@ module AsterismMsgGen
616
618
  PRIMITIVES.fetch(f.base)[1]
617
619
  end
618
620
 
621
+ # What a field is, for tools that look at a type without its .msg (the
622
+ # same plain values on mruby and CRuby: Arrays, Strings, Integers, nil):
623
+ # [name, base, kind, nested, capacity, string_capacity]
624
+ # base: the .msg type ("float64", "string", "uint8", ...) or, for a
625
+ # message, its full name; kind: "scalar", "array" (T[N]),
626
+ # "bounded_sequence" (T[<=N]) or "sequence" (T[]); nested: the full name
627
+ # of the message type, or nil; capacity: N of an array or a bounded
628
+ # sequence, else nil; string_capacity: N of string<=N, else nil.
629
+ FIELD_KINDS = { nil => "scalar", fixed: "array", bounded: "bounded_sequence", unbounded: "sequence" }.freeze
630
+
631
+ def self.field_type(f)
632
+ cap = f.array == :fixed || f.array == :bounded ? f.size : nil
633
+ [f.name, f.base, FIELD_KINDS.fetch(f.array), f.nested, cap, f.string_max]
634
+ end
635
+
636
+ def field_types_lit(fs)
637
+ return " FIELD_TYPES = []\n" if fs.empty?
638
+ out = +" FIELD_TYPES = [\n"
639
+ fs.each_with_index do |f, i|
640
+ items = Emitter.field_type(f).map { |v| v.nil? ? "nil" : v.inspect }
641
+ out << " [#{items.join(', ')}]#{i < fs.size - 1 ? ',' : ''}\n"
642
+ end
643
+ out << " ]\n"
644
+ end
645
+
619
646
  def byte_kind?(f)
620
647
  BYTE_KINDS.include?(f.base)
621
648
  end
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.1
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,13 +46,18 @@ 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
52
53
  - data/msgs/LICENSE-Apache-2.0.txt
54
+ - data/msgs/LICENSE-BSD-3-Clause-tf2_msgs.txt
53
55
  - data/msgs/NOTICE
54
56
  - data/msgs/builtin_interfaces/msg/Duration.rb
55
57
  - data/msgs/builtin_interfaces/msg/Time.rb
58
+ - data/msgs/diagnostic_msgs/msg/DiagnosticArray.rb
59
+ - data/msgs/diagnostic_msgs/msg/DiagnosticStatus.rb
60
+ - data/msgs/diagnostic_msgs/msg/KeyValue.rb
56
61
  - data/msgs/example_interfaces/srv/AddTwoInts.rb
57
62
  - data/msgs/geometry_msgs/msg/Accel.rb
58
63
  - data/msgs/geometry_msgs/msg/AccelStamped.rb
@@ -62,24 +67,38 @@ files:
62
67
  - data/msgs/geometry_msgs/msg/Pose.rb
63
68
  - data/msgs/geometry_msgs/msg/Pose2D.rb
64
69
  - data/msgs/geometry_msgs/msg/PoseStamped.rb
70
+ - data/msgs/geometry_msgs/msg/PoseWithCovariance.rb
65
71
  - data/msgs/geometry_msgs/msg/Quaternion.rb
66
72
  - data/msgs/geometry_msgs/msg/QuaternionStamped.rb
67
73
  - data/msgs/geometry_msgs/msg/Transform.rb
68
74
  - data/msgs/geometry_msgs/msg/TransformStamped.rb
69
75
  - data/msgs/geometry_msgs/msg/Twist.rb
70
76
  - data/msgs/geometry_msgs/msg/TwistStamped.rb
77
+ - data/msgs/geometry_msgs/msg/TwistWithCovariance.rb
71
78
  - data/msgs/geometry_msgs/msg/Vector3.rb
72
79
  - data/msgs/geometry_msgs/msg/Vector3Stamped.rb
73
80
  - data/msgs/geometry_msgs/msg/Wrench.rb
74
81
  - data/msgs/geometry_msgs/msg/WrenchStamped.rb
82
+ - data/msgs/nav_msgs/msg/MapMetaData.rb
83
+ - data/msgs/nav_msgs/msg/OccupancyGrid.rb
84
+ - data/msgs/nav_msgs/msg/Odometry.rb
85
+ - data/msgs/nav_msgs/msg/Path.rb
86
+ - data/msgs/rcl_interfaces/msg/Log.rb
87
+ - data/msgs/rcl_interfaces/msg/Parameter.rb
88
+ - data/msgs/rcl_interfaces/msg/ParameterEvent.rb
89
+ - data/msgs/rcl_interfaces/msg/ParameterValue.rb
75
90
  - data/msgs/sensor_msgs/msg/BatteryState.rb
91
+ - data/msgs/sensor_msgs/msg/CompressedImage.rb
76
92
  - data/msgs/sensor_msgs/msg/FluidPressure.rb
77
93
  - data/msgs/sensor_msgs/msg/Illuminance.rb
78
94
  - data/msgs/sensor_msgs/msg/Imu.rb
79
95
  - data/msgs/sensor_msgs/msg/JointState.rb
96
+ - data/msgs/sensor_msgs/msg/LaserScan.rb
80
97
  - data/msgs/sensor_msgs/msg/MagneticField.rb
81
98
  - data/msgs/sensor_msgs/msg/NavSatFix.rb
82
99
  - data/msgs/sensor_msgs/msg/NavSatStatus.rb
100
+ - data/msgs/sensor_msgs/msg/PointCloud2.rb
101
+ - data/msgs/sensor_msgs/msg/PointField.rb
83
102
  - data/msgs/sensor_msgs/msg/Range.rb
84
103
  - data/msgs/sensor_msgs/msg/RelativeHumidity.rb
85
104
  - data/msgs/sensor_msgs/msg/Temperature.rb
@@ -113,6 +132,11 @@ files:
113
132
  - data/msgs/std_msgs/msg/UInt64MultiArray.rb
114
133
  - data/msgs/std_msgs/msg/UInt8.rb
115
134
  - data/msgs/std_msgs/msg/UInt8MultiArray.rb
135
+ - data/msgs/tf2_msgs/msg/TFMessage.rb
136
+ - data/msgs/visualization_msgs/msg/Marker.rb
137
+ - data/msgs/visualization_msgs/msg/MarkerArray.rb
138
+ - data/msgs/visualization_msgs/msg/MeshFile.rb
139
+ - data/msgs/visualization_msgs/msg/UVCoordinate.rb
116
140
  - lib/asterism.rb
117
141
  - lib/asterism/cruby.rb
118
142
  - lib/asterism/cruby/objects.rb
@@ -130,6 +154,7 @@ homepage: https://github.com/ruby-asterism/asterism
130
154
  licenses:
131
155
  - MIT
132
156
  - Apache-2.0
157
+ - BSD-3-Clause
133
158
  metadata:
134
159
  homepage_uri: https://github.com/ruby-asterism/asterism
135
160
  source_code_uri: https://github.com/ruby-asterism/asterism