asterism 0.1.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.
- checksums.yaml +4 -4
- data/README.md +189 -13
- data/lib/asterism/cruby/objects.rb +224 -0
- data/lib/asterism/cruby/ros.rb +305 -0
- data/lib/asterism/cruby/runner.rb +253 -0
- data/lib/asterism/cruby/zenoh.rb +422 -0
- data/lib/asterism/cruby.rb +6 -0
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +5 -0
- data/mrblib/asterism.rb +81 -9
- data/mrblib/future.rb +6 -1
- data/mrblib/ros.rb +182 -12
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdb7bf2126361f3b9ae4097467832d950440b9a65c419a92394f230b4d7b0277
|
|
4
|
+
data.tar.gz: ba8621f58ee2d5fb22f5eeb3b68917cb6ce9f9420d07230d5d69e246b9c53564
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc43c0ab1f0076a148bcd65a648b0220eb8559912b296a7fdbb7b4810f108ab8f66d439e951e3c1a3fe24329ac6fb5a76593214c7571e3f18b55d833a917276d
|
|
7
|
+
data.tar.gz: 48114f986885d604142a4fb369b1a31f4a7cb6d1dbff95ffc0b0566e97801bbd646278cb498b4c0e1003c8a3b425a526262ffb384557eeb0cdc4a7e450b6bc63
|
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 |
|
|
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
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) |
|
|
16
|
-
| `examples/` | CRuby: `node.rb` (objects with a board), `ros2_talker.rb` (
|
|
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
17
|
|
|
18
18
|
The Zenoh binding underneath is a separate gem with the same Ruby API on
|
|
19
19
|
each Ruby:
|
|
@@ -23,7 +23,7 @@ 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
|
-
Neither gem is on rubygems.org yet (version 0.
|
|
26
|
+
Neither gem is on rubygems.org yet (version 0.2.0 builds from these
|
|
27
27
|
repositories with `rake gem`).
|
|
28
28
|
|
|
29
29
|
## Using it
|
|
@@ -31,7 +31,7 @@ repositories with `rake gem`).
|
|
|
31
31
|
CRuby (needs CRuby 3.2+ and a C compiler):
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
gem install asterism # also installs asterism-zenoh (~> 0.
|
|
34
|
+
gem install asterism # also installs asterism-zenoh (~> 0.2.0) and msgpack
|
|
35
35
|
ruby -e 'require "asterism"'
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -44,7 +44,7 @@ build but not yet tested. To install from the repositories instead:
|
|
|
44
44
|
|
|
45
45
|
```
|
|
46
46
|
(cd ../asterism-zenoh && rake gem) && rake gem
|
|
47
|
-
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.
|
|
47
|
+
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.2.0.gem pkg/asterism-0.2.0.gem
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
From the working trees, without installing:
|
|
@@ -67,13 +67,14 @@ these repositories.
|
|
|
67
67
|
## Tests
|
|
68
68
|
|
|
69
69
|
```
|
|
70
|
-
rake #
|
|
70
|
+
rake # every suite
|
|
71
71
|
rake test:msgs # generator, type hashes, CDR, bundled types (CRuby only; no Zenoh, no docker)
|
|
72
72
|
rake test:objects # objects and Asterism::ROS between CRuby sessions over a local peer link
|
|
73
|
-
|
|
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
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
`test:objects`
|
|
77
|
+
`test:objects` and `test:api` need asterism-zenoh: built next to this repository
|
|
77
78
|
(`../asterism-zenoh`, its `rake compile`), or `ASTERISM_ZENOH_DIR`, or the
|
|
78
79
|
installed gem.
|
|
79
80
|
|
|
@@ -83,11 +84,147 @@ installed gem.
|
|
|
83
84
|
# objects: join the app "demo" of Family mruby's asterism_demo, call a board,
|
|
84
85
|
# and answer its calls (its keys s / a call screen.say / apu.play here)
|
|
85
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
|
|
86
88
|
|
|
87
|
-
# ROS 2:
|
|
89
|
+
# ROS 2: /chatter and /cmd_vel from a timer, /chatter_in printed,
|
|
90
|
+
# /cruby/add_two_ints served, AddTwoInts calls
|
|
88
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
|
|
89
154
|
```
|
|
90
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
|
+
|
|
91
228
|
## License
|
|
92
229
|
|
|
93
230
|
MIT (LICENSE) for Asterism's code. The ROS 2 definitions in
|
|
@@ -108,7 +245,8 @@ RubyGems, and on mruby / PicoRuby the Zenoh binding is the separate mrbgem
|
|
|
108
245
|
|
|
109
246
|
# The API
|
|
110
247
|
|
|
111
|
-
The same on every Ruby
|
|
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
|
|
112
250
|
write `sleep`. When PicoRuby's `Machine` is there it is used for the clock
|
|
113
251
|
and the short pauses while waiting; otherwise `Time` and `sleep`. Besides
|
|
114
252
|
`Asterism::Zenoh`, the layers need a `MessagePack` module with `pack` /
|
|
@@ -138,7 +276,8 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
138
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). |
|
|
139
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. |
|
|
140
278
|
| `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. |
|
|
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). |
|
|
142
281
|
| `Asterism[path, timeout_ms = 2000]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). Nothing is sent until a method is called. |
|
|
143
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`). |
|
|
144
283
|
| `proxy.async.<method>(...)` | `Future` | Sends and returns at once. `EncodeError` / `Disconnected` are raised here. |
|
|
@@ -214,6 +353,22 @@ took zenoh replies through blocks and fetched the meta from inside
|
|
|
214
353
|
`respond_to_missing?`, overflowed the stack there. Make waiting calls from
|
|
215
354
|
the update loop, or use `async`.
|
|
216
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
|
+
|
|
217
372
|
## Limits
|
|
218
373
|
|
|
219
374
|
- Objects cannot be passed by reference (a return value is a copy), blocks
|
|
@@ -222,6 +377,14 @@ the update loop, or use `async`.
|
|
|
222
377
|
node; when one of them closes, the others still list the node through
|
|
223
378
|
their objects, but a watcher may see the node token go away.
|
|
224
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.
|
|
225
388
|
|
|
226
389
|
## ROS 2 (rmw_zenoh): `Asterism::ROS` and `Asterism::CDR`
|
|
227
390
|
|
|
@@ -244,6 +407,17 @@ loop do
|
|
|
244
407
|
pub << { data: "hello" } # or pub.publish(str.new(data: "hello"))
|
|
245
408
|
sub.each_pending { |msg, info| puts "#{info && info.sequence}: #{msg.data}" }
|
|
246
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
|
|
247
421
|
node.close # or let the session close
|
|
248
422
|
```
|
|
249
423
|
|
|
@@ -271,7 +445,9 @@ end
|
|
|
271
445
|
| `node.subscription(topic, type, qos: DEFAULT_QOS, depth: 16)` | `Subscription` | Subscribes and declares the subscription token. |
|
|
272
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`. |
|
|
273
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. |
|
|
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. |
|
|
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. |
|
|
275
451
|
| `node.client(name, type, qos: DEFAULT_QOS)` | `Client` | Declares the client token. |
|
|
276
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. |
|
|
277
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`. |
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# The object layer (Asterism.connect / expose / []) with blocks and a
|
|
2
|
+
# receiving thread (CRuby only). Asterism.connect without a block,
|
|
3
|
+
# Asterism.poll and the rest of the polled API work as on the boards.
|
|
4
|
+
#
|
|
5
|
+
# Asterism.connect("tcp/192.0.2.2:7447", node: "mypc", app: "demo") do |net|
|
|
6
|
+
# net.expose("screen", Screen.new, methods: [:say])
|
|
7
|
+
# net.on_join { |node| puts "joined #{node}" }
|
|
8
|
+
# net.on_leave { |node| puts "left #{node}" }
|
|
9
|
+
# net["fmruby-aaaaaa/demo/screen"].say("hello")
|
|
10
|
+
# net.each("*/demo/info").map(&:status)
|
|
11
|
+
# net.run # answer calls until Ctrl-C
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Locking: the object layer is one connection per process with its state in
|
|
15
|
+
# the Asterism module, written for one thread. Every entry into it (polling,
|
|
16
|
+
# answering, calling, exposing, listing) holds Asterism::LOCK, so the
|
|
17
|
+
# receiving thread and the application's threads take turns. A call that
|
|
18
|
+
# waits for its answer while the receiving thread runs does not poll; it
|
|
19
|
+
# waits for the receiving thread's ticks (Asterism::Runner#wait_until).
|
|
20
|
+
require "monitor"
|
|
21
|
+
|
|
22
|
+
module Asterism
|
|
23
|
+
# Held by every entry into the object layer (reentrant).
|
|
24
|
+
LOCK = Monitor.new
|
|
25
|
+
|
|
26
|
+
# The polled API, entered under LOCK. Prepended to the Asterism module's
|
|
27
|
+
# singleton class, so the shared methods themselves are not changed.
|
|
28
|
+
module Locked
|
|
29
|
+
%i[expose unexpose poll nodes [] call_async meta connected? exposed on_join on_leave].each do |m|
|
|
30
|
+
define_method(m) do |*args, **kw, &blk|
|
|
31
|
+
::Asterism::LOCK.synchronize { super(*args, **kw, &blk) }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# With a block: yields an Asterism::Net and closes when the block ends
|
|
36
|
+
# (also on an exception); returns the block's value. Without a block,
|
|
37
|
+
# as before (returns Asterism).
|
|
38
|
+
def connect(locator, node:, app:, mode: nil, listen: nil, &blk)
|
|
39
|
+
::Asterism::LOCK.synchronize do
|
|
40
|
+
super(locator, node: node, app: app, mode: mode, listen: listen)
|
|
41
|
+
# The Net of a connection that was lost (not closed) belongs to the
|
|
42
|
+
# old session.
|
|
43
|
+
old = @net
|
|
44
|
+
@net = nil
|
|
45
|
+
old&.release
|
|
46
|
+
end
|
|
47
|
+
return self unless blk
|
|
48
|
+
net = ::Asterism.net
|
|
49
|
+
begin
|
|
50
|
+
blk.call(net)
|
|
51
|
+
ensure
|
|
52
|
+
net.close
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Stops the receiving thread first (outside the lock: it may be inside
|
|
57
|
+
# a tick), then closes.
|
|
58
|
+
def close
|
|
59
|
+
net = @net
|
|
60
|
+
@net = nil
|
|
61
|
+
net&.release
|
|
62
|
+
::Asterism::LOCK.synchronize { super }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The on_join / on_leave blocks (called from Asterism.poll): with a Net,
|
|
66
|
+
# what they raise goes to its on_error and the other blocks still run.
|
|
67
|
+
def tell(where, blk, node)
|
|
68
|
+
net = @net
|
|
69
|
+
return super unless net
|
|
70
|
+
net.runner.guard(where) { super }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Lists under the lock, yields outside it (the block may make calls).
|
|
74
|
+
def each(pattern = "**", &blk)
|
|
75
|
+
list = ::Asterism::LOCK.synchronize { super(pattern, &nil) }
|
|
76
|
+
return list unless blk
|
|
77
|
+
list.each(&blk)
|
|
78
|
+
list.size
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A call waiting for its answer. On the receiving thread, or when it
|
|
82
|
+
# does not run: polls, as on the boards. On another thread while it
|
|
83
|
+
# runs: waits for its ticks to bring the answer.
|
|
84
|
+
def wait_until(&cond)
|
|
85
|
+
loop do
|
|
86
|
+
r = @net&.runner
|
|
87
|
+
if r && r.running? && !r.current?
|
|
88
|
+
r.wait_until(&cond)
|
|
89
|
+
return if ::Asterism::LOCK.synchronize(&cond)
|
|
90
|
+
# The receiving thread stopped before the answer: poll here.
|
|
91
|
+
else
|
|
92
|
+
return ::Asterism::LOCK.synchronize { super(&cond) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
singleton_class.prepend(Locked)
|
|
98
|
+
|
|
99
|
+
class Future
|
|
100
|
+
# Prepended: done? reads the connection state, under the lock.
|
|
101
|
+
module Locked
|
|
102
|
+
def done?
|
|
103
|
+
::Asterism::LOCK.synchronize { super }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
prepend Locked
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The connection of this process (Asterism is one per process) as an
|
|
110
|
+
# object, with a receiving thread. nil when not connected.
|
|
111
|
+
def self.net
|
|
112
|
+
LOCK.synchronize do
|
|
113
|
+
return nil unless connected?
|
|
114
|
+
@net ||= Net.new
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The block API of the object layer. Made by Asterism.connect with a
|
|
119
|
+
# block, or Asterism.net after a connect without one.
|
|
120
|
+
class Net
|
|
121
|
+
attr_reader :runner
|
|
122
|
+
|
|
123
|
+
def initialize
|
|
124
|
+
@runner = Runner.new(::Asterism.instance_variable_get(:@session), lock: ::Asterism::LOCK, name: "asterism")
|
|
125
|
+
# One tick: Asterism.poll (answers the calls, tells on_join /
|
|
126
|
+
# on_leave); false once the connection is gone.
|
|
127
|
+
@runner.add { ::Asterism.poll ? 0 : false }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def node_id = ::Asterism.node_id
|
|
131
|
+
def app = ::Asterism.app
|
|
132
|
+
def connected? = ::Asterism.connected?
|
|
133
|
+
def lost_reason = ::Asterism.lost_reason
|
|
134
|
+
def nodes = ::Asterism.nodes
|
|
135
|
+
def exposed = ::Asterism.exposed
|
|
136
|
+
|
|
137
|
+
# Asterism.expose: methods: an Array of names or a Hash name => number
|
|
138
|
+
# of arguments. The object's methods run on the receiving thread (or in
|
|
139
|
+
# run, or in whatever polls).
|
|
140
|
+
def expose(name, obj, methods:)
|
|
141
|
+
::Asterism.expose(name, obj, methods: methods)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def unexpose(name)
|
|
145
|
+
::Asterism.unexpose(name)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A proxy for <node>/<app>/<object>; timeout in seconds.
|
|
149
|
+
def [](path, timeout: nil)
|
|
150
|
+
::Asterism[path, Zenoh.ms(timeout, ::Asterism::DEFAULT_TIMEOUT_MS)]
|
|
151
|
+
end
|
|
152
|
+
alias proxy []
|
|
153
|
+
|
|
154
|
+
# The exposed objects alive now matching the pattern, as proxies. With
|
|
155
|
+
# a block, yields each and returns their number; without, an
|
|
156
|
+
# Enumerator (net.each("*/demo/info").map(&:status)).
|
|
157
|
+
def each(pattern = "**", &blk)
|
|
158
|
+
return enum_for(:each, pattern) unless blk
|
|
159
|
+
::Asterism.each(pattern, &blk)
|
|
160
|
+
end
|
|
161
|
+
include Enumerable
|
|
162
|
+
|
|
163
|
+
# on_join { |node| } / on_leave { |node| }: Asterism.on_join /
|
|
164
|
+
# on_leave, the same as on the boards (called from Asterism.poll), here
|
|
165
|
+
# on the receiving thread. What they raise goes to on_error.
|
|
166
|
+
def on_join(&blk)
|
|
167
|
+
::Asterism.on_join(&blk)
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def on_leave(&blk)
|
|
172
|
+
::Asterism.on_leave(&blk)
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# on_error { |error, where| }: what on_join / on_leave blocks raise.
|
|
177
|
+
# (An exposed method that raises is answered to its caller as a
|
|
178
|
+
# RemoteError, as before; it does not come here.)
|
|
179
|
+
def on_error(&blk)
|
|
180
|
+
@runner.on_error(&blk)
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Answers calls on a thread of its own. Returns self.
|
|
185
|
+
def start
|
|
186
|
+
@runner.start
|
|
187
|
+
self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def stop
|
|
191
|
+
@runner.stop
|
|
192
|
+
self
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Answers calls on this thread until stop, the connection closing, or
|
|
196
|
+
# Ctrl-C. Returns nil.
|
|
197
|
+
def run
|
|
198
|
+
@runner.run
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def running?
|
|
202
|
+
@runner.running?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def poll
|
|
206
|
+
::Asterism.poll
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Stops the receiving thread and closes the connection (Asterism.close).
|
|
210
|
+
def close
|
|
211
|
+
::Asterism.close
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# For Asterism.close: stop receiving and forget the session.
|
|
216
|
+
def release
|
|
217
|
+
@runner.close
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def inspect
|
|
221
|
+
"#<Asterism::Net #{node_id}/#{app}#{running? ? ' running' : ''}>"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|