asterism 0.0.0 → 0.2.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/NOTICE +27 -0
  4. data/README.md +581 -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/objects.rb +224 -0
  70. data/lib/asterism/cruby/ros.rb +305 -0
  71. data/lib/asterism/cruby/runner.rb +253 -0
  72. data/lib/asterism/cruby/zenoh.rb +422 -0
  73. data/lib/asterism/cruby.rb +30 -0
  74. data/lib/asterism/version.rb +5 -0
  75. data/lib/asterism.rb +22 -4
  76. data/mrblib/asterism.rb +617 -0
  77. data/mrblib/cdr.rb +512 -0
  78. data/mrblib/future.rb +88 -0
  79. data/mrblib/proxy.rb +97 -0
  80. data/mrblib/ros.rb +861 -0
  81. data/tools/asterism_msggen.rb +826 -0
  82. metadata +122 -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: bdb7bf2126361f3b9ae4097467832d950440b9a65c419a92394f230b4d7b0277
4
+ data.tar.gz: ba8621f58ee2d5fb22f5eeb3b68917cb6ce9f9420d07230d5d69e246b9c53564
5
5
  SHA512:
6
- metadata.gz: e737aae6f7612b8f5463889e962d3811795df2ab5d6baae0359334877759158b901dfc922afdf1425fcac98441a800ec1663d077b6cb741d8a6e1b728998b9ce
7
- data.tar.gz: '0048c2a5735d71bb46c1048578f676065f8c82bf3391df4e21feff73a35f1da4cd5f824c718cc00bf764e80f16cf5234ecf6a9dba83623b2d4ec09c66e154890'
6
+ metadata.gz: cc43c0ab1f0076a148bcd65a648b0220eb8559912b296a7fdbb7b4810f108ab8f66d439e951e3c1a3fe24329ac6fb5a76593214c7571e3f18b55d833a917276d
7
+ data.tar.gz: 48114f986885d604142a4fb369b1a31f4a7cb6d1dbff95ffc0b0566e97801bbd646278cb498b4c0e1003c8a3b425a526262ffb384557eeb0cdc4a7e450b6bc63
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,584 @@
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, `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) |
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) |
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.2.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.2.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.2.0.gem pkg/asterism-0.2.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 # every suite
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
+ rake test:api # the Ruby-like API (blocks, receiving threads, Enumerators)
74
+ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
75
+ ```
76
+
77
+ `test:objects` and `test:api` need asterism-zenoh: built next to this repository
78
+ (`../asterism-zenoh`, its `rake compile`), or `ASTERISM_ZENOH_DIR`, or the
79
+ installed gem.
80
+
81
+ ## Examples
82
+
83
+ ```
84
+ # objects: join the app "demo" of Family mruby's asterism_demo, call a board,
85
+ # and answer its calls (its keys s / a call screen.say / apu.play here)
86
+ ruby examples/node.rb --router tcp/192.0.2.2:7447 [--peer fmruby-aaaaaa]
87
+ ruby examples/node_polled.rb --router tcp/192.0.2.2:7447 # the same, polled
88
+
89
+ # ROS 2: /chatter and /cmd_vel from a timer, /chatter_in printed,
90
+ # /cruby/add_two_ints served, AddTwoInts calls
91
+ ruby examples/ros2_talker.rb --router tcp/192.0.2.2:7447 [--service NAME]
92
+
93
+ # plain Zenoh: run two with different --name
94
+ ruby examples/zenoh.rb --router tcp/192.0.2.2:7447 --name pc1
95
+ ```
96
+
97
+ ## The Ruby-like API (CRuby only)
98
+
99
+ On CRuby, `require "asterism"` also brings a layer with blocks, a receiving
100
+ thread, Enumerators and pattern matching (`lib/asterism/cruby/`). It is
101
+ built on the polled API below, which stays as it is: code written for a
102
+ board still runs unchanged on CRuby, and code using this layer does not run
103
+ on a board.
104
+
105
+ ### Zenoh
106
+
107
+ ```ruby
108
+ Asterism::Zenoh.open("tcp/192.0.2.2:7447") do |s| # closed when the block ends
109
+ s.subscribe("home/**") { |sample| puts "#{sample.key} = #{sample.payload}" }
110
+ s.queryable("home/pc/status") { |q| q.reply("ok") }
111
+ s.put("home/pc/hello", "hi")
112
+ s.get("home/**/status").each { |reply| p reply } # until every answer came or the time ran out
113
+ sub = s.subscribe("sensor/temp") # no block: take from it
114
+ sub.each.lazy.map { _1.payload.to_f }.first(3) # waits for the samples
115
+ s.liveliness_watch("asterism/**") { |key, alive| puts "#{key} #{alive}" }
116
+ s.run # receive on this thread until Ctrl-C
117
+ end
118
+ ```
119
+
120
+ | Call | Notes |
121
+ |---|---|
122
+ | `Asterism::Zenoh.open(locator = nil, mode:, listen:, 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 |
123
+ | `Connection.new(session)` | wraps a session opened with the polled API |
124
+ | `s.put(key, payload, attachment: nil)` | a payload that is not a String is sent as `to_s` |
125
+ | `s.subscribe(key, depth: 16) { \|sample\| }` | the block gets each `Sample` on the receiving thread. Returns a `Subscription` (`close` ends it) |
126
+ | `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` |
127
+ | `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 |
128
+ | `s.get(key, timeout: 2.0, params:, payload:, attachment:, target:, consolidation:)` | an Enumerator of `Reply`; each iteration sends the get again. With a block: yields each, returns their number |
129
+ | `s.liveliness(key)` | a token (`close`) |
130
+ | `s.liveliness_watch(key) { \|key, alive\| }` | on the receiving thread; the tokens alive now come first. Without a block, `each` yields `Liveliness` (`key`, `alive?`) |
131
+ | `s.liveliness_get(key, timeout: 2.0)` | the keys alive now (an Array) |
132
+ | `s.start` / `s.stop` / `s.running?` | receive on a thread of its own |
133
+ | `s.run` | receive on this thread until `stop`, the connection closing, or Ctrl-C (returns nil) |
134
+ | `s.on_error { \|error, where\| }` | what the blocks raise (`StandardError`); without a handler it is printed with `warn`. Receiving goes on |
135
+ | `s.session`, `zid`, `peers`, `poll`, `closed?`, `close` | |
136
+
137
+ `Sample`, `Reply` (`key`, `payload`, `attachment`; `text` is the payload as
138
+ UTF-8) and `Liveliness` are `Data` values, so they work with pattern
139
+ matching: `case sample in {key: %r{/temp\z}, payload:}`. Times are seconds
140
+ here (the polled API counts milliseconds).
141
+
142
+ ### Objects
143
+
144
+ ```ruby
145
+ Asterism.connect("tcp/192.0.2.2:7447", node: "mypc", app: "demo") do |net| # closed after
146
+ net.expose("screen", Screen.new, methods: [:say])
147
+ net.on_join { |node| puts "joined #{node}" }
148
+ net.on_leave { |node| puts "left #{node}" }
149
+ board = net["fmruby-aaaaaa/demo/screen"]
150
+ board.say("hello")
151
+ net.each("*/demo/info").map(&:status) # Enumerable
152
+ net.run # answer calls until Ctrl-C
153
+ end
154
+ ```
155
+
156
+ | Call | Notes |
157
+ |---|---|
158
+ | `Asterism.connect(...) { \|net\| }` | yields an `Asterism::Net`, `Asterism.close` when the block ends; returns the block's value. Without a block, as before |
159
+ | `Asterism.net` | the `Net` of the connection made without a block (nil when not connected) |
160
+ | `net.expose` / `unexpose` / `exposed` | as `Asterism.expose`. The methods run on the receiving thread, in `run`, or in whatever polls |
161
+ | `net[path, timeout: nil]` | a proxy (`timeout` in seconds) |
162
+ | `net.each(pattern = "**")` | the proxies alive now; an Enumerator without a block |
163
+ | `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` |
164
+ | `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 |
165
+ | `net.nodes`, `node_id`, `app`, `connected?`, `lost_reason`, `poll`, `close` | |
166
+
167
+ ### ROS 2
168
+
169
+ ```ruby
170
+ Asterism::ROS.connect("tcp/192.0.2.2:7447", domain: 0) do |ros|
171
+ node = ros.node("ruby_talker")
172
+ chatter = node.publisher("/chatter", "std_msgs/msg/String")
173
+ chatter << { data: "hello from Ruby" }
174
+ node.subscribe("/odom", "nav_msgs/msg/Odometry") { |odom| puts odom.pose.pose.position.x }
175
+ node.every(0.1) { chatter << { data: Time.now.to_s } }
176
+ node.service("/add_ruby", "example_interfaces/srv/AddTwoInts") { |req| { sum: req.a + req.b } }
177
+ p node.call("/add_two_ints", "example_interfaces/srv/AddTwoInts", a: 1, b: 2).sum
178
+ node.topic("/scan", "sensor_msgs/msg/LaserScan").each.lazy.first(1)
179
+ ros.spin # until Ctrl-C
180
+ end
181
+ ```
182
+
183
+ | Call | Notes |
184
+ |---|---|
185
+ | `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` |
186
+ | `ros.node(name, namespace: "/", enclave: "/")` | an `Asterism::ROS::Node` (every method of the polled API) with the calls below |
187
+ | `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) |
188
+ | `node.topic(topic, type).each` | subscribes for the iteration and withdraws after it (`first(n)`, `break`, the time running out) |
189
+ | `node.every(seconds) { }` | the polled API's timer (run from `node.poll`), here on the receiving thread; returns a `Timer` (`cancel`, `fired`) |
190
+ | `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 |
191
+ | `node.call(name, type, request = nil, timeout: 2.0, **fields)` | waits for the response |
192
+ | `ros.spin` (`run`) / `start` / `stop` / `running?` / `on_error` / `close` | |
193
+ | `ros.zenoh` | the `Asterism::Zenoh::Connection` underneath (same receiving thread) |
194
+
195
+ `type` is a generated type or its name (`"geometry_msgs/msg/Twist"`).
196
+ Messages and `Attachment` take part in pattern matching (`deconstruct_keys`,
197
+ on the boards too): `case msg in {linear: {x:}, angular: {z:}}`. A type
198
+ that is not bundled: generate it with `tools/asterism_msggen.rb -o DIR`
199
+ (see [Message types](#message-types)) and give the directory, as
200
+ `Asterism::ROS.connect(..., type_path: "DIR")` or
201
+ `Asterism::ROS::TYPE_PATH << "DIR"`; then pass its name like any other.
202
+
203
+ ### Threads
204
+
205
+ - Each connection has at most one receiving thread, and only when the
206
+ application asks for it (`start`), or the application's own thread
207
+ inside `run` / `spin`. The blocks (subscriptions, queryables, timers,
208
+ services, exposed objects, `on_join`) run there, one after another. **A
209
+ block that takes long delays everything else that connection receives**:
210
+ hand long work to a thread or a Queue of your own.
211
+ - zenoh-c never calls Ruby: it fills the queues, and the receiving thread
212
+ empties them every 2 ms (`interval:`) with the polled API.
213
+ - A tick of the receiving thread holds the connection's lock (a `Monitor`;
214
+ for the object layer, which is one connection per process,
215
+ `Asterism::LOCK`). The application's calls into the same layer take it
216
+ too, so the layers written for one thread are never entered by two at
217
+ once.
218
+ - A call that waits for an answer (`proxy.method`, `node.call`) from another
219
+ thread while the receiving thread runs does not poll by itself: it waits
220
+ until a tick brings the answer. On the receiving thread itself (a block
221
+ that calls), or while nothing receives, it polls as on the boards.
222
+ - `stop`, `close` and the end of the block wait for the receiving thread to
223
+ end; no thread is left behind. When the connection is lost the receiving
224
+ thread ends by itself, and waiting calls raise as before.
225
+ - Enumerators (`each`, `get`, `topic`) wait on the thread that iterates
226
+ them.
227
+
228
+ ## License
229
+
230
+ MIT (LICENSE) for Asterism's code. The ROS 2 definitions in
231
+ `tools/ros2_jazzy/` (with the package.xml of their packages), the types
232
+ generated from them in `data/msgs/` and the fixtures computed by ROS 2's
233
+ tools in `test/msgs/` are under the Apache License 2.0 (NOTICE,
234
+ `data/msgs/NOTICE`, `data/msgs/LICENSE-Apache-2.0.txt`). Each generated
235
+ type names its package and license at its top. The gem `asterism` is
236
+ therefore `MIT` and `Apache-2.0`, each for its own files.
237
+
238
+ Not in this repository: the gems Asterism depends on are not bundled.
239
+ `msgpack` (CRuby) and `asterism-zenoh` (the CRuby Zenoh binding, which in
240
+ turn fetches zenoh-c at build time; see its README) are installed by
241
+ RubyGems, and on mruby / PicoRuby the Zenoh binding is the separate mrbgem
242
+ `picoruby-asterism-zenoh`. Their licenses are their own.
243
+
244
+ ---
245
+
246
+ # The API
247
+
248
+ The same on every Ruby (the polled API; CRuby adds the layer described in
249
+ [The Ruby-like API](#the-ruby-like-api-cruby-only)). The examples use PicoRuby's `sleep_ms`; on CRuby
250
+ write `sleep`. When PicoRuby's `Machine` is there it is used for the clock
251
+ and the short pauses while waiting; otherwise `Time` and `sleep`. Besides
252
+ `Asterism::Zenoh`, the layers need a `MessagePack` module with `pack` /
253
+ `unpack` (the API of the msgpack gem; whichever gem provides it).
254
+
255
+ ```ruby
256
+ # on the machine that has the object
257
+ Asterism.connect("tcp/192.0.2.2:7447", node: "fmruby-bbbbbb", app: "demo")
258
+ Asterism.expose("apu", apu, methods: [:play, :stop])
259
+ loop { Asterism.poll; ... } # in the update loop
260
+
261
+ # on another machine
262
+ Asterism.connect("tcp/192.0.2.2:7447", node: "linux", app: "demo")
263
+ apu = Asterism["fmruby-bbbbbb/demo/apu"] # <node>/<app>/<object>
264
+ apu.play("t120 o4 cdefg") # runs there, returns its value
265
+ apu.respond_to?(:play) # => true (from the exposed list)
266
+ f = apu.async.play("cde") # does not wait
267
+ f.done?; f.value
268
+ Asterism.each("*/*/apu") { |a| a.stop } # every apu alive now
269
+ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
270
+ ```
271
+
272
+ ## API
273
+
274
+ | Call | Returns | Raises / notes |
275
+ |---|---|---|
276
+ | `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). |
277
+ | `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. |
278
+ | `Asterism.unexpose(name)` | true / false | |
279
+ | `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. |
280
+ | `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). |
281
+ | `Asterism[path, timeout_ms = 2000]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). Nothing is sent until a method is called. |
282
+ | `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`). |
283
+ | `proxy.async.<method>(...)` | `Future` | Sends and returns at once. `EncodeError` / `Disconnected` are raised here. |
284
+ | `future.done?` | true / false | Never waits (`Asterism.poll` moves it on). True once the answer came, the time ran out or the connection closed. |
285
+ | `future.value` | the return value | Waits (polling) if not done; raises like a waiting call. `future.took_ms`: time to the answer. |
286
+ | `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. |
287
+ | `proxy.asterism_meta` / `proxy.asterism_path` | Hash, String | `{"methods" => [[name, arity], ...]}` |
288
+ | `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. |
289
+ | `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
290
+ | `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
291
+ | `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
292
+
293
+ Errors: `Asterism::Error` (base, a `StandardError`), `EncodeError`, `RemoteError`
294
+ (`remote_class`, `remote_message`), `Timeout`, `Disconnected`.
295
+
296
+ ### When the connection is lost
297
+
298
+ Asterism follows `Asterism::Zenoh`: the connection is closed when the router
299
+ (or the only peer) goes away, and is not reopened by itself. From then on
300
+ `Asterism.poll` returns `false`, `connected?` is false, `lost_reason` says
301
+ why, and calls raise `Asterism::Disconnected`. **`Asterism::Zenoh::Error`
302
+ never comes out of Asterism; it is always wrapped in `Disconnected`** (the
303
+ message is kept). The exposed objects are gone with the connection: to go
304
+ on, the application calls `connect` and `expose` again.
305
+
306
+ ## Values
307
+
308
+ Only what MessagePack carries: `nil`, `true`, `false`, `Integer` (64 bit),
309
+ `Float`, `String`, `Array`, `Hash`. A `Symbol` is sent as a `String` (also
310
+ as a Hash key: keyword arguments arrive as Symbols again). Anything else in
311
+ the arguments raises `EncodeError` before sending; in a return value the
312
+ caller gets `RemoteError` (`Asterism::EncodeError`). Nesting is limited to
313
+ 16 levels.
314
+
315
+ ## Keys and encoding
316
+
317
+ | Key | Zenoh | Payload |
318
+ |---|---|---|
319
+ | `asterism/<node>/<app>/<object>/call` | get / queryable | query: `[method, [args...], {kwargs}]`; reply: `["ok", value]` or `["error", class name, message]` |
320
+ | `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 |
321
+ | `asterism/<node>` | liveliness | the node is connected |
322
+ | `asterism/<node>/<app>/<object>` | liveliness | the object is exposed |
323
+
324
+ One queryable per application (`asterism/<node>/<app>/**`). A call whose
325
+ node or app is a wildcard reaches every application that exposes that
326
+ object, each answering with its own key; `Asterism` itself only sends calls
327
+ to one object.
328
+
329
+ ## How waiting works
330
+
331
+ Everything is polled; nothing runs behind the application. A call that
332
+ waits keeps polling (pausing 2 ms between polls) and answers the calls that
333
+ come in meanwhile, so two machines calling each other at the same time do
334
+ not lock up. An answered call may itself call and wait; such waits nest at
335
+ most `Asterism::MAX_NESTING` (4) deep, beyond that the call raises `Error`
336
+ (the caller of the outer call gets it as `RemoteError`). While a call
337
+ waits, the rest of that application (drawing, input) waits too; other
338
+ applications do not.
339
+
340
+ Calls to an object of the same application (`<node>/<app>` is this one) do
341
+ not go out (a Zenoh session does not see its own queryable): they are run
342
+ in place, with the same encoding and checks.
343
+
344
+ ## Stack
345
+
346
+ A waiting call polls Zenoh on the caller's C stack. Measured on the P4
347
+ (Family mruby, 16 KB application stack): the application idles at 8.1 KB
348
+ used; a waiting call made from the update loop takes it to 10.7 KB, also
349
+ with calls answered while waiting (two machines relaying calls to each
350
+ other). The same call made from an input handler that C calls into (one more
351
+ interpreter entry) reached 12.4 KB; an earlier version of this gem, which
352
+ took zenoh replies through blocks and fetched the meta from inside
353
+ `respond_to_missing?`, overflowed the stack there. Make waiting calls from
354
+ the update loop, or use `async`.
355
+
356
+ The blocks the layers call (`Asterism.on_join` / `on_leave`, the ROS
357
+ `node.subscribe` and `node.every` blocks) run from `Asterism.poll` /
358
+ `node.poll`, that is on the update loop's stack, one Ruby block call deeper.
359
+ The layers walk their lists with `while` loops so that nothing else stands
360
+ between the update loop and the block. **Do not wait inside these blocks**
361
+ (no proxy call, no `node.call` / `client.call`): note what came, and make
362
+ the call from the update loop, or start a `call_async` / `async` call and
363
+ look at it later.
364
+
365
+ Measured on the P4 (Family mruby, 16 KB): an application with
366
+ `on_join` / `on_leave` blocks, relaying calls with another machine in both
367
+ directions at once, keeps 5.8 KB free (as before the blocks); a ROS node
368
+ publishing from `node.every` and receiving through a `node.subscribe` block
369
+ keeps 6.9 KB free, the same as polling `each_pending` by hand. Loading a
370
+ nested type (`geometry_msgs/msg/Twist`) at start-up took 0.6 KB more.
371
+
372
+ ## Limits
373
+
374
+ - Objects cannot be passed by reference (a return value is a copy), blocks
375
+ cannot be sent, there are no events (later stages).
376
+ - The node token `asterism/<node>` is shared by every application of that
377
+ node; when one of them closes, the others still list the node through
378
+ their objects, but a watcher may see the node token go away.
379
+ - No authentication: a trusted LAN is assumed.
380
+ - Pattern matching on the boards: PicoRuby's compiler (checked on Family
381
+ mruby's application VM) runs `case/in` with `deconstruct_keys`, nested
382
+ hash patterns, array patterns, guards, alternatives, ranges, pins and
383
+ `**rest`, but not two forms: a class as the value in a hash pattern
384
+ (`in {x: Float}` does not match) and, inside a block, binding a variable
385
+ of the enclosing method (`v = nil; list.each { |m| case m in {a: v} ... }`
386
+ leaves it nil). Put the `case` in a method of its own, as in the example
387
+ above.
388
+
389
+ ## ROS 2 (rmw_zenoh): `Asterism::ROS` and `Asterism::CDR`
390
+
391
+ A minimal ROS 2 node that talks to ROS 2 systems using rmw_zenoh (checked
392
+ with ROS 2 Jazzy, rmw_zenoh_cpp 0.2.11, Zenoh 1.8.0), directly on an
393
+ `Asterism::Zenoh::Session`. Pure Ruby, independent of the object layer above
394
+ (no `Asterism.connect`, no MessagePack; only its clock and pause helpers).
395
+ Topics and services. The message and service types are generated from
396
+ `.msg` / `.srv` files (see [Message types](#message-types)) and loaded when
397
+ the application asks for them.
398
+
399
+ ```ruby
400
+ s = Asterism::Zenoh::Session.open("tcp/192.0.2.2:7447")
401
+ node = Asterism::ROS::Node.new(s, "fmruby_talker") # namespace: "/", domain: 0
402
+ str = Asterism::ROS.require_type("std_msgs/msg/String")
403
+ pub = node.publisher("/chatter", str)
404
+ sub = node.subscription("/chatter_back", "std_msgs/msg/String") # by name works too
405
+ loop do
406
+ s.poll
407
+ pub << { data: "hello" } # or pub.publish(str.new(data: "hello"))
408
+ sub.each_pending { |msg, info| puts "#{info && info.sequence}: #{msg.data}" }
409
+ end
410
+
411
+ # or with blocks, run from node.poll (in the update loop):
412
+ node.every(1) { pub << { data: "tick" } }
413
+ node.subscribe("/cmd_vel", "geometry_msgs/msg/Twist") { |msg, _info| drive(msg) }
414
+ loop { node.poll }
415
+
416
+ def drive(msg)
417
+ case msg # deconstruct_keys
418
+ in { linear: { x: }, angular: { z: } } then move(x, z)
419
+ end
420
+ end
421
+ node.close # or let the session close
422
+ ```
423
+
424
+ Services:
425
+
426
+ ```ruby
427
+ add = Asterism::ROS.require_type("example_interfaces/srv/AddTwoInts")
428
+ node.service("/fmruby/add_two_ints", add) { |req| { sum: req.a + req.b } }
429
+ cli = node.client("/add_two_ints", add)
430
+ loop do
431
+ node.poll # session.poll, then answers the requests
432
+ res = cli.call(a: 2, b: 3) # waits (polling), raises Asterism::ROS::Timeout
433
+ puts res.sum
434
+ c = cli.call_async(a: 2, b: 3) # returns at once
435
+ # ... later updates:
436
+ puts c.value.sum if c.done?
437
+ end
438
+ ```
439
+
440
+ | Call | Returns | Notes |
441
+ |---|---|---|
442
+ | `Asterism::ROS::Node.new(session, name, namespace: "/", domain: 0, enclave: "/")` | `Node` | Declares the node's liveliness token (`ros2 node list`). `name` has no `/`. |
443
+ | `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`). |
444
+ | `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). |
445
+ | `node.subscription(topic, type, qos: DEFAULT_QOS, depth: 16)` | `Subscription` | Subscribes and declares the subscription token. |
446
+ | `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`. |
447
+ | `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. |
448
+ | `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. |
449
+ | `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)). |
450
+ | `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. |
451
+ | `node.client(name, type, qos: DEFAULT_QOS)` | `Client` | Declares the client token. |
452
+ | `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. |
453
+ | `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`. |
454
+ | `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. |
455
+ | `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. |
456
+ | `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. |
457
+ | `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. |
458
+
459
+ What goes on the wire (rmw_zenoh_cpp 0.2.x):
460
+
461
+ | Item | Form |
462
+ |---|---|
463
+ | Data key | `<domain>/<topic without the outer "/">/<DDS type>/<type hash>`, e.g. `0/chatter/std_msgs::msg::dds_::String_/RIHS01_df668c74...` |
464
+ | Payload | CDR: `00 01 00 00`, uint32 length with the NUL, bytes, NUL |
465
+ | 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 |
466
+ | Node token | `@ros2_lv/<domain>/<zid>/<nid>/<nid>/NN/<enclave>/<namespace>/<node>` |
467
+ | Topic token | `@ros2_lv/<domain>/<zid>/<nid>/<id>/MP` (or `MS`) `/<enclave>/<namespace>/<node>/<topic>/<DDS type>/<type hash>/<qos>` |
468
+ | 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 |
469
+ | 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) |
470
+ | 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 |
471
+ | Service tokens | like topic tokens, with `SS` (server) and `SC` (client) |
472
+
473
+ In tokens, `/` inside a name is written `%` (`/chatter` is `%chatter`, the
474
+ root namespace `%`). `<zid>` is the Zenoh session ID (`session.zid`).
475
+ `DEFAULT_QOS` (`::,10:,:,:,,`) is rmw_zenoh's form of the default profile:
476
+ reliable, volatile, keep last 10.
477
+
478
+ Not here (later stages): actions, QoS other than the default (transient
479
+ local needs Zenoh's advanced publisher), name remapping, receiving a type
480
+ the application has not loaded.
481
+
482
+ ## Message types
483
+
484
+ ### Values
485
+
486
+ A generated message is a plain Ruby class (`Asterism::ROS::Message` is its
487
+ base; the app VM has neither `Data` nor `Struct`). The same code runs on
488
+ CRuby.
489
+
490
+ ```ruby
491
+ Twist = Asterism::ROS.require_type("geometry_msgs/msg/Twist")
492
+ t = Twist.new(linear: { x: 0.1 }) # missing fields take their defaults
493
+ t = Twist.from({ "linear" => { "x" => 0.1 } }) # Hash, String keys too
494
+ t.linear.x # => 0.1
495
+ t.angular # => #<geometry_msgs/msg/Vector3 {x: 0.0, ...}>
496
+ t.to_h # => {linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {...}}
497
+ bytes = Twist.encode(t) # or Twist.encode(linear: { x: 0.1 })
498
+ Twist.decode(bytes) == t # => true
499
+ Twist::TYPE_NAME # "geometry_msgs::msg::dds_::Twist_"
500
+ Twist::TYPE_HASH # "RIHS01_9c45bf16..."
501
+ Asterism::ROS::SensorMsgs::BatteryState::POWER_SUPPLY_STATUS_FULL # constants
502
+ ```
503
+
504
+ | Field in the .msg | Ruby value | Default |
505
+ |---|---|---|
506
+ | `bool` | true / false | false |
507
+ | integers, `byte`, `char` | Integer (a `uint64` at or above 2**63 reads back negative) | 0 |
508
+ | `float32` / `float64` | Float | 0.0 |
509
+ | `string`, `string<=N` | String (UTF-8 bytes; longer than N raises `ArgumentError` on encode) | "" |
510
+ | a message | that type (a Hash is turned into it) | its defaults |
511
+ | `T[N]`, `T[]`, `T[<=N]` | Array (a wrong length or over the bound raises `ArgumentError` on encode) | N defaults, or [] |
512
+ | `byte[]`, `uint8[]`, `char[]` (any size) | a binary String (an Array of Integers is taken too) | "\0" * N, or "" |
513
+ | `wstring` | not supported: encode / decode raise `NotImplementedError` | |
514
+
515
+ Defaults written in the .msg (`float64 w 1`) are used. Unknown field names
516
+ raise `ArgumentError`; integers are not range-checked (they wrap).
517
+
518
+ ### The bundled types
519
+
520
+ Made from ROS 2 Jazzy's definitions, in `data/msgs` (on a board:
521
+ `/usr/share/asterism/msgs`), one file per type:
522
+ std_msgs (Bool, Byte, Char, String, Empty, the integer and float types,
523
+ Header, ColorRGBA, MultiArrayDimension / Layout and every *MultiArray),
524
+ builtin_interfaces (Time, Duration), geometry_msgs (Vector3, Point, Point32,
525
+ Quaternion, Pose, Pose2D, Twist, Accel, Transform, Wrench and their
526
+ *Stamped), sensor_msgs (Imu, BatteryState, Temperature, Range,
527
+ MagneticField, Illuminance, FluidPressure, RelativeHumidity, JointState,
528
+ NavSatStatus, NavSatFix), example_interfaces/srv/AddTwoInts. The list is
529
+ `tools/bundled_types.txt`. Nothing of them is compiled into the mrbgem: an
530
+ application loads what it uses, and each file loads the types it is made of.
531
+
532
+ ### Making types: `tools/asterism_msggen.rb`
533
+
534
+ CRuby, standard library only, no ROS 2 installation needed (only the
535
+ `.msg` / `.srv` files of the packages involved):
536
+
537
+ ```
538
+ # a type of an installed ROS 2 (and the types it uses)
539
+ ruby tools/asterism_msggen.rb -I /opt/ros/jazzy/share -o out geometry_msgs/msg/Twist
540
+ # your own package (my_pkg/msg/Foo.msg; -I for the packages it refers to)
541
+ ruby tools/asterism_msggen.rb -I /opt/ros/jazzy/share -o out path/to/my_pkg/msg/Foo.msg
542
+ # print the type hashes
543
+ ruby tools/asterism_msggen.rb -I share --hash example_interfaces/srv/AddTwoInts
544
+ # compare the hashes with the type description JSON ROS 2 installs
545
+ ruby tools/asterism_msggen.rb -I /opt/ros/jazzy/share --check-json /opt/ros/jazzy/share sensor_msgs/msg/Imu
546
+ ```
547
+
548
+ `-I` directories are laid out like a ROS 2 `share`: `<pkg>/msg/*.msg`,
549
+ `<pkg>/srv/*.srv`. The output is `<out>/<pkg>/<msg|srv>/<Name>.rb`; copy it
550
+ to the device (`/usr/share/asterism/msgs` or a directory added to
551
+ `Asterism::ROS::TYPE_PATH`), or put the output directory in `TYPE_PATH` on
552
+ CRuby. A service's type hash needs `service_msgs/msg/ServiceEventInfo` and
553
+ `builtin_interfaces/msg/Time` under `-I` (every ROS 2 `share` has them).
554
+
555
+ The type hash (RIHS01) is computed the way ROS 2 Jazzy does
556
+ (`rosidl_generator_type_description`): the type and every type it refers
557
+ to, each as `{type_name, fields: [{name, type: {type_id, capacity,
558
+ string_capacity, nested_type_name}}]}` without default values, the referred
559
+ ones sorted by name, written as Python's `json.dumps(..., separators=(", ",
560
+ ": "))`, then SHA-256. A service's description is `request_message`,
561
+ `response_message` and `event_message` (`<Name>_Event`: `info`, and
562
+ `request` / `response` as sequences of at most one). Other ROS 2 versions
563
+ may change the rules; regenerate and check with `--check-json` against that
564
+ version.
565
+
566
+ `tools/ros2_jazzy/` holds the Jazzy definitions of the bundled types and
567
+ the hashes from Jazzy's JSON (`type_hashes.txt`) that the tests
568
+ (`rake test:msgs`) compare with. `tools/ros2_types.rb` refreshes both, and
569
+ `data/msgs`, from a ROS 2 Jazzy docker image (`rake types:refresh` with
570
+ `ASTERISM_ROS2_IMAGE`; `rake types:check` only compares). The definitions
571
+ are from ros2/common_interfaces, ros2/rcl_interfaces and
572
+ ros2/example_interfaces (Apache License 2.0, see NOTICE).
573
+
574
+ ### Loading
575
+
576
+ On mruby, `require_type` reads the file and evaluates it in the
577
+ application's VM (`Kernel#eval`); PicoRuby's `require` would run each file
578
+ in a Sandbox task that stays for the life of the VM. On CRuby it uses
579
+ `require`. Measured on the P4 (Family mruby, a 1 MB VM pool):
580
+ geometry_msgs/Twist with Vector3 15.3 KB, sensor_msgs/Imu with Header,
581
+ Time and Quaternion 32.8 KB more, std_msgs/Float32MultiArray with its
582
+ layout types 20.0 KB more (68 KB for nine files, 340 ms). Evaluating a file
583
+ compiles it on the caller's stack (about 2.5 KB deeper than the update
584
+ loop); load the types in `on_create`, not in a deep call chain.