spatialdds-ros 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {spatialdds_ros-0.3.0/spatialdds_ros.egg-info → spatialdds_ros-0.4.0}/PKG-INFO +144 -8
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/README.md +143 -7
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/package.xml +3 -1
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/setup.py +1 -1
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/__init__.py +1 -1
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/adapter.py +50 -7
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/announce.py +19 -0
- spatialdds_ros-0.4.0/spatialdds_ros/awareness.py +421 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/config.py +73 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/frames.py +80 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/qos.py +15 -1
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/robot_node.py +143 -4
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/standalone.py +8 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/transport.py +1 -1
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0/spatialdds_ros.egg-info}/PKG-INFO +144 -8
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros.egg-info/SOURCES.txt +3 -0
- spatialdds_ros-0.4.0/test/test_acceptance_two_domains.py +337 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_adapter.py +68 -0
- spatialdds_ros-0.4.0/test/test_awareness.py +456 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_frames.py +79 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_readme.py +56 -1
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_transport.py +93 -5
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/LICENSE +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/resource/spatialdds_ros +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/setup.cfg +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/PROVENANCE.md +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/_field_aliases.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/builtin/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/builtin/_types.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/anchors/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/anchors/_anchors.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/argeo/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/argeo/_argeo.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/common/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/common/_types.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/core/GnssService/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/core/GnssService/_core.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/core/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/core/_core.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/disco/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/disco/_discovery.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/_events.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/crossing_direction_enum/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/crossing_direction_enum/_events.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/event_state_enum/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/event_state_enum/_events.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/event_type_enum/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/event_type_enum/_events.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/severity_enum/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/severity_enum/_events.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/zone_kind_enum/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/events/enums/zone_kind_enum/_events.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/_mapping.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/alignment_method/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/alignment_method/_mapping.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/edge_type/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/edge_type/_mapping.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/map_event_kind/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/map_event_kind/_mapping.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/map_kind/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/map_kind/_mapping.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/map_status/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/mapping/enums/map_status/_mapping.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/owm/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/owm/_owm.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/semantics/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/semantics/_semantics.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/common/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/common/_common.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/lidar/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/lidar/_lidar.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/rad/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/rad/_rad.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/radio/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/radio/_radio.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/rf_beam/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/rf_beam/_rf_beam.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/vision/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/sensing/vision/_vision.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/slam_frontend/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/slam_frontend/_slam_frontend.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/vio/__init__.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_idl18/spatial/vio/_vio.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/kinematic.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/recording.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/tempo.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros/topics.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros.egg-info/dependency_links.txt +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros.egg-info/entry_points.txt +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros.egg-info/requires.txt +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros.egg-info/top_level.txt +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/spatialdds_ros.egg-info/zip-safe +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_acceptance_bare_scene.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_acceptance_mcap_gateway.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_acceptance_web_to_robot.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_config.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_generated_types.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_kinematic.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_no_ros_needed.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_packaging.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_qos_and_tempo.py +0 -0
- {spatialdds_ros-0.3.0 → spatialdds_ros-0.4.0}/test/test_types_resolve_at_definitions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spatialdds-ros
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A ROS 2 robot as a SpatialDDS participant
|
|
5
5
|
Home-page: https://github.com/OpenArCloud/spatialdds-ros
|
|
6
6
|
Author: Open AR Cloud
|
|
@@ -48,8 +48,11 @@ Dynamic: summary
|
|
|
48
48
|
|
|
49
49
|
Make a ROS 2 robot a SpatialDDS participant: its pose published as a
|
|
50
50
|
`spatial.owm` entity on scene-scoped topics, `goto` commands consumed from
|
|
51
|
-
`ModelCommand` and dispatched to nav2,
|
|
52
|
-
|
|
51
|
+
`ModelCommand` and dispatched to nav2, its presence announced, and — when
|
|
52
|
+
asked — the scene's other moving entities put into its own costmap. All on the
|
|
53
|
+
same DDS bus ROS already runs on: one pip install makes a robot seen,
|
|
54
|
+
steerable and aware, including of robots in other ROS domains it shares no
|
|
55
|
+
middleware with.
|
|
53
56
|
|
|
54
57
|
**A type adapter on a shared bus, not a protocol gateway.** Nothing is
|
|
55
58
|
relayed. ROS and SpatialDDS meet on the wire because both are DDS, so this is
|
|
@@ -76,7 +79,7 @@ The split runs through the whole package:
|
|
|
76
79
|
|
|
77
80
|
Only `spatialdds_ros.robot_node` requires a ROS 2 environment. It imports
|
|
78
81
|
`rclpy` inside its entry point rather than at module scope, which is what
|
|
79
|
-
lets the other
|
|
82
|
+
lets the other thirteen modules — and the entire test suite — run on a machine
|
|
80
83
|
that has never had ROS installed.
|
|
81
84
|
|
|
82
85
|
**That claim is a gate, not a promise.**
|
|
@@ -143,6 +146,117 @@ static and identity, which is a simulator and not a localizing robot.
|
|
|
143
146
|
deferred** — there is no released binary package; install from source or with
|
|
144
147
|
pip.
|
|
145
148
|
|
|
149
|
+
## A robot that sees the scene
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
ros2 run spatialdds_ros robot_node --ros-args \
|
|
153
|
+
-p scene:=bare-scene \
|
|
154
|
+
-p entity_id:=ent:robot:b \
|
|
155
|
+
-p frame_id:=map/bare-scene \
|
|
156
|
+
-p awareness:=true \
|
|
157
|
+
-p awareness_range_m:=8.0
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The node then reads the scene's other moving entities off the bus and
|
|
161
|
+
publishes them where nav2 can plan around them: `~/peers/cloud` marks them,
|
|
162
|
+
`~/peers/clearing` is a ring at `awareness_range_m` that clears whatever has
|
|
163
|
+
gone, and `~/peers/markers` shows an operator what is being avoided. nav2
|
|
164
|
+
needs one obstacle layer reading both clouds:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
peers_layer:
|
|
168
|
+
plugin: "nav2_costmap_2d::ObstacleLayer"
|
|
169
|
+
enabled: true
|
|
170
|
+
observation_sources: peers clearing
|
|
171
|
+
peers:
|
|
172
|
+
topic: /robot_b/peers/cloud
|
|
173
|
+
data_type: PointCloud2
|
|
174
|
+
marking: true
|
|
175
|
+
clearing: false
|
|
176
|
+
obstacle_max_range: 8.0
|
|
177
|
+
raytrace_max_range: 8.5
|
|
178
|
+
min_obstacle_height: 0.0
|
|
179
|
+
max_obstacle_height: 2.0
|
|
180
|
+
clearing:
|
|
181
|
+
topic: /robot_b/peers/clearing
|
|
182
|
+
data_type: PointCloud2
|
|
183
|
+
marking: false
|
|
184
|
+
clearing: true
|
|
185
|
+
obstacle_max_range: 8.0
|
|
186
|
+
raytrace_max_range: 8.5
|
|
187
|
+
min_obstacle_height: 0.0
|
|
188
|
+
max_obstacle_height: 2.0
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Add `peers_layer` to the costmap's `plugins`, before the inflation layer, in
|
|
192
|
+
every costmap that should avoid peers — the global one, for the planner to
|
|
193
|
+
route around them. The topics carry the node's name (`robot_b` here).
|
|
194
|
+
|
|
195
|
+
**Set `awareness_range_m` to your costmap's obstacle range or less** — both
|
|
196
|
+
`obstacle_max_range` and `raytrace_max_range` above. The node refuses to start
|
|
197
|
+
with awareness on and no range, because the right value lives in your nav2
|
|
198
|
+
YAML, which it cannot read, and a guess beyond your raytrace range draws marks
|
|
199
|
+
nothing will ever clear.
|
|
200
|
+
|
|
201
|
+
**Why two clouds.** A marking cloud that empties raytraces nothing, so the
|
|
202
|
+
last place a peer was seen stays lethal for good. Measured, with the ring
|
|
203
|
+
beside it and without, in
|
|
204
|
+
[`docs/awareness-census.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/awareness-census.md)
|
|
205
|
+
§1.
|
|
206
|
+
|
|
207
|
+
### What is drawn, and when it stops
|
|
208
|
+
|
|
209
|
+
- **Moving things only:** `FAST`, `ACTIVE`, with a pose, within range, and of
|
|
210
|
+
an `awareness_types` URI if you set any. A declared region is not a peer;
|
|
211
|
+
the keep-out filter is the mechanism for those.
|
|
212
|
+
- **Never this robot.** Its own record does reach its own reader, so this is
|
|
213
|
+
a filter that does work, on its own `entity_id`.
|
|
214
|
+
- **Its own size, or `peer_radius_m`** (0.3 m) for a peer that publishes no
|
|
215
|
+
extent — which includes every robot this library publishes.
|
|
216
|
+
- **Dropped when it goes quiet, and remembered.** A peer is drawn while its
|
|
217
|
+
poses keep arriving, within a window learned from its own cadence — at
|
|
218
|
+
least one second, three times its interval, measured on this robot's clock.
|
|
219
|
+
The record is kept, so the next pose brings it back with no rediscovery. An
|
|
220
|
+
`UNOBSERVED`, retired or disposed record, or a writer that has gone, drops
|
|
221
|
+
it at once.
|
|
222
|
+
- **Never at a guessed position.** A peer in another frame is placed through
|
|
223
|
+
both frames' georeferences; with either one missing it is skipped, and the
|
|
224
|
+
log says which.
|
|
225
|
+
|
|
226
|
+
### Two robots, two ROS domains, one bus
|
|
227
|
+
|
|
228
|
+
The walkthrough is the acceptance test. Robot A runs in `ROS_DOMAIN_ID` 10,
|
|
229
|
+
localized by an AMCL that converges at (3, 0) and then says nothing; robot B
|
|
230
|
+
runs in 20 at the origin with nav2's planner; the SpatialDDS bus is DDS domain
|
|
231
|
+
30. Nothing else connects them.
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
docker build -t spatialdds-ros-nav2 -f test/ros/Dockerfile .
|
|
235
|
+
python3 -m pytest test/test_acceptance_two_domains.py -q -s
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
A absent: path 6.00 m, max |y| 0.000; cloud empty
|
|
240
|
+
A present: cloud 109 points near (3, 0), cost 100; path 6.30 m, max |y| 0.84
|
|
241
|
+
A still for 20 s: drawn in all 31 samples
|
|
242
|
+
quiet but latched: not drawing ent:robot:a: UNOBSERVED: no pose from svc:robot:a for 1.6s
|
|
243
|
+
killed: cost at A back to 0 within 2.8s (probe overhead included)
|
|
244
|
+
gateway served and validated 2 entities: ['ent:robot:a', 'ent:robot:b']
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
B's plan to (6, 0) bends around A, and straightens again once A is gone. Both
|
|
248
|
+
ROS graphs are listed from one container, and neither holds a node or topic of
|
|
249
|
+
the other: what separates them is the domain id, not the network.
|
|
250
|
+
|
|
251
|
+
**What it does not do.** A peer beyond the range is not drawn. A mark left
|
|
252
|
+
behind when this robot drives away from it stays until the robot comes back
|
|
253
|
+
within range, which is how nav2 treats anything a sensor saw. A peer whose
|
|
254
|
+
publisher sends a pose only when it moves will drop out when it stops — every
|
|
255
|
+
`spatialdds_ros` robot sends at least `pose_floor_hz` for exactly this reason,
|
|
256
|
+
but the wire gives a consumer no way to know what a foreign one promises
|
|
257
|
+
([`docs/spec-queue.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/spec-queue.md)
|
|
258
|
+
§3).
|
|
259
|
+
|
|
146
260
|
## Configuration
|
|
147
261
|
|
|
148
262
|
Three parameters have no default and the node refuses to start without them,
|
|
@@ -156,7 +270,9 @@ because every possible default would be a false statement about the world:
|
|
|
156
270
|
|
|
157
271
|
Everything else has one: `domain` 0, `ground_z` 0.0, `pose_source` tf,
|
|
158
272
|
`nav2_action` navigate_to_pose, `source_id` derived from `entity_id`,
|
|
159
|
-
`content_refs` and `external_refs` empty.
|
|
273
|
+
`content_refs` and `external_refs` empty, `pose_floor_hz` 2.0, `awareness`
|
|
274
|
+
off, `peer_radius_m` 0.3, `awareness_types` empty. One more has none, and is
|
|
275
|
+
needed only with awareness on: `awareness_range_m`, for the reason above.
|
|
160
276
|
|
|
161
277
|
### What the robot *is*, and what it is *also known as*
|
|
162
278
|
|
|
@@ -244,8 +360,20 @@ not neutral when the thing being defaulted is a claim.
|
|
|
244
360
|
The adapter publishes `basis = OBSERVED` and offers no parameter to change it:
|
|
245
361
|
a self-localizing robot is reporting where it has worked out that it is, which
|
|
246
362
|
is a sighting rather than an assertion. It claims no extent — a robot locates
|
|
247
|
-
itself,
|
|
248
|
-
keep-out by
|
|
363
|
+
itself, and its footprint is not a region anybody should plan by. (Until 0.4.0
|
|
364
|
+
this said an extent would be read as keep-out by the `DECLARED` rule; the
|
|
365
|
+
robot is `OBSERVED`, so that rule excludes it before it looks at extent. The
|
|
366
|
+
conclusion stands; the reason was wrong. Publishing a true footprint is a
|
|
367
|
+
follow-up.)
|
|
368
|
+
|
|
369
|
+
**Its pose goes out at least `pose_floor_hz` (2 Hz), moving or not.** A
|
|
370
|
+
consumer stops drawing a peer it has not heard from within that peer's
|
|
371
|
+
cadence, and some localizers fall silent when the robot stops — AMCL updates
|
|
372
|
+
on motion — so without a floor a stationary robot would vanish from its
|
|
373
|
+
peers' costmaps while standing in their way. In `pose_source=amcl` a live
|
|
374
|
+
AMCL publisher counts as holding the estimate; a robot whose localizer has
|
|
375
|
+
actually gone still goes `UNOBSERVED` on time. Anything at or below 1 Hz is
|
|
376
|
+
refused, as slower than a consumer's shortest window.
|
|
249
377
|
|
|
250
378
|
Three things it refuses to do quietly:
|
|
251
379
|
|
|
@@ -307,6 +435,13 @@ This adapter still does not *act* on disposal: it owns one key, never disposes
|
|
|
307
435
|
it, and reports going quiet as `UNOBSERVED` — a weaker statement than "this
|
|
308
436
|
robot is gone". Not acting on an event is not the same as not receiving one.
|
|
309
437
|
|
|
438
|
+
**And nothing it writes is disposed for it.** CycloneDDS's default writer QoS
|
|
439
|
+
disposes a writer's instances when the writer goes, so until 0.4.0 a robot
|
|
440
|
+
that crashed reached every reader as `NOT_ALIVE_DISPOSED` — a deliberate
|
|
441
|
+
removal, by §2.14's reading — ten seconds after the crash. Every writer here
|
|
442
|
+
now turns that off, and the same events arrive as `NOT_ALIVE_NO_WRITERS`:
|
|
443
|
+
liveliness lost, which is what happened.
|
|
444
|
+
|
|
310
445
|
## Tests
|
|
311
446
|
|
|
312
447
|
```sh
|
|
@@ -321,8 +456,9 @@ Everything runs with no ROS installed — that is a gate
|
|
|
321
456
|
| document | what is in it |
|
|
322
457
|
|---|---|
|
|
323
458
|
| [`docs/extraction-census.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/extraction-census.md) | what became a parameter, what the library fills, and why |
|
|
459
|
+
| [`docs/awareness-census.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/awareness-census.md) | what becomes an obstacle, when it stops, in which frame — measured, and ruled |
|
|
324
460
|
| [`docs/failability.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/failability.md) | the two claims broken deliberately, with the failures |
|
|
325
|
-
| [`docs/spec-queue.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/spec-queue.md) |
|
|
461
|
+
| [`docs/spec-queue.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/spec-queue.md) | things for the specification rather than worked around |
|
|
326
462
|
| [`spatialdds_idl18/PROVENANCE.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/spatialdds_idl18/PROVENANCE.md) | where the vendored 1.8 bindings came from |
|
|
327
463
|
| [`docs/RELEASING.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/RELEASING.md) | how a version reaches PyPI, and how the artifact is checked against the commit |
|
|
328
464
|
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Make a ROS 2 robot a SpatialDDS participant: its pose published as a
|
|
4
4
|
`spatial.owm` entity on scene-scoped topics, `goto` commands consumed from
|
|
5
|
-
`ModelCommand` and dispatched to nav2,
|
|
6
|
-
|
|
5
|
+
`ModelCommand` and dispatched to nav2, its presence announced, and — when
|
|
6
|
+
asked — the scene's other moving entities put into its own costmap. All on the
|
|
7
|
+
same DDS bus ROS already runs on: one pip install makes a robot seen,
|
|
8
|
+
steerable and aware, including of robots in other ROS domains it shares no
|
|
9
|
+
middleware with.
|
|
7
10
|
|
|
8
11
|
**A type adapter on a shared bus, not a protocol gateway.** Nothing is
|
|
9
12
|
relayed. ROS and SpatialDDS meet on the wire because both are DDS, so this is
|
|
@@ -30,7 +33,7 @@ The split runs through the whole package:
|
|
|
30
33
|
|
|
31
34
|
Only `spatialdds_ros.robot_node` requires a ROS 2 environment. It imports
|
|
32
35
|
`rclpy` inside its entry point rather than at module scope, which is what
|
|
33
|
-
lets the other
|
|
36
|
+
lets the other thirteen modules — and the entire test suite — run on a machine
|
|
34
37
|
that has never had ROS installed.
|
|
35
38
|
|
|
36
39
|
**That claim is a gate, not a promise.**
|
|
@@ -97,6 +100,117 @@ static and identity, which is a simulator and not a localizing robot.
|
|
|
97
100
|
deferred** — there is no released binary package; install from source or with
|
|
98
101
|
pip.
|
|
99
102
|
|
|
103
|
+
## A robot that sees the scene
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
ros2 run spatialdds_ros robot_node --ros-args \
|
|
107
|
+
-p scene:=bare-scene \
|
|
108
|
+
-p entity_id:=ent:robot:b \
|
|
109
|
+
-p frame_id:=map/bare-scene \
|
|
110
|
+
-p awareness:=true \
|
|
111
|
+
-p awareness_range_m:=8.0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The node then reads the scene's other moving entities off the bus and
|
|
115
|
+
publishes them where nav2 can plan around them: `~/peers/cloud` marks them,
|
|
116
|
+
`~/peers/clearing` is a ring at `awareness_range_m` that clears whatever has
|
|
117
|
+
gone, and `~/peers/markers` shows an operator what is being avoided. nav2
|
|
118
|
+
needs one obstacle layer reading both clouds:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
peers_layer:
|
|
122
|
+
plugin: "nav2_costmap_2d::ObstacleLayer"
|
|
123
|
+
enabled: true
|
|
124
|
+
observation_sources: peers clearing
|
|
125
|
+
peers:
|
|
126
|
+
topic: /robot_b/peers/cloud
|
|
127
|
+
data_type: PointCloud2
|
|
128
|
+
marking: true
|
|
129
|
+
clearing: false
|
|
130
|
+
obstacle_max_range: 8.0
|
|
131
|
+
raytrace_max_range: 8.5
|
|
132
|
+
min_obstacle_height: 0.0
|
|
133
|
+
max_obstacle_height: 2.0
|
|
134
|
+
clearing:
|
|
135
|
+
topic: /robot_b/peers/clearing
|
|
136
|
+
data_type: PointCloud2
|
|
137
|
+
marking: false
|
|
138
|
+
clearing: true
|
|
139
|
+
obstacle_max_range: 8.0
|
|
140
|
+
raytrace_max_range: 8.5
|
|
141
|
+
min_obstacle_height: 0.0
|
|
142
|
+
max_obstacle_height: 2.0
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Add `peers_layer` to the costmap's `plugins`, before the inflation layer, in
|
|
146
|
+
every costmap that should avoid peers — the global one, for the planner to
|
|
147
|
+
route around them. The topics carry the node's name (`robot_b` here).
|
|
148
|
+
|
|
149
|
+
**Set `awareness_range_m` to your costmap's obstacle range or less** — both
|
|
150
|
+
`obstacle_max_range` and `raytrace_max_range` above. The node refuses to start
|
|
151
|
+
with awareness on and no range, because the right value lives in your nav2
|
|
152
|
+
YAML, which it cannot read, and a guess beyond your raytrace range draws marks
|
|
153
|
+
nothing will ever clear.
|
|
154
|
+
|
|
155
|
+
**Why two clouds.** A marking cloud that empties raytraces nothing, so the
|
|
156
|
+
last place a peer was seen stays lethal for good. Measured, with the ring
|
|
157
|
+
beside it and without, in
|
|
158
|
+
[`docs/awareness-census.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/awareness-census.md)
|
|
159
|
+
§1.
|
|
160
|
+
|
|
161
|
+
### What is drawn, and when it stops
|
|
162
|
+
|
|
163
|
+
- **Moving things only:** `FAST`, `ACTIVE`, with a pose, within range, and of
|
|
164
|
+
an `awareness_types` URI if you set any. A declared region is not a peer;
|
|
165
|
+
the keep-out filter is the mechanism for those.
|
|
166
|
+
- **Never this robot.** Its own record does reach its own reader, so this is
|
|
167
|
+
a filter that does work, on its own `entity_id`.
|
|
168
|
+
- **Its own size, or `peer_radius_m`** (0.3 m) for a peer that publishes no
|
|
169
|
+
extent — which includes every robot this library publishes.
|
|
170
|
+
- **Dropped when it goes quiet, and remembered.** A peer is drawn while its
|
|
171
|
+
poses keep arriving, within a window learned from its own cadence — at
|
|
172
|
+
least one second, three times its interval, measured on this robot's clock.
|
|
173
|
+
The record is kept, so the next pose brings it back with no rediscovery. An
|
|
174
|
+
`UNOBSERVED`, retired or disposed record, or a writer that has gone, drops
|
|
175
|
+
it at once.
|
|
176
|
+
- **Never at a guessed position.** A peer in another frame is placed through
|
|
177
|
+
both frames' georeferences; with either one missing it is skipped, and the
|
|
178
|
+
log says which.
|
|
179
|
+
|
|
180
|
+
### Two robots, two ROS domains, one bus
|
|
181
|
+
|
|
182
|
+
The walkthrough is the acceptance test. Robot A runs in `ROS_DOMAIN_ID` 10,
|
|
183
|
+
localized by an AMCL that converges at (3, 0) and then says nothing; robot B
|
|
184
|
+
runs in 20 at the origin with nav2's planner; the SpatialDDS bus is DDS domain
|
|
185
|
+
30. Nothing else connects them.
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
docker build -t spatialdds-ros-nav2 -f test/ros/Dockerfile .
|
|
189
|
+
python3 -m pytest test/test_acceptance_two_domains.py -q -s
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
A absent: path 6.00 m, max |y| 0.000; cloud empty
|
|
194
|
+
A present: cloud 109 points near (3, 0), cost 100; path 6.30 m, max |y| 0.84
|
|
195
|
+
A still for 20 s: drawn in all 31 samples
|
|
196
|
+
quiet but latched: not drawing ent:robot:a: UNOBSERVED: no pose from svc:robot:a for 1.6s
|
|
197
|
+
killed: cost at A back to 0 within 2.8s (probe overhead included)
|
|
198
|
+
gateway served and validated 2 entities: ['ent:robot:a', 'ent:robot:b']
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
B's plan to (6, 0) bends around A, and straightens again once A is gone. Both
|
|
202
|
+
ROS graphs are listed from one container, and neither holds a node or topic of
|
|
203
|
+
the other: what separates them is the domain id, not the network.
|
|
204
|
+
|
|
205
|
+
**What it does not do.** A peer beyond the range is not drawn. A mark left
|
|
206
|
+
behind when this robot drives away from it stays until the robot comes back
|
|
207
|
+
within range, which is how nav2 treats anything a sensor saw. A peer whose
|
|
208
|
+
publisher sends a pose only when it moves will drop out when it stops — every
|
|
209
|
+
`spatialdds_ros` robot sends at least `pose_floor_hz` for exactly this reason,
|
|
210
|
+
but the wire gives a consumer no way to know what a foreign one promises
|
|
211
|
+
([`docs/spec-queue.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/spec-queue.md)
|
|
212
|
+
§3).
|
|
213
|
+
|
|
100
214
|
## Configuration
|
|
101
215
|
|
|
102
216
|
Three parameters have no default and the node refuses to start without them,
|
|
@@ -110,7 +224,9 @@ because every possible default would be a false statement about the world:
|
|
|
110
224
|
|
|
111
225
|
Everything else has one: `domain` 0, `ground_z` 0.0, `pose_source` tf,
|
|
112
226
|
`nav2_action` navigate_to_pose, `source_id` derived from `entity_id`,
|
|
113
|
-
`content_refs` and `external_refs` empty.
|
|
227
|
+
`content_refs` and `external_refs` empty, `pose_floor_hz` 2.0, `awareness`
|
|
228
|
+
off, `peer_radius_m` 0.3, `awareness_types` empty. One more has none, and is
|
|
229
|
+
needed only with awareness on: `awareness_range_m`, for the reason above.
|
|
114
230
|
|
|
115
231
|
### What the robot *is*, and what it is *also known as*
|
|
116
232
|
|
|
@@ -198,8 +314,20 @@ not neutral when the thing being defaulted is a claim.
|
|
|
198
314
|
The adapter publishes `basis = OBSERVED` and offers no parameter to change it:
|
|
199
315
|
a self-localizing robot is reporting where it has worked out that it is, which
|
|
200
316
|
is a sighting rather than an assertion. It claims no extent — a robot locates
|
|
201
|
-
itself,
|
|
202
|
-
keep-out by
|
|
317
|
+
itself, and its footprint is not a region anybody should plan by. (Until 0.4.0
|
|
318
|
+
this said an extent would be read as keep-out by the `DECLARED` rule; the
|
|
319
|
+
robot is `OBSERVED`, so that rule excludes it before it looks at extent. The
|
|
320
|
+
conclusion stands; the reason was wrong. Publishing a true footprint is a
|
|
321
|
+
follow-up.)
|
|
322
|
+
|
|
323
|
+
**Its pose goes out at least `pose_floor_hz` (2 Hz), moving or not.** A
|
|
324
|
+
consumer stops drawing a peer it has not heard from within that peer's
|
|
325
|
+
cadence, and some localizers fall silent when the robot stops — AMCL updates
|
|
326
|
+
on motion — so without a floor a stationary robot would vanish from its
|
|
327
|
+
peers' costmaps while standing in their way. In `pose_source=amcl` a live
|
|
328
|
+
AMCL publisher counts as holding the estimate; a robot whose localizer has
|
|
329
|
+
actually gone still goes `UNOBSERVED` on time. Anything at or below 1 Hz is
|
|
330
|
+
refused, as slower than a consumer's shortest window.
|
|
203
331
|
|
|
204
332
|
Three things it refuses to do quietly:
|
|
205
333
|
|
|
@@ -261,6 +389,13 @@ This adapter still does not *act* on disposal: it owns one key, never disposes
|
|
|
261
389
|
it, and reports going quiet as `UNOBSERVED` — a weaker statement than "this
|
|
262
390
|
robot is gone". Not acting on an event is not the same as not receiving one.
|
|
263
391
|
|
|
392
|
+
**And nothing it writes is disposed for it.** CycloneDDS's default writer QoS
|
|
393
|
+
disposes a writer's instances when the writer goes, so until 0.4.0 a robot
|
|
394
|
+
that crashed reached every reader as `NOT_ALIVE_DISPOSED` — a deliberate
|
|
395
|
+
removal, by §2.14's reading — ten seconds after the crash. Every writer here
|
|
396
|
+
now turns that off, and the same events arrive as `NOT_ALIVE_NO_WRITERS`:
|
|
397
|
+
liveliness lost, which is what happened.
|
|
398
|
+
|
|
264
399
|
## Tests
|
|
265
400
|
|
|
266
401
|
```sh
|
|
@@ -275,8 +410,9 @@ Everything runs with no ROS installed — that is a gate
|
|
|
275
410
|
| document | what is in it |
|
|
276
411
|
|---|---|
|
|
277
412
|
| [`docs/extraction-census.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/extraction-census.md) | what became a parameter, what the library fills, and why |
|
|
413
|
+
| [`docs/awareness-census.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/awareness-census.md) | what becomes an obstacle, when it stops, in which frame — measured, and ruled |
|
|
278
414
|
| [`docs/failability.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/failability.md) | the two claims broken deliberately, with the failures |
|
|
279
|
-
| [`docs/spec-queue.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/spec-queue.md) |
|
|
415
|
+
| [`docs/spec-queue.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/spec-queue.md) | things for the specification rather than worked around |
|
|
280
416
|
| [`spatialdds_idl18/PROVENANCE.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/spatialdds_idl18/PROVENANCE.md) | where the vendored 1.8 bindings came from |
|
|
281
417
|
| [`docs/RELEASING.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/RELEASING.md) | how a version reaches PyPI, and how the artifact is checked against the commit |
|
|
282
418
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://relaxng.org/ns/structure/1.0"?>
|
|
3
3
|
<package format="3">
|
|
4
4
|
<name>spatialdds_ros</name>
|
|
5
|
-
<version>0.
|
|
5
|
+
<version>0.4.0</version>
|
|
6
6
|
<description>
|
|
7
7
|
Makes a ROS 2 robot a SpatialDDS participant: its pose published as a
|
|
8
8
|
spatial.owm entity on scene-scoped topics, goto commands consumed from
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
<exec_depend>nav_msgs</exec_depend>
|
|
21
21
|
<exec_depend>nav2_msgs</exec_depend>
|
|
22
22
|
<exec_depend>tf2_ros</exec_depend>
|
|
23
|
+
<exec_depend>sensor_msgs</exec_depend>
|
|
24
|
+
<exec_depend>visualization_msgs</exec_depend>
|
|
23
25
|
|
|
24
26
|
<test_depend>python3-pytest</test_depend>
|
|
25
27
|
|
|
@@ -42,7 +42,7 @@ LONG_DESCRIPTION = (HERE / "README.md").read_text(encoding="utf-8")
|
|
|
42
42
|
|
|
43
43
|
setup(
|
|
44
44
|
name="spatialdds-ros",
|
|
45
|
-
version="0.
|
|
45
|
+
version="0.4.0",
|
|
46
46
|
# `spatialdds_idl18` ships alongside: the generated bindings annotate
|
|
47
47
|
# nested members with their own fully-qualified names, so the package has
|
|
48
48
|
# to be importable as `spatialdds_idl18` and cannot be nested inside this
|
|
@@ -30,6 +30,7 @@ statement:
|
|
|
30
30
|
"""
|
|
31
31
|
from __future__ import annotations
|
|
32
32
|
|
|
33
|
+
import math
|
|
33
34
|
import time
|
|
34
35
|
from typing import List, Optional, Protocol, Tuple
|
|
35
36
|
|
|
@@ -90,6 +91,7 @@ class RobotAdapter:
|
|
|
90
91
|
self._goal: Optional[Tuple[float, float]] = None
|
|
91
92
|
self._silent = False
|
|
92
93
|
self._last_seen = 0.0
|
|
94
|
+
self._last_pose_write = 0.0
|
|
93
95
|
self._navigator = navigator
|
|
94
96
|
# **The last thing that happened to a goal, held until something
|
|
95
97
|
# changes it.** A refusal published once and then superseded by the
|
|
@@ -120,9 +122,12 @@ class RobotAdapter:
|
|
|
120
122
|
frame_ref=robot_frame(self.config.frame_id),
|
|
121
123
|
has_pose=True,
|
|
122
124
|
pose=pose,
|
|
123
|
-
# A robot locates itself;
|
|
124
|
-
#
|
|
125
|
-
# DECLARED rule
|
|
125
|
+
# A robot locates itself; its footprint is not a region anybody
|
|
126
|
+
# should plan by. (This once said an extent would be read as
|
|
127
|
+
# keep-out by the DECLARED rule; the robot is OBSERVED, and that
|
|
128
|
+
# rule excludes it on basis first — awareness census §2.) A true
|
|
129
|
+
# footprint, for consumers that draw peers by size, is a filed
|
|
130
|
+
# follow-up rather than something to guess here.
|
|
126
131
|
has_extent=False,
|
|
127
132
|
extent=Aabb3(min_xyz=[0.0, 0.0, 0.0], max_xyz=[0.0, 0.0, 0.0]),
|
|
128
133
|
properties=properties,
|
|
@@ -173,10 +178,7 @@ class RobotAdapter:
|
|
|
173
178
|
self._last_seen = now
|
|
174
179
|
self._silent = False
|
|
175
180
|
|
|
176
|
-
self.
|
|
177
|
-
entity_id=self.config.entity_id, pose=pose,
|
|
178
|
-
source_id=self.config.effective_source_id,
|
|
179
|
-
stamp=self._entity.stamp))
|
|
181
|
+
self._write_pose(pose, now, self._entity.stamp)
|
|
180
182
|
|
|
181
183
|
# The expensive record goes on the cadence — and unconditionally when
|
|
182
184
|
# the robot first appears or comes back, because those are changes of
|
|
@@ -265,6 +267,29 @@ class RobotAdapter:
|
|
|
265
267
|
notes.append(note)
|
|
266
268
|
return notes
|
|
267
269
|
|
|
270
|
+
def _write_pose(self, pose: PoseSE3, now: float, stamp=None) -> None:
|
|
271
|
+
# A floor republish is stamped when it is sent: it says where the
|
|
272
|
+
# robot is as of now, which is true for as long as the robot is not
|
|
273
|
+
# silent — and a silent robot gets no floor.
|
|
274
|
+
self._poses.write(ModelPose(
|
|
275
|
+
entity_id=self.config.entity_id, pose=pose,
|
|
276
|
+
source_id=self.config.effective_source_id,
|
|
277
|
+
stamp=stamp if stamp is not None else now_time()))
|
|
278
|
+
self._last_pose_write = now
|
|
279
|
+
|
|
280
|
+
def holding(self, now: Optional[float] = None) -> None:
|
|
281
|
+
"""
|
|
282
|
+
The pose source is alive and its estimate has not changed.
|
|
283
|
+
|
|
284
|
+
For a source that only speaks when the robot moves — AMCL publishes
|
|
285
|
+
on filter updates — silence is not evidence of anything going wrong,
|
|
286
|
+
so the caller says what it knows: the localizer is still running.
|
|
287
|
+
That keeps the robot ACTIVE without inventing a new observation; the
|
|
288
|
+
floor in `tick` then keeps it fresh on the bus.
|
|
289
|
+
"""
|
|
290
|
+
if self._entity is not None and not self._silent:
|
|
291
|
+
self._last_seen = time.time() if now is None else now
|
|
292
|
+
|
|
268
293
|
def tick(self, now: Optional[float] = None) -> Optional[str]:
|
|
269
294
|
"""
|
|
270
295
|
Housekeeping between poses: flush the latch when the robot stops
|
|
@@ -286,6 +311,14 @@ class RobotAdapter:
|
|
|
286
311
|
self._tempo.forget(self.config.entity_id)
|
|
287
312
|
return f"unobserved after {quiet_for:.1f}s of silence"
|
|
288
313
|
|
|
314
|
+
# The floor: the last pose again, freshly stamped, when nothing newer
|
|
315
|
+
# has gone out for a floor interval. Republishing is not a new
|
|
316
|
+
# sighting, so it touches neither the silence clock nor the latch
|
|
317
|
+
# cadence — a robot gone quiet still goes UNOBSERVED on time.
|
|
318
|
+
if (not self._silent
|
|
319
|
+
and now - self._last_pose_write >= 1.0 / self.config.pose_floor_hz):
|
|
320
|
+
self._write_pose(self._entity.pose, now)
|
|
321
|
+
|
|
289
322
|
if not self._silent and self._tempo.idle(now):
|
|
290
323
|
self._entities.write(self._entity)
|
|
291
324
|
return "latch flushed — it stopped moving"
|
|
@@ -301,6 +334,16 @@ class RobotAdapter:
|
|
|
301
334
|
def clear_goal(self) -> None:
|
|
302
335
|
self._goal = None
|
|
303
336
|
|
|
337
|
+
@property
|
|
338
|
+
def pose(self) -> Optional[Tuple[float, float, float]]:
|
|
339
|
+
"""The last published (x, y, yaw) in this robot's frame, or `None`."""
|
|
340
|
+
if self._entity is None:
|
|
341
|
+
return None
|
|
342
|
+
q = self._entity.pose.q
|
|
343
|
+
yaw = math.atan2(2.0 * (q[3] * q[2] + q[0] * q[1]),
|
|
344
|
+
1.0 - 2.0 * (q[1] * q[1] + q[2] * q[2]))
|
|
345
|
+
return self._entity.pose.t[0], self._entity.pose.t[1], yaw
|
|
346
|
+
|
|
304
347
|
@property
|
|
305
348
|
def state(self) -> str:
|
|
306
349
|
return "UNOBSERVED" if self._silent else "ACTIVE"
|
|
@@ -218,13 +218,32 @@ class Announcer:
|
|
|
218
218
|
self._writer = tt.make_writer(
|
|
219
219
|
participant, ANNOUNCE_TOPIC, Announce, MODEL_LATCHED.name,
|
|
220
220
|
lifespan_sec=float(ttl_sec))
|
|
221
|
+
self._last: Optional[Announce] = None
|
|
221
222
|
|
|
222
223
|
def publish(self) -> str:
|
|
223
224
|
announce = announce_for(self.config, self.kind, self.ttl_sec)
|
|
224
225
|
self._writer.write(announce)
|
|
226
|
+
self._last = announce
|
|
225
227
|
return (f"announce: {announce.service_id} as {announce.kind.name}, "
|
|
226
228
|
f"{len(announce.topics)} topics, ttl {announce.ttl_sec}s")
|
|
227
229
|
|
|
230
|
+
def depart(self) -> Optional[str]:
|
|
231
|
+
"""
|
|
232
|
+
Leave the discovery lane on purpose: dispose the announce.
|
|
233
|
+
|
|
234
|
+
§3.3's Announce lifecycle: "A node leaving the bus gracefully MUST
|
|
235
|
+
dispose its Announce instance". Until 0.4.0 this happened only by
|
|
236
|
+
accident — CycloneDDS disposed every instance of a deleted writer —
|
|
237
|
+
and that same default made a *crash* look like a departure. With it
|
|
238
|
+
off (`qos.py`), a graceful exit has to say so, and this is where. An
|
|
239
|
+
ungraceful one is left to the ttl, which is the spec's backstop.
|
|
240
|
+
`Depart` (a SHOULD, for non-DDS transports) is not published.
|
|
241
|
+
"""
|
|
242
|
+
if self._last is None:
|
|
243
|
+
return None
|
|
244
|
+
self._writer.dispose(self._last)
|
|
245
|
+
return f"departed: disposed the announce for {self._last.service_id}"
|
|
246
|
+
|
|
228
247
|
@property
|
|
229
248
|
def refresh_interval_s(self) -> float:
|
|
230
249
|
"""Comfortably inside the TTL, so one missed refresh is not an outage."""
|