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/README.md CHANGED
@@ -9,11 +9,11 @@ repository holds the pure Ruby layers, written once for every Ruby:
9
9
  |---|---|
10
10
  | `mrblib/` | The layers: remote objects (`Asterism`), `Asterism::CDR`, `Asterism::ROS`. The source of truth for both forms below |
11
11
  | `mrbgem.rake` | The mrbgem `picoruby-asterism` (mruby / PicoRuby): compiles `mrblib/` |
12
- | `asterism.gemspec`, `lib/` | The CRuby gem `asterism`: `lib/asterism.rb` loads `mrblib/` as it is, `lib/asterism/cruby.rb` adds what CRuby needs, `lib/asterism/cruby/` the Ruby-like API (CRuby only) |
13
- | `data/msgs/` | ROS 2 message types generated from ROS 2 Jazzy's definitions (Apache-2.0, see NOTICE) |
12
+ | `asterism.gemspec`, `lib/` | The CRuby gem `asterism`: `lib/asterism.rb` loads `mrblib/` as it is, `lib/asterism/cruby.rb` adds what CRuby needs, `lib/asterism/cruby/` the CRuby API (CRuby only) |
13
+ | `data/msgs/` | ROS 2 message types generated from ROS 2 Jazzy's definitions (Apache-2.0; tf2_msgs BSD-3-Clause; see NOTICE) |
14
14
  | `tools/` | The type generator `asterism_msggen.rb`, the Jazzy definitions it reads, `ros2_types.rb` (refreshes them from a ROS 2 image) |
15
- | `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_api_*.rb` (the Ruby-like API) |
16
- | `examples/` | CRuby: `node.rb` (objects with a board), `node_polled.rb` (the same with the polled API of the boards), `ros2_talker.rb` (topics, a timer, services with ROS 2), `zenoh.rb` (plain Zenoh) |
15
+ | `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_api_*.rb` (the CRuby API) |
16
+ | `examples/` | CRuby: `node.rb` (objects with a board), `node_polled.rb` (the same with the portable API of the boards), `ros2_talker.rb` (topics, a timer, services with ROS 2), `zenoh.rb` (plain Zenoh) |
17
17
 
18
18
  The Zenoh binding underneath is a separate gem with the same Ruby API on
19
19
  each Ruby:
@@ -31,15 +31,46 @@ in asterism-zenoh's
31
31
  The ones added in 0.3.0 are CRuby only: the shared layer (`mrblib/`) does
32
32
  not use them.
33
33
 
34
- Neither gem is on rubygems.org yet (version 0.3.0 builds from these
35
- repositories with `rake gem`).
34
+ Both gems are on rubygems.org (`gem install asterism` installs
35
+ asterism-zenoh too). What changed in each version is in
36
+ [CHANGELOG.md](CHANGELOG.md); 0.4.0 is the first step toward 1.0
37
+ ([docs/api_review.md](docs/api_review.md)) and only adds and deprecates
38
+ (see [Deprecations and 1.0](#deprecations-and-10)).
39
+
40
+ ## Which API?
41
+
42
+ Asterism has two APIs over the same layers. Both are in the CRuby gem;
43
+ the boards have the first one only.
44
+
45
+ | You write for | Use | Why |
46
+ |---|---|---|
47
+ | A board, or code that must run on both | the **portable API** ([below](#the-portable-api): `Asterism.poll`, `node.poll`, `each_pending`) | the only one on the boards; nothing runs behind your back |
48
+ | CRuby only (tools, servers, scripts) | the **CRuby API** ([The CRuby API](#the-cruby-api): `Asterism.connect { }`, `Asterism::Zenoh.open { }`, `Asterism::ROS.connect { }`) | blocks, a receiving thread, Enumerators, `Data` values |
49
+ | CRuby, but you own the loop (a game loop, a test) | the portable API | same as the boards |
50
+
51
+ Entry points and what they give:
52
+
53
+ | Call | Gives | Underneath |
54
+ |---|---|---|
55
+ | `Asterism::Zenoh::Session.open` | a `Session` (portable) | zenoh-c / zenoh-pico |
56
+ | `Asterism::Zenoh.open` | a `Zenoh::Connection` (CRuby) | `c.session` is the `Session` |
57
+ | `Asterism.connect` | `Asterism` itself (portable; one connection per process or VM), or a `Net` to the block (CRuby) | a `Session` of its own |
58
+ | `Asterism::ROS::Node.new(session, ...)` | a `Node` (portable) | the `Session` given |
59
+ | `Asterism::ROS.connect` | a `ROS::Connection` (CRuby) | `ros.zenoh` is the `Zenoh::Connection` |
60
+
61
+ Units: a time with no unit in its name is seconds (`timeout:`,
62
+ `connect_timeout:`, `check_timeout:`, `node.every(0.5)`, `DEFAULT_TIMEOUT`);
63
+ anything else says its unit (`timeout_ms:`, `every(ms: 500)`, `took_ms`,
64
+ `DEFAULT_TIMEOUT_MS`, `stamp_ns`). Both units are accepted everywhere a
65
+ time is taken, on both APIs and every Ruby; giving one twice raises
66
+ `ArgumentError`.
36
67
 
37
68
  ## Using it
38
69
 
39
70
  CRuby (needs CRuby 3.2+ and a C compiler):
40
71
 
41
72
  ```
42
- gem install asterism # also installs asterism-zenoh (~> 0.3.0) and msgpack
73
+ gem install asterism # also installs asterism-zenoh (~> 0.4.0) and msgpack
43
74
  ruby -e 'require "asterism"'
44
75
  ```
45
76
 
@@ -52,7 +83,7 @@ build but not yet tested. To install from the repositories instead:
52
83
 
53
84
  ```
54
85
  (cd ../asterism-zenoh && rake gem) && rake gem
55
- gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.3.0.gem pkg/asterism-0.3.0.gem
86
+ gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.0.gem pkg/asterism-0.4.0.gem
56
87
  ```
57
88
 
58
89
  From the working trees, without installing:
@@ -78,13 +109,15 @@ these repositories.
78
109
  rake # every suite
79
110
  rake test:msgs # generator, type hashes, CDR, bundled types (CRuby only; no Zenoh, no docker)
80
111
  rake test:objects # objects and Asterism::ROS between CRuby sessions over a local peer link
81
- rake test:api # the Ruby-like API (blocks, receiving threads, Enumerators)
112
+ rake test:api # the CRuby API (blocks, receiving threads, Enumerators)
82
113
  ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
83
114
  ```
84
115
 
85
116
  `test:objects` and `test:api` need asterism-zenoh: built next to this repository
86
117
  (`../asterism-zenoh`, its `rake compile`), or `ASTERISM_ZENOH_DIR`, or the
87
- installed gem.
118
+ installed gem. The tests run with deprecated calls raising
119
+ (`Asterism.deprecations = :raise`); the tests of the old forms switch it
120
+ back around themselves.
88
121
 
89
122
  ## Examples
90
123
 
@@ -102,11 +135,11 @@ ruby examples/ros2_talker.rb --router tcp/192.0.2.2:7447 [--service NAME]
102
135
  ruby examples/zenoh.rb --router tcp/192.0.2.2:7447 --name pc1
103
136
  ```
104
137
 
105
- ## The Ruby-like API (CRuby only)
138
+ ## The CRuby API
106
139
 
107
140
  On CRuby, `require "asterism"` also brings a layer with blocks, a receiving
108
141
  thread, Enumerators and pattern matching (`lib/asterism/cruby/`). It is
109
- built on the polled API below, which stays as it is: code written for a
142
+ built on the portable API below, which stays as it is: code written for a
110
143
  board still runs unchanged on CRuby, and code using this layer does not run
111
144
  on a board.
112
145
 
@@ -124,7 +157,7 @@ Asterism::Zenoh.open("tcp/192.0.2.2:7447") do |s| # closed when the block ends
124
157
  s.run # receive on this thread until Ctrl-C
125
158
  end
126
159
 
127
- # asterism-zenoh 0.3.0 (CRuby only)
160
+ # asterism-zenoh 0.3.0 and later (CRuby only)
128
161
  Asterism::Zenoh.open("tls/192.0.2.2:7447",
129
162
  config: { "transport/link/tls/root_ca_certificate" => "ca.pem" }) do |s|
130
163
  pub = s.publisher("home/pc/temp", encoding: "text/plain", priority: :data_high)
@@ -144,33 +177,32 @@ end
144
177
 
145
178
  | Call | Notes |
146
179
  |---|---|
147
- | `Asterism::Zenoh.open(locator = nil, mode:, listen:, scouting:, timestamping:, config:, config_file:, interval: 0.002)` | `Session.open` wrapped in a `Connection`. With a block: yields it, closes it after (also on an exception), returns the block's value |
148
- | `Connection.new(session)` | wraps a session opened with the polled API |
180
+ | `Asterism::Zenoh.open(locator = nil, mode:, listen:, scouting:, timestamping:, config:, config_file:, connect_timeout:, interval: 0.002)` | `Session.open` wrapped in a `Connection`. With a block: yields it, closes it after (also on an exception), returns the block's value |
181
+ | `Connection.new(session)` | wraps a session opened with the portable API |
149
182
  | `s.put(key, payload, attachment: nil, **opts)` | a payload that is not a String is sent as `to_s`. opts: `encoding:`, `priority:`, `congestion_control:`, `express:`, `reliability:`, `timestamp:`, `allowed_destination:` (asterism-zenoh's `Session#put`) |
150
183
  | `s.delete(key, **opts)` | subscribers get a `Sample` of kind `:delete` |
151
- | `s.publisher(key, **opts)` | a `Publisher`: `put(payload, attachment:, encoding:, timestamp:)`, `delete`, `matching?`, `on_matching { \|listening\| }` (on the receiving thread), `close` |
184
+ | `s.publisher(key, **opts)` | a `Publisher`: `put(payload, attachment:, encoding:, timestamp:)`, `delete`, `matching?`, `on_matching { \|listening\| }` (on the receiving thread), `close`. With a block: yields it and closes it after |
152
185
  | `s.advanced_publisher(key, cache:, sample_miss_detection:, publisher_detection:, **opts)` | the same `Publisher`, keeping the last samples for late subscribers (ROS 2's transient local) |
153
- | `s.advanced_subscriber(key, depth: 16, history:, recovery:, **opts) { \|sample\| }` | a `Subscription` that also gets the publishers' history; `on_publisher { \|key, alive\| }`, `on_miss { \|miss\| }` |
154
- | `s.querier(key, timeout: 2.0, **opts)` | a `Querier`: `get(params:, payload:, attachment:, encoding:, errors: false)` like `s.get`, `matching?`, `on_matching`, `close` |
186
+ | `s.advanced_subscriber(key, depth: 16, history:, recovery:, query_timeout:, **opts) { \|sample\| }` | a `Subscription` that also gets the publishers' history; `on_publisher { \|key, alive\| }`, `on_miss { \|miss\| }` |
187
+ | `s.querier(key, timeout: 2.0, **opts)` (or `timeout_ms:`) | a `Querier`: `get(params:, payload:, attachment:, encoding:, errors: false)` like `s.get`, `matching?`, `on_matching`, `close` |
155
188
  | `s.on_transport(history: false) { \|ev\| }` / `s.on_link { \|ev\| }` | a `TransportEvent` / `LinkEvent` (`added?` / `removed?`, `zid`, ...) on the receiving thread; returns an `Events` (`close`) |
156
189
  | `s.peer_zids`, `router_zids`, `transports`, `links`, `new_timestamp`, `declare_keyexpr(key)` | as in asterism-zenoh |
157
190
  | `s.subscribe(key, depth: 16) { \|sample\| }` | the block gets each `Sample` on the receiving thread. Returns a `Subscription` (`close` ends it) |
158
- | `s.subscribe(key, depth: 16)` | a `Subscription` to take from: `each(timeout: nil)` waits for samples (an Enumerator without a block; Enumerable), `each_pending` gives what is there now as Samples, `pending` / `received` / `dropped` / `closed?` / `close` |
191
+ | `s.subscribe(key, depth: 16)` | a `Subscription` to take from: `each(timeout: nil)` waits for samples (an Enumerator without a block; Enumerable), `each_sample` gives what is there now as Samples (`each_pending`, its old name here, is deprecated: in the portable API `each_pending` gives Arrays), `pending` / `received` / `dropped` / `closed?` / `close` |
159
192
  | `s.queryable(key, depth: 16, complete: false) { \|q\| }` | each query on the receiving thread, finished when the block returns. `q.reply(payload)` answers on the queryable's key (when it has no wildcard), `q.reply(key, payload, attachment:)` as before. Without a block, `each(timeout:)` yields the queries |
160
- | `s.get(key, timeout: 2.0, params:, payload:, attachment:, target:, consolidation:, errors: false, **opts)` | an Enumerator of `Reply`; each iteration sends the get again. With a block: yields each, returns their number. `errors: true` also yields the error replies (`reply.error?`); opts: `encoding:`, `priority:`, `congestion_control:`, `express:`, `accept_replies:` |
161
- | `s.liveliness(key)` | a token (`close`) |
162
- | `s.liveliness_watch(key) { \|key, alive\| }` | on the receiving thread; the tokens alive now come first. Without a block, `each` yields `Liveliness` (`key`, `alive?`) |
163
- | `s.liveliness_get(key, timeout: 2.0)` | the keys alive now (an Array) |
193
+ | `s.get(key, timeout: 2.0, params:, payload:, attachment:, target:, consolidation:, errors: false, depth: 1024, **opts)` | a `GetEnumerator` of `Reply`; each iteration sends the get again, and `dropped` / `received` / `errors` are those of the last one. With a block: yields each, returns their number. `depth:` the replies kept until taken (see [Queues](#queues)). `errors: true` also yields the error replies (`reply.error?`; left out by default, as `each_reply` does: look there when "nobody answers"); `consolidation: :none` keeps every reply (zenoh's own default is `:auto`). opts: `encoding:`, `priority:`, `congestion_control:`, `express:`, `accept_replies:`, `timeout_ms:` |
194
+ | `s.liveliness(key)` | a token (`close`). With a block: withdrawn after the block |
195
+ | `s.liveliness_watch(key, depth: 1024) { \|key, alive\| }` | on the receiving thread; the tokens alive now come first, in one burst. Without a block, `each` yields `Liveliness` (`key`, `alive?`). `pending` / `received` / `dropped` / `closed?` / `close` |
196
+ | `s.liveliness_get(key, timeout: 2.0, depth: 1024)` | the keys alive now (a `KeyList`: an Array with `dropped` and `received`; waits) |
164
197
  | `s.start` / `s.stop` / `s.running?` | receive on a thread of its own |
165
198
  | `s.run` | receive on this thread until `stop`, the connection closing, or Ctrl-C (returns nil) |
166
- | `s.on_error { \|error, where\| }` | what the blocks raise (`StandardError`); without a handler it is printed with `warn`. Receiving goes on |
167
- | `s.session`, `zid`, `peers`, `poll`, `closed?`, `close` | |
199
+ | `s.on_error { \|error, where\| }` | what the blocks raise (`StandardError`); without a handler it is printed with `warn`. Receiving goes on. `on_error` replaces the handler; every other `on_*` adds one |
200
+ | `s.session`, `zid`, `connection_count`, `poll`, `closed?`, `close` | (`peers` is the deprecated name of `connection_count`) |
168
201
 
169
202
  `Sample`, `Reply` (`key`, `payload`, `attachment`, and since 0.3.0 `kind`,
170
203
  `encoding`, `timestamp` and the rest; `text` is the payload as UTF-8; both
171
204
  are asterism-zenoh's) and `Liveliness` are `Data` values, so they work with pattern
172
- matching: `case sample in {key: %r{/temp\z}, payload:}`. Times are seconds
173
- here (the polled API counts milliseconds).
205
+ matching: `case sample in {key: %r{/temp\z}, payload:}`.
174
206
 
175
207
  ### Objects
176
208
 
@@ -191,12 +223,38 @@ end
191
223
  | `Asterism.connect(...) { \|net\| }` | yields an `Asterism::Net`, `Asterism.close` when the block ends; returns the block's value. Without a block, as before |
192
224
  | `Asterism.net` | the `Net` of the connection made without a block (nil when not connected) |
193
225
  | `net.expose` / `unexpose` / `exposed` | as `Asterism.expose`. The methods run on the receiving thread, in `run`, or in whatever polls |
194
- | `net[path, timeout: nil]` | a proxy (`timeout` in seconds) |
195
- | `net.each(pattern = "**")` | the proxies alive now; an Enumerator without a block |
196
- | `net.on_join { \|node\| }` / `net.on_leave { \|node\| }` | `Asterism.on_join` / `on_leave` of the polled API (the same blocks, told from `Asterism.poll`), here on the receiving thread; what they raise goes to `on_error` |
226
+ | `net[path, timeout: 2.0]` | a proxy (`timeout` in seconds, or `timeout_ms:`) |
227
+ | `net.each(pattern = "**")` | the proxies alive now; an Enumerator without a block. With a block it returns their number (1.0: `self`, as Ruby's `each` does; `net.count` is the number) |
228
+ | `net.on_join { \|node\| }` / `net.on_leave { \|node\| }` | `Asterism.on_join` / `on_leave` of the portable API (the same blocks, told from `Asterism.poll`), here on the receiving thread; what they raise goes to `on_error`. `net.off_join(proc)` / `off_leave(proc)` remove one |
197
229
  | `net.start` / `stop` / `run` / `running?` / `on_error` | as for Zenoh. A method of an exposed object that raises is answered to its caller as a `RemoteError`, as before; `on_error` gets what `on_join` / `on_leave` raise |
198
230
  | `net.nodes`, `node_id`, `app`, `connected?`, `lost_reason`, `poll`, `close` | |
199
231
 
232
+ ### Over TLS (a router on the internet)
233
+
234
+ `config:` of `Asterism.connect` (and of `Asterism::Zenoh.open`,
235
+ `Asterism::ROS.connect`) goes to the zenoh session: a Hash of zenoh's
236
+ configuration keys, or a JSON5 String (`File.read("client.json5")`). For a
237
+ router that asks for a client certificate (mutual TLS):
238
+
239
+ ```ruby
240
+ tls = {
241
+ "transport/link/tls/root_ca_certificate" => "certs/ca.pem", # the CA that signed the router
242
+ "transport/link/tls/connect_certificate" => "certs/cruby.pem", # this node's certificate
243
+ "transport/link/tls/connect_private_key" => "certs/cruby.key",
244
+ "transport/link/tls/enable_mtls" => true
245
+ }
246
+ Asterism.connect("tls/router.example.org:7448", node: "cruby", app: "demo", config: tls) do |net|
247
+ net["fmruby-aaaaaa/demo/info"].status # a board on the router's other side
248
+ end
249
+ ```
250
+
251
+ The router checks the certificate against its CA, and its ACL can tell the
252
+ nodes apart by the certificate's common name. The name in the locator must
253
+ be one the router's certificate is valid for (zenoh checks it; it is
254
+ `verify_name_on_connect`). `examples/node.rb --ca --cert --key` does the same.
255
+ The boards (zenoh-pico on ESP32) have no TLS: they stay on a router in their
256
+ own network, which connects to the outside one.
257
+
200
258
  ### ROS 2
201
259
 
202
260
  ```ruby
@@ -216,12 +274,12 @@ end
216
274
  | Call | Notes |
217
275
  |---|---|
218
276
  | `Asterism::ROS.connect(locator = nil, domain: 0, type_path: nil, mode:, listen:)` | opens a session; yields an `Asterism::ROS::Connection` and closes it after. `type_path:` adds directories to `TYPE_PATH` |
219
- | `ros.node(name, namespace: "/", enclave: "/")` | an `Asterism::ROS::Node` (every method of the polled API) with the calls below |
220
- | `node.subscribe(topic, type) { \|msg, info\| }` | the polled API's (run from `node.poll`), here on the receiving thread; `info` is the `Attachment` or nil. Without a block: a subscription whose `each(timeout: nil)` waits for `[msg, info]` (Enumerable) |
277
+ | `ros.node(name, namespace: "/", enclave: "/")` | an `Asterism::ROS::Node` (every method of the portable API) with the calls below |
278
+ | `node.subscribe(topic, type) { \|msg, info\| }` | the portable API's (run from `node.poll`), here on the receiving thread; `info` is the `Attachment` or nil. Without a block: a subscription whose `each(timeout: nil)` waits for `[msg, info]` (Enumerable) |
221
279
  | `node.topic(topic, type).each` | subscribes for the iteration and withdraws after it (`first(n)`, `break`, the time running out) |
222
- | `node.every(seconds) { }` | the polled API's timer (run from `node.poll`), here on the receiving thread; returns a `Timer` (`cancel`, `fired`) |
280
+ | `node.every(seconds) { }` / `node.every(ms: 500) { }` | the portable API's timer (run from `node.poll`), here on the receiving thread; returns a `Timer` (`cancel`, `fired`) |
223
281
  | `node.service(name, type) { \|req\| response }` | answered while the connection spins (or from `node.poll`). What the block raises goes to `on_error`, and that request gets no answer |
224
- | `node.call(name, type, request = nil, timeout: 2.0, **fields)` | waits for the response |
282
+ | `node.call(name, type, request = nil, request:, timeout: 2.0, **fields)` | waits for the response. Here `timeout:` is always the time limit (see [Reserved keywords](#reserved-keywords)) |
225
283
  | `ros.spin` (`run`) / `start` / `stop` / `running?` / `on_error` / `close` | |
226
284
  | `ros.zenoh` | the `Asterism::Zenoh::Connection` underneath (same receiving thread) |
227
285
 
@@ -242,7 +300,7 @@ that is not bundled: generate it with `tools/asterism_msggen.rb -o DIR`
242
300
  block that takes long delays everything else that connection receives**:
243
301
  hand long work to a thread or a Queue of your own.
244
302
  - zenoh-c never calls Ruby: it fills the queues, and the receiving thread
245
- empties them every 2 ms (`interval:`) with the polled API.
303
+ empties them every 2 ms (`interval:`) with the portable API.
246
304
  - A tick of the receiving thread holds the connection's lock (a `Monitor`;
247
305
  for the object layer, which is one connection per process,
248
306
  `Asterism::LOCK`). The application's calls into the same layer take it
@@ -257,6 +315,23 @@ that is not bundled: generate it with `tools/asterism_msggen.rb -o DIR`
257
315
  thread ends by itself, and waiting calls raise as before.
258
316
  - Enumerators (`each`, `get`, `topic`) wait on the thread that iterates
259
317
  them.
318
+ - **Do not wait in a block.** On CRuby a block that calls and waits (a
319
+ proxy call, `node.call`) works, but delays every other block of that
320
+ connection; on a board it can overflow the stack ([Stack](#stack)).
321
+
322
+ What may be shared between threads:
323
+
324
+ | Object | Use from several threads? |
325
+ |---|---|
326
+ | `Asterism::Zenoh::Session` and its subscribers, queryables, gets, publishers, queriers (CRuby) | yes; each queued entry goes to exactly one taker |
327
+ | `Zenoh::Connection` | yes; its blocks run one at a time on the receiving thread |
328
+ | An Enumerator (`Subscription#each`, `Watch#each`, `get`, `topic`) | one thread per iteration |
329
+ | The object layer (`Asterism.*`, `Net`, proxies, `Future`) | yes; serialized by `Asterism::LOCK` (one connection per process) |
330
+ | `ROS::Connection` and the nodes it makes | yes (its runner's lock) |
331
+ | A `ROS::Node` made with `Node.new` (portable API) on CRuby | **no**: one thread, as on the boards |
332
+ | A session opened before `fork` | not in the child: its calls raise `Zenoh::ClosedError`; connect after fork |
333
+ | The boards | one thread (the application's update loop) |
334
+ | Ractors | not supported |
260
335
 
261
336
  ## License
262
337
 
@@ -264,9 +339,11 @@ MIT (LICENSE) for Asterism's code. The ROS 2 definitions in
264
339
  `tools/ros2_jazzy/` (with the package.xml of their packages), the types
265
340
  generated from them in `data/msgs/` and the fixtures computed by ROS 2's
266
341
  tools in `test/msgs/` are under the Apache License 2.0 (NOTICE,
267
- `data/msgs/NOTICE`, `data/msgs/LICENSE-Apache-2.0.txt`). Each generated
268
- type names its package and license at its top. The gem `asterism` is
269
- therefore `MIT` and `Apache-2.0`, each for its own files.
342
+ `data/msgs/NOTICE`, `data/msgs/LICENSE-Apache-2.0.txt`), except those of
343
+ tf2_msgs, which are under the BSD 3-Clause License
344
+ (`data/msgs/LICENSE-BSD-3-Clause-tf2_msgs.txt`). Each generated type names
345
+ its package and license at its top. The gem `asterism` is therefore `MIT`,
346
+ `Apache-2.0` and `BSD-3-Clause`, each for its own files.
270
347
 
271
348
  Not in this repository: the gems Asterism depends on are not bundled.
272
349
  `msgpack` (CRuby) and `asterism-zenoh` (the CRuby Zenoh binding, which in
@@ -276,10 +353,10 @@ RubyGems, and on mruby / PicoRuby the Zenoh binding is the separate mrbgem
276
353
 
277
354
  ---
278
355
 
279
- # The API
356
+ # The portable API
280
357
 
281
- The same on every Ruby (the polled API; CRuby adds the layer described in
282
- [The Ruby-like API](#the-ruby-like-api-cruby-only)). The examples use PicoRuby's `sleep_ms`; on CRuby
358
+ The same on every Ruby (CRuby adds the layer described in
359
+ [The CRuby API](#the-cruby-api)). The examples use PicoRuby's `sleep_ms`; on CRuby
283
360
  write `sleep`. When PicoRuby's `Machine` is there it is used for the clock
284
361
  and the short pauses while waiting; otherwise `Time` and `sleep`. Besides
285
362
  `Asterism::Zenoh`, the layers need a `MessagePack` module with `pack` /
@@ -298,6 +375,7 @@ apu.play("t120 o4 cdefg") # runs there, returns its value
298
375
  apu.respond_to?(:play) # => true (from the exposed list)
299
376
  f = apu.async.play("cde") # does not wait
300
377
  f.done?; f.value
378
+ Asterism["fmruby-bbbbbb/demo/apu", timeout: 5.0].play("c") # a longer time limit
301
379
  Asterism.each("*/*/apu") { |a| a.stop } # every apu alive now
302
380
  Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
303
381
  ```
@@ -306,25 +384,124 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
306
384
 
307
385
  | Call | Returns | Raises / notes |
308
386
  |---|---|---|
309
- | `Asterism.connect(locator, node:, app:, mode: nil, listen: nil)` | `Asterism` | `locator`, `mode:`, `listen:` go to `Asterism::Zenoh::Session.open` (client of a router by default; `mode: :peer` with or without `listen:` without one). `node:` is this machine's ID, `app:` the application's name (one key chunk each: no `/ * $ ? #`, not starting with `@`; else `ArgumentError`). `Disconnected` when it cannot connect; `Error` when objects of the same `<node>/<app>` are already alive (a second copy of the application), or when already connected. Waits up to about 1.5 s for that check (a router answers at once). |
387
+ | `Asterism.connect(locator, node:, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: 1.5)` | `Asterism` | `locator`, `mode:`, `listen:` (and `config:` / `connect_timeout:` when given, CRuby only: TLS and the rest of zenoh's configuration; the boards raise `ArgumentError`) go to `Asterism::Zenoh::Session.open` (client of a router by default; `mode: :peer` with or without `listen:` without one). `node:` is this machine's ID, `app:` the application's name (one key chunk each: no `/ * $ ? #`, not starting with `@`; else `ArgumentError`). `Disconnected` when it cannot connect; `Error` when objects of the same `<node>/<app>` are already alive (a second copy of the application), or when already connected. Waits up to `check_timeout:` seconds for that check (a router answers at once; give more over a slow relay). Returns the module itself: on a board the module is the connection (one per application). The default is plain text without authentication; see [Over TLS](#over-tls-a-router-on-the-internet). |
310
388
  | `Asterism.expose(name, obj, methods:)` | `"<node>/<app>/<name>"` | `methods:` is an Array of names, or a Hash `name => number of arguments` (checked before the call; `-1` or the Array form: any). `ArgumentError` when `obj` has no such public method. Exposing a name again replaces it. |
311
389
  | `Asterism.unexpose(name)` | true / false | |
312
390
  | `Asterism.poll` | true / false | Call from the update loop: polls Zenoh, answers the calls that came in, follows who is alive and calls the `on_join` / `on_leave` blocks. `false` once the connection is closed or lost. |
313
- | `Asterism.on_join { \|node\| }` / `Asterism.on_leave { \|node\| }` | `Asterism` | Another node appeared (its node token or one of its objects) / is gone (no token and no object left). The nodes there already join on the first polls after connecting; when the connection is lost, every node known until then leaves (on the next `Asterism.poll`). The blocks run from `Asterism.poll` only, never from the polling inside a waiting call; what they raise comes out of `Asterism.poll`. `Asterism.close` forgets them (a lost connection does not). |
314
- | `Asterism[path, timeout_ms = 2000]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). Nothing is sent until a method is called. |
315
- | `proxy.<method>(*args, **kw)` | the remote return value | Waits for the answer (`timeout_ms`). `RemoteError` when it raised there or could not be called (not exposed: `NoMethodError`; wrong number of arguments: `ArgumentError`; no such object: `NameError`), `Timeout` when no answer came in time (also at once when nobody answers that key), `Disconnected`, `EncodeError` (before anything is sent) for a value MessagePack cannot carry. A block cannot be sent (`ArgumentError`). |
391
+ | `Asterism.on_join { \|node\| }` / `Asterism.on_leave { \|node\| }` | `Asterism` | Another node appeared (its node token or one of its objects) / is gone (no token and no object left). The nodes there already join on the first polls after connecting; when the connection is lost, every node known until then leaves (on the next `Asterism.poll`). The blocks run from `Asterism.poll` only, never from the polling inside a waiting call; what they raise comes out of `Asterism.poll`. Each call adds a block; `Asterism.off_join(proc)` / `off_leave(proc)` remove one; `Asterism.close` forgets them (a lost connection does not). |
392
+ | `Asterism[path, timeout: 2.0]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). `timeout:` (seconds) or `timeout_ms:` is the time limit of its calls. Nothing is sent until a method is called. |
393
+ | `proxy.<method>(*args, **kw)` | the remote return value | Waits for the answer (the proxy's time limit). `RemoteError` when it raised there or could not be called (not exposed: `NoMethodError`; wrong number of arguments: `ArgumentError`; no such object: `NameError`), `TimeoutError` when no answer came in time (also at once when nobody answers that key), `Disconnected`, `EncodeError` (before anything is sent) for a value MessagePack cannot carry. A block cannot be sent (`ArgumentError`). |
316
394
  | `proxy.async.<method>(...)` | `Future` | Sends and returns at once. `EncodeError` / `Disconnected` are raised here. |
317
395
  | `future.done?` | true / false | Never waits (`Asterism.poll` moves it on). True once the answer came, the time ran out or the connection closed. |
318
- | `future.value` | the return value | Waits (polling) if not done; raises like a waiting call. `future.took_ms`: time to the answer. |
319
- | `proxy.respond_to?(name)` / `proxy.methods` | true / false, `[Symbol]` | From the object's meta (fetched once, `proxy.asterism_refresh` forgets it). `respond_to?` is false when the object does not answer; `methods` raises then. |
396
+ | `future.value` | the return value | Waits (polling) if not done; raises like a waiting call. `future.took` (seconds) / `took_ms`: time to the answer. |
397
+ | `proxy.respond_to?(name)` / `proxy.remote_methods` | true / false, `[Symbol]` | From the object's meta (fetched once, `proxy.asterism_refresh` forgets it). `respond_to?` is false when the object does not answer; `remote_methods` raises then. (`proxy.methods` returns the same with a deprecation warning; from 1.0 it is `Object#methods`.) |
320
398
  | `proxy.asterism_meta` / `proxy.asterism_path` | Hash, String | `{"methods" => [[name, arity], ...]}` |
321
- | `Asterism.each(pattern = "**") { \|proxy\| }` | count (Array without a block) | The exposed objects alive now (this application's own included) whose `<node>/<app>/<object>` matches; `*` is one chunk, `**` any number. |
399
+ | `Asterism.each(pattern = "**") { \|proxy\| }` | count (Array without a block) | The exposed objects alive now (this application's own included) whose `<node>/<app>/<object>` matches; `*` is one chunk, `**` any number. 1.0 returns the proxies from the block form instead of their number (Ruby's `each` returns its receiver); write `Asterism.each(pattern).size` for the count. |
322
400
  | `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
323
401
  | `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
324
402
  | `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
325
403
 
326
- Errors: `Asterism::Error` (base, a `StandardError`), `EncodeError`, `RemoteError`
327
- (`remote_class`, `remote_message`), `Timeout`, `Disconnected`.
404
+ ## Errors
405
+
406
+ Every error Asterism raises is an `Asterism::Error` (a `StandardError`):
407
+
408
+ ```
409
+ Asterism::Error (the Zenoh bindings define it)
410
+ ├── Asterism::Zenoh::Error (binding failures; #code is zenoh's result code)
411
+ │ └── Asterism::Zenoh::ClosedError (the session is closed or its connection was lost)
412
+ ├── Asterism::Disconnected (object layer: the connection is closed or lost)
413
+ ├── Asterism::TimeoutError (no answer in time; old name Asterism::Timeout)
414
+ │ └── Asterism::ROS::TimeoutError (old name Asterism::ROS::Timeout)
415
+ ├── Asterism::EncodeError
416
+ ├── Asterism::RemoteError (remote_class, remote_message)
417
+ ├── Asterism::ROS::UnknownType
418
+ ├── Asterism::CDR::DecodeError
419
+ └── Asterism::DeprecationError (only with Asterism.deprecations = :raise)
420
+ ```
421
+
422
+ The old names `Asterism::Timeout` and `Asterism::ROS::Timeout` still work
423
+ (they warn once). Inside `module Asterism`, `Timeout` is Ruby's `Timeout`
424
+ module again. The ROS layer raises `Zenoh::ClosedError` when its session
425
+ is closed (1.0: `Disconnected`, as the object layer).
426
+
427
+ ## Defaults
428
+
429
+ | What | Default | Changed by |
430
+ |---|---|---|
431
+ | A proxy call | 2 s | `Asterism[path, timeout:]` / `timeout_ms:`; `DEFAULT_TIMEOUT` (s), `DEFAULT_TIMEOUT_MS` |
432
+ | A service call | 2 s | `client.call(..., timeout:)` / `timeout_ms:`; `Client::DEFAULT_TIMEOUT` |
433
+ | `get`, `liveliness_get`, a querier | 2 s | `timeout:` / `timeout_ms:` |
434
+ | Connecting | 3 s | `connect_timeout:` (CRuby); `Zenoh::CONNECT_TIMEOUT_MS` (fixed at build time on the boards) |
435
+ | A send that cannot go out | 3 s | `Zenoh::SEND_TIMEOUT_MS` |
436
+ | The duplicate check of `Asterism.connect` | 1.5 s | `check_timeout:`; `CHECK_TIMEOUT` |
437
+ | Queue depth (subscriptions, queryables) | 16 (services 8; the object layer's queryable 32) | `depth:` |
438
+ | Queue depth of gets, liveliness gets and liveliness watches | CRuby 1024, the boards 16 (the object layer's watch: 1024 on CRuby, 64 on the boards) | `depth:`; `Zenoh::DEFAULT_GET_DEPTH`, `Zenoh::DEFAULT_WATCH_DEPTH` |
439
+ | A full queue | drops the oldest (counted in `dropped`); the CRuby API warns once | `depth:` |
440
+ | Pause between polls of a waiting call | 2 ms | `WAIT_STEP_MS`, `Client::WAIT_STEP_MS` |
441
+ | Receiving interval of the CRuby API | 2 ms | `interval:` |
442
+ | Calls waiting inside each other | 4 | `MAX_NESTING` |
443
+ | Encoding depth of a value | 16 | `Codec::MAX_DEPTH` |
444
+ | `get` consolidation | `:none` (every reply; zenoh's own default is `:auto`, but services and the object layer want every reply) | `consolidation:` |
445
+
446
+ ### Queues
447
+
448
+ Everything received waits in a bounded queue until taken; when it is full
449
+ the oldest entry goes and is counted in `dropped`. A router answers a
450
+ wildcard get or liveliness get, and a new liveliness watch, with
451
+ everything at once, so on CRuby those three queues hold 1024 by default
452
+ (they grow as entries come; nothing is allocated up front). On the boards
453
+ they stay at 16: a get's queue is allocated in full when it is sent (in
454
+ PSRAM on ESP-IDF) and a watch's comes from the VM's pool; pass `depth:`
455
+ when a wildcard can match more.
456
+
457
+ On CRuby, a get (`get`, `Querier#get`, `liveliness_get`), a liveliness
458
+ watch or a subscription of the CRuby API whose depth was left at the
459
+ default warns once (`asterism: get demo/**: 3 replies dropped, ...`) when
460
+ something was dropped; with `depth:` given it stays quiet, and `dropped`
461
+ tells. The object layer warns once when its own watch dropped changes
462
+ (then `Asterism.each` and `Asterism.nodes` may miss objects).
463
+ `Asterism.warn_once(obj, message)` is the helper.
464
+
465
+ Every `each_*` of the portable API (`each_pending`, `each_reply`,
466
+ `each_result`) takes what is there now, without waiting, and returns an
467
+ Array without a block (it drains the queue; an Enumerator would take only
468
+ when iterated).
469
+
470
+ ## Deprecations and 1.0
471
+
472
+ 0.4.0 adds and deprecates; nothing that worked in 0.3.0 stops working. A
473
+ deprecated call warns once per name (`warn` on CRuby, so it goes to
474
+ `$stderr`; on the boards `warn` or `puts`, once per VM).
475
+ `Asterism.deprecations = :raise` (or the environment variable
476
+ `ASTERISM_DEPRECATIONS=raise`) raises `Asterism::DeprecationError` instead,
477
+ for CI; `:silent` turns them off.
478
+
479
+ | Deprecated (0.4.0) | Use | In 1.0 |
480
+ |---|---|---|
481
+ | `Asterism[path, 2000]` (the time as a positional argument) | `Asterism[path, timeout: 2.0]` or `timeout_ms: 2000` | removed |
482
+ | A Float there (`Asterism[path, 2.0]` waits 2 ms) | `timeout: 2.0` | removed (warns with its own message now) |
483
+ | `session.get(key, ms, params, payload)`, `session.liveliness_get(key, ms)` | `timeout:` / `timeout_ms:`, `params:`, `payload:` | removed (a positional depth stays) |
484
+ | `session.peers`, `connection.peers` | `connection_count` | removed |
485
+ | `Asterism::Timeout`, `Asterism::ROS::Timeout` | `TimeoutError` | kept as aliases through 1.x |
486
+ | `proxy.methods` (the remote list) | `proxy.remote_methods` | `Object#methods` again |
487
+ | `Connection::Subscription#each_pending` (CRuby API; it gave Samples) | `each_sample` | removed |
488
+ | `q.reply(payload)` where the query's key differs from the queryable's own plain key | `q.reply(key, payload)` | answers on the queryable's key (as the CRuby API does) |
489
+ | `client.call(field: ...)` with a field named `timeout` / `timeout_ms` | `request: { ... }` (or a Hash) | the keyword is the time limit (`ArgumentError` when ambiguous) |
490
+
491
+ Also changing in 1.0, without a warning now: `Asterism.each { }` and
492
+ `net.each { }` return the proxies / `self` instead of a count; the ROS
493
+ layer raises `Disconnected` when its session is closed; the internal
494
+ methods (marked `@api private` in the source) are hidden.
495
+
496
+ ### Reserved keywords
497
+
498
+ `client.call` and `node.call` take the request's fields as keywords, next
499
+ to `request:`, `timeout:` and `timeout_ms:`. A request type with a field
500
+ of one of those names cannot be given that field as a keyword without
501
+ ambiguity: give the request as `request: { timeout: 5 }` (or a Hash /
502
+ message as the first argument) and the time limit as `timeout:`. In 0.4.0
503
+ the ambiguous keyword warns; on the portable API a `timeout:` field stays
504
+ a field, as before.
328
505
 
329
506
  ### When the connection is lost
330
507
 
@@ -409,15 +586,22 @@ nested type (`geometry_msgs/msg/Twist`) at start-up took 0.6 KB more.
409
586
  - The node token `asterism/<node>` is shared by every application of that
410
587
  node; when one of them closes, the others still list the node through
411
588
  their objects, but a watcher may see the node token go away.
412
- - No authentication: a trusted LAN is assumed.
413
- - Pattern matching on the boards: PicoRuby's compiler (checked on Family
414
- mruby's application VM) runs `case/in` with `deconstruct_keys`, nested
415
- hash patterns, array patterns, guards, alternatives, ranges, pins and
416
- `**rest`, but not two forms: a class as the value in a hash pattern
417
- (`in {x: Float}` does not match) and, inside a block, binding a variable
418
- of the enclosing method (`v = nil; list.each { |m| case m in {a: v} ... }`
419
- leaves it nil). Put the `case` in a method of its own, as in the example
420
- above.
589
+ - No authentication by default (plain TCP): a trusted LAN is assumed, or
590
+ TLS through a router (CRuby; [Over TLS](#over-tls-a-router-on-the-internet)).
591
+ - One object-layer connection per process (per VM on the boards).
592
+ - Pattern matching on the boards: `case/in` with `deconstruct_keys`, nested
593
+ hash patterns, classes and ranges as values, array and find patterns,
594
+ guards, alternatives, pins of a local of the same scope and binding from
595
+ inside a block work with the compiler of mruby-compiler2 `a2c72afb` or
596
+ later (PicoRuby master), and in Family mruby, which carries the two fixes
597
+ on its older PicoRuby. An older PicoRuby compares a hash pattern's value
598
+ the wrong way round (a class or a range never matches, a literal matches
599
+ any value) and does not bind an outer variable from inside a block;
600
+ there, match on the key alone and test the value in Ruby. Still
601
+ different on Family mruby's PicoRuby: `^x` of an outer local inside a
602
+ block, `^(expr)`, `Const[...]` and `**rest` next to another key. The full
603
+ list of what the boards' Ruby can and cannot do is in
604
+ [docs/ruby_profile.md](docs/ruby_profile.md).
421
605
 
422
606
  ## ROS 2 (rmw_zenoh): `Asterism::ROS` and `Asterism::CDR`
423
607
 
@@ -442,7 +626,7 @@ loop do
442
626
  end
443
627
 
444
628
  # or with blocks, run from node.poll (in the update loop):
445
- node.every(1) { pub << { data: "tick" } }
629
+ node.every(1) { pub << { data: "tick" } } # or every(ms: 1000)
446
630
  node.subscribe("/cmd_vel", "geometry_msgs/msg/Twist") { |msg, _info| drive(msg) }
447
631
  loop { node.poll }
448
632
 
@@ -462,7 +646,7 @@ node.service("/fmruby/add_two_ints", add) { |req| { sum: req.a + req.b } }
462
646
  cli = node.client("/add_two_ints", add)
463
647
  loop do
464
648
  node.poll # session.poll, then answers the requests
465
- res = cli.call(a: 2, b: 3) # waits (polling), raises Asterism::ROS::Timeout
649
+ res = cli.call(a: 2, b: 3) # waits (polling), raises Asterism::ROS::TimeoutError
466
650
  puts res.sum
467
651
  c = cli.call_async(a: 2, b: 3) # returns at once
468
652
  # ... later updates:
@@ -476,15 +660,15 @@ end
476
660
  | `node.publisher(topic, type, qos: DEFAULT_QOS)` | `Publisher` | `topic` absolute, or relative to the namespace. `type` is a generated type or its ROS name (`"geometry_msgs/msg/Twist"`, loaded with `require_type`); the same for subscriptions, services and clients. Declares the publisher token (`ros2 topic list`). |
477
661
  | `pub.publish(msg)` / `pub << msg` | nil / `pub` | `msg` is a message of the type or a Hash of its fields. Puts the CDR payload with rmw_zenoh's attachment (sequence number, time, GID). |
478
662
  | `node.subscription(topic, type, qos: DEFAULT_QOS, depth: 16)` | `Subscription` | Subscribes and declares the subscription token. |
479
- | `sub.each_pending { \|msg, info\| }` | count | `info` is an `Attachment` (`sequence`, `stamp_ns`, `gid`) or nil. Samples that are not valid CDR are skipped and counted in `sub.errors`. |
663
+ | `sub.each_pending { \|msg, info\| }` | count | `info` is an `Attachment` (`sequence`, `stamp_ns`, `gid`) or nil. Samples that are not valid CDR are skipped and counted in `sub.errors`. `sub.pending` / `received` / `dropped` count as the Zenoh subscriber's do. |
480
664
  | `node.service(name, type, qos: DEFAULT_QOS, depth: 8) { \|req\| response }` | `Service` | Serves `name` (`ros2 service list`). The block gets a `type::Request` and returns a `type::Response` or a Hash of its fields. It runs from `node.poll` (or `service.handle_pending`), never behind the application. A request without rmw_zenoh's attachment, or that does not decode, is counted in `service.errors` and gets no answer. An exception from the block ends that request without an answer and is raised from `node.poll`. `service.handled`: answered so far. |
481
665
  | `node.poll(steps = 8)` | true / false | `session.poll(steps)`, then answers the requests waiting for this node's services, then gives what came to the `subscribe` blocks and fires the `every` timers that are due. Returns what `session.poll` returns. A waiting `client.call` polls only the session and the services (`node.pump`), not the blocks. |
482
- | `node.subscribe(topic, type, qos: DEFAULT_QOS, depth: 16) { \|msg, info\| }` | `Subscription` | `node.subscription` whose messages (decoded, with their `Attachment` or nil) go to the block, from `node.poll`. `sub.close` ends it. Do not wait in the block (see [Stack](#stack)). |
483
- | `node.every(seconds) { }` | `Timer` | Calls the block every `seconds` (a Float works), the first time one period from now, from `node.poll` (so no more often than the update loop polls). It keeps the period; after a long stall it starts again from now instead of firing the missed times at once. `timer.cancel`, `timer.fired` (count), `timer.period`. Do not wait in the block. |
666
+ | `node.subscribe(topic, type, qos: DEFAULT_QOS, depth: 16) { \|msg, info\| }` | `Subscription` | `node.subscription` whose messages (decoded, with their `Attachment` or nil) go to the block, from `node.poll`. `sub.close` ends it. Do not wait in the block (see [Stack](#stack)). Without a block it is `node.subscription`. |
667
+ | `node.every(seconds) { }` / `node.every(ms: 500) { }` | `Timer` | Calls the block every `seconds` (a Float works) or `ms:` milliseconds, the first time one period from now, from `node.poll` (so no more often than the update loop polls). It keeps the period; after a long stall it starts again from now instead of firing the missed times at once. `timer.cancel`, `timer.fired` (count), `timer.period` (s), `timer.period_ms`. Do not wait in the block. |
484
668
  | `node.client(name, type, qos: DEFAULT_QOS)` | `Client` | Declares the client token. |
485
- | `client.call(request = nil, timeout_ms: 2000, **fields)` | `type::Response` | Sends the request (a `type::Request`, a Hash, or the fields as keywords) and waits, polling the node (its services keep answering). `Asterism::ROS::Timeout` when no response came in time, at once when nobody serves the name; `Asterism::Zenoh::Error` when the session closed. |
486
- | `client.call_async(request = nil, timeout_ms: 2000, **fields)` | `Call` | Sends and returns at once. `call.done?` never waits (`node.poll` moves it on); `call.value` waits and returns the response or raises like `call`; `call.response` (nil until it came), `call.took_ms`, `call.sequence`. |
487
- | `node.call(name, type, request = nil, timeout_ms: 2000, **fields)` | `type::Response` | `client.call` through a client made on first use and kept per name. |
669
+ | `client.call(request = nil, request: nil, timeout: 2.0, **fields)` | `type::Response` | Sends the request (a `type::Request` or a Hash, positional or `request:`, or the fields as keywords) and waits, polling the node (its services keep answering). The time limit: `timeout:` (seconds) or `timeout_ms:`. `Asterism::ROS::TimeoutError` when no response came in time, at once when nobody serves the name; `Asterism::Zenoh::ClosedError` when the session closed. See [Reserved keywords](#reserved-keywords). |
670
+ | `client.call_async(...)` | `Call` | The same arguments; sends and returns at once. `call.done?` never waits (`node.poll` moves it on); `call.value` waits and returns the response or raises like `call`; `call.response` (nil until it came), `call.took` (s) / `took_ms`, `call.sequence`. |
671
+ | `node.call(name, type, request = nil, request:, timeout:, **fields)` | `type::Response` | `client.call` through a client made on first use and kept per name. |
488
672
  | `node.close` / `pub.close` / `sub.close` / `service.close` / `client.close` | nil | Withdraws the tokens (they also go when the session closes). `node.close` closes everything the node made. |
489
673
  | `Asterism::ROS.require_type(name)` | the type | Loads `<pkg>/<msg\|srv>/<Name>.rb` from `Asterism::ROS::TYPE_PATH` (`["/usr/share/asterism/msgs"]`; add directories to it) and returns `Asterism::ROS::<Pkg>::<Name>`. A type already loaded (or defined in Ruby) is returned at once. `UnknownType` when there is no file. |
490
674
  | `Asterism::CDR::Writer` / `Reader` | | Plain CDR with the 4-byte header, aligned from the end of the header: `bool int8 uint8 int16 uint16 int32 uint32 int64 uint64 float32 float64 string`, `array(kind, v, fixed, max)`, `bytes`, `structs(type, ...)`. Writes little endian; reads either order. |
@@ -556,11 +740,51 @@ std_msgs (Bool, Byte, Char, String, Empty, the integer and float types,
556
740
  Header, ColorRGBA, MultiArrayDimension / Layout and every *MultiArray),
557
741
  builtin_interfaces (Time, Duration), geometry_msgs (Vector3, Point, Point32,
558
742
  Quaternion, Pose, Pose2D, Twist, Accel, Transform, Wrench and their
559
- *Stamped), sensor_msgs (Imu, BatteryState, Temperature, Range,
560
- MagneticField, Illuminance, FluidPressure, RelativeHumidity, JointState,
561
- NavSatStatus, NavSatFix), example_interfaces/srv/AddTwoInts. The list is
562
- `tools/bundled_types.txt`. Nothing of them is compiled into the mrbgem: an
563
- application loads what it uses, and each file loads the types it is made of.
743
+ *Stamped, PoseWithCovariance, TwistWithCovariance), sensor_msgs (Imu,
744
+ BatteryState, Temperature, Range, MagneticField, Illuminance,
745
+ FluidPressure, RelativeHumidity, JointState, NavSatStatus, NavSatFix,
746
+ LaserScan, PointField, PointCloud2, CompressedImage), nav_msgs (Odometry,
747
+ Path, MapMetaData, OccupancyGrid), diagnostic_msgs (DiagnosticArray,
748
+ DiagnosticStatus, KeyValue), tf2_msgs/TFMessage, visualization_msgs
749
+ (Marker, MarkerArray, MeshFile, UVCoordinate), rcl_interfaces (Log for
750
+ `/rosout`, ParameterEvent, Parameter, ParameterValue),
751
+ example_interfaces/srv/AddTwoInts. The list is `tools/bundled_types.txt`
752
+ (84 files, 0.23 MB). Nothing of them is compiled into the mrbgem: an
753
+ application loads what it uses, and each file loads the types it is made
754
+ of.
755
+
756
+ ### Field types
757
+
758
+ Each generated message has `FIELD_TYPES` next to `FIELDS`: what each field
759
+ is, as plain Arrays, Strings, Integers and nil (the same on the boards),
760
+ one `[name, base, kind, nested, capacity, string_capacity]` per field:
761
+
762
+ | Element | Value |
763
+ |---|---|
764
+ | `name` | the field name (a String) |
765
+ | `base` | the .msg type: `"float64"`, `"string"`, `"uint8"`, ..., or the full name of a message (`"std_msgs/msg/Header"`) |
766
+ | `kind` | `"scalar"`, `"array"` (`T[N]`), `"bounded_sequence"` (`T[<=N]`) or `"sequence"` (`T[]`) |
767
+ | `nested` | the full name of the message type, or nil |
768
+ | `capacity` | N of an array or a bounded sequence, else nil |
769
+ | `string_capacity` | N of `string<=N`, else nil |
770
+
771
+ ```ruby
772
+ Asterism::ROS::SensorMsgs::JointState::FIELD_TYPES
773
+ # => [["header", "std_msgs/msg/Header", "scalar", "std_msgs/msg/Header", nil, nil],
774
+ # ["name", "string", "sequence", nil, nil, nil],
775
+ # ["position", "float64", "sequence", nil, nil, nil], ...]
776
+ Asterism::ROS.field_types("sensor_msgs/msg/Imu")
777
+ # => {"sensor_msgs/msg/Imu" => [...], "std_msgs/msg/Header" => [...],
778
+ # "geometry_msgs/msg/Quaternion" => [...], "geometry_msgs/msg/Vector3" => [...],
779
+ # "builtin_interfaces/msg/Time" => [...]}
780
+ ```
781
+
782
+ `Asterism::ROS.field_types(type)` takes a type or its name, loads the
783
+ types it is made of and gives the `FIELD_TYPES` of each, the type first
784
+ (for a service, its Request and Response). A byte array or sequence
785
+ (`uint8`, `byte`, `char`) is a binary String in Ruby (see the table
786
+ above). Types generated before 0.4.1 have no `FIELD_TYPES`; regenerate
787
+ them (`field_types` raises `Asterism::ROS::UnknownType` for them).
564
788
 
565
789
  ### Making types: `tools/asterism_msggen.rb`
566
790
 
@@ -601,8 +825,9 @@ the hashes from Jazzy's JSON (`type_hashes.txt`) that the tests
601
825
  (`rake test:msgs`) compare with. `tools/ros2_types.rb` refreshes both, and
602
826
  `data/msgs`, from a ROS 2 Jazzy docker image (`rake types:refresh` with
603
827
  `ASTERISM_ROS2_IMAGE`; `rake types:check` only compares). The definitions
604
- are from ros2/common_interfaces, ros2/rcl_interfaces and
605
- ros2/example_interfaces (Apache License 2.0, see NOTICE).
828
+ are from ros2/common_interfaces, ros2/rcl_interfaces,
829
+ ros2/example_interfaces (Apache License 2.0) and ros2/geometry2 (tf2_msgs,
830
+ BSD 3-Clause License); see NOTICE.
606
831
 
607
832
  ### Loading
608
833
 
@@ -615,3 +840,9 @@ Time and Quaternion 32.8 KB more, std_msgs/Float32MultiArray with its
615
840
  layout types 20.0 KB more (68 KB for nine files, 340 ms). Evaluating a file
616
841
  compiles it on the caller's stack (about 2.5 KB deeper than the update
617
842
  loop); load the types in `on_create`, not in a deep call chain.
843
+
844
+ A type name may come from the network, so `require_type` checks it before
845
+ it builds a path: `pkg/msg/Name` or `pkg/srv/Name`, the package
846
+ `[a-z][a-z0-9_]*`, the name `[A-Z][A-Za-z0-9]*` (ROS 2's rules for
847
+ interface names). Anything else (`../msg/Foo`, an empty part, an upper-case
848
+ package, more slashes, NUL) raises `ArgumentError`.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2008, Willow Garage, Inc.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.