asterism 0.5.0 → 0.6.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 +40 -0
- data/README.md +70 -7
- data/lib/asterism/cruby/objects.rb +16 -1
- data/lib/asterism/cruby/ros.rb +16 -4
- data/lib/asterism/cruby/zenoh.rb +32 -6
- data/lib/asterism/version.rb +3 -3
- data/mrblib/asterism.rb +132 -18
- data/mrblib/profile.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba14899dd37f9c4c21e148a2256d71545785da6f61a1f8f6701e2d0d189e9061
|
|
4
|
+
data.tar.gz: 207e08ed177ffa93cd5c7173a84c31f519f59c8e076c6c641ccaa8db27712ea9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de0f85b8c36d4b3c991603004f151ea68952bd7fcc7fdbc76b992af80505b45be3612c5975e509e4f1415f553db8d91ea94940817c00768705bdbf1a011d45e6
|
|
7
|
+
data.tar.gz: df7634a6c9453fb647c0f148199ff056c646451e3c839ac5cf02704db0ee7dfa7508d8aabdd9fde8005b8909dd6c7217f2d2b96a6e066ac6895d39d9a2af9481
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,46 @@ the earlier ones are only in this repository. From 0.4.0 the two Zenoh
|
|
|
6
6
|
bindings (asterism-zenoh, picoruby-asterism-zenoh) carry the same minor
|
|
7
7
|
version (a patch release of one may come without the other).
|
|
8
8
|
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
One connection for everything and a lighter `Asterism.poll`; every 0.5.x
|
|
12
|
+
call works as before. Needs asterism-zenoh / picoruby-asterism-zenoh 0.4.0
|
|
13
|
+
(unchanged).
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
|
|
17
|
+
- `Asterism.session` (README "One connection"): the Zenoh session of
|
|
18
|
+
`Asterism.connect` (an `Asterism::Zenoh::Session`), for put, subscribe, a
|
|
19
|
+
ROS 2 node (`Asterism::ROS::Node.new(Asterism.session, ...)`) and the rest
|
|
20
|
+
of the Zenoh API on the same connection. `Disconnected` when not
|
|
21
|
+
connected. The session stays Asterism's: its `close` does nothing (it
|
|
22
|
+
warns once); `Asterism.close` and a lost connection close it, with
|
|
23
|
+
everything declared on it. A router that lets a one-time password in
|
|
24
|
+
once now takes such an application, and its ROS 2 node has the node's
|
|
25
|
+
Zenoh ID (a console draws it inside the node).
|
|
26
|
+
- The CRuby API: `net.session`, the same session as a `Zenoh::Connection`
|
|
27
|
+
on the `Net`'s receiving thread (`shared?`; its `close` does nothing);
|
|
28
|
+
`Asterism::ROS::Connection.new(net.session)` makes ROS 2 nodes on it, and
|
|
29
|
+
its `close` withdraws only its nodes. `Zenoh::Connection#shared?`.
|
|
30
|
+
`ROS::Node#close` on a `ROS::Connection` also stops polling the node.
|
|
31
|
+
- `rake bench:poll` (`test/bench_poll.rb`): the time and the allocations
|
|
32
|
+
of one `Asterism.poll` by the number of nodes and objects.
|
|
33
|
+
- Tests: `test/test_session.rb`, `test/test_api_session.rb`, and on mruby
|
|
34
|
+
`host_test/test_session.rb` with `host_test/peer.rb` (the mruby test
|
|
35
|
+
build adds mruby-eval for the ROS 2 type files).
|
|
36
|
+
|
|
37
|
+
Changed
|
|
38
|
+
|
|
39
|
+
- `Asterism.poll` does less when nothing came: each queue (calls, profile
|
|
40
|
+
queries, liveliness) is read only when its count is not zero, and the
|
|
41
|
+
list of nodes and the comparison behind `on_join` / `on_leave` are made
|
|
42
|
+
again only after a liveliness change, with Hashes instead of `include?`.
|
|
43
|
+
The behaviour is the same (the order and number of the `on_join` /
|
|
44
|
+
`on_leave` calls, the order of `Asterism.nodes`). With 4 other nodes and
|
|
45
|
+
5 objects, a poll in which nothing changed: CRuby 3.2 us and 49
|
|
46
|
+
allocations -> 0.6 us and 7; mruby (host) 14-17 us -> 2.2 us.
|
|
47
|
+
`Asterism.each` drops duplicates with a Hash.
|
|
48
|
+
|
|
9
49
|
## 0.5.0
|
|
10
50
|
|
|
11
51
|
Node profiles; every 0.4.x call works as before. Needs asterism-zenoh /
|
data/README.md
CHANGED
|
@@ -13,8 +13,8 @@ repository holds the pure Ruby layers, written once for every Ruby:
|
|
|
13
13
|
| `data/icon.png` | CRuby's default profile icon (32x32 PNG; [Profiles](#profiles)) |
|
|
14
14
|
| `data/msgs/` | ROS 2 message types generated from ROS 2 Jazzy's definitions (Apache-2.0; tf2_msgs BSD-3-Clause; see NOTICE) |
|
|
15
15
|
| `tools/` | The type generator `asterism_msggen.rb`, the Jazzy definitions it reads, `ros2_types.rb` (refreshes them from a ROS 2 image) |
|
|
16
|
-
| `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_profile.rb` (node profiles), `test_api_*.rb` (the CRuby API) |
|
|
17
|
-
| `host_test/` | `rake test:mruby`: the shared layer on mruby (profiles between two mruby processes) |
|
|
16
|
+
| `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_profile.rb` (node profiles), `test_session.rb` (`Asterism.session`, `on_join` / `on_leave`), `test_api_*.rb` (the CRuby API), `bench_poll.rb` (`rake bench:poll`) |
|
|
17
|
+
| `host_test/` | `rake test:mruby`: the shared layer on mruby (profiles between two mruby processes; `Asterism.session`, `on_join` / `on_leave` and the time of a poll against `peer.rb`) |
|
|
18
18
|
| `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), `ros2_rover.rb` (drive a ROS 2 robot: `/cmd_vel`, `/odom`, `/imu`), `zenoh.rb` (plain Zenoh), `mujoco/` (MuJoCo run from CRuby through Fiddle, its robot exposed as Asterism objects; see its README) |
|
|
19
19
|
|
|
20
20
|
The Zenoh binding underneath is a separate gem with the same Ruby API on
|
|
@@ -39,7 +39,10 @@ asterism-zenoh too). What changed in each version is in
|
|
|
39
39
|
([docs/api_review.md](docs/api_review.md)) and only adds and deprecates
|
|
40
40
|
(see [Deprecations and 1.0](#deprecations-and-10)); 0.5.0 adds node
|
|
41
41
|
profiles ([Profiles](#profiles): what a node runs on, where, a name and an
|
|
42
|
-
icon).
|
|
42
|
+
icon); 0.6.0 adds `Asterism.session`
|
|
43
|
+
([One connection](#one-connection-asterismsession): the object layer's
|
|
44
|
+
session for plain Zenoh and ROS 2 too) and makes `Asterism.poll` cheap when
|
|
45
|
+
nothing changed.
|
|
43
46
|
|
|
44
47
|
## Which API?
|
|
45
48
|
|
|
@@ -59,6 +62,7 @@ Entry points and what they give:
|
|
|
59
62
|
| `Asterism::Zenoh::Session.open` | a `Session` (portable) | zenoh-c / zenoh-pico |
|
|
60
63
|
| `Asterism::Zenoh.open` | a `Zenoh::Connection` (CRuby) | `c.session` is the `Session` |
|
|
61
64
|
| `Asterism.connect` | `Asterism` itself (portable; one connection per process or VM), or a `Net` to the block (CRuby) | a `Session` of its own |
|
|
65
|
+
| `Asterism.session` | that `Session` (portable), for put, subscribe and a ROS 2 node on the same connection; `net.session` is it as a `Zenoh::Connection` (CRuby) | the object layer's (Asterism closes it) |
|
|
62
66
|
| `Asterism::ROS::Node.new(session, ...)` | a `Node` (portable) | the `Session` given |
|
|
63
67
|
| `Asterism::ROS.connect` | a `ROS::Connection` (CRuby) | `ros.zenoh` is the `Zenoh::Connection` |
|
|
64
68
|
|
|
@@ -87,7 +91,7 @@ build but not yet tested. To install from the repositories instead:
|
|
|
87
91
|
|
|
88
92
|
```
|
|
89
93
|
(cd ../asterism-zenoh && rake gem) && rake gem
|
|
90
|
-
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.0.gem pkg/asterism-0.
|
|
94
|
+
gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.0.gem pkg/asterism-0.6.0.gem
|
|
91
95
|
```
|
|
92
96
|
|
|
93
97
|
From the working trees, without installing:
|
|
@@ -116,6 +120,7 @@ rake test:objects # objects and Asterism::ROS between CRuby sessions over a
|
|
|
116
120
|
rake test:api # the CRuby API (blocks, receiving threads, Enumerators)
|
|
117
121
|
ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
|
|
118
122
|
rake test:mruby # the shared layer on mruby (builds mruby; not part of `rake`)
|
|
123
|
+
rake bench:poll # the time and allocations of one Asterism.poll by nodes and objects (prints a table)
|
|
119
124
|
```
|
|
120
125
|
|
|
121
126
|
`test:mruby` builds mruby with picoruby-asterism-zenoh and this gem
|
|
@@ -252,6 +257,7 @@ end
|
|
|
252
257
|
| `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 |
|
|
253
258
|
| `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 |
|
|
254
259
|
| `net.profile(**fields)` / `net.profile_of(node, icon: true, timeout: 2.0)` | `Asterism.profile` / `profile_of` ([Profiles](#profiles)); `profile_of` waits for the receiving thread's ticks while it runs |
|
|
260
|
+
| `net.session` | `Asterism.session` as a `Zenoh::Connection` (`subscribe { }`, `put`, `publisher`, ...) on this receiving thread: its blocks run there, between the calls answered. `shared?` is true; its `close` does nothing and `start` / `stop` / `run` are the `Net`'s. A ROS 2 node on it: `Asterism::ROS::Connection.new(net.session).node("name")`. See [One connection](#one-connection-asterismsession) |
|
|
255
261
|
| `net.nodes`, `node_id`, `app`, `connected?`, `lost_reason`, `poll`, `close` | |
|
|
256
262
|
|
|
257
263
|
### Over TLS (a router on the internet)
|
|
@@ -307,6 +313,7 @@ end
|
|
|
307
313
|
| `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)) |
|
|
308
314
|
| `ros.spin` (`run`) / `start` / `stop` / `running?` / `on_error` / `close` | |
|
|
309
315
|
| `ros.zenoh` | the `Asterism::Zenoh::Connection` underneath (same receiving thread) |
|
|
316
|
+
| `Asterism::ROS::Connection.new(net.session, domain: 0)` | nodes on the object layer's session (the same Zenoh ID as the objects), spun by the `Net`'s receiving thread. Its `close` withdraws its nodes only |
|
|
310
317
|
|
|
311
318
|
`type` is a generated type or its name (`"geometry_msgs/msg/Twist"`).
|
|
312
319
|
Messages and `Attachment` take part in pattern matching (`deconstruct_keys`,
|
|
@@ -412,7 +419,7 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
412
419
|
| `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). |
|
|
413
420
|
| `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. |
|
|
414
421
|
| `Asterism.unexpose(name)` | true / false | |
|
|
415
|
-
| `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. |
|
|
422
|
+
| `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. Cheap when nothing came (0.6.0): each queue is looked at by its count, and the list of nodes is made again only after a liveliness change. |
|
|
416
423
|
| `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). |
|
|
417
424
|
| `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. |
|
|
418
425
|
| `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`). |
|
|
@@ -424,6 +431,7 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
|
|
|
424
431
|
| `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. |
|
|
425
432
|
| `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
|
|
426
433
|
| `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
|
|
434
|
+
| `Asterism.session` | `Asterism::Zenoh::Session` | The connection's own session, for put / subscribe / a ROS 2 node on the same connection ([One connection](#one-connection-asterismsession)). The same object until the connection closes. `Disconnected` when not connected. Its `close` does nothing (warns once); `Asterism.close` closes it. |
|
|
427
435
|
| `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
|
|
428
436
|
| `Asterism.profile(kind:, name:, host:, icon:)` / `Asterism.profile` | Hash | This node's profile: sets the fields given and returns it; without arguments, returns it. See [Profiles](#profiles). |
|
|
429
437
|
| `Asterism.profile_of(node, icon: true, timeout: 2.0)` | Hash or nil | Another node's profile, nil when it does not answer (an older version, a ROS 2 node). |
|
|
@@ -480,6 +488,59 @@ Asterism.profile_of("an-older-node") # => nil
|
|
|
480
488
|
an acceptable PNG (nil otherwise), for a tool that wants to check an icon
|
|
481
489
|
first.
|
|
482
490
|
|
|
491
|
+
## One connection: `Asterism.session`
|
|
492
|
+
|
|
493
|
+
An application that also puts, subscribes or runs a ROS 2 node uses the
|
|
494
|
+
object layer's own session for it (0.6.0) instead of opening a second one:
|
|
495
|
+
|
|
496
|
+
```ruby
|
|
497
|
+
Asterism.connect("tcp/192.0.2.2:7447", node: "fmruby-aaaaaa", app: "game")
|
|
498
|
+
Asterism.expose("othello", game, methods: [:move, :state])
|
|
499
|
+
s = Asterism.session # an Asterism::Zenoh::Session
|
|
500
|
+
s.put("asterism/fmruby-aaaaaa/game/othello/state", MessagePack.pack(state))
|
|
501
|
+
moves = s.subscribe("asterism/*/game/othello/state") # moves.each_pending in the update loop
|
|
502
|
+
node = Asterism::ROS::Node.new(s, "brain") # a ROS 2 node with the same Zenoh ID
|
|
503
|
+
cmd = node.publisher("/cmd_vel", "geometry_msgs/msg/Twist")
|
|
504
|
+
# update loop: Asterism.poll; node.poll
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
CRuby API:
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
Asterism.connect(router, node: "mypc", app: "demo") do |net|
|
|
511
|
+
net.session.subscribe("asterism/*/game/**") { |sample| p sample.key } # on the receiving thread
|
|
512
|
+
ros = Asterism::ROS::Connection.new(net.session)
|
|
513
|
+
ros.node("brain").subscribe("/odom", "nav_msgs/msg/Odometry") { |odom| puts odom.pose.pose.position.x }
|
|
514
|
+
net.run
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
- **Why one.** A router that lets each pass in once (a one-time password
|
|
519
|
+
at a `wss://` door) lets only the first session in; and a ROS 2 node on
|
|
520
|
+
the object layer's session has the node's Zenoh ID, so a console draws it
|
|
521
|
+
inside the node.
|
|
522
|
+
- **Who closes it.** The session is Asterism's. `Asterism.session.close`
|
|
523
|
+
(and `net.session.close`) does nothing but warn once, so a library handed
|
|
524
|
+
the session cannot break the connection. `Asterism.close`, the end of the
|
|
525
|
+
connect block and a lost connection close it, and with it everything
|
|
526
|
+
declared on it: the application's subscribers, publishers, ROS 2 nodes.
|
|
527
|
+
Close those yourself when you are done with them earlier (`sub.close`,
|
|
528
|
+
`node.close`; on CRuby `ros.close` withdraws its nodes and leaves the
|
|
529
|
+
rest).
|
|
530
|
+
- **After a lost connection** (`Asterism.poll` returned false), connect
|
|
531
|
+
again, take `Asterism.session` again (a new object) and declare again
|
|
532
|
+
what you had on the old one.
|
|
533
|
+
- **Polling.** `Asterism.poll` polls the session; `node.poll` polls it too
|
|
534
|
+
and only then serves the node. Each subscriber, queryable and node keeps
|
|
535
|
+
what came for it, so the order does not matter and nothing is taken from
|
|
536
|
+
another. Your own `put` does not come back to your own subscriber on the
|
|
537
|
+
same session.
|
|
538
|
+
- **Keys.** Keep your keys out of the object layer's: `asterism/<node>/<app>/**`
|
|
539
|
+
is answered by its queryable (`get` there is a call), `asterism/<node>` and
|
|
540
|
+
`asterism/<node>/<app>/<object>` are its liveliness tokens. Putting under
|
|
541
|
+
`asterism/<node>/<app>/<object>/<anything>` (a state, a log) is fine:
|
|
542
|
+
puts and subscriptions do not meet queryables or liveliness.
|
|
543
|
+
|
|
483
544
|
## Errors
|
|
484
545
|
|
|
485
546
|
Every error Asterism raises is an `Asterism::Error` (a `StandardError`):
|
|
@@ -591,7 +652,9 @@ Asterism follows `Asterism::Zenoh`: the connection is closed when the router
|
|
|
591
652
|
why, and calls raise `Asterism::Disconnected`. **`Asterism::Zenoh::Error`
|
|
592
653
|
never comes out of Asterism; it is always wrapped in `Disconnected`** (the
|
|
593
654
|
message is kept). The exposed objects are gone with the connection: to go
|
|
594
|
-
on, the application calls `connect` and `expose` again
|
|
655
|
+
on, the application calls `connect` and `expose` again, and declares again
|
|
656
|
+
what it had on `Asterism.session` (subscribers, ROS 2 nodes; the old
|
|
657
|
+
session is closed, its entities raise `Zenoh::ClosedError`).
|
|
595
658
|
|
|
596
659
|
## Values
|
|
597
660
|
|
|
@@ -610,7 +673,7 @@ caller gets `RemoteError` (`Asterism::EncodeError`). Nesting is limited to
|
|
|
610
673
|
| `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 |
|
|
611
674
|
| `asterism/<node>` | liveliness | the node is connected |
|
|
612
675
|
| `asterism/<node>/<app>/<object>` | liveliness | the object is exposed |
|
|
613
|
-
| `asterism/<node>/@profile` | get / queryable, and put | query (parameter `icon=0` for no icon): reply the node's profile, a Hash (`kind`, `host`, `name`, `icon`, `icon_hash`; [Profiles](#profiles)). The node also puts the profile without the icon at connect and on each change. `@profile` is a verbatim chunk, so `asterism/**` does not match it: the object layer's wildcards never see it. An application's own profile would go at `asterism/<node>/<app>/@profile` (not in 0.
|
|
676
|
+
| `asterism/<node>/@profile` | get / queryable, and put | query (parameter `icon=0` for no icon): reply the node's profile, a Hash (`kind`, `host`, `name`, `icon`, `icon_hash`; [Profiles](#profiles)). The node also puts the profile without the icon at connect and on each change. `@profile` is a verbatim chunk, so `asterism/**` does not match it: the object layer's wildcards never see it. An application's own profile would go at `asterism/<node>/<app>/@profile` (not in 0.6.0) |
|
|
614
677
|
|
|
615
678
|
One queryable per application (`asterism/<node>/<app>/**`), and one for the
|
|
616
679
|
profile (`asterism/<node>/@profile`). A call whose
|
|
@@ -27,7 +27,7 @@ module Asterism
|
|
|
27
27
|
# singleton class, so the shared methods themselves are not changed.
|
|
28
28
|
module Locked
|
|
29
29
|
%i[expose unexpose poll nodes [] call_async meta connected? exposed on_join on_leave off_join off_leave
|
|
30
|
-
profile profile_async].each do |m|
|
|
30
|
+
profile profile_async session].each do |m|
|
|
31
31
|
define_method(m) do |*args, **kw, &blk|
|
|
32
32
|
::Asterism::LOCK.synchronize { super(*args, **kw, &blk) }
|
|
33
33
|
end
|
|
@@ -130,6 +130,21 @@ module Asterism
|
|
|
130
130
|
@runner.add { ::Asterism.poll ? 0 : false }
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
# Asterism.session as a Zenoh::Connection (the block API of
|
|
134
|
+
# Asterism::Zenoh.open) on the same receiving thread as the objects:
|
|
135
|
+
# subscribe / queryable blocks run there, between the calls answered.
|
|
136
|
+
# For a ROS 2 node on the same session:
|
|
137
|
+
# Asterism::ROS::Connection.new(net.session).node("name"). The session
|
|
138
|
+
# stays Asterism's: net.session.close does nothing (shared?), net.close
|
|
139
|
+
# closes it. Disconnected when not connected.
|
|
140
|
+
def session
|
|
141
|
+
::Asterism::LOCK.synchronize do
|
|
142
|
+
s = ::Asterism.session
|
|
143
|
+
@session = nil if @session && !@session.session.equal?(s)
|
|
144
|
+
@session ||= ::Asterism::Zenoh::Connection.new(s, runner: @runner)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
133
148
|
def node_id = ::Asterism.node_id
|
|
134
149
|
def app = ::Asterism.app
|
|
135
150
|
def connected? = ::Asterism.connected?
|
data/lib/asterism/cruby/ros.rb
CHANGED
|
@@ -95,12 +95,15 @@ module Asterism
|
|
|
95
95
|
self
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# Stops spinning, withdraws the nodes and closes the session.
|
|
98
|
+
# Stops spinning, withdraws the nodes and closes the session. On
|
|
99
|
+
# Asterism's session (ROS::Connection.new(net.session)) it only
|
|
100
|
+
# withdraws the nodes: the receiving thread and the session stay the
|
|
101
|
+
# object layer's.
|
|
99
102
|
def close
|
|
100
|
-
runner.stop
|
|
103
|
+
runner.stop unless @zenoh.shared?
|
|
101
104
|
@nodes.each { |n| n.close rescue ::Asterism::Zenoh::Error }
|
|
102
105
|
@nodes = []
|
|
103
|
-
@zenoh.close
|
|
106
|
+
@zenoh.close unless @zenoh.shared?
|
|
104
107
|
nil
|
|
105
108
|
end
|
|
106
109
|
|
|
@@ -142,8 +145,17 @@ module Asterism
|
|
|
142
145
|
|
|
143
146
|
def asterism_runner = @asterism_runner
|
|
144
147
|
|
|
148
|
+
# Withdraws the node and stops polling it.
|
|
149
|
+
def close
|
|
150
|
+
@asterism_runner.lock.synchronize do
|
|
151
|
+
super
|
|
152
|
+
@asterism_runner.remove(@asterism_task) if @asterism_task
|
|
153
|
+
@asterism_task = nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
145
157
|
# The calls that change the node take the connection's lock.
|
|
146
|
-
%i[subscription client
|
|
158
|
+
%i[subscription client every cancel_timer].each do |m|
|
|
147
159
|
define_method(m) do |*args, **kw, &blk|
|
|
148
160
|
@asterism_runner.lock.synchronize { super(*args, **kw, &blk) }
|
|
149
161
|
end
|
data/lib/asterism/cruby/zenoh.rb
CHANGED
|
@@ -158,11 +158,29 @@ module Asterism
|
|
|
158
158
|
class Connection
|
|
159
159
|
attr_reader :session, :runner
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
# runner: an existing Runner to receive on instead of a new one
|
|
162
|
+
# (Asterism::Net#session gives the object layer's: the session and
|
|
163
|
+
# its receiving thread are Asterism's, see shared?). @api private
|
|
164
|
+
def initialize(session, interval: Runner::DEFAULT_INTERVAL, runner: nil)
|
|
162
165
|
@session = session
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
if runner
|
|
167
|
+
@runner = runner
|
|
168
|
+
@shared = true
|
|
169
|
+
else
|
|
170
|
+
@runner = Runner.new(session, interval: interval, name: "zenoh")
|
|
171
|
+
# The first task: false (the runner stops) once the session closed.
|
|
172
|
+
@runner.add { @session.poll ? 0 : false }
|
|
173
|
+
@shared = false
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# True for the Connection of Asterism's own session (net.session):
|
|
178
|
+
# its close does nothing, start / stop / run are the object layer's
|
|
179
|
+
# receiving thread (net.start / net.stop / net.run), and
|
|
180
|
+
# Asterism.close (or the end of the connect block) closes the session
|
|
181
|
+
# with everything declared on it.
|
|
182
|
+
def shared?
|
|
183
|
+
@shared
|
|
166
184
|
end
|
|
167
185
|
|
|
168
186
|
# opts: those of Session#put (attachment:, encoding:, priority:,
|
|
@@ -370,15 +388,23 @@ module Asterism
|
|
|
370
388
|
def declare_keyexpr(key) = @session.declare_keyexpr(key)
|
|
371
389
|
|
|
372
390
|
# Stops receiving and closes the session (with everything declared on
|
|
373
|
-
# it). Idempotent.
|
|
391
|
+
# it). Idempotent. On a shared? Connection (net.session) it does
|
|
392
|
+
# nothing: close what you declared on it (subscriptions, publishers,
|
|
393
|
+
# ROS nodes) one by one, or let Asterism.close close them.
|
|
374
394
|
def close
|
|
395
|
+
if @shared
|
|
396
|
+
::Asterism.warn_once(self, "asterism: net.session belongs to Asterism; its close is ignored " \
|
|
397
|
+
"(net.close closes it)")
|
|
398
|
+
return nil
|
|
399
|
+
end
|
|
375
400
|
@runner.close
|
|
376
401
|
@session.close
|
|
377
402
|
nil
|
|
378
403
|
end
|
|
379
404
|
|
|
380
405
|
def inspect
|
|
381
|
-
"#<Asterism::Zenoh::Connection #{closed? ? 'closed' : zid}#{
|
|
406
|
+
"#<Asterism::Zenoh::Connection #{closed? ? 'closed' : zid}#{@shared ? ' shared' : ''}" \
|
|
407
|
+
"#{running? ? ' running' : ''}>"
|
|
382
408
|
end
|
|
383
409
|
|
|
384
410
|
private
|
data/lib/asterism/version.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Asterism
|
|
2
2
|
# The gem's version (the CRuby gem asterism). The Zenoh binding it needs
|
|
3
|
-
# is in asterism.gemspec (asterism-zenoh ~> 0.4.0 for 0.5.0
|
|
4
|
-
# did not change; see CHANGELOG.md).
|
|
5
|
-
VERSION = "0.
|
|
3
|
+
# is in asterism.gemspec (asterism-zenoh ~> 0.4.0 for 0.5.0 and 0.6.0:
|
|
4
|
+
# the bindings did not change; see CHANGELOG.md).
|
|
5
|
+
VERSION = "0.6.0"
|
|
6
6
|
end
|
data/mrblib/asterism.rb
CHANGED
|
@@ -227,16 +227,20 @@ module Asterism
|
|
|
227
227
|
s.close
|
|
228
228
|
raise Disconnected, e.message
|
|
229
229
|
end
|
|
230
|
+
s.extend(SharedSession) # Asterism.session: the application's close does not close it
|
|
230
231
|
@session = s
|
|
231
232
|
@node = node
|
|
232
233
|
@app = app
|
|
233
234
|
@objects = {} # name => [object, {method name => arity}, token]
|
|
234
|
-
@alive = {} # "node/app/object" =>
|
|
235
|
+
@alive = {} # "node/app/object" => node (from liveliness)
|
|
235
236
|
@nodes = {} # node => true (from liveliness)
|
|
236
237
|
@proxies = {}
|
|
237
238
|
@depth = 0
|
|
238
239
|
@lost = nil
|
|
239
240
|
@known = [] # the other nodes told to on_join so far
|
|
241
|
+
@known_set = {} # the same, as node => true
|
|
242
|
+
@node_list = [node] # what nodes returns (this node first), rebuilt on a change
|
|
243
|
+
@nodes_changed = false # liveliness changed since tell_nodes last compared
|
|
240
244
|
tell_profile # for those who follow the profiles (the reply is the queryable's)
|
|
241
245
|
self
|
|
242
246
|
end
|
|
@@ -298,20 +302,67 @@ module Asterism
|
|
|
298
302
|
@objects = {}
|
|
299
303
|
@alive = {}
|
|
300
304
|
@nodes = {}
|
|
305
|
+
@node_list = nil
|
|
306
|
+
@nodes_changed = true
|
|
301
307
|
@proxies = {}
|
|
302
|
-
|
|
308
|
+
if s
|
|
309
|
+
@releasing = s
|
|
310
|
+
begin
|
|
311
|
+
s.close
|
|
312
|
+
ensure
|
|
313
|
+
@releasing = nil
|
|
314
|
+
end
|
|
315
|
+
end
|
|
303
316
|
@node_token = nil
|
|
304
317
|
@queryable = nil
|
|
305
318
|
@watch = nil
|
|
306
319
|
@profile_queryable = nil
|
|
307
320
|
end
|
|
308
321
|
|
|
309
|
-
#
|
|
322
|
+
# The Zenoh session of the connection (an Asterism::Zenoh::Session), for
|
|
323
|
+
# what the object layer does not do: put and subscribe, a ROS 2 node
|
|
324
|
+
# (Asterism::ROS::Node.new(Asterism.session, ...)), anything of the
|
|
325
|
+
# Zenoh API. One connection for everything: on a router that lets each
|
|
326
|
+
# pass in once, a second session would not get in, and a ROS 2 node on
|
|
327
|
+
# this session is part of this node (the same Zenoh ID).
|
|
328
|
+
#
|
|
329
|
+
# The session stays Asterism's: its close does nothing (it warns once);
|
|
330
|
+
# Asterism.close closes it, and with it everything declared on it (the
|
|
331
|
+
# application's subscribers, publishers, ROS 2 nodes). After a lost
|
|
332
|
+
# connection and a new Asterism.connect, take Asterism.session again and
|
|
333
|
+
# declare those again. Polling it (session.poll, node.poll) is fine:
|
|
334
|
+
# Asterism.poll polls it too, each entity keeps what came for it.
|
|
335
|
+
# Raises Disconnected when not connected (as the calls do).
|
|
336
|
+
def self.session
|
|
337
|
+
raise Disconnected, (@lost || "not connected") unless connected?
|
|
338
|
+
@session
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# @api private (the name before 0.6.0; Asterism.session is the same)
|
|
310
342
|
def self.session!
|
|
311
343
|
raise Disconnected, (@lost || "not connected") unless @session
|
|
312
344
|
@session
|
|
313
345
|
end
|
|
314
346
|
|
|
347
|
+
# Whether Asterism itself is closing s (SharedSession#close).
|
|
348
|
+
# @api private
|
|
349
|
+
def self.releasing?(s)
|
|
350
|
+
!@releasing.nil? && @releasing.equal?(s)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Extended onto the session of Asterism.connect: the session belongs to
|
|
354
|
+
# Asterism, so a close from anyone else (the application, a library that
|
|
355
|
+
# was handed Asterism.session) does nothing. Asterism.close and a lost
|
|
356
|
+
# connection close it.
|
|
357
|
+
module SharedSession
|
|
358
|
+
def close
|
|
359
|
+
return super if ::Asterism.releasing?(self)
|
|
360
|
+
::Asterism.warn_once(self, "asterism: Asterism.session belongs to Asterism; its close is ignored " \
|
|
361
|
+
"(Asterism.close closes it)")
|
|
362
|
+
nil
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
315
366
|
# The connection went away: closed, but the next Asterism.poll still tells
|
|
316
367
|
# on_leave about every node known until then.
|
|
317
368
|
# @api private
|
|
@@ -361,6 +412,10 @@ module Asterism
|
|
|
361
412
|
# Call from the application's update loop. Answers the calls that came
|
|
362
413
|
# in, follows who is alive and calls the on_join / on_leave blocks. false
|
|
363
414
|
# once the connection is closed.
|
|
415
|
+
#
|
|
416
|
+
# Cheap when nothing came: each queue is looked at by its count first,
|
|
417
|
+
# and the list of nodes (and the comparison behind on_join / on_leave)
|
|
418
|
+
# is made again only after a liveliness change.
|
|
364
419
|
def self.poll
|
|
365
420
|
ok = @session ? pump : false
|
|
366
421
|
tell_nodes
|
|
@@ -408,14 +463,43 @@ module Asterism
|
|
|
408
463
|
def self.tell_nodes
|
|
409
464
|
known = @known
|
|
410
465
|
return if known.nil?
|
|
411
|
-
|
|
412
|
-
|
|
466
|
+
# Nothing came on the liveliness watch since the last comparison (and
|
|
467
|
+
# the connection is neither new nor lost): nothing can have changed.
|
|
468
|
+
return unless @nodes_changed
|
|
469
|
+
@nodes_changed = false
|
|
470
|
+
now = []
|
|
471
|
+
now_set = {}
|
|
472
|
+
if @session
|
|
473
|
+
list = node_list
|
|
474
|
+
i = 1 # list[0] is this node
|
|
475
|
+
while i < list.size
|
|
476
|
+
n = list[i]
|
|
477
|
+
now << n
|
|
478
|
+
now_set[n] = true
|
|
479
|
+
i += 1
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
known_set = @known_set || {}
|
|
413
483
|
joined = []
|
|
414
484
|
left = []
|
|
415
|
-
|
|
416
|
-
|
|
485
|
+
i = 0
|
|
486
|
+
while i < now.size
|
|
487
|
+
joined << now[i] unless known_set[now[i]]
|
|
488
|
+
i += 1
|
|
489
|
+
end
|
|
490
|
+
i = 0
|
|
491
|
+
while i < known.size
|
|
492
|
+
left << known[i] unless now_set[known[i]]
|
|
493
|
+
i += 1
|
|
494
|
+
end
|
|
417
495
|
# After a loss, everything has been told: nothing more to compare.
|
|
418
|
-
|
|
496
|
+
if @session
|
|
497
|
+
@known = now
|
|
498
|
+
@known_set = now_set
|
|
499
|
+
else
|
|
500
|
+
@known = nil
|
|
501
|
+
@known_set = nil
|
|
502
|
+
end
|
|
419
503
|
tell_each("on_join", @on_join, joined)
|
|
420
504
|
tell_each("on_leave", @on_leave, left)
|
|
421
505
|
end
|
|
@@ -493,9 +577,10 @@ module Asterism
|
|
|
493
577
|
# @api private
|
|
494
578
|
def self.follow_liveliness
|
|
495
579
|
w = @watch
|
|
496
|
-
return unless w
|
|
580
|
+
return unless w && w.pending > 0
|
|
497
581
|
# Array forms instead of blocks called from C (see Future#collect).
|
|
498
582
|
events = w.each_pending
|
|
583
|
+
@nodes_changed = true if events.size > 0
|
|
499
584
|
if events.size > 0 && w.dropped > 0
|
|
500
585
|
warn_once(w, "asterism: #{w.dropped} liveliness changes dropped (more than #{watch_depth} at once); " \
|
|
501
586
|
"Asterism.each and Asterism.nodes may miss some objects")
|
|
@@ -515,24 +600,49 @@ module Asterism
|
|
|
515
600
|
end
|
|
516
601
|
elsif parts.size == 3
|
|
517
602
|
if alive
|
|
518
|
-
@alive[rest] =
|
|
603
|
+
@alive[rest] = parts[0]
|
|
519
604
|
else
|
|
520
605
|
@alive.delete(rest)
|
|
521
606
|
end
|
|
522
607
|
end
|
|
523
608
|
end
|
|
609
|
+
@node_list = nil
|
|
524
610
|
end
|
|
525
611
|
|
|
526
612
|
# The node IDs alive now, this one first.
|
|
527
613
|
def self.nodes
|
|
528
614
|
return [] unless @session
|
|
615
|
+
node_list.dup
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# The list behind nodes, made again only after a liveliness change (the
|
|
619
|
+
# caller must not change it). @api private
|
|
620
|
+
def self.node_list
|
|
621
|
+
list = @node_list
|
|
622
|
+
return list if list
|
|
529
623
|
list = [@node]
|
|
530
|
-
|
|
531
|
-
@
|
|
532
|
-
|
|
533
|
-
|
|
624
|
+
seen = { @node => true }
|
|
625
|
+
names = @nodes.keys
|
|
626
|
+
i = 0
|
|
627
|
+
while i < names.size
|
|
628
|
+
n = names[i]
|
|
629
|
+
unless seen[n]
|
|
630
|
+
seen[n] = true
|
|
631
|
+
list << n
|
|
632
|
+
end
|
|
633
|
+
i += 1
|
|
534
634
|
end
|
|
535
|
-
|
|
635
|
+
owners = @alive.values
|
|
636
|
+
i = 0
|
|
637
|
+
while i < owners.size
|
|
638
|
+
n = owners[i]
|
|
639
|
+
unless seen[n]
|
|
640
|
+
seen[n] = true
|
|
641
|
+
list << n
|
|
642
|
+
end
|
|
643
|
+
i += 1
|
|
644
|
+
end
|
|
645
|
+
@node_list = list
|
|
536
646
|
end
|
|
537
647
|
|
|
538
648
|
# The exposed objects alive now whose <node>/<app>/<object> matches the
|
|
@@ -543,11 +653,15 @@ module Asterism
|
|
|
543
653
|
def self.each(pattern = "**")
|
|
544
654
|
paths = []
|
|
545
655
|
if @session
|
|
656
|
+
seen = {}
|
|
546
657
|
@objects.each_key do |name|
|
|
547
658
|
path = "#{@node}/#{@app}/#{name}"
|
|
548
|
-
|
|
659
|
+
if glob?(pattern, path)
|
|
660
|
+
paths << path
|
|
661
|
+
seen[path] = true
|
|
662
|
+
end
|
|
549
663
|
end
|
|
550
|
-
@alive.each_key { |path| paths << path if glob?(pattern, path)
|
|
664
|
+
@alive.each_key { |path| paths << path if !seen[path] && glob?(pattern, path) }
|
|
551
665
|
end
|
|
552
666
|
proxies = paths.map { |path| self[path] }
|
|
553
667
|
return proxies unless block_given?
|
|
@@ -619,7 +733,7 @@ module Asterism
|
|
|
619
733
|
# @api private
|
|
620
734
|
def self.answer_calls
|
|
621
735
|
qa = @queryable
|
|
622
|
-
return unless qa
|
|
736
|
+
return unless qa && qa.pending > 0
|
|
623
737
|
# Without a block, so that a handler that waits (and so polls) does not
|
|
624
738
|
# re-enter the queryable's iteration.
|
|
625
739
|
queries = qa.each_pending
|
data/mrblib/profile.rb
CHANGED