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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/NOTICE +27 -0
- data/README.md +405 -3
- data/data/msgs/LICENSE-Apache-2.0.txt +202 -0
- data/data/msgs/NOTICE +17 -0
- data/data/msgs/builtin_interfaces/msg/Duration.rb +33 -0
- data/data/msgs/builtin_interfaces/msg/Time.rb +33 -0
- data/data/msgs/example_interfaces/srv/AddTwoInts.rb +64 -0
- data/data/msgs/geometry_msgs/msg/Accel.rb +35 -0
- data/data/msgs/geometry_msgs/msg/AccelStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Point.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Point32.rb +36 -0
- data/data/msgs/geometry_msgs/msg/PointStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Pose.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Pose2D.rb +36 -0
- data/data/msgs/geometry_msgs/msg/PoseStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Quaternion.rb +39 -0
- data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Transform.rb +36 -0
- data/data/msgs/geometry_msgs/msg/TransformStamped.rb +39 -0
- data/data/msgs/geometry_msgs/msg/Twist.rb +35 -0
- data/data/msgs/geometry_msgs/msg/TwistStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Vector3.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Wrench.rb +35 -0
- data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +36 -0
- data/data/msgs/sensor_msgs/msg/BatteryState.rb +100 -0
- data/data/msgs/sensor_msgs/msg/FluidPressure.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Illuminance.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Imu.rb +52 -0
- data/data/msgs/sensor_msgs/msg/JointState.rb +44 -0
- data/data/msgs/sensor_msgs/msg/MagneticField.rb +39 -0
- data/data/msgs/sensor_msgs/msg/NavSatFix.rb +55 -0
- data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +43 -0
- data/data/msgs/sensor_msgs/msg/Range.rb +52 -0
- data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Temperature.rb +38 -0
- data/data/msgs/std_msgs/msg/Bool.rb +30 -0
- data/data/msgs/std_msgs/msg/Byte.rb +30 -0
- data/data/msgs/std_msgs/msg/ByteMultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Char.rb +30 -0
- data/data/msgs/std_msgs/msg/ColorRGBA.rb +39 -0
- data/data/msgs/std_msgs/msg/Empty.rb +26 -0
- data/data/msgs/std_msgs/msg/Float32.rb +30 -0
- data/data/msgs/std_msgs/msg/Float32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Float64.rb +30 -0
- data/data/msgs/std_msgs/msg/Float64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Header.rb +35 -0
- data/data/msgs/std_msgs/msg/Int16.rb +30 -0
- data/data/msgs/std_msgs/msg/Int16MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int32.rb +30 -0
- data/data/msgs/std_msgs/msg/Int32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int64.rb +30 -0
- data/data/msgs/std_msgs/msg/Int64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int8.rb +30 -0
- data/data/msgs/std_msgs/msg/Int8MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +36 -0
- data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +35 -0
- data/data/msgs/std_msgs/msg/String.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt16.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt32.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt64.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt8.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +35 -0
- data/lib/asterism/cruby.rb +24 -0
- data/lib/asterism/version.rb +5 -0
- data/lib/asterism.rb +17 -4
- data/mrblib/asterism.rb +545 -0
- data/mrblib/cdr.rb +512 -0
- data/mrblib/future.rb +83 -0
- data/mrblib/proxy.rb +97 -0
- data/mrblib/ros.rb +691 -0
- data/tools/asterism_msggen.rb +826 -0
- metadata +118 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16a71827f6f79aa024b98f6aa686efa127228644d12f6bef3469dad98d819dac
|
|
4
|
+
data.tar.gz: b564b747d9cb0dd6d890e86e95cbc04ca75f7d1a55b24490bfd315edfc1cf25a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
4
|
-
network
|
|
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
|
-
|
|
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.
|