asterism 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/NOTICE +27 -0
  4. data/README.md +405 -3
  5. data/data/msgs/LICENSE-Apache-2.0.txt +202 -0
  6. data/data/msgs/NOTICE +17 -0
  7. data/data/msgs/builtin_interfaces/msg/Duration.rb +33 -0
  8. data/data/msgs/builtin_interfaces/msg/Time.rb +33 -0
  9. data/data/msgs/example_interfaces/srv/AddTwoInts.rb +64 -0
  10. data/data/msgs/geometry_msgs/msg/Accel.rb +35 -0
  11. data/data/msgs/geometry_msgs/msg/AccelStamped.rb +36 -0
  12. data/data/msgs/geometry_msgs/msg/Point.rb +36 -0
  13. data/data/msgs/geometry_msgs/msg/Point32.rb +36 -0
  14. data/data/msgs/geometry_msgs/msg/PointStamped.rb +36 -0
  15. data/data/msgs/geometry_msgs/msg/Pose.rb +36 -0
  16. data/data/msgs/geometry_msgs/msg/Pose2D.rb +36 -0
  17. data/data/msgs/geometry_msgs/msg/PoseStamped.rb +36 -0
  18. data/data/msgs/geometry_msgs/msg/Quaternion.rb +39 -0
  19. data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +36 -0
  20. data/data/msgs/geometry_msgs/msg/Transform.rb +36 -0
  21. data/data/msgs/geometry_msgs/msg/TransformStamped.rb +39 -0
  22. data/data/msgs/geometry_msgs/msg/Twist.rb +35 -0
  23. data/data/msgs/geometry_msgs/msg/TwistStamped.rb +36 -0
  24. data/data/msgs/geometry_msgs/msg/Vector3.rb +36 -0
  25. data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +36 -0
  26. data/data/msgs/geometry_msgs/msg/Wrench.rb +35 -0
  27. data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +36 -0
  28. data/data/msgs/sensor_msgs/msg/BatteryState.rb +100 -0
  29. data/data/msgs/sensor_msgs/msg/FluidPressure.rb +38 -0
  30. data/data/msgs/sensor_msgs/msg/Illuminance.rb +38 -0
  31. data/data/msgs/sensor_msgs/msg/Imu.rb +52 -0
  32. data/data/msgs/sensor_msgs/msg/JointState.rb +44 -0
  33. data/data/msgs/sensor_msgs/msg/MagneticField.rb +39 -0
  34. data/data/msgs/sensor_msgs/msg/NavSatFix.rb +55 -0
  35. data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +43 -0
  36. data/data/msgs/sensor_msgs/msg/Range.rb +52 -0
  37. data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +38 -0
  38. data/data/msgs/sensor_msgs/msg/Temperature.rb +38 -0
  39. data/data/msgs/std_msgs/msg/Bool.rb +30 -0
  40. data/data/msgs/std_msgs/msg/Byte.rb +30 -0
  41. data/data/msgs/std_msgs/msg/ByteMultiArray.rb +35 -0
  42. data/data/msgs/std_msgs/msg/Char.rb +30 -0
  43. data/data/msgs/std_msgs/msg/ColorRGBA.rb +39 -0
  44. data/data/msgs/std_msgs/msg/Empty.rb +26 -0
  45. data/data/msgs/std_msgs/msg/Float32.rb +30 -0
  46. data/data/msgs/std_msgs/msg/Float32MultiArray.rb +35 -0
  47. data/data/msgs/std_msgs/msg/Float64.rb +30 -0
  48. data/data/msgs/std_msgs/msg/Float64MultiArray.rb +35 -0
  49. data/data/msgs/std_msgs/msg/Header.rb +35 -0
  50. data/data/msgs/std_msgs/msg/Int16.rb +30 -0
  51. data/data/msgs/std_msgs/msg/Int16MultiArray.rb +35 -0
  52. data/data/msgs/std_msgs/msg/Int32.rb +30 -0
  53. data/data/msgs/std_msgs/msg/Int32MultiArray.rb +35 -0
  54. data/data/msgs/std_msgs/msg/Int64.rb +30 -0
  55. data/data/msgs/std_msgs/msg/Int64MultiArray.rb +35 -0
  56. data/data/msgs/std_msgs/msg/Int8.rb +30 -0
  57. data/data/msgs/std_msgs/msg/Int8MultiArray.rb +35 -0
  58. data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +36 -0
  59. data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +35 -0
  60. data/data/msgs/std_msgs/msg/String.rb +30 -0
  61. data/data/msgs/std_msgs/msg/UInt16.rb +30 -0
  62. data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +35 -0
  63. data/data/msgs/std_msgs/msg/UInt32.rb +30 -0
  64. data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +35 -0
  65. data/data/msgs/std_msgs/msg/UInt64.rb +30 -0
  66. data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +35 -0
  67. data/data/msgs/std_msgs/msg/UInt8.rb +30 -0
  68. data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +35 -0
  69. data/lib/asterism/cruby.rb +24 -0
  70. data/lib/asterism/version.rb +5 -0
  71. data/lib/asterism.rb +17 -4
  72. data/mrblib/asterism.rb +545 -0
  73. data/mrblib/cdr.rb +512 -0
  74. data/mrblib/future.rb +83 -0
  75. data/mrblib/proxy.rb +97 -0
  76. data/mrblib/ros.rb +691 -0
  77. data/tools/asterism_msggen.rb +826 -0
  78. metadata +118 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 692f8e51b504a0316c880540228a14a5319b4c05f692390858ceeac92cca0737
4
- data.tar.gz: 31c909f9a3244f6e731e916ad40643f1271dcc0bf7b9a1694e3c8235bf97c695
3
+ metadata.gz: 16a71827f6f79aa024b98f6aa686efa127228644d12f6bef3469dad98d819dac
4
+ data.tar.gz: b564b747d9cb0dd6d890e86e95cbc04ca75f7d1a55b24490bfd315edfc1cf25a
5
5
  SHA512:
6
- metadata.gz: e737aae6f7612b8f5463889e962d3811795df2ab5d6baae0359334877759158b901dfc922afdf1425fcac98441a800ec1663d077b6cb741d8a6e1b728998b9ce
7
- data.tar.gz: '0048c2a5735d71bb46c1048578f676065f8c82bf3391df4e21feff73a35f1da4cd5f824c718cc00bf764e80f16cf5234ecf6a9dba83623b2d4ec09c66e154890'
6
+ metadata.gz: bff73ce315295e1d2a4426eef3deda857ba8c77db3c789cb48b0ec489c101cd35633fb6d73fbba31d641ad9d467bbaa7db80aefcf4686af1a798a9f4719a66c4
7
+ data.tar.gz: 594b310948679e9471dab6853846692c0ffc94a670456b9fcccfe44f7c0e4fbc3cf90fde19e401f992e31897b2b385f7a1856c6a8bef5920b1b6cf75c868e7bf
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Katsuhiko Kageyama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/NOTICE ADDED
@@ -0,0 +1,27 @@
1
+ Asterism
2
+ Copyright (c) 2026 Katsuhiko Kageyama
3
+ Asterism's own code is under the MIT License (LICENSE).
4
+
5
+ This repository also contains material made from ROS 2 message and service
6
+ definitions, which are licensed under the Apache License, Version 2.0
7
+ (data/msgs/LICENSE-Apache-2.0.txt):
8
+
9
+ - tools/ros2_jazzy/: the .msg / .srv definitions and the package.xml of
10
+ their packages as distributed with ROS 2 Jazzy
11
+ (/opt/ros/jazzy/share/<package>/), unchanged, and type_hashes.txt, the
12
+ type hashes from the type description JSON files of the same packages.
13
+ - data/msgs/: Ruby files generated from those definitions by
14
+ tools/asterism_msggen.rb (each file names, at its top, the definition and
15
+ the package it was made from and the package's license).
16
+ - test/msgs/golden_cdr.tsv and test/msgs/test_type_hashes.txt: values
17
+ computed by ROS 2's tools (rclpy, rosidl) in a ROS 2 Jazzy image.
18
+
19
+ The ROS 2 packages these come from, all "Apache License 2.0" in their
20
+ package.xml:
21
+
22
+ - std_msgs, geometry_msgs, sensor_msgs (ros2/common_interfaces)
23
+ - builtin_interfaces, service_msgs (ros2/rcl_interfaces)
24
+ - example_interfaces (ros2/example_interfaces)
25
+
26
+ Copyright in the definitions belongs to their authors (Open Source Robotics
27
+ Foundation, Inc. and the ROS 2 contributors); see those repositories.
data/README.md CHANGED
@@ -1,6 +1,408 @@
1
1
  # Asterism
2
2
 
3
- Treat many Rubies -- CRuby, mruby on microcontrollers, the browser -- as one
4
- network of objects, built on Zenoh. Part of the Family mruby project.
3
+ Asterism joins Ruby on PCs, boards (mruby / PicoRuby) and ROS 2 into one
4
+ Zenoh network: Ruby objects on other machines are called like local ones,
5
+ and a Ruby program can be a ROS 2 node (rmw_zenoh's wire format). This
6
+ repository holds the pure Ruby layers, written once for every Ruby:
5
7
 
6
- This release only reserves the name. Nothing works yet.
8
+ | Path | What |
9
+ |---|---|
10
+ | `mrblib/` | The layers: remote objects (`Asterism`), `Asterism::CDR`, `Asterism::ROS`. The source of truth for both forms below |
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 |
13
+ | `data/msgs/` | ROS 2 message types generated from ROS 2 Jazzy's definitions (Apache-2.0, see NOTICE) |
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) |
16
+ | `examples/` | CRuby: `node.rb` (objects with a board), `ros2_talker.rb` (Twist and AddTwoInts with ROS 2) |
17
+
18
+ The Zenoh binding underneath is a separate gem with the same Ruby API on
19
+ each Ruby:
20
+
21
+ - CRuby: [asterism-zenoh](https://github.com/ruby-asterism/asterism-zenoh)
22
+ (over zenoh-c)
23
+ - mruby / PicoRuby: [picoruby-asterism-zenoh](https://github.com/ruby-asterism/picoruby-asterism-zenoh)
24
+ (over zenoh-pico)
25
+
26
+ Neither gem is on rubygems.org yet (version 0.1.0 builds from these
27
+ repositories with `rake gem`).
28
+
29
+ ## Using it
30
+
31
+ CRuby (needs CRuby 3.2+ and a C compiler):
32
+
33
+ ```
34
+ gem install asterism # also installs asterism-zenoh (~> 0.1.0) and msgpack
35
+ ruby -e 'require "asterism"'
36
+ ```
37
+
38
+ `asterism-zenoh` compiles its C extension when it is installed and
39
+ downloads the prebuilt zenoh-c for the machine (x86_64 / aarch64 Linux,
40
+ glibc or musl; x86_64 / arm64 macOS), checked against a pinned sha256; see
41
+ its README for machines without network access. Linux needs glibc 2.34 or
42
+ newer (Ubuntu 22.04, Debian 12 and later) or musl; macOS is supported by the
43
+ build but not yet tested. To install from the repositories instead:
44
+
45
+ ```
46
+ (cd ../asterism-zenoh && rake gem) && rake gem
47
+ gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.1.0.gem pkg/asterism-0.1.0.gem
48
+ ```
49
+
50
+ From the working trees, without installing:
51
+
52
+ ```
53
+ ruby -I lib -I ../asterism-zenoh/lib your_script.rb # after `rake compile` in ../asterism-zenoh
54
+ ```
55
+
56
+ `Asterism::ROS::TYPE_PATH` is `data/msgs` there; add your own directories.
57
+ MessagePack sends every String as str (compatibility mode), as the boards
58
+ do: their MessagePack does not read the bin type.
59
+
60
+ mruby / PicoRuby: add this repository and picoruby-asterism-zenoh as gems
61
+ (`conf.gem "path/to/asterism"`; a `MessagePack` module with `pack` /
62
+ `unpack` is needed too), and put `data/msgs` on the device's file system at
63
+ `/usr/share/asterism/msgs` (or add the place to
64
+ `Asterism::ROS::TYPE_PATH`). Family mruby does this from pinned commits of
65
+ these repositories.
66
+
67
+ ## Tests
68
+
69
+ ```
70
+ rake # both suites
71
+ rake test:msgs # generator, type hashes, CDR, bundled types (CRuby only; no Zenoh, no docker)
72
+ rake test:objects # objects and Asterism::ROS between CRuby sessions over a local peer link
73
+ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects # the same through a zenohd router
74
+ ```
75
+
76
+ `test:objects` needs asterism-zenoh: built next to this repository
77
+ (`../asterism-zenoh`, its `rake compile`), or `ASTERISM_ZENOH_DIR`, or the
78
+ installed gem.
79
+
80
+ ## Examples
81
+
82
+ ```
83
+ # objects: join the app "demo" of Family mruby's asterism_demo, call a board,
84
+ # and answer its calls (its keys s / a call screen.say / apu.play here)
85
+ ruby examples/node.rb --router tcp/192.0.2.2:7447 [--peer fmruby-aaaaaa]
86
+
87
+ # ROS 2: geometry_msgs/Twist on /cmd_vel, then AddTwoInts calls
88
+ ruby examples/ros2_talker.rb --router tcp/192.0.2.2:7447 [--service NAME]
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT (LICENSE) for Asterism's code. The ROS 2 definitions in
94
+ `tools/ros2_jazzy/` (with the package.xml of their packages), the types
95
+ generated from them in `data/msgs/` and the fixtures computed by ROS 2's
96
+ tools in `test/msgs/` are under the Apache License 2.0 (NOTICE,
97
+ `data/msgs/NOTICE`, `data/msgs/LICENSE-Apache-2.0.txt`). Each generated
98
+ type names its package and license at its top. The gem `asterism` is
99
+ therefore `MIT` and `Apache-2.0`, each for its own files.
100
+
101
+ Not in this repository: the gems Asterism depends on are not bundled.
102
+ `msgpack` (CRuby) and `asterism-zenoh` (the CRuby Zenoh binding, which in
103
+ turn fetches zenoh-c at build time; see its README) are installed by
104
+ RubyGems, and on mruby / PicoRuby the Zenoh binding is the separate mrbgem
105
+ `picoruby-asterism-zenoh`. Their licenses are their own.
106
+
107
+ ---
108
+
109
+ # The API
110
+
111
+ The same on every Ruby. The examples use PicoRuby's `sleep_ms`; on CRuby
112
+ write `sleep`. When PicoRuby's `Machine` is there it is used for the clock
113
+ and the short pauses while waiting; otherwise `Time` and `sleep`. Besides
114
+ `Asterism::Zenoh`, the layers need a `MessagePack` module with `pack` /
115
+ `unpack` (the API of the msgpack gem; whichever gem provides it).
116
+
117
+ ```ruby
118
+ # on the machine that has the object
119
+ Asterism.connect("tcp/192.0.2.2:7447", node: "fmruby-bbbbbb", app: "demo")
120
+ Asterism.expose("apu", apu, methods: [:play, :stop])
121
+ loop { Asterism.poll; ... } # in the update loop
122
+
123
+ # on another machine
124
+ Asterism.connect("tcp/192.0.2.2:7447", node: "linux", app: "demo")
125
+ apu = Asterism["fmruby-bbbbbb/demo/apu"] # <node>/<app>/<object>
126
+ apu.play("t120 o4 cdefg") # runs there, returns its value
127
+ apu.respond_to?(:play) # => true (from the exposed list)
128
+ f = apu.async.play("cde") # does not wait
129
+ f.done?; f.value
130
+ Asterism.each("*/*/apu") { |a| a.stop } # every apu alive now
131
+ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
132
+ ```
133
+
134
+ ## API
135
+
136
+ | Call | Returns | Raises / notes |
137
+ |---|---|---|
138
+ | `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). |
139
+ | `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. |
140
+ | `Asterism.unexpose(name)` | true / false | |
141
+ | `Asterism.poll` | true / false | Call from the update loop: polls Zenoh, answers the calls that came in, follows who is alive. `false` once the connection is closed or lost. |
142
+ | `Asterism[path, timeout_ms = 2000]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). Nothing is sent until a method is called. |
143
+ | `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`). |
144
+ | `proxy.async.<method>(...)` | `Future` | Sends and returns at once. `EncodeError` / `Disconnected` are raised here. |
145
+ | `future.done?` | true / false | Never waits (`Asterism.poll` moves it on). True once the answer came, the time ran out or the connection closed. |
146
+ | `future.value` | the return value | Waits (polling) if not done; raises like a waiting call. `future.took_ms`: time to the answer. |
147
+ | `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. |
148
+ | `proxy.asterism_meta` / `proxy.asterism_path` | Hash, String | `{"methods" => [[name, arity], ...]}` |
149
+ | `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. |
150
+ | `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
151
+ | `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
152
+ | `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
153
+
154
+ Errors: `Asterism::Error` (base, a `StandardError`), `EncodeError`, `RemoteError`
155
+ (`remote_class`, `remote_message`), `Timeout`, `Disconnected`.
156
+
157
+ ### When the connection is lost
158
+
159
+ Asterism follows `Asterism::Zenoh`: the connection is closed when the router
160
+ (or the only peer) goes away, and is not reopened by itself. From then on
161
+ `Asterism.poll` returns `false`, `connected?` is false, `lost_reason` says
162
+ why, and calls raise `Asterism::Disconnected`. **`Asterism::Zenoh::Error`
163
+ never comes out of Asterism; it is always wrapped in `Disconnected`** (the
164
+ message is kept). The exposed objects are gone with the connection: to go
165
+ on, the application calls `connect` and `expose` again.
166
+
167
+ ## Values
168
+
169
+ Only what MessagePack carries: `nil`, `true`, `false`, `Integer` (64 bit),
170
+ `Float`, `String`, `Array`, `Hash`. A `Symbol` is sent as a `String` (also
171
+ as a Hash key: keyword arguments arrive as Symbols again). Anything else in
172
+ the arguments raises `EncodeError` before sending; in a return value the
173
+ caller gets `RemoteError` (`Asterism::EncodeError`). Nesting is limited to
174
+ 16 levels.
175
+
176
+ ## Keys and encoding
177
+
178
+ | Key | Zenoh | Payload |
179
+ |---|---|---|
180
+ | `asterism/<node>/<app>/<object>/call` | get / queryable | query: `[method, [args...], {kwargs}]`; reply: `["ok", value]` or `["error", class name, message]` |
181
+ | `asterism/<node>/<app>/<object>/meta` | get / queryable | reply: `{"methods" => [[name, arity], ...]}` (arity `-1` when not declared). The object may be `*` in the query: one reply per object |
182
+ | `asterism/<node>` | liveliness | the node is connected |
183
+ | `asterism/<node>/<app>/<object>` | liveliness | the object is exposed |
184
+
185
+ One queryable per application (`asterism/<node>/<app>/**`). A call whose
186
+ node or app is a wildcard reaches every application that exposes that
187
+ object, each answering with its own key; `Asterism` itself only sends calls
188
+ to one object.
189
+
190
+ ## How waiting works
191
+
192
+ Everything is polled; nothing runs behind the application. A call that
193
+ waits keeps polling (pausing 2 ms between polls) and answers the calls that
194
+ come in meanwhile, so two machines calling each other at the same time do
195
+ not lock up. An answered call may itself call and wait; such waits nest at
196
+ most `Asterism::MAX_NESTING` (4) deep, beyond that the call raises `Error`
197
+ (the caller of the outer call gets it as `RemoteError`). While a call
198
+ waits, the rest of that application (drawing, input) waits too; other
199
+ applications do not.
200
+
201
+ Calls to an object of the same application (`<node>/<app>` is this one) do
202
+ not go out (a Zenoh session does not see its own queryable): they are run
203
+ in place, with the same encoding and checks.
204
+
205
+ ## Stack
206
+
207
+ A waiting call polls Zenoh on the caller's C stack. Measured on the P4
208
+ (Family mruby, 16 KB application stack): the application idles at 8.1 KB
209
+ used; a waiting call made from the update loop takes it to 10.7 KB, also
210
+ with calls answered while waiting (two machines relaying calls to each
211
+ other). The same call made from an input handler that C calls into (one more
212
+ interpreter entry) reached 12.4 KB; an earlier version of this gem, which
213
+ took zenoh replies through blocks and fetched the meta from inside
214
+ `respond_to_missing?`, overflowed the stack there. Make waiting calls from
215
+ the update loop, or use `async`.
216
+
217
+ ## Limits
218
+
219
+ - Objects cannot be passed by reference (a return value is a copy), blocks
220
+ cannot be sent, there are no events (later stages).
221
+ - The node token `asterism/<node>` is shared by every application of that
222
+ node; when one of them closes, the others still list the node through
223
+ their objects, but a watcher may see the node token go away.
224
+ - No authentication: a trusted LAN is assumed.
225
+
226
+ ## ROS 2 (rmw_zenoh): `Asterism::ROS` and `Asterism::CDR`
227
+
228
+ A minimal ROS 2 node that talks to ROS 2 systems using rmw_zenoh (checked
229
+ with ROS 2 Jazzy, rmw_zenoh_cpp 0.2.11, Zenoh 1.8.0), directly on an
230
+ `Asterism::Zenoh::Session`. Pure Ruby, independent of the object layer above
231
+ (no `Asterism.connect`, no MessagePack; only its clock and pause helpers).
232
+ Topics and services. The message and service types are generated from
233
+ `.msg` / `.srv` files (see [Message types](#message-types)) and loaded when
234
+ the application asks for them.
235
+
236
+ ```ruby
237
+ s = Asterism::Zenoh::Session.open("tcp/192.0.2.2:7447")
238
+ node = Asterism::ROS::Node.new(s, "fmruby_talker") # namespace: "/", domain: 0
239
+ str = Asterism::ROS.require_type("std_msgs/msg/String")
240
+ pub = node.publisher("/chatter", str)
241
+ sub = node.subscription("/chatter_back", "std_msgs/msg/String") # by name works too
242
+ loop do
243
+ s.poll
244
+ pub << { data: "hello" } # or pub.publish(str.new(data: "hello"))
245
+ sub.each_pending { |msg, info| puts "#{info && info.sequence}: #{msg.data}" }
246
+ end
247
+ node.close # or let the session close
248
+ ```
249
+
250
+ Services:
251
+
252
+ ```ruby
253
+ add = Asterism::ROS.require_type("example_interfaces/srv/AddTwoInts")
254
+ node.service("/fmruby/add_two_ints", add) { |req| { sum: req.a + req.b } }
255
+ cli = node.client("/add_two_ints", add)
256
+ loop do
257
+ node.poll # session.poll, then answers the requests
258
+ res = cli.call(a: 2, b: 3) # waits (polling), raises Asterism::ROS::Timeout
259
+ puts res.sum
260
+ c = cli.call_async(a: 2, b: 3) # returns at once
261
+ # ... later updates:
262
+ puts c.value.sum if c.done?
263
+ end
264
+ ```
265
+
266
+ | Call | Returns | Notes |
267
+ |---|---|---|
268
+ | `Asterism::ROS::Node.new(session, name, namespace: "/", domain: 0, enclave: "/")` | `Node` | Declares the node's liveliness token (`ros2 node list`). `name` has no `/`. |
269
+ | `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`). |
270
+ | `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). |
271
+ | `node.subscription(topic, type, qos: DEFAULT_QOS, depth: 16)` | `Subscription` | Subscribes and declares the subscription token. |
272
+ | `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`. |
273
+ | `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. |
274
+ | `node.poll(steps = 8)` | true / false | `session.poll(steps)`, then answers the requests waiting for this node's services. Returns what `session.poll` returns. |
275
+ | `node.client(name, type, qos: DEFAULT_QOS)` | `Client` | Declares the client token. |
276
+ | `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. |
277
+ | `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`. |
278
+ | `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. |
279
+ | `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. |
280
+ | `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. |
281
+ | `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. |
282
+
283
+ What goes on the wire (rmw_zenoh_cpp 0.2.x):
284
+
285
+ | Item | Form |
286
+ |---|---|
287
+ | Data key | `<domain>/<topic without the outer "/">/<DDS type>/<type hash>`, e.g. `0/chatter/std_msgs::msg::dds_::String_/RIHS01_df668c74...` |
288
+ | Payload | CDR: `00 01 00 00`, uint32 length with the NUL, bytes, NUL |
289
+ | Attachment | int64 sequence, int64 time (ns since the epoch), both little endian, one byte GID length (16), 16 bytes GID: 33 bytes. **Required**: rmw_zenoh drops a sample without it |
290
+ | Node token | `@ros2_lv/<domain>/<zid>/<nid>/<nid>/NN/<enclave>/<namespace>/<node>` |
291
+ | Topic token | `@ros2_lv/<domain>/<zid>/<nid>/<id>/MP` (or `MS`) `/<enclave>/<namespace>/<node>/<topic>/<DDS type>/<type hash>/<qos>` |
292
+ | Service key | `<domain>/<service without the outer "/">/<DDS service type>/<service type hash>`, e.g. `0/add_two_ints/example_interfaces::srv::dds_::AddTwoInts_/RIHS01_e118de6b...`. The type and hash are those of the service, not of its Request / Response |
293
+ | Service request | a get on the service key, target ALL_COMPLETE, no consolidation, payload the request's CDR, attachment as above (the client's sequence number from 1, time, the client's GID) |
294
+ | Service reply | from a **complete** queryable (a non-complete one never sees ALL_COMPLETE queries), on the service key, payload the response's CDR, attachment: the request's sequence number and GID with the server's time. The client pairs reply and request by the sequence number |
295
+ | Service tokens | like topic tokens, with `SS` (server) and `SC` (client) |
296
+
297
+ In tokens, `/` inside a name is written `%` (`/chatter` is `%chatter`, the
298
+ root namespace `%`). `<zid>` is the Zenoh session ID (`session.zid`).
299
+ `DEFAULT_QOS` (`::,10:,:,:,,`) is rmw_zenoh's form of the default profile:
300
+ reliable, volatile, keep last 10.
301
+
302
+ Not here (later stages): actions, QoS other than the default (transient
303
+ local needs Zenoh's advanced publisher), name remapping, receiving a type
304
+ the application has not loaded.
305
+
306
+ ## Message types
307
+
308
+ ### Values
309
+
310
+ A generated message is a plain Ruby class (`Asterism::ROS::Message` is its
311
+ base; the app VM has neither `Data` nor `Struct`). The same code runs on
312
+ CRuby.
313
+
314
+ ```ruby
315
+ Twist = Asterism::ROS.require_type("geometry_msgs/msg/Twist")
316
+ t = Twist.new(linear: { x: 0.1 }) # missing fields take their defaults
317
+ t = Twist.from({ "linear" => { "x" => 0.1 } }) # Hash, String keys too
318
+ t.linear.x # => 0.1
319
+ t.angular # => #<geometry_msgs/msg/Vector3 {x: 0.0, ...}>
320
+ t.to_h # => {linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {...}}
321
+ bytes = Twist.encode(t) # or Twist.encode(linear: { x: 0.1 })
322
+ Twist.decode(bytes) == t # => true
323
+ Twist::TYPE_NAME # "geometry_msgs::msg::dds_::Twist_"
324
+ Twist::TYPE_HASH # "RIHS01_9c45bf16..."
325
+ Asterism::ROS::SensorMsgs::BatteryState::POWER_SUPPLY_STATUS_FULL # constants
326
+ ```
327
+
328
+ | Field in the .msg | Ruby value | Default |
329
+ |---|---|---|
330
+ | `bool` | true / false | false |
331
+ | integers, `byte`, `char` | Integer (a `uint64` at or above 2**63 reads back negative) | 0 |
332
+ | `float32` / `float64` | Float | 0.0 |
333
+ | `string`, `string<=N` | String (UTF-8 bytes; longer than N raises `ArgumentError` on encode) | "" |
334
+ | a message | that type (a Hash is turned into it) | its defaults |
335
+ | `T[N]`, `T[]`, `T[<=N]` | Array (a wrong length or over the bound raises `ArgumentError` on encode) | N defaults, or [] |
336
+ | `byte[]`, `uint8[]`, `char[]` (any size) | a binary String (an Array of Integers is taken too) | "\0" * N, or "" |
337
+ | `wstring` | not supported: encode / decode raise `NotImplementedError` | |
338
+
339
+ Defaults written in the .msg (`float64 w 1`) are used. Unknown field names
340
+ raise `ArgumentError`; integers are not range-checked (they wrap).
341
+
342
+ ### The bundled types
343
+
344
+ Made from ROS 2 Jazzy's definitions, in `data/msgs` (on a board:
345
+ `/usr/share/asterism/msgs`), one file per type:
346
+ std_msgs (Bool, Byte, Char, String, Empty, the integer and float types,
347
+ Header, ColorRGBA, MultiArrayDimension / Layout and every *MultiArray),
348
+ builtin_interfaces (Time, Duration), geometry_msgs (Vector3, Point, Point32,
349
+ Quaternion, Pose, Pose2D, Twist, Accel, Transform, Wrench and their
350
+ *Stamped), sensor_msgs (Imu, BatteryState, Temperature, Range,
351
+ MagneticField, Illuminance, FluidPressure, RelativeHumidity, JointState,
352
+ NavSatStatus, NavSatFix), example_interfaces/srv/AddTwoInts. The list is
353
+ `tools/bundled_types.txt`. Nothing of them is compiled into the mrbgem: an
354
+ application loads what it uses, and each file loads the types it is made of.
355
+
356
+ ### Making types: `tools/asterism_msggen.rb`
357
+
358
+ CRuby, standard library only, no ROS 2 installation needed (only the
359
+ `.msg` / `.srv` files of the packages involved):
360
+
361
+ ```
362
+ # a type of an installed ROS 2 (and the types it uses)
363
+ ruby tools/asterism_msggen.rb -I /opt/ros/jazzy/share -o out geometry_msgs/msg/Twist
364
+ # your own package (my_pkg/msg/Foo.msg; -I for the packages it refers to)
365
+ ruby tools/asterism_msggen.rb -I /opt/ros/jazzy/share -o out path/to/my_pkg/msg/Foo.msg
366
+ # print the type hashes
367
+ ruby tools/asterism_msggen.rb -I share --hash example_interfaces/srv/AddTwoInts
368
+ # compare the hashes with the type description JSON ROS 2 installs
369
+ ruby tools/asterism_msggen.rb -I /opt/ros/jazzy/share --check-json /opt/ros/jazzy/share sensor_msgs/msg/Imu
370
+ ```
371
+
372
+ `-I` directories are laid out like a ROS 2 `share`: `<pkg>/msg/*.msg`,
373
+ `<pkg>/srv/*.srv`. The output is `<out>/<pkg>/<msg|srv>/<Name>.rb`; copy it
374
+ to the device (`/usr/share/asterism/msgs` or a directory added to
375
+ `Asterism::ROS::TYPE_PATH`), or put the output directory in `TYPE_PATH` on
376
+ CRuby. A service's type hash needs `service_msgs/msg/ServiceEventInfo` and
377
+ `builtin_interfaces/msg/Time` under `-I` (every ROS 2 `share` has them).
378
+
379
+ The type hash (RIHS01) is computed the way ROS 2 Jazzy does
380
+ (`rosidl_generator_type_description`): the type and every type it refers
381
+ to, each as `{type_name, fields: [{name, type: {type_id, capacity,
382
+ string_capacity, nested_type_name}}]}` without default values, the referred
383
+ ones sorted by name, written as Python's `json.dumps(..., separators=(", ",
384
+ ": "))`, then SHA-256. A service's description is `request_message`,
385
+ `response_message` and `event_message` (`<Name>_Event`: `info`, and
386
+ `request` / `response` as sequences of at most one). Other ROS 2 versions
387
+ may change the rules; regenerate and check with `--check-json` against that
388
+ version.
389
+
390
+ `tools/ros2_jazzy/` holds the Jazzy definitions of the bundled types and
391
+ the hashes from Jazzy's JSON (`type_hashes.txt`) that the tests
392
+ (`rake test:msgs`) compare with. `tools/ros2_types.rb` refreshes both, and
393
+ `data/msgs`, from a ROS 2 Jazzy docker image (`rake types:refresh` with
394
+ `ASTERISM_ROS2_IMAGE`; `rake types:check` only compares). The definitions
395
+ are from ros2/common_interfaces, ros2/rcl_interfaces and
396
+ ros2/example_interfaces (Apache License 2.0, see NOTICE).
397
+
398
+ ### Loading
399
+
400
+ On mruby, `require_type` reads the file and evaluates it in the
401
+ application's VM (`Kernel#eval`); PicoRuby's `require` would run each file
402
+ in a Sandbox task that stays for the life of the VM. On CRuby it uses
403
+ `require`. Measured on the P4 (Family mruby, a 1 MB VM pool):
404
+ geometry_msgs/Twist with Vector3 15.3 KB, sensor_msgs/Imu with Header,
405
+ Time and Quaternion 32.8 KB more, std_msgs/Float32MultiArray with its
406
+ layout types 20.0 KB more (68 KB for nine files, 340 ms). Evaluating a file
407
+ compiles it on the caller's stack (about 2.5 KB deeper than the update
408
+ loop); load the types in `on_create`, not in a deep call chain.