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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b55c825f8859d86ba09baad0585c0f416a168023b8032c3f871d63bebef0538c
4
- data.tar.gz: 4d6fd26b326e62348e4fba8c6881c48b5b7145dd0b0c701eb7ba1dcc1bf5b261
3
+ metadata.gz: ba14899dd37f9c4c21e148a2256d71545785da6f61a1f8f6701e2d0d189e9061
4
+ data.tar.gz: 207e08ed177ffa93cd5c7173a84c31f519f59c8e076c6c641ccaa8db27712ea9
5
5
  SHA512:
6
- metadata.gz: 0acc8e1c4e4f028124d247a54e35ebda3451449dcfc46da503d345c857502200b1b68b4f15a4d59fbb7a603d9331f6996aa9072b1426e039fea87b43efbddd5e
7
- data.tar.gz: 03b912800f24b881a7e01eb6479c2d8c3c2c32a3346538ea60ae5f65c431f3035e097f5fc90fdcaec7734efbf95496544c4b0dd174b12e6be20cabfec2db3635
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.5.0.gem
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.5.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?
@@ -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 close every cancel_timer].each do |m|
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
@@ -158,11 +158,29 @@ module Asterism
158
158
  class Connection
159
159
  attr_reader :session, :runner
160
160
 
161
- def initialize(session, interval: Runner::DEFAULT_INTERVAL)
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
- @runner = Runner.new(session, interval: interval, name: "zenoh")
164
- # The first task: false (the runner stops) once the session closed.
165
- @runner.add { @session.poll ? 0 : false }
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}#{running? ? ' running' : ''}>"
406
+ "#<Asterism::Zenoh::Connection #{closed? ? 'closed' : zid}#{@shared ? ' shared' : ''}" \
407
+ "#{running? ? ' running' : ''}>"
382
408
  end
383
409
 
384
410
  private
@@ -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: the bindings
4
- # did not change; see CHANGELOG.md).
5
- VERSION = "0.5.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" => true (from liveliness)
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
- s.close if s
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
- # @api private
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
- now = @session ? nodes : []
412
- now.shift if @session # this node
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
- now.each { |n| joined << n unless known.include?(n) }
416
- known.each { |n| left << n unless now.include?(n) }
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
- @known = @session ? now : nil
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] = true
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
- @nodes.each_key { |n| list << n unless list.include?(n) }
531
- @alive.each_key do |path|
532
- n = path.split("/")[0]
533
- list << n unless list.include?(n)
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
- list
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
- paths << path if glob?(pattern, path)
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) && !paths.include?(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
@@ -342,7 +342,7 @@ module Asterism
342
342
  # @api private
343
343
  def self.answer_profile
344
344
  qa = @profile_queryable
345
- return unless qa
345
+ return unless qa && qa.pending > 0
346
346
  queries = qa.each_pending
347
347
  i = 0
348
348
  while i < queries.size
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asterism
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katsuhiko Kageyama