asterism 0.2.0 → 0.4.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/CHANGELOG.md +110 -0
- data/README.md +277 -62
- data/lib/asterism/cruby/objects.rb +25 -10
- data/lib/asterism/cruby/ros.rb +6 -4
- data/lib/asterism/cruby/zenoh.rb +428 -72
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +2 -2
- data/mrblib/asterism.rb +112 -19
- data/mrblib/cdr.rb +5 -1
- data/mrblib/future.rb +11 -2
- data/mrblib/proxy.rb +13 -3
- data/mrblib/ros.rb +121 -41
- metadata +5 -4
data/README.md
CHANGED
|
@@ -9,11 +9,11 @@ repository holds the pure Ruby layers, written once for every Ruby:
|
|
|
9
9
|
|---|---|
|
|
10
10
|
| `mrblib/` | The layers: remote objects (`Asterism`), `Asterism::CDR`, `Asterism::ROS`. The source of truth for both forms below |
|
|
11
11
|
| `mrbgem.rake` | The mrbgem `picoruby-asterism` (mruby / PicoRuby): compiles `mrblib/` |
|
|
12
|
-
| `asterism.gemspec`, `lib/` | The CRuby gem `asterism`: `lib/asterism.rb` loads `mrblib/` as it is, `lib/asterism/cruby.rb` adds what CRuby needs, `lib/asterism/cruby/` the
|
|
12
|
+
| `asterism.gemspec`, `lib/` | The CRuby gem `asterism`: `lib/asterism.rb` loads `mrblib/` as it is, `lib/asterism/cruby.rb` adds what CRuby needs, `lib/asterism/cruby/` the CRuby API (CRuby only) |
|
|
13
13
|
| `data/msgs/` | ROS 2 message types generated from ROS 2 Jazzy's definitions (Apache-2.0, see NOTICE) |
|
|
14
14
|
| `tools/` | The type generator `asterism_msggen.rb`, the Jazzy definitions it reads, `ros2_types.rb` (refreshes them from a ROS 2 image) |
|
|
15
|
-
| `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_api_*.rb` (the
|
|
16
|
-
| `examples/` | CRuby: `node.rb` (objects with a board), `node_polled.rb` (the same with the
|
|
15
|
+
| `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_api_*.rb` (the CRuby API) |
|
|
16
|
+
| `examples/` | CRuby: `node.rb` (objects with a board), `node_polled.rb` (the same with the portable API of the boards), `ros2_talker.rb` (topics, a timer, services with ROS 2), `zenoh.rb` (plain Zenoh) |
|
|
17
17
|
|
|
18
18
|
The Zenoh binding underneath is a separate gem with the same Ruby API on
|
|
19
19
|
each Ruby:
|
|
@@ -23,15 +23,54 @@ each Ruby:
|
|
|
23
23
|
- mruby / PicoRuby: [picoruby-asterism-zenoh](https://github.com/ruby-asterism/picoruby-asterism-zenoh)
|
|
24
24
|
(over zenoh-pico)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
Which zenoh-c features the CRuby binding exposes (configuration and TLS,
|
|
27
|
+
scouting, publishers, queriers, the advanced publisher / subscriber,
|
|
28
|
+
events, key expressions, timestamps) and which zenoh-pico could offer is
|
|
29
|
+
in asterism-zenoh's
|
|
30
|
+
[feature coverage](https://github.com/ruby-asterism/asterism-zenoh/blob/main/docs/feature_coverage.md).
|
|
31
|
+
The ones added in 0.3.0 are CRuby only: the shared layer (`mrblib/`) does
|
|
32
|
+
not use them.
|
|
33
|
+
|
|
34
|
+
Both gems are on rubygems.org (`gem install asterism` installs
|
|
35
|
+
asterism-zenoh too). What changed in each version is in
|
|
36
|
+
[CHANGELOG.md](CHANGELOG.md); 0.4.0 is the first step toward 1.0
|
|
37
|
+
([docs/api_review.md](docs/api_review.md)) and only adds and deprecates
|
|
38
|
+
(see [Deprecations and 1.0](#deprecations-and-10)).
|
|
39
|
+
|
|
40
|
+
## Which API?
|
|
41
|
+
|
|
42
|
+
Asterism has two APIs over the same layers. Both are in the CRuby gem;
|
|
43
|
+
the boards have the first one only.
|
|
44
|
+
|
|
45
|
+
| You write for | Use | Why |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| A board, or code that must run on both | the **portable API** ([below](#the-portable-api): `Asterism.poll`, `node.poll`, `each_pending`) | the only one on the boards; nothing runs behind your back |
|
|
48
|
+
| CRuby only (tools, servers, scripts) | the **CRuby API** ([The CRuby API](#the-cruby-api): `Asterism.connect { }`, `Asterism::Zenoh.open { }`, `Asterism::ROS.connect { }`) | blocks, a receiving thread, Enumerators, `Data` values |
|
|
49
|
+
| CRuby, but you own the loop (a game loop, a test) | the portable API | same as the boards |
|
|
50
|
+
|
|
51
|
+
Entry points and what they give:
|
|
52
|
+
|
|
53
|
+
| Call | Gives | Underneath |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `Asterism::Zenoh::Session.open` | a `Session` (portable) | zenoh-c / zenoh-pico |
|
|
56
|
+
| `Asterism::Zenoh.open` | a `Zenoh::Connection` (CRuby) | `c.session` is the `Session` |
|
|
57
|
+
| `Asterism.connect` | `Asterism` itself (portable; one connection per process or VM), or a `Net` to the block (CRuby) | a `Session` of its own |
|
|
58
|
+
| `Asterism::ROS::Node.new(session, ...)` | a `Node` (portable) | the `Session` given |
|
|
59
|
+
| `Asterism::ROS.connect` | a `ROS::Connection` (CRuby) | `ros.zenoh` is the `Zenoh::Connection` |
|
|
60
|
+
|
|
61
|
+
Units: a time with no unit in its name is seconds (`timeout:`,
|
|
62
|
+
`connect_timeout:`, `check_timeout:`, `node.every(0.5)`, `DEFAULT_TIMEOUT`);
|
|
63
|
+
anything else says its unit (`timeout_ms:`, `every(ms: 500)`, `took_ms`,
|
|
64
|
+
`DEFAULT_TIMEOUT_MS`, `stamp_ns`). Both units are accepted everywhere a
|
|
65
|
+
time is taken, on both APIs and every Ruby; giving one twice raises
|
|
66
|
+
`ArgumentError`.
|
|
28
67
|
|
|
29
68
|
## Using it
|
|
30
69
|
|
|
31
70
|
CRuby (needs CRuby 3.2+ and a C compiler):
|
|
32
71
|
|
|
33
72
|
```
|
|
34
|
-
gem install asterism # also installs asterism-zenoh (~> 0.
|
|
73
|
+
gem install asterism # also installs asterism-zenoh (~> 0.4.0) and msgpack
|
|
35
74
|
ruby -e 'require "asterism"'
|
|
36
75
|
```
|
|
37
76
|
|
|
@@ -44,7 +83,7 @@ build but not yet tested. To install from the repositories instead:
|
|
|
44
83
|
|
|
45
84
|
```
|
|
46
85
|
(cd ../asterism-zenoh && rake gem) && rake gem
|
|
47
|
-
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.
|
|
86
|
+
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.0.gem pkg/asterism-0.4.0.gem
|
|
48
87
|
```
|
|
49
88
|
|
|
50
89
|
From the working trees, without installing:
|
|
@@ -70,13 +109,15 @@ these repositories.
|
|
|
70
109
|
rake # every suite
|
|
71
110
|
rake test:msgs # generator, type hashes, CDR, bundled types (CRuby only; no Zenoh, no docker)
|
|
72
111
|
rake test:objects # objects and Asterism::ROS between CRuby sessions over a local peer link
|
|
73
|
-
rake test:api # the
|
|
112
|
+
rake test:api # the CRuby API (blocks, receiving threads, Enumerators)
|
|
74
113
|
ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
|
|
75
114
|
```
|
|
76
115
|
|
|
77
116
|
`test:objects` and `test:api` need asterism-zenoh: built next to this repository
|
|
78
117
|
(`../asterism-zenoh`, its `rake compile`), or `ASTERISM_ZENOH_DIR`, or the
|
|
79
|
-
installed gem.
|
|
118
|
+
installed gem. The tests run with deprecated calls raising
|
|
119
|
+
(`Asterism.deprecations = :raise`); the tests of the old forms switch it
|
|
120
|
+
back around themselves.
|
|
80
121
|
|
|
81
122
|
## Examples
|
|
82
123
|
|
|
@@ -94,11 +135,11 @@ ruby examples/ros2_talker.rb --router tcp/192.0.2.2:7447 [--service NAME]
|
|
|
94
135
|
ruby examples/zenoh.rb --router tcp/192.0.2.2:7447 --name pc1
|
|
95
136
|
```
|
|
96
137
|
|
|
97
|
-
## The
|
|
138
|
+
## The CRuby API
|
|
98
139
|
|
|
99
140
|
On CRuby, `require "asterism"` also brings a layer with blocks, a receiving
|
|
100
141
|
thread, Enumerators and pattern matching (`lib/asterism/cruby/`). It is
|
|
101
|
-
built on the
|
|
142
|
+
built on the portable API below, which stays as it is: code written for a
|
|
102
143
|
board still runs unchanged on CRuby, and code using this layer does not run
|
|
103
144
|
on a board.
|
|
104
145
|
|
|
@@ -115,29 +156,53 @@ Asterism::Zenoh.open("tcp/192.0.2.2:7447") do |s| # closed when the block ends
|
|
|
115
156
|
s.liveliness_watch("asterism/**") { |key, alive| puts "#{key} #{alive}" }
|
|
116
157
|
s.run # receive on this thread until Ctrl-C
|
|
117
158
|
end
|
|
159
|
+
|
|
160
|
+
# asterism-zenoh 0.3.0 and later (CRuby only)
|
|
161
|
+
Asterism::Zenoh.open("tls/192.0.2.2:7447",
|
|
162
|
+
config: { "transport/link/tls/root_ca_certificate" => "ca.pem" }) do |s|
|
|
163
|
+
pub = s.publisher("home/pc/temp", encoding: "text/plain", priority: :data_high)
|
|
164
|
+
pub.on_matching { |listening| puts "listened to: #{listening}" }
|
|
165
|
+
pub.put(21.5)
|
|
166
|
+
s.delete("home/pc/old")
|
|
167
|
+
s.subscribe("home/**") { |sm| p [sm.kind, sm.encoding, sm.timestamp&.to_time] }
|
|
168
|
+
q = s.querier("home/**/status", timeout: 1.0)
|
|
169
|
+
q.get(errors: true).each { |r| puts r.error? ? "error: #{r.payload}" : r.payload }
|
|
170
|
+
latched = s.advanced_publisher("home/pc/mode", cache: 1, sample_miss_detection: true)
|
|
171
|
+
latched.put("eco") # late subscribers with history: get it
|
|
172
|
+
s.advanced_subscriber("home/**/mode", history: true) { |sm| puts sm.payload }
|
|
173
|
+
s.on_transport { |ev| puts "#{ev.zid} #{ev.kind}" }
|
|
174
|
+
s.run
|
|
175
|
+
end
|
|
118
176
|
```
|
|
119
177
|
|
|
120
178
|
| Call | Notes |
|
|
121
179
|
|---|---|
|
|
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
|
|
124
|
-
| `s.put(key, payload, attachment: nil)` | a payload that is not a String is sent as `to_s` |
|
|
180
|
+
| `Asterism::Zenoh.open(locator = nil, mode:, listen:, scouting:, timestamping:, config:, config_file:, connect_timeout:, interval: 0.002)` | `Session.open` wrapped in a `Connection`. With a block: yields it, closes it after (also on an exception), returns the block's value |
|
|
181
|
+
| `Connection.new(session)` | wraps a session opened with the portable API |
|
|
182
|
+
| `s.put(key, payload, attachment: nil, **opts)` | a payload that is not a String is sent as `to_s`. opts: `encoding:`, `priority:`, `congestion_control:`, `express:`, `reliability:`, `timestamp:`, `allowed_destination:` (asterism-zenoh's `Session#put`) |
|
|
183
|
+
| `s.delete(key, **opts)` | subscribers get a `Sample` of kind `:delete` |
|
|
184
|
+
| `s.publisher(key, **opts)` | a `Publisher`: `put(payload, attachment:, encoding:, timestamp:)`, `delete`, `matching?`, `on_matching { \|listening\| }` (on the receiving thread), `close`. With a block: yields it and closes it after |
|
|
185
|
+
| `s.advanced_publisher(key, cache:, sample_miss_detection:, publisher_detection:, **opts)` | the same `Publisher`, keeping the last samples for late subscribers (ROS 2's transient local) |
|
|
186
|
+
| `s.advanced_subscriber(key, depth: 16, history:, recovery:, query_timeout:, **opts) { \|sample\| }` | a `Subscription` that also gets the publishers' history; `on_publisher { \|key, alive\| }`, `on_miss { \|miss\| }` |
|
|
187
|
+
| `s.querier(key, timeout: 2.0, **opts)` (or `timeout_ms:`) | a `Querier`: `get(params:, payload:, attachment:, encoding:, errors: false)` like `s.get`, `matching?`, `on_matching`, `close` |
|
|
188
|
+
| `s.on_transport(history: false) { \|ev\| }` / `s.on_link { \|ev\| }` | a `TransportEvent` / `LinkEvent` (`added?` / `removed?`, `zid`, ...) on the receiving thread; returns an `Events` (`close`) |
|
|
189
|
+
| `s.peer_zids`, `router_zids`, `transports`, `links`, `new_timestamp`, `declare_keyexpr(key)` | as in asterism-zenoh |
|
|
125
190
|
| `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), `
|
|
191
|
+
| `s.subscribe(key, depth: 16)` | a `Subscription` to take from: `each(timeout: nil)` waits for samples (an Enumerator without a block; Enumerable), `each_sample` gives what is there now as Samples (`each_pending`, its old name here, is deprecated: in the portable API `each_pending` gives Arrays), `pending` / `received` / `dropped` / `closed?` / `close` |
|
|
127
192
|
| `s.queryable(key, depth: 16, complete: false) { \|q\| }` | each query on the receiving thread, finished when the block returns. `q.reply(payload)` answers on the queryable's key (when it has no wildcard), `q.reply(key, payload, attachment:)` as before. Without a block, `each(timeout:)` yields the queries |
|
|
128
|
-
| `s.get(key, timeout: 2.0, params:, payload:, attachment:, target:, consolidation:)` |
|
|
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) |
|
|
193
|
+
| `s.get(key, timeout: 2.0, params:, payload:, attachment:, target:, consolidation:, errors: false, depth: 1024, **opts)` | a `GetEnumerator` of `Reply`; each iteration sends the get again, and `dropped` / `received` / `errors` are those of the last one. With a block: yields each, returns their number. `depth:` the replies kept until taken (see [Queues](#queues)). `errors: true` also yields the error replies (`reply.error?`; left out by default, as `each_reply` does: look there when "nobody answers"); `consolidation: :none` keeps every reply (zenoh's own default is `:auto`). opts: `encoding:`, `priority:`, `congestion_control:`, `express:`, `accept_replies:`, `timeout_ms:` |
|
|
194
|
+
| `s.liveliness(key)` | a token (`close`). With a block: withdrawn after the block |
|
|
195
|
+
| `s.liveliness_watch(key, depth: 1024) { \|key, alive\| }` | on the receiving thread; the tokens alive now come first, in one burst. Without a block, `each` yields `Liveliness` (`key`, `alive?`). `pending` / `received` / `dropped` / `closed?` / `close` |
|
|
196
|
+
| `s.liveliness_get(key, timeout: 2.0, depth: 1024)` | the keys alive now (a `KeyList`: an Array with `dropped` and `received`; waits) |
|
|
132
197
|
| `s.start` / `s.stop` / `s.running?` | receive on a thread of its own |
|
|
133
198
|
| `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`, `
|
|
199
|
+
| `s.on_error { \|error, where\| }` | what the blocks raise (`StandardError`); without a handler it is printed with `warn`. Receiving goes on. `on_error` replaces the handler; every other `on_*` adds one |
|
|
200
|
+
| `s.session`, `zid`, `connection_count`, `poll`, `closed?`, `close` | (`peers` is the deprecated name of `connection_count`) |
|
|
136
201
|
|
|
137
|
-
`Sample`, `Reply` (`key`, `payload`, `attachment
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
202
|
+
`Sample`, `Reply` (`key`, `payload`, `attachment`, and since 0.3.0 `kind`,
|
|
203
|
+
`encoding`, `timestamp` and the rest; `text` is the payload as UTF-8; both
|
|
204
|
+
are asterism-zenoh's) and `Liveliness` are `Data` values, so they work with pattern
|
|
205
|
+
matching: `case sample in {key: %r{/temp\z}, payload:}`.
|
|
141
206
|
|
|
142
207
|
### Objects
|
|
143
208
|
|
|
@@ -158,12 +223,38 @@ end
|
|
|
158
223
|
| `Asterism.connect(...) { \|net\| }` | yields an `Asterism::Net`, `Asterism.close` when the block ends; returns the block's value. Without a block, as before |
|
|
159
224
|
| `Asterism.net` | the `Net` of the connection made without a block (nil when not connected) |
|
|
160
225
|
| `net.expose` / `unexpose` / `exposed` | as `Asterism.expose`. The methods run on the receiving thread, in `run`, or in whatever polls |
|
|
161
|
-
| `net[path, timeout:
|
|
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
|
|
226
|
+
| `net[path, timeout: 2.0]` | a proxy (`timeout` in seconds, or `timeout_ms:`) |
|
|
227
|
+
| `net.each(pattern = "**")` | the proxies alive now; an Enumerator without a block. With a block it returns their number (1.0: `self`, as Ruby's `each` does; `net.count` is the number) |
|
|
228
|
+
| `net.on_join { \|node\| }` / `net.on_leave { \|node\| }` | `Asterism.on_join` / `on_leave` of the portable API (the same blocks, told from `Asterism.poll`), here on the receiving thread; what they raise goes to `on_error`. `net.off_join(proc)` / `off_leave(proc)` remove one |
|
|
164
229
|
| `net.start` / `stop` / `run` / `running?` / `on_error` | as for Zenoh. A method of an exposed object that raises is answered to its caller as a `RemoteError`, as before; `on_error` gets what `on_join` / `on_leave` raise |
|
|
165
230
|
| `net.nodes`, `node_id`, `app`, `connected?`, `lost_reason`, `poll`, `close` | |
|
|
166
231
|
|
|
232
|
+
### Over TLS (a router on the internet)
|
|
233
|
+
|
|
234
|
+
`config:` of `Asterism.connect` (and of `Asterism::Zenoh.open`,
|
|
235
|
+
`Asterism::ROS.connect`) goes to the zenoh session: a Hash of zenoh's
|
|
236
|
+
configuration keys, or a JSON5 String (`File.read("client.json5")`). For a
|
|
237
|
+
router that asks for a client certificate (mutual TLS):
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
tls = {
|
|
241
|
+
"transport/link/tls/root_ca_certificate" => "certs/ca.pem", # the CA that signed the router
|
|
242
|
+
"transport/link/tls/connect_certificate" => "certs/cruby.pem", # this node's certificate
|
|
243
|
+
"transport/link/tls/connect_private_key" => "certs/cruby.key",
|
|
244
|
+
"transport/link/tls/enable_mtls" => true
|
|
245
|
+
}
|
|
246
|
+
Asterism.connect("tls/router.example.org:7448", node: "cruby", app: "demo", config: tls) do |net|
|
|
247
|
+
net["fmruby-aaaaaa/demo/info"].status # a board on the router's other side
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The router checks the certificate against its CA, and its ACL can tell the
|
|
252
|
+
nodes apart by the certificate's common name. The name in the locator must
|
|
253
|
+
be one the router's certificate is valid for (zenoh checks it; it is
|
|
254
|
+
`verify_name_on_connect`). `examples/node.rb --ca --cert --key` does the same.
|
|
255
|
+
The boards (zenoh-pico on ESP32) have no TLS: they stay on a router in their
|
|
256
|
+
own network, which connects to the outside one.
|
|
257
|
+
|
|
167
258
|
### ROS 2
|
|
168
259
|
|
|
169
260
|
```ruby
|
|
@@ -183,12 +274,12 @@ end
|
|
|
183
274
|
| Call | Notes |
|
|
184
275
|
|---|---|
|
|
185
276
|
| `Asterism::ROS.connect(locator = nil, domain: 0, type_path: nil, mode:, listen:)` | opens a session; yields an `Asterism::ROS::Connection` and closes it after. `type_path:` adds directories to `TYPE_PATH` |
|
|
186
|
-
| `ros.node(name, namespace: "/", enclave: "/")` | an `Asterism::ROS::Node` (every method of the
|
|
187
|
-
| `node.subscribe(topic, type) { \|msg, info\| }` | the
|
|
277
|
+
| `ros.node(name, namespace: "/", enclave: "/")` | an `Asterism::ROS::Node` (every method of the portable API) with the calls below |
|
|
278
|
+
| `node.subscribe(topic, type) { \|msg, info\| }` | the portable API's (run from `node.poll`), here on the receiving thread; `info` is the `Attachment` or nil. Without a block: a subscription whose `each(timeout: nil)` waits for `[msg, info]` (Enumerable) |
|
|
188
279
|
| `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
|
|
280
|
+
| `node.every(seconds) { }` / `node.every(ms: 500) { }` | the portable API's timer (run from `node.poll`), here on the receiving thread; returns a `Timer` (`cancel`, `fired`) |
|
|
190
281
|
| `node.service(name, type) { \|req\| response }` | answered while the connection spins (or from `node.poll`). What the block raises goes to `on_error`, and that request gets no answer |
|
|
191
|
-
| `node.call(name, type, request = nil, timeout: 2.0, **fields)` | waits for the response |
|
|
282
|
+
| `node.call(name, type, request = nil, request:, timeout: 2.0, **fields)` | waits for the response. Here `timeout:` is always the time limit (see [Reserved keywords](#reserved-keywords)) |
|
|
192
283
|
| `ros.spin` (`run`) / `start` / `stop` / `running?` / `on_error` / `close` | |
|
|
193
284
|
| `ros.zenoh` | the `Asterism::Zenoh::Connection` underneath (same receiving thread) |
|
|
194
285
|
|
|
@@ -209,7 +300,7 @@ that is not bundled: generate it with `tools/asterism_msggen.rb -o DIR`
|
|
|
209
300
|
block that takes long delays everything else that connection receives**:
|
|
210
301
|
hand long work to a thread or a Queue of your own.
|
|
211
302
|
- zenoh-c never calls Ruby: it fills the queues, and the receiving thread
|
|
212
|
-
empties them every 2 ms (`interval:`) with the
|
|
303
|
+
empties them every 2 ms (`interval:`) with the portable API.
|
|
213
304
|
- A tick of the receiving thread holds the connection's lock (a `Monitor`;
|
|
214
305
|
for the object layer, which is one connection per process,
|
|
215
306
|
`Asterism::LOCK`). The application's calls into the same layer take it
|
|
@@ -224,6 +315,23 @@ that is not bundled: generate it with `tools/asterism_msggen.rb -o DIR`
|
|
|
224
315
|
thread ends by itself, and waiting calls raise as before.
|
|
225
316
|
- Enumerators (`each`, `get`, `topic`) wait on the thread that iterates
|
|
226
317
|
them.
|
|
318
|
+
- **Do not wait in a block.** On CRuby a block that calls and waits (a
|
|
319
|
+
proxy call, `node.call`) works, but delays every other block of that
|
|
320
|
+
connection; on a board it can overflow the stack ([Stack](#stack)).
|
|
321
|
+
|
|
322
|
+
What may be shared between threads:
|
|
323
|
+
|
|
324
|
+
| Object | Use from several threads? |
|
|
325
|
+
|---|---|
|
|
326
|
+
| `Asterism::Zenoh::Session` and its subscribers, queryables, gets, publishers, queriers (CRuby) | yes; each queued entry goes to exactly one taker |
|
|
327
|
+
| `Zenoh::Connection` | yes; its blocks run one at a time on the receiving thread |
|
|
328
|
+
| An Enumerator (`Subscription#each`, `Watch#each`, `get`, `topic`) | one thread per iteration |
|
|
329
|
+
| The object layer (`Asterism.*`, `Net`, proxies, `Future`) | yes; serialized by `Asterism::LOCK` (one connection per process) |
|
|
330
|
+
| `ROS::Connection` and the nodes it makes | yes (its runner's lock) |
|
|
331
|
+
| A `ROS::Node` made with `Node.new` (portable API) on CRuby | **no**: one thread, as on the boards |
|
|
332
|
+
| A session opened before `fork` | not in the child: its calls raise `Zenoh::ClosedError`; connect after fork |
|
|
333
|
+
| The boards | one thread (the application's update loop) |
|
|
334
|
+
| Ractors | not supported |
|
|
227
335
|
|
|
228
336
|
## License
|
|
229
337
|
|
|
@@ -243,10 +351,10 @@ RubyGems, and on mruby / PicoRuby the Zenoh binding is the separate mrbgem
|
|
|
243
351
|
|
|
244
352
|
---
|
|
245
353
|
|
|
246
|
-
# The API
|
|
354
|
+
# The portable API
|
|
247
355
|
|
|
248
|
-
The same on every Ruby (
|
|
249
|
-
[The
|
|
356
|
+
The same on every Ruby (CRuby adds the layer described in
|
|
357
|
+
[The CRuby API](#the-cruby-api)). The examples use PicoRuby's `sleep_ms`; on CRuby
|
|
250
358
|
write `sleep`. When PicoRuby's `Machine` is there it is used for the clock
|
|
251
359
|
and the short pauses while waiting; otherwise `Time` and `sleep`. Besides
|
|
252
360
|
`Asterism::Zenoh`, the layers need a `MessagePack` module with `pack` /
|
|
@@ -265,6 +373,7 @@ apu.play("t120 o4 cdefg") # runs there, returns its value
|
|
|
265
373
|
apu.respond_to?(:play) # => true (from the exposed list)
|
|
266
374
|
f = apu.async.play("cde") # does not wait
|
|
267
375
|
f.done?; f.value
|
|
376
|
+
Asterism["fmruby-bbbbbb/demo/apu", timeout: 5.0].play("c") # a longer time limit
|
|
268
377
|
Asterism.each("*/*/apu") { |a| a.stop } # every apu alive now
|
|
269
378
|
Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
270
379
|
```
|
|
@@ -273,25 +382,124 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
273
382
|
|
|
274
383
|
| Call | Returns | Raises / notes |
|
|
275
384
|
|---|---|---|
|
|
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
|
|
385
|
+
| `Asterism.connect(locator, node:, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: 1.5)` | `Asterism` | `locator`, `mode:`, `listen:` (and `config:` / `connect_timeout:` when given, CRuby only: TLS and the rest of zenoh's configuration; the boards raise `ArgumentError`) go to `Asterism::Zenoh::Session.open` (client of a router by default; `mode: :peer` with or without `listen:` without one). `node:` is this machine's ID, `app:` the application's name (one key chunk each: no `/ * $ ? #`, not starting with `@`; else `ArgumentError`). `Disconnected` when it cannot connect; `Error` when objects of the same `<node>/<app>` are already alive (a second copy of the application), or when already connected. Waits up to `check_timeout:` seconds for that check (a router answers at once; give more over a slow relay). Returns the module itself: on a board the module is the connection (one per application). The default is plain text without authentication; see [Over TLS](#over-tls-a-router-on-the-internet). |
|
|
277
386
|
| `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
387
|
| `Asterism.unexpose(name)` | true / false | |
|
|
279
388
|
| `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,
|
|
282
|
-
| `proxy.<method>(*args, **kw)` | the remote return value | Waits for the answer (
|
|
389
|
+
| `Asterism.on_join { \|node\| }` / `Asterism.on_leave { \|node\| }` | `Asterism` | Another node appeared (its node token or one of its objects) / is gone (no token and no object left). The nodes there already join on the first polls after connecting; when the connection is lost, every node known until then leaves (on the next `Asterism.poll`). The blocks run from `Asterism.poll` only, never from the polling inside a waiting call; what they raise comes out of `Asterism.poll`. Each call adds a block; `Asterism.off_join(proc)` / `off_leave(proc)` remove one; `Asterism.close` forgets them (a lost connection does not). |
|
|
390
|
+
| `Asterism[path, timeout: 2.0]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). `timeout:` (seconds) or `timeout_ms:` is the time limit of its calls. Nothing is sent until a method is called. |
|
|
391
|
+
| `proxy.<method>(*args, **kw)` | the remote return value | Waits for the answer (the proxy's time limit). `RemoteError` when it raised there or could not be called (not exposed: `NoMethodError`; wrong number of arguments: `ArgumentError`; no such object: `NameError`), `TimeoutError` when no answer came in time (also at once when nobody answers that key), `Disconnected`, `EncodeError` (before anything is sent) for a value MessagePack cannot carry. A block cannot be sent (`ArgumentError`). |
|
|
283
392
|
| `proxy.async.<method>(...)` | `Future` | Sends and returns at once. `EncodeError` / `Disconnected` are raised here. |
|
|
284
393
|
| `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.
|
|
394
|
+
| `future.value` | the return value | Waits (polling) if not done; raises like a waiting call. `future.took` (seconds) / `took_ms`: time to the answer. |
|
|
395
|
+
| `proxy.respond_to?(name)` / `proxy.remote_methods` | true / false, `[Symbol]` | From the object's meta (fetched once, `proxy.asterism_refresh` forgets it). `respond_to?` is false when the object does not answer; `remote_methods` raises then. (`proxy.methods` returns the same with a deprecation warning; from 1.0 it is `Object#methods`.) |
|
|
287
396
|
| `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. |
|
|
397
|
+
| `Asterism.each(pattern = "**") { \|proxy\| }` | count (Array without a block) | The exposed objects alive now (this application's own included) whose `<node>/<app>/<object>` matches; `*` is one chunk, `**` any number. 1.0 returns the proxies from the block form instead of their number (Ruby's `each` returns its receiver); write `Asterism.each(pattern).size` for the count. |
|
|
289
398
|
| `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
|
|
290
399
|
| `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
|
|
291
400
|
| `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
|
|
292
401
|
|
|
293
|
-
Errors
|
|
294
|
-
|
|
402
|
+
## Errors
|
|
403
|
+
|
|
404
|
+
Every error Asterism raises is an `Asterism::Error` (a `StandardError`):
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
Asterism::Error (the Zenoh bindings define it)
|
|
408
|
+
├── Asterism::Zenoh::Error (binding failures; #code is zenoh's result code)
|
|
409
|
+
│ └── Asterism::Zenoh::ClosedError (the session is closed or its connection was lost)
|
|
410
|
+
├── Asterism::Disconnected (object layer: the connection is closed or lost)
|
|
411
|
+
├── Asterism::TimeoutError (no answer in time; old name Asterism::Timeout)
|
|
412
|
+
│ └── Asterism::ROS::TimeoutError (old name Asterism::ROS::Timeout)
|
|
413
|
+
├── Asterism::EncodeError
|
|
414
|
+
├── Asterism::RemoteError (remote_class, remote_message)
|
|
415
|
+
├── Asterism::ROS::UnknownType
|
|
416
|
+
├── Asterism::CDR::DecodeError
|
|
417
|
+
└── Asterism::DeprecationError (only with Asterism.deprecations = :raise)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The old names `Asterism::Timeout` and `Asterism::ROS::Timeout` still work
|
|
421
|
+
(they warn once). Inside `module Asterism`, `Timeout` is Ruby's `Timeout`
|
|
422
|
+
module again. The ROS layer raises `Zenoh::ClosedError` when its session
|
|
423
|
+
is closed (1.0: `Disconnected`, as the object layer).
|
|
424
|
+
|
|
425
|
+
## Defaults
|
|
426
|
+
|
|
427
|
+
| What | Default | Changed by |
|
|
428
|
+
|---|---|---|
|
|
429
|
+
| A proxy call | 2 s | `Asterism[path, timeout:]` / `timeout_ms:`; `DEFAULT_TIMEOUT` (s), `DEFAULT_TIMEOUT_MS` |
|
|
430
|
+
| A service call | 2 s | `client.call(..., timeout:)` / `timeout_ms:`; `Client::DEFAULT_TIMEOUT` |
|
|
431
|
+
| `get`, `liveliness_get`, a querier | 2 s | `timeout:` / `timeout_ms:` |
|
|
432
|
+
| Connecting | 3 s | `connect_timeout:` (CRuby); `Zenoh::CONNECT_TIMEOUT_MS` (fixed at build time on the boards) |
|
|
433
|
+
| A send that cannot go out | 3 s | `Zenoh::SEND_TIMEOUT_MS` |
|
|
434
|
+
| The duplicate check of `Asterism.connect` | 1.5 s | `check_timeout:`; `CHECK_TIMEOUT` |
|
|
435
|
+
| Queue depth (subscriptions, queryables) | 16 (services 8; the object layer's queryable 32) | `depth:` |
|
|
436
|
+
| Queue depth of gets, liveliness gets and liveliness watches | CRuby 1024, the boards 16 (the object layer's watch: 1024 on CRuby, 64 on the boards) | `depth:`; `Zenoh::DEFAULT_GET_DEPTH`, `Zenoh::DEFAULT_WATCH_DEPTH` |
|
|
437
|
+
| A full queue | drops the oldest (counted in `dropped`); the CRuby API warns once | `depth:` |
|
|
438
|
+
| Pause between polls of a waiting call | 2 ms | `WAIT_STEP_MS`, `Client::WAIT_STEP_MS` |
|
|
439
|
+
| Receiving interval of the CRuby API | 2 ms | `interval:` |
|
|
440
|
+
| Calls waiting inside each other | 4 | `MAX_NESTING` |
|
|
441
|
+
| Encoding depth of a value | 16 | `Codec::MAX_DEPTH` |
|
|
442
|
+
| `get` consolidation | `:none` (every reply; zenoh's own default is `:auto`, but services and the object layer want every reply) | `consolidation:` |
|
|
443
|
+
|
|
444
|
+
### Queues
|
|
445
|
+
|
|
446
|
+
Everything received waits in a bounded queue until taken; when it is full
|
|
447
|
+
the oldest entry goes and is counted in `dropped`. A router answers a
|
|
448
|
+
wildcard get or liveliness get, and a new liveliness watch, with
|
|
449
|
+
everything at once, so on CRuby those three queues hold 1024 by default
|
|
450
|
+
(they grow as entries come; nothing is allocated up front). On the boards
|
|
451
|
+
they stay at 16: a get's queue is allocated in full when it is sent (in
|
|
452
|
+
PSRAM on ESP-IDF) and a watch's comes from the VM's pool; pass `depth:`
|
|
453
|
+
when a wildcard can match more.
|
|
454
|
+
|
|
455
|
+
On CRuby, a get (`get`, `Querier#get`, `liveliness_get`), a liveliness
|
|
456
|
+
watch or a subscription of the CRuby API whose depth was left at the
|
|
457
|
+
default warns once (`asterism: get demo/**: 3 replies dropped, ...`) when
|
|
458
|
+
something was dropped; with `depth:` given it stays quiet, and `dropped`
|
|
459
|
+
tells. The object layer warns once when its own watch dropped changes
|
|
460
|
+
(then `Asterism.each` and `Asterism.nodes` may miss objects).
|
|
461
|
+
`Asterism.warn_once(obj, message)` is the helper.
|
|
462
|
+
|
|
463
|
+
Every `each_*` of the portable API (`each_pending`, `each_reply`,
|
|
464
|
+
`each_result`) takes what is there now, without waiting, and returns an
|
|
465
|
+
Array without a block (it drains the queue; an Enumerator would take only
|
|
466
|
+
when iterated).
|
|
467
|
+
|
|
468
|
+
## Deprecations and 1.0
|
|
469
|
+
|
|
470
|
+
0.4.0 adds and deprecates; nothing that worked in 0.3.0 stops working. A
|
|
471
|
+
deprecated call warns once per name (`warn` on CRuby, so it goes to
|
|
472
|
+
`$stderr`; on the boards `warn` or `puts`, once per VM).
|
|
473
|
+
`Asterism.deprecations = :raise` (or the environment variable
|
|
474
|
+
`ASTERISM_DEPRECATIONS=raise`) raises `Asterism::DeprecationError` instead,
|
|
475
|
+
for CI; `:silent` turns them off.
|
|
476
|
+
|
|
477
|
+
| Deprecated (0.4.0) | Use | In 1.0 |
|
|
478
|
+
|---|---|---|
|
|
479
|
+
| `Asterism[path, 2000]` (the time as a positional argument) | `Asterism[path, timeout: 2.0]` or `timeout_ms: 2000` | removed |
|
|
480
|
+
| A Float there (`Asterism[path, 2.0]` waits 2 ms) | `timeout: 2.0` | removed (warns with its own message now) |
|
|
481
|
+
| `session.get(key, ms, params, payload)`, `session.liveliness_get(key, ms)` | `timeout:` / `timeout_ms:`, `params:`, `payload:` | removed (a positional depth stays) |
|
|
482
|
+
| `session.peers`, `connection.peers` | `connection_count` | removed |
|
|
483
|
+
| `Asterism::Timeout`, `Asterism::ROS::Timeout` | `TimeoutError` | kept as aliases through 1.x |
|
|
484
|
+
| `proxy.methods` (the remote list) | `proxy.remote_methods` | `Object#methods` again |
|
|
485
|
+
| `Connection::Subscription#each_pending` (CRuby API; it gave Samples) | `each_sample` | removed |
|
|
486
|
+
| `q.reply(payload)` where the query's key differs from the queryable's own plain key | `q.reply(key, payload)` | answers on the queryable's key (as the CRuby API does) |
|
|
487
|
+
| `client.call(field: ...)` with a field named `timeout` / `timeout_ms` | `request: { ... }` (or a Hash) | the keyword is the time limit (`ArgumentError` when ambiguous) |
|
|
488
|
+
|
|
489
|
+
Also changing in 1.0, without a warning now: `Asterism.each { }` and
|
|
490
|
+
`net.each { }` return the proxies / `self` instead of a count; the ROS
|
|
491
|
+
layer raises `Disconnected` when its session is closed; the internal
|
|
492
|
+
methods (marked `@api private` in the source) are hidden.
|
|
493
|
+
|
|
494
|
+
### Reserved keywords
|
|
495
|
+
|
|
496
|
+
`client.call` and `node.call` take the request's fields as keywords, next
|
|
497
|
+
to `request:`, `timeout:` and `timeout_ms:`. A request type with a field
|
|
498
|
+
of one of those names cannot be given that field as a keyword without
|
|
499
|
+
ambiguity: give the request as `request: { timeout: 5 }` (or a Hash /
|
|
500
|
+
message as the first argument) and the time limit as `timeout:`. In 0.4.0
|
|
501
|
+
the ambiguous keyword warns; on the portable API a `timeout:` field stays
|
|
502
|
+
a field, as before.
|
|
295
503
|
|
|
296
504
|
### When the connection is lost
|
|
297
505
|
|
|
@@ -376,15 +584,22 @@ nested type (`geometry_msgs/msg/Twist`) at start-up took 0.6 KB more.
|
|
|
376
584
|
- The node token `asterism/<node>` is shared by every application of that
|
|
377
585
|
node; when one of them closes, the others still list the node through
|
|
378
586
|
their objects, but a watcher may see the node token go away.
|
|
379
|
-
- No authentication: a trusted LAN is assumed
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
587
|
+
- No authentication by default (plain TCP): a trusted LAN is assumed, or
|
|
588
|
+
TLS through a router (CRuby; [Over TLS](#over-tls-a-router-on-the-internet)).
|
|
589
|
+
- One object-layer connection per process (per VM on the boards).
|
|
590
|
+
- Pattern matching on the boards: `case/in` with `deconstruct_keys`, nested
|
|
591
|
+
hash patterns, classes and ranges as values, array and find patterns,
|
|
592
|
+
guards, alternatives, pins of a local of the same scope and binding from
|
|
593
|
+
inside a block work with the compiler of mruby-compiler2 `a2c72afb` or
|
|
594
|
+
later (PicoRuby master), and in Family mruby, which carries the two fixes
|
|
595
|
+
on its older PicoRuby. An older PicoRuby compares a hash pattern's value
|
|
596
|
+
the wrong way round (a class or a range never matches, a literal matches
|
|
597
|
+
any value) and does not bind an outer variable from inside a block;
|
|
598
|
+
there, match on the key alone and test the value in Ruby. Still
|
|
599
|
+
different on Family mruby's PicoRuby: `^x` of an outer local inside a
|
|
600
|
+
block, `^(expr)`, `Const[...]` and `**rest` next to another key. The full
|
|
601
|
+
list of what the boards' Ruby can and cannot do is in
|
|
602
|
+
[docs/ruby_profile.md](docs/ruby_profile.md).
|
|
388
603
|
|
|
389
604
|
## ROS 2 (rmw_zenoh): `Asterism::ROS` and `Asterism::CDR`
|
|
390
605
|
|
|
@@ -409,7 +624,7 @@ loop do
|
|
|
409
624
|
end
|
|
410
625
|
|
|
411
626
|
# or with blocks, run from node.poll (in the update loop):
|
|
412
|
-
node.every(1) { pub << { data: "tick" } }
|
|
627
|
+
node.every(1) { pub << { data: "tick" } } # or every(ms: 1000)
|
|
413
628
|
node.subscribe("/cmd_vel", "geometry_msgs/msg/Twist") { |msg, _info| drive(msg) }
|
|
414
629
|
loop { node.poll }
|
|
415
630
|
|
|
@@ -429,7 +644,7 @@ node.service("/fmruby/add_two_ints", add) { |req| { sum: req.a + req.b } }
|
|
|
429
644
|
cli = node.client("/add_two_ints", add)
|
|
430
645
|
loop do
|
|
431
646
|
node.poll # session.poll, then answers the requests
|
|
432
|
-
res = cli.call(a: 2, b: 3) # waits (polling), raises Asterism::ROS::
|
|
647
|
+
res = cli.call(a: 2, b: 3) # waits (polling), raises Asterism::ROS::TimeoutError
|
|
433
648
|
puts res.sum
|
|
434
649
|
c = cli.call_async(a: 2, b: 3) # returns at once
|
|
435
650
|
# ... later updates:
|
|
@@ -443,15 +658,15 @@ end
|
|
|
443
658
|
| `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
659
|
| `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
660
|
| `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`. |
|
|
661
|
+
| `sub.each_pending { \|msg, info\| }` | count | `info` is an `Attachment` (`sequence`, `stamp_ns`, `gid`) or nil. Samples that are not valid CDR are skipped and counted in `sub.errors`. `sub.pending` / `received` / `dropped` count as the Zenoh subscriber's do. |
|
|
447
662
|
| `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
663
|
| `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. |
|
|
664
|
+
| `node.subscribe(topic, type, qos: DEFAULT_QOS, depth: 16) { \|msg, info\| }` | `Subscription` | `node.subscription` whose messages (decoded, with their `Attachment` or nil) go to the block, from `node.poll`. `sub.close` ends it. Do not wait in the block (see [Stack](#stack)). Without a block it is `node.subscription`. |
|
|
665
|
+
| `node.every(seconds) { }` / `node.every(ms: 500) { }` | `Timer` | Calls the block every `seconds` (a Float works) or `ms:` milliseconds, the first time one period from now, from `node.poll` (so no more often than the update loop polls). It keeps the period; after a long stall it starts again from now instead of firing the missed times at once. `timer.cancel`, `timer.fired` (count), `timer.period` (s), `timer.period_ms`. Do not wait in the block. |
|
|
451
666
|
| `node.client(name, type, qos: DEFAULT_QOS)` | `Client` | Declares the client token. |
|
|
452
|
-
| `client.call(request = nil,
|
|
453
|
-
| `client.call_async(
|
|
454
|
-
| `node.call(name, type, request = nil,
|
|
667
|
+
| `client.call(request = nil, request: nil, timeout: 2.0, **fields)` | `type::Response` | Sends the request (a `type::Request` or a Hash, positional or `request:`, or the fields as keywords) and waits, polling the node (its services keep answering). The time limit: `timeout:` (seconds) or `timeout_ms:`. `Asterism::ROS::TimeoutError` when no response came in time, at once when nobody serves the name; `Asterism::Zenoh::ClosedError` when the session closed. See [Reserved keywords](#reserved-keywords). |
|
|
668
|
+
| `client.call_async(...)` | `Call` | The same arguments; sends and returns at once. `call.done?` never waits (`node.poll` moves it on); `call.value` waits and returns the response or raises like `call`; `call.response` (nil until it came), `call.took` (s) / `took_ms`, `call.sequence`. |
|
|
669
|
+
| `node.call(name, type, request = nil, request:, timeout:, **fields)` | `type::Response` | `client.call` through a client made on first use and kept per name. |
|
|
455
670
|
| `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
671
|
| `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
672
|
| `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. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The object layer (Asterism.connect / expose / []) with blocks and a
|
|
2
2
|
# receiving thread (CRuby only). Asterism.connect without a block,
|
|
3
|
-
# Asterism.poll and the rest of the
|
|
3
|
+
# Asterism.poll and the rest of the portable API work as on the boards.
|
|
4
4
|
#
|
|
5
5
|
# Asterism.connect("tcp/192.0.2.2:7447", node: "mypc", app: "demo") do |net|
|
|
6
6
|
# net.expose("screen", Screen.new, methods: [:say])
|
|
@@ -23,10 +23,10 @@ module Asterism
|
|
|
23
23
|
# Held by every entry into the object layer (reentrant).
|
|
24
24
|
LOCK = Monitor.new
|
|
25
25
|
|
|
26
|
-
# The
|
|
26
|
+
# The portable API, entered under LOCK. Prepended to the Asterism module's
|
|
27
27
|
# singleton class, so the shared methods themselves are not changed.
|
|
28
28
|
module Locked
|
|
29
|
-
%i[expose unexpose poll nodes [] call_async meta connected? exposed on_join on_leave].each do |m|
|
|
29
|
+
%i[expose unexpose poll nodes [] call_async meta connected? exposed on_join on_leave off_join off_leave].each do |m|
|
|
30
30
|
define_method(m) do |*args, **kw, &blk|
|
|
31
31
|
::Asterism::LOCK.synchronize { super(*args, **kw, &blk) }
|
|
32
32
|
end
|
|
@@ -35,9 +35,11 @@ module Asterism
|
|
|
35
35
|
# With a block: yields an Asterism::Net and closes when the block ends
|
|
36
36
|
# (also on an exception); returns the block's value. Without a block,
|
|
37
37
|
# as before (returns Asterism).
|
|
38
|
-
def connect(locator, node:, app:, mode: nil, listen: nil,
|
|
38
|
+
def connect(locator, node:, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: nil,
|
|
39
|
+
&blk)
|
|
39
40
|
::Asterism::LOCK.synchronize do
|
|
40
|
-
super(locator, node: node, app: app, mode: mode, listen: listen
|
|
41
|
+
super(locator, node: node, app: app, mode: mode, listen: listen, config: config,
|
|
42
|
+
connect_timeout: connect_timeout, check_timeout: check_timeout)
|
|
41
43
|
# The Net of a connection that was lost (not closed) belongs to the
|
|
42
44
|
# old session.
|
|
43
45
|
old = @net
|
|
@@ -145,15 +147,17 @@ module Asterism
|
|
|
145
147
|
::Asterism.unexpose(name)
|
|
146
148
|
end
|
|
147
149
|
|
|
148
|
-
# A proxy for <node>/<app>/<object>; timeout in seconds
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
# A proxy for <node>/<app>/<object>; timeout: in seconds (or
|
|
151
|
+
# timeout_ms:).
|
|
152
|
+
def [](path, timeout: nil, timeout_ms: nil)
|
|
153
|
+
::Asterism[path, timeout: timeout, timeout_ms: timeout_ms]
|
|
151
154
|
end
|
|
152
155
|
alias proxy []
|
|
153
156
|
|
|
154
157
|
# The exposed objects alive now matching the pattern, as proxies. With
|
|
155
|
-
# a block, yields each and returns their number
|
|
156
|
-
#
|
|
158
|
+
# a block, yields each and returns their number (1.0: returns self, as
|
|
159
|
+
# Ruby's each does; net.count is the number); without, an Enumerator
|
|
160
|
+
# (net.each("*/demo/info").map(&:status)).
|
|
157
161
|
def each(pattern = "**", &blk)
|
|
158
162
|
return enum_for(:each, pattern) unless blk
|
|
159
163
|
::Asterism.each(pattern, &blk)
|
|
@@ -173,6 +177,17 @@ module Asterism
|
|
|
173
177
|
self
|
|
174
178
|
end
|
|
175
179
|
|
|
180
|
+
# Removes a block given to on_join / on_leave (keep the Proc).
|
|
181
|
+
def off_join(blk)
|
|
182
|
+
::Asterism.off_join(blk)
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def off_leave(blk)
|
|
187
|
+
::Asterism.off_leave(blk)
|
|
188
|
+
self
|
|
189
|
+
end
|
|
190
|
+
|
|
176
191
|
# on_error { |error, where| }: what on_join / on_leave blocks raise.
|
|
177
192
|
# (An exposed method that raises is answered to its caller as a
|
|
178
193
|
# RemoteError, as before; it does not come here.)
|
data/lib/asterism/cruby/ros.rb
CHANGED
|
@@ -197,9 +197,11 @@ module Asterism
|
|
|
197
197
|
Topic.new(self, topic, type, qos, depth)
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
-
# Node#call
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
# Node#call; here timeout: is always the time limit, also for a
|
|
201
|
+
# request type with a field named timeout (give such a request as
|
|
202
|
+
# request: { ... }).
|
|
203
|
+
def call(service, type, req = nil, request: nil, timeout: nil, timeout_ms: nil, **fields)
|
|
204
|
+
ms = ::Asterism.time_ms("Node#call", timeout, timeout_ms, nil, nil)
|
|
203
205
|
c = @asterism_runner.lock.synchronize do
|
|
204
206
|
@clients ||= {}
|
|
205
207
|
cl = @clients[service]
|
|
@@ -209,7 +211,7 @@ module Asterism
|
|
|
209
211
|
end
|
|
210
212
|
cl
|
|
211
213
|
end
|
|
212
|
-
c.call(request, timeout_ms: ms, **fields)
|
|
214
|
+
c.call(req, request: request, timeout_ms: ms, **fields)
|
|
213
215
|
end
|
|
214
216
|
end
|
|
215
217
|
|