spatialdds-ros 0.3.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/LICENSE +21 -0
- spatialdds_ros-0.3.0/PKG-INFO +331 -0
- spatialdds_ros-0.3.0/README.md +285 -0
- spatialdds_ros-0.3.0/package.xml +29 -0
- spatialdds_ros-0.3.0/resource/spatialdds_ros +0 -0
- spatialdds_ros-0.3.0/setup.cfg +10 -0
- spatialdds_ros-0.3.0/setup.py +111 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/PROVENANCE.md +66 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/__init__.py +9 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/_field_aliases.py +26 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/builtin/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/builtin/_types.py +28 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/__init__.py +33 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/anchors/__init__.py +20 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/anchors/_anchors.py +102 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/argeo/__init__.py +22 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/argeo/_argeo.py +97 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/common/__init__.py +27 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/common/_types.py +104 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/core/GnssService/__init__.py +17 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/core/GnssService/_core.py +27 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/core/__init__.py +40 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/core/_core.py +338 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/disco/__init__.py +34 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/disco/_discovery.py +252 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/__init__.py +29 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/_events.py +164 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/__init__.py +19 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/crossing_direction_enum/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/crossing_direction_enum/_events.py +25 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/event_state_enum/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/event_state_enum/_events.py +26 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/event_type_enum/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/event_type_enum/_events.py +36 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/severity_enum/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/severity_enum/_events.py +26 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/zone_kind_enum/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/events/enums/zone_kind_enum/_events.py +31 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/__init__.py +30 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/_mapping.py +168 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/__init__.py +19 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/alignment_method/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/alignment_method/_mapping.py +31 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/edge_type/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/edge_type/_mapping.py +34 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/map_event_kind/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/map_event_kind/_mapping.py +29 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/map_kind/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/map_kind/_mapping.py +32 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/map_status/__init__.py +11 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/mapping/enums/map_status/_mapping.py +27 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/owm/__init__.py +22 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/owm/_owm.py +103 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/semantics/__init__.py +22 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/semantics/_semantics.py +174 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/__init__.py +21 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/common/__init__.py +34 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/common/_common.py +192 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/lidar/__init__.py +32 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/lidar/_lidar.py +135 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/rad/__init__.py +33 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/rad/_rad.py +191 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/radio/__init__.py +22 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/radio/_radio.py +141 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/rf_beam/__init__.py +25 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/rf_beam/_rf_beam.py +117 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/vision/__init__.py +36 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/sensing/vision/_vision.py +170 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/slam_frontend/__init__.py +22 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/slam_frontend/_slam_frontend.py +144 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/vio/__init__.py +20 -0
- spatialdds_ros-0.3.0/spatialdds_idl18/spatial/vio/_vio.py +134 -0
- spatialdds_ros-0.3.0/spatialdds_ros/__init__.py +6 -0
- spatialdds_ros-0.3.0/spatialdds_ros/adapter.py +336 -0
- spatialdds_ros-0.3.0/spatialdds_ros/announce.py +231 -0
- spatialdds_ros-0.3.0/spatialdds_ros/config.py +293 -0
- spatialdds_ros-0.3.0/spatialdds_ros/frames.py +219 -0
- spatialdds_ros-0.3.0/spatialdds_ros/kinematic.py +140 -0
- spatialdds_ros-0.3.0/spatialdds_ros/qos.py +101 -0
- spatialdds_ros-0.3.0/spatialdds_ros/recording.py +167 -0
- spatialdds_ros-0.3.0/spatialdds_ros/robot_node.py +287 -0
- spatialdds_ros-0.3.0/spatialdds_ros/standalone.py +221 -0
- spatialdds_ros-0.3.0/spatialdds_ros/tempo.py +114 -0
- spatialdds_ros-0.3.0/spatialdds_ros/topics.py +64 -0
- spatialdds_ros-0.3.0/spatialdds_ros/transport.py +170 -0
- spatialdds_ros-0.3.0/spatialdds_ros.egg-info/PKG-INFO +331 -0
- spatialdds_ros-0.3.0/spatialdds_ros.egg-info/SOURCES.txt +105 -0
- spatialdds_ros-0.3.0/spatialdds_ros.egg-info/dependency_links.txt +1 -0
- spatialdds_ros-0.3.0/spatialdds_ros.egg-info/entry_points.txt +3 -0
- spatialdds_ros-0.3.0/spatialdds_ros.egg-info/requires.txt +4 -0
- spatialdds_ros-0.3.0/spatialdds_ros.egg-info/top_level.txt +2 -0
- spatialdds_ros-0.3.0/spatialdds_ros.egg-info/zip-safe +1 -0
- spatialdds_ros-0.3.0/test/test_acceptance_bare_scene.py +246 -0
- spatialdds_ros-0.3.0/test/test_acceptance_mcap_gateway.py +295 -0
- spatialdds_ros-0.3.0/test/test_acceptance_web_to_robot.py +221 -0
- spatialdds_ros-0.3.0/test/test_adapter.py +578 -0
- spatialdds_ros-0.3.0/test/test_config.py +266 -0
- spatialdds_ros-0.3.0/test/test_frames.py +202 -0
- spatialdds_ros-0.3.0/test/test_generated_types.py +131 -0
- spatialdds_ros-0.3.0/test/test_kinematic.py +160 -0
- spatialdds_ros-0.3.0/test/test_no_ros_needed.py +156 -0
- spatialdds_ros-0.3.0/test/test_packaging.py +252 -0
- spatialdds_ros-0.3.0/test/test_qos_and_tempo.py +149 -0
- spatialdds_ros-0.3.0/test/test_readme.py +252 -0
- spatialdds_ros-0.3.0/test/test_transport.py +445 -0
- spatialdds_ros-0.3.0/test/test_types_resolve_at_definitions.py +168 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open AR Cloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spatialdds-ros
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A ROS 2 robot as a SpatialDDS participant
|
|
5
|
+
Home-page: https://github.com/OpenArCloud/spatialdds-ros
|
|
6
|
+
Author: Open AR Cloud
|
|
7
|
+
Maintainer: James Jackson
|
|
8
|
+
Maintainer-email: fiberhog@gmail.com
|
|
9
|
+
License: MIT
|
|
10
|
+
Project-URL: Source, https://github.com/OpenArCloud/spatialdds-ros
|
|
11
|
+
Project-URL: Issues, https://github.com/OpenArCloud/spatialdds-ros/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/OpenArCloud/spatialdds-ros/releases
|
|
13
|
+
Keywords: spatialdds,ros2,dds,robotics,nav2,digital-twin,spatial-computing
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: cyclonedds>=0.10
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == "test"
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: keywords
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: maintainer
|
|
40
|
+
Dynamic: maintainer-email
|
|
41
|
+
Dynamic: project-url
|
|
42
|
+
Dynamic: provides-extra
|
|
43
|
+
Dynamic: requires-dist
|
|
44
|
+
Dynamic: requires-python
|
|
45
|
+
Dynamic: summary
|
|
46
|
+
|
|
47
|
+
# spatialdds_ros
|
|
48
|
+
|
|
49
|
+
Make a ROS 2 robot a SpatialDDS participant: its pose published as a
|
|
50
|
+
`spatial.owm` entity on scene-scoped topics, `goto` commands consumed from
|
|
51
|
+
`ModelCommand` and dispatched to nav2, and its presence announced — all on the
|
|
52
|
+
same DDS bus ROS already runs on.
|
|
53
|
+
|
|
54
|
+
**A type adapter on a shared bus, not a protocol gateway.** Nothing is
|
|
55
|
+
relayed. ROS and SpatialDDS meet on the wire because both are DDS, so this is
|
|
56
|
+
one process holding both stacks, publishing the model as model and reading ROS
|
|
57
|
+
as ROS.
|
|
58
|
+
|
|
59
|
+
## Installing, and what you are not installing
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
pip install spatialdds-ros
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That gets `cyclonedds` and nothing else. **No ROS.** The library puts a robot
|
|
66
|
+
on a SpatialDDS bus, reads commands off it and drives a navigator, and none of
|
|
67
|
+
that needs a ROS install — so `rclpy` is not a dependency, and the absence is
|
|
68
|
+
deliberate rather than an omission waiting to be tidied up.
|
|
69
|
+
|
|
70
|
+
The split runs through the whole package:
|
|
71
|
+
|
|
72
|
+
| dependency | declared in | what it is for |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `cyclonedds` | `install_requires` | the bus. Everything here needs it |
|
|
75
|
+
| `rclpy`, `tf2_ros`, `nav2_msgs` | `package.xml` | the ROS node alone, resolved by `rosdep`, never by pip |
|
|
76
|
+
|
|
77
|
+
Only `spatialdds_ros.robot_node` requires a ROS 2 environment. It imports
|
|
78
|
+
`rclpy` inside its entry point rather than at module scope, which is what
|
|
79
|
+
lets the other twelve modules — and the entire test suite — run on a machine
|
|
80
|
+
that has never had ROS installed.
|
|
81
|
+
|
|
82
|
+
**That claim is a gate, not a promise.**
|
|
83
|
+
[`test/test_no_ros_needed.py`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/test/test_no_ros_needed.py)
|
|
84
|
+
walks every module reachable from `import spatialdds_ros` and fails on a
|
|
85
|
+
module-scope `import rclpy` anywhere in it. By source inspection, so it
|
|
86
|
+
catches the violation on a ROS workstation where the import would have
|
|
87
|
+
succeeded — a test that only checked "the import worked" would pass there and
|
|
88
|
+
fail in CI. If the split ever stops being real, that is where it is reported.
|
|
89
|
+
|
|
90
|
+
## A robot on the bus, with no robot
|
|
91
|
+
|
|
92
|
+
No ROS, no nav2, no hardware:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
pip install -e .
|
|
96
|
+
python3 -m spatialdds_ros.standalone \
|
|
97
|
+
--scene bare-scene \
|
|
98
|
+
--entity-id ent:robot:r1 \
|
|
99
|
+
--frame-id map/bare-scene
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
robot: scene bare-scene, entity ent:robot:r1 in frame map/bare-scene, DDS domain 0
|
|
104
|
+
robot: no anchor configured, so this adapter publishes no FrameTransform: its
|
|
105
|
+
entity and poses are on the bus and complete, and it makes no claim
|
|
106
|
+
about where that frame sits on the earth. Another participant may own
|
|
107
|
+
that claim — a venue publishing its own georeference is the ordinary
|
|
108
|
+
case, and then this is the correct configuration. If nobody publishes
|
|
109
|
+
one, a consumer cannot place this robot on a globe and should say so
|
|
110
|
+
rather than assume a position
|
|
111
|
+
robot: announce: svc:robot:r1 as OTHER, 3 topics, ttl 300s
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It is now on three lanes, and discoverable on a fourth:
|
|
115
|
+
|
|
116
|
+
| lane | topic | QoS |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| what exists | `spatialdds/bare-scene/world/entity/v1` | `MODEL_LATCHED` |
|
|
119
|
+
| where it is | `spatialdds/bare-scene/world/model_pose/v1` | `MODEL_FAST` |
|
|
120
|
+
| what to do | `spatialdds/bare-scene/world/model_command/v1` | `MODEL_COMMAND` |
|
|
121
|
+
| who it is | `spatialdds/discovery/announce/v1` | `MODEL_LATCHED` |
|
|
122
|
+
|
|
123
|
+
Add `--bounds -10,-10,10,10` and it will refuse a goal that leaves the box, so
|
|
124
|
+
the refusal path can be exercised without hardware.
|
|
125
|
+
|
|
126
|
+
## On a real robot
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
ros2 run spatialdds_ros robot_node --ros-args \
|
|
130
|
+
-p scene:=bare-scene \
|
|
131
|
+
-p entity_id:=ent:robot:r1 \
|
|
132
|
+
-p frame_id:=map/bare-scene
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The pose input defaults to the TF chain `map -> base_link`. That default
|
|
136
|
+
matters: it is what nav2 itself consumes, it works whatever the localizer is,
|
|
137
|
+
and it assumes no topic name. `pose_source:=amcl` and `pose_source:=odom` are
|
|
138
|
+
available, and `odom` logs its own consequence — odom drifts against map, so
|
|
139
|
+
publishing it as a georeferenced pose is correct only where `map -> odom` is
|
|
140
|
+
static and identity, which is a simulator and not a localizing robot.
|
|
141
|
+
|
|
142
|
+
`ROS 2 Humble` and `Jazzy` are the named targets. **The ROS buildfarm is
|
|
143
|
+
deferred** — there is no released binary package; install from source or with
|
|
144
|
+
pip.
|
|
145
|
+
|
|
146
|
+
## Configuration
|
|
147
|
+
|
|
148
|
+
Three parameters have no default and the node refuses to start without them,
|
|
149
|
+
because every possible default would be a false statement about the world:
|
|
150
|
+
|
|
151
|
+
| parameter | why there is no default |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `scene` | which world this robot is in. A wrong scene is an empty bus, not an error |
|
|
154
|
+
| `entity_id` | which robot it is. A default collides the moment a second one runs |
|
|
155
|
+
| `frame_id` | which frame its pose is in. A pose without its frame is meaningless |
|
|
156
|
+
|
|
157
|
+
Everything else has one: `domain` 0, `ground_z` 0.0, `pose_source` tf,
|
|
158
|
+
`nav2_action` navigate_to_pose, `source_id` derived from `entity_id`,
|
|
159
|
+
`content_refs` and `external_refs` empty.
|
|
160
|
+
|
|
161
|
+
### What the robot *is*, and what it is *also known as*
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
python3 -m spatialdds_ros.standalone \
|
|
165
|
+
--scene bare-scene --entity-id ent:robot:r1 --frame-id map/bare-scene \
|
|
166
|
+
--content-ref catalog:dcef7c31-1d5f-54aa-9344-899912f9d34a \
|
|
167
|
+
--external-ref wikidata=Q4810574
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`content_refs` is how a consumer finds out what to *draw*. A client that reads
|
|
171
|
+
a `catalog:` reference can resolve the model behind it; without one it has
|
|
172
|
+
nothing to resolve and will render a marker. That is not a bug in the client —
|
|
173
|
+
it is the honest rendering of a robot that has not said what it looks like.
|
|
174
|
+
|
|
175
|
+
`external_refs` is what it is also known as: a wikidata id, an
|
|
176
|
+
asset-management number, a fleet number.
|
|
177
|
+
|
|
178
|
+
**Both are empty unless configured, and empty is a statement.** The adapter
|
|
179
|
+
publishes what the operator told it and nothing else — it invents no reference
|
|
180
|
+
and guesses no asset, because a reference nobody authorised is a claim on
|
|
181
|
+
nobody's authority. An empty sequence says "this robot names no content"; it
|
|
182
|
+
is not an accident of omission, and `test/test_adapter.py` asserts both
|
|
183
|
+
directions.
|
|
184
|
+
|
|
185
|
+
On a ROS robot the same two are string-array parameters, with `external_refs`
|
|
186
|
+
as `key=value` entries:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
ros2 run spatialdds_ros robot_node --ros-args \
|
|
190
|
+
-p scene:=bare-scene -p entity_id:=ent:robot:r1 \
|
|
191
|
+
-p frame_id:=map/bare-scene \
|
|
192
|
+
-p content_refs:='[catalog:dcef7c31-1d5f-54aa-9344-899912f9d34a]' \
|
|
193
|
+
-p external_refs:='[wikidata=Q4810574]'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Property keys
|
|
197
|
+
|
|
198
|
+
The adapter publishes two entity properties, and these names are **the
|
|
199
|
+
convention** rather than this library's private spelling — a consumer should
|
|
200
|
+
be able to read any `spatialdds_ros` robot without diffing two records to
|
|
201
|
+
discover what they are called:
|
|
202
|
+
|
|
203
|
+
| key | meaning |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `label` | the human-facing name, from `label` or the `entity_id`'s tail |
|
|
206
|
+
| `nav.status` | what became of the last goal, held until a new one is accepted |
|
|
207
|
+
|
|
208
|
+
Unprefixed on purpose. An earlier consumer had published `demo.label` and
|
|
209
|
+
`demo.nav_status`, which scoped a general fact to one deployment; a second
|
|
210
|
+
consumer reading that record would have had to know whose demo it was.
|
|
211
|
+
|
|
212
|
+
### The anchor, and what its absence means for a consumer
|
|
213
|
+
|
|
214
|
+
`anchor_lat_deg`, `anchor_lon_deg` and `anchor_height_m` say where the robot's
|
|
215
|
+
frame sits on the earth. Set all three and the node publishes a
|
|
216
|
+
`FrameTransform` from that frame to `earth-fixed`. Set none and it publishes
|
|
217
|
+
none — a supported mode, not a misconfiguration. Set one or two and it refuses
|
|
218
|
+
to start, because a partial anchor is not a weaker claim about where the frame
|
|
219
|
+
is, it is an unusable one.
|
|
220
|
+
|
|
221
|
+
**What a reader of the scene sees in that mode.** The robot's entity and its
|
|
222
|
+
poses are present, complete and moving, and *this adapter* makes no claim
|
|
223
|
+
about where that frame sits on the earth.
|
|
224
|
+
|
|
225
|
+
Whether anybody else does is not something it can see. **A venue publishing
|
|
226
|
+
its own georeference is the ordinary case**, and then anchorless is the
|
|
227
|
+
correct configuration — the scene keeps the single claim and the robot does
|
|
228
|
+
not contradict it. That is how SpatialDDS-demo runs. If nobody publishes one,
|
|
229
|
+
a client that wants to place the robot on a globe cannot, and should say so
|
|
230
|
+
rather than assume a position.
|
|
231
|
+
|
|
232
|
+
The note said "nothing earth-references them" until 2026-10-19, which assumed
|
|
233
|
+
this adapter was the only thing that could. In the first real deployment it
|
|
234
|
+
was not, and anchorless was correct *because* somebody else owned the claim —
|
|
235
|
+
the opposite of the absence the wording described.
|
|
236
|
+
|
|
237
|
+
The degradation is deliberate either way. The alternative, defaulting the
|
|
238
|
+
anchor to the frame origin, would have every unconfigured robot claiming to be
|
|
239
|
+
in the Gulf of Guinea with a pose that looks entirely plausible. A default is
|
|
240
|
+
not neutral when the thing being defaulted is a claim.
|
|
241
|
+
|
|
242
|
+
## What it claims, and what it will not
|
|
243
|
+
|
|
244
|
+
The adapter publishes `basis = OBSERVED` and offers no parameter to change it:
|
|
245
|
+
a self-localizing robot is reporting where it has worked out that it is, which
|
|
246
|
+
is a sighting rather than an assertion. It claims no extent — a robot locates
|
|
247
|
+
itself, it does not legislate a region, and an extent would be read as
|
|
248
|
+
keep-out by anything applying the `DECLARED` rule.
|
|
249
|
+
|
|
250
|
+
Three things it refuses to do quietly:
|
|
251
|
+
|
|
252
|
+
- **It does not dispose its entity when the feed goes quiet.** Disposal says
|
|
253
|
+
the robot is gone; silence only licenses "this is where it was last seen,
|
|
254
|
+
not where it is". The record is republished `UNOBSERVED` with a reason and
|
|
255
|
+
the last pose kept.
|
|
256
|
+
- **It does not accept a goal nothing will act on.** No pose publishing, or
|
|
257
|
+
nav2 not accepting goals, is declined *with the reason* — a declined goal
|
|
258
|
+
must not look like an ignored one.
|
|
259
|
+
- **It does not let an accepted goal fail silently.** Whether a goal is
|
|
260
|
+
drivable can depend on where the robot is when it meets a boundary, so the
|
|
261
|
+
refusal arrives after the acceptance, on the latched record, and stays there
|
|
262
|
+
until a new goal is accepted.
|
|
263
|
+
|
|
264
|
+
QoS and topic names are not configurable. Both are interop contracts whose
|
|
265
|
+
failure mode is silence: a mismatched QoS means endpoints that never meet with
|
|
266
|
+
nothing reporting it, and a hand-written topic name means a robot publishing
|
|
267
|
+
where nobody is listening.
|
|
268
|
+
|
|
269
|
+
### Sharing a participant with another SpatialDDS library
|
|
270
|
+
|
|
271
|
+
The §3.3.3 profiles go on the **endpoints**. The `Topic` objects this library
|
|
272
|
+
creates carry default QoS, and that is deliberate rather than an omission.
|
|
273
|
+
|
|
274
|
+
A topic name is unique within a participant, and its QoS has to agree among
|
|
275
|
+
every live `Topic` object for that name — in either direction, with the
|
|
276
|
+
refusal (`DDS_RETCODE_INCONSISTENT_POLICY`) landing on whichever call came
|
|
277
|
+
second. Putting the profile on the topic therefore made topic QoS a contract
|
|
278
|
+
every *other* participant in the process had to match, and nothing in this
|
|
279
|
+
API said so. Version 0.2.0 did that, and SpatialDDS-demo was refused an entity
|
|
280
|
+
reader on a lane this adapter had already opened — both conventions legal DDS,
|
|
281
|
+
neither legal beside the other.
|
|
282
|
+
|
|
283
|
+
So this library now asks for the least it can: the default, which is what any
|
|
284
|
+
participant that passes no QoS gets, including every ROS and DDS example.
|
|
285
|
+
Nothing leaves the wire — matching is a reader/writer negotiation over
|
|
286
|
+
endpoint QoS, and a topic's QoS enters it only as a default for endpoints that
|
|
287
|
+
ask for one, which these never do.
|
|
288
|
+
|
|
289
|
+
If you run another SpatialDDS stack in the same process and it creates its
|
|
290
|
+
topics with an explicit QoS, it is the one imposing the contract, and the two
|
|
291
|
+
cannot share that name. `test/test_transport.py::TwoConventionsOnOneTopic`
|
|
292
|
+
pins all of it, including a library writer exchanging real samples with a
|
|
293
|
+
reader built the other convention's way.
|
|
294
|
+
|
|
295
|
+
### Reads see the lifecycle
|
|
296
|
+
|
|
297
|
+
`transport.take_samples` returns payloads; `transport.take_with_state` returns
|
|
298
|
+
them alongside instance state for a caller that wants the lifecycle. Both
|
|
299
|
+
matter because CycloneDDS's `take()` does not return payloads only — it also
|
|
300
|
+
yields `InvalidSample` notifications for a dispose or a departed writer, and a
|
|
301
|
+
read that assumes otherwise raises on the first peer that joins a lane and
|
|
302
|
+
leaves. Until 0.2.1 this library made that assumption in one place, which
|
|
303
|
+
reached two call sites, and the first real deployment found it within a
|
|
304
|
+
minute. [`docs/failability.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/failability.md) §3 has the tracebacks.
|
|
305
|
+
|
|
306
|
+
This adapter still does not *act* on disposal: it owns one key, never disposes
|
|
307
|
+
it, and reports going quiet as `UNOBSERVED` — a weaker statement than "this
|
|
308
|
+
robot is gone". Not acting on an event is not the same as not receiving one.
|
|
309
|
+
|
|
310
|
+
## Tests
|
|
311
|
+
|
|
312
|
+
```sh
|
|
313
|
+
python3 -m pytest test/ -q
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Everything runs with no ROS installed — that is a gate
|
|
317
|
+
(`test/test_no_ros_needed.py`), not a habit, since one module-scope
|
|
318
|
+
`import rclpy` would take the whole suite offline. The DDS-backed tests need
|
|
319
|
+
`cyclonedds` and skip with a reason without it.
|
|
320
|
+
|
|
321
|
+
| document | what is in it |
|
|
322
|
+
|---|---|
|
|
323
|
+
| [`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 |
|
|
324
|
+
| [`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) | two things for the specification rather than worked around |
|
|
326
|
+
| [`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
|
+
| [`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
|
+
|
|
329
|
+
## Licence
|
|
330
|
+
|
|
331
|
+
MIT. See [`LICENSE`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/LICENSE).
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# spatialdds_ros
|
|
2
|
+
|
|
3
|
+
Make a ROS 2 robot a SpatialDDS participant: its pose published as a
|
|
4
|
+
`spatial.owm` entity on scene-scoped topics, `goto` commands consumed from
|
|
5
|
+
`ModelCommand` and dispatched to nav2, and its presence announced — all on the
|
|
6
|
+
same DDS bus ROS already runs on.
|
|
7
|
+
|
|
8
|
+
**A type adapter on a shared bus, not a protocol gateway.** Nothing is
|
|
9
|
+
relayed. ROS and SpatialDDS meet on the wire because both are DDS, so this is
|
|
10
|
+
one process holding both stacks, publishing the model as model and reading ROS
|
|
11
|
+
as ROS.
|
|
12
|
+
|
|
13
|
+
## Installing, and what you are not installing
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install spatialdds-ros
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That gets `cyclonedds` and nothing else. **No ROS.** The library puts a robot
|
|
20
|
+
on a SpatialDDS bus, reads commands off it and drives a navigator, and none of
|
|
21
|
+
that needs a ROS install — so `rclpy` is not a dependency, and the absence is
|
|
22
|
+
deliberate rather than an omission waiting to be tidied up.
|
|
23
|
+
|
|
24
|
+
The split runs through the whole package:
|
|
25
|
+
|
|
26
|
+
| dependency | declared in | what it is for |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `cyclonedds` | `install_requires` | the bus. Everything here needs it |
|
|
29
|
+
| `rclpy`, `tf2_ros`, `nav2_msgs` | `package.xml` | the ROS node alone, resolved by `rosdep`, never by pip |
|
|
30
|
+
|
|
31
|
+
Only `spatialdds_ros.robot_node` requires a ROS 2 environment. It imports
|
|
32
|
+
`rclpy` inside its entry point rather than at module scope, which is what
|
|
33
|
+
lets the other twelve modules — and the entire test suite — run on a machine
|
|
34
|
+
that has never had ROS installed.
|
|
35
|
+
|
|
36
|
+
**That claim is a gate, not a promise.**
|
|
37
|
+
[`test/test_no_ros_needed.py`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/test/test_no_ros_needed.py)
|
|
38
|
+
walks every module reachable from `import spatialdds_ros` and fails on a
|
|
39
|
+
module-scope `import rclpy` anywhere in it. By source inspection, so it
|
|
40
|
+
catches the violation on a ROS workstation where the import would have
|
|
41
|
+
succeeded — a test that only checked "the import worked" would pass there and
|
|
42
|
+
fail in CI. If the split ever stops being real, that is where it is reported.
|
|
43
|
+
|
|
44
|
+
## A robot on the bus, with no robot
|
|
45
|
+
|
|
46
|
+
No ROS, no nav2, no hardware:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install -e .
|
|
50
|
+
python3 -m spatialdds_ros.standalone \
|
|
51
|
+
--scene bare-scene \
|
|
52
|
+
--entity-id ent:robot:r1 \
|
|
53
|
+
--frame-id map/bare-scene
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
robot: scene bare-scene, entity ent:robot:r1 in frame map/bare-scene, DDS domain 0
|
|
58
|
+
robot: no anchor configured, so this adapter publishes no FrameTransform: its
|
|
59
|
+
entity and poses are on the bus and complete, and it makes no claim
|
|
60
|
+
about where that frame sits on the earth. Another participant may own
|
|
61
|
+
that claim — a venue publishing its own georeference is the ordinary
|
|
62
|
+
case, and then this is the correct configuration. If nobody publishes
|
|
63
|
+
one, a consumer cannot place this robot on a globe and should say so
|
|
64
|
+
rather than assume a position
|
|
65
|
+
robot: announce: svc:robot:r1 as OTHER, 3 topics, ttl 300s
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It is now on three lanes, and discoverable on a fourth:
|
|
69
|
+
|
|
70
|
+
| lane | topic | QoS |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| what exists | `spatialdds/bare-scene/world/entity/v1` | `MODEL_LATCHED` |
|
|
73
|
+
| where it is | `spatialdds/bare-scene/world/model_pose/v1` | `MODEL_FAST` |
|
|
74
|
+
| what to do | `spatialdds/bare-scene/world/model_command/v1` | `MODEL_COMMAND` |
|
|
75
|
+
| who it is | `spatialdds/discovery/announce/v1` | `MODEL_LATCHED` |
|
|
76
|
+
|
|
77
|
+
Add `--bounds -10,-10,10,10` and it will refuse a goal that leaves the box, so
|
|
78
|
+
the refusal path can be exercised without hardware.
|
|
79
|
+
|
|
80
|
+
## On a real robot
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
ros2 run spatialdds_ros robot_node --ros-args \
|
|
84
|
+
-p scene:=bare-scene \
|
|
85
|
+
-p entity_id:=ent:robot:r1 \
|
|
86
|
+
-p frame_id:=map/bare-scene
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The pose input defaults to the TF chain `map -> base_link`. That default
|
|
90
|
+
matters: it is what nav2 itself consumes, it works whatever the localizer is,
|
|
91
|
+
and it assumes no topic name. `pose_source:=amcl` and `pose_source:=odom` are
|
|
92
|
+
available, and `odom` logs its own consequence — odom drifts against map, so
|
|
93
|
+
publishing it as a georeferenced pose is correct only where `map -> odom` is
|
|
94
|
+
static and identity, which is a simulator and not a localizing robot.
|
|
95
|
+
|
|
96
|
+
`ROS 2 Humble` and `Jazzy` are the named targets. **The ROS buildfarm is
|
|
97
|
+
deferred** — there is no released binary package; install from source or with
|
|
98
|
+
pip.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
Three parameters have no default and the node refuses to start without them,
|
|
103
|
+
because every possible default would be a false statement about the world:
|
|
104
|
+
|
|
105
|
+
| parameter | why there is no default |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `scene` | which world this robot is in. A wrong scene is an empty bus, not an error |
|
|
108
|
+
| `entity_id` | which robot it is. A default collides the moment a second one runs |
|
|
109
|
+
| `frame_id` | which frame its pose is in. A pose without its frame is meaningless |
|
|
110
|
+
|
|
111
|
+
Everything else has one: `domain` 0, `ground_z` 0.0, `pose_source` tf,
|
|
112
|
+
`nav2_action` navigate_to_pose, `source_id` derived from `entity_id`,
|
|
113
|
+
`content_refs` and `external_refs` empty.
|
|
114
|
+
|
|
115
|
+
### What the robot *is*, and what it is *also known as*
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
python3 -m spatialdds_ros.standalone \
|
|
119
|
+
--scene bare-scene --entity-id ent:robot:r1 --frame-id map/bare-scene \
|
|
120
|
+
--content-ref catalog:dcef7c31-1d5f-54aa-9344-899912f9d34a \
|
|
121
|
+
--external-ref wikidata=Q4810574
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`content_refs` is how a consumer finds out what to *draw*. A client that reads
|
|
125
|
+
a `catalog:` reference can resolve the model behind it; without one it has
|
|
126
|
+
nothing to resolve and will render a marker. That is not a bug in the client —
|
|
127
|
+
it is the honest rendering of a robot that has not said what it looks like.
|
|
128
|
+
|
|
129
|
+
`external_refs` is what it is also known as: a wikidata id, an
|
|
130
|
+
asset-management number, a fleet number.
|
|
131
|
+
|
|
132
|
+
**Both are empty unless configured, and empty is a statement.** The adapter
|
|
133
|
+
publishes what the operator told it and nothing else — it invents no reference
|
|
134
|
+
and guesses no asset, because a reference nobody authorised is a claim on
|
|
135
|
+
nobody's authority. An empty sequence says "this robot names no content"; it
|
|
136
|
+
is not an accident of omission, and `test/test_adapter.py` asserts both
|
|
137
|
+
directions.
|
|
138
|
+
|
|
139
|
+
On a ROS robot the same two are string-array parameters, with `external_refs`
|
|
140
|
+
as `key=value` entries:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
ros2 run spatialdds_ros robot_node --ros-args \
|
|
144
|
+
-p scene:=bare-scene -p entity_id:=ent:robot:r1 \
|
|
145
|
+
-p frame_id:=map/bare-scene \
|
|
146
|
+
-p content_refs:='[catalog:dcef7c31-1d5f-54aa-9344-899912f9d34a]' \
|
|
147
|
+
-p external_refs:='[wikidata=Q4810574]'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Property keys
|
|
151
|
+
|
|
152
|
+
The adapter publishes two entity properties, and these names are **the
|
|
153
|
+
convention** rather than this library's private spelling — a consumer should
|
|
154
|
+
be able to read any `spatialdds_ros` robot without diffing two records to
|
|
155
|
+
discover what they are called:
|
|
156
|
+
|
|
157
|
+
| key | meaning |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `label` | the human-facing name, from `label` or the `entity_id`'s tail |
|
|
160
|
+
| `nav.status` | what became of the last goal, held until a new one is accepted |
|
|
161
|
+
|
|
162
|
+
Unprefixed on purpose. An earlier consumer had published `demo.label` and
|
|
163
|
+
`demo.nav_status`, which scoped a general fact to one deployment; a second
|
|
164
|
+
consumer reading that record would have had to know whose demo it was.
|
|
165
|
+
|
|
166
|
+
### The anchor, and what its absence means for a consumer
|
|
167
|
+
|
|
168
|
+
`anchor_lat_deg`, `anchor_lon_deg` and `anchor_height_m` say where the robot's
|
|
169
|
+
frame sits on the earth. Set all three and the node publishes a
|
|
170
|
+
`FrameTransform` from that frame to `earth-fixed`. Set none and it publishes
|
|
171
|
+
none — a supported mode, not a misconfiguration. Set one or two and it refuses
|
|
172
|
+
to start, because a partial anchor is not a weaker claim about where the frame
|
|
173
|
+
is, it is an unusable one.
|
|
174
|
+
|
|
175
|
+
**What a reader of the scene sees in that mode.** The robot's entity and its
|
|
176
|
+
poses are present, complete and moving, and *this adapter* makes no claim
|
|
177
|
+
about where that frame sits on the earth.
|
|
178
|
+
|
|
179
|
+
Whether anybody else does is not something it can see. **A venue publishing
|
|
180
|
+
its own georeference is the ordinary case**, and then anchorless is the
|
|
181
|
+
correct configuration — the scene keeps the single claim and the robot does
|
|
182
|
+
not contradict it. That is how SpatialDDS-demo runs. If nobody publishes one,
|
|
183
|
+
a client that wants to place the robot on a globe cannot, and should say so
|
|
184
|
+
rather than assume a position.
|
|
185
|
+
|
|
186
|
+
The note said "nothing earth-references them" until 2026-10-19, which assumed
|
|
187
|
+
this adapter was the only thing that could. In the first real deployment it
|
|
188
|
+
was not, and anchorless was correct *because* somebody else owned the claim —
|
|
189
|
+
the opposite of the absence the wording described.
|
|
190
|
+
|
|
191
|
+
The degradation is deliberate either way. The alternative, defaulting the
|
|
192
|
+
anchor to the frame origin, would have every unconfigured robot claiming to be
|
|
193
|
+
in the Gulf of Guinea with a pose that looks entirely plausible. A default is
|
|
194
|
+
not neutral when the thing being defaulted is a claim.
|
|
195
|
+
|
|
196
|
+
## What it claims, and what it will not
|
|
197
|
+
|
|
198
|
+
The adapter publishes `basis = OBSERVED` and offers no parameter to change it:
|
|
199
|
+
a self-localizing robot is reporting where it has worked out that it is, which
|
|
200
|
+
is a sighting rather than an assertion. It claims no extent — a robot locates
|
|
201
|
+
itself, it does not legislate a region, and an extent would be read as
|
|
202
|
+
keep-out by anything applying the `DECLARED` rule.
|
|
203
|
+
|
|
204
|
+
Three things it refuses to do quietly:
|
|
205
|
+
|
|
206
|
+
- **It does not dispose its entity when the feed goes quiet.** Disposal says
|
|
207
|
+
the robot is gone; silence only licenses "this is where it was last seen,
|
|
208
|
+
not where it is". The record is republished `UNOBSERVED` with a reason and
|
|
209
|
+
the last pose kept.
|
|
210
|
+
- **It does not accept a goal nothing will act on.** No pose publishing, or
|
|
211
|
+
nav2 not accepting goals, is declined *with the reason* — a declined goal
|
|
212
|
+
must not look like an ignored one.
|
|
213
|
+
- **It does not let an accepted goal fail silently.** Whether a goal is
|
|
214
|
+
drivable can depend on where the robot is when it meets a boundary, so the
|
|
215
|
+
refusal arrives after the acceptance, on the latched record, and stays there
|
|
216
|
+
until a new goal is accepted.
|
|
217
|
+
|
|
218
|
+
QoS and topic names are not configurable. Both are interop contracts whose
|
|
219
|
+
failure mode is silence: a mismatched QoS means endpoints that never meet with
|
|
220
|
+
nothing reporting it, and a hand-written topic name means a robot publishing
|
|
221
|
+
where nobody is listening.
|
|
222
|
+
|
|
223
|
+
### Sharing a participant with another SpatialDDS library
|
|
224
|
+
|
|
225
|
+
The §3.3.3 profiles go on the **endpoints**. The `Topic` objects this library
|
|
226
|
+
creates carry default QoS, and that is deliberate rather than an omission.
|
|
227
|
+
|
|
228
|
+
A topic name is unique within a participant, and its QoS has to agree among
|
|
229
|
+
every live `Topic` object for that name — in either direction, with the
|
|
230
|
+
refusal (`DDS_RETCODE_INCONSISTENT_POLICY`) landing on whichever call came
|
|
231
|
+
second. Putting the profile on the topic therefore made topic QoS a contract
|
|
232
|
+
every *other* participant in the process had to match, and nothing in this
|
|
233
|
+
API said so. Version 0.2.0 did that, and SpatialDDS-demo was refused an entity
|
|
234
|
+
reader on a lane this adapter had already opened — both conventions legal DDS,
|
|
235
|
+
neither legal beside the other.
|
|
236
|
+
|
|
237
|
+
So this library now asks for the least it can: the default, which is what any
|
|
238
|
+
participant that passes no QoS gets, including every ROS and DDS example.
|
|
239
|
+
Nothing leaves the wire — matching is a reader/writer negotiation over
|
|
240
|
+
endpoint QoS, and a topic's QoS enters it only as a default for endpoints that
|
|
241
|
+
ask for one, which these never do.
|
|
242
|
+
|
|
243
|
+
If you run another SpatialDDS stack in the same process and it creates its
|
|
244
|
+
topics with an explicit QoS, it is the one imposing the contract, and the two
|
|
245
|
+
cannot share that name. `test/test_transport.py::TwoConventionsOnOneTopic`
|
|
246
|
+
pins all of it, including a library writer exchanging real samples with a
|
|
247
|
+
reader built the other convention's way.
|
|
248
|
+
|
|
249
|
+
### Reads see the lifecycle
|
|
250
|
+
|
|
251
|
+
`transport.take_samples` returns payloads; `transport.take_with_state` returns
|
|
252
|
+
them alongside instance state for a caller that wants the lifecycle. Both
|
|
253
|
+
matter because CycloneDDS's `take()` does not return payloads only — it also
|
|
254
|
+
yields `InvalidSample` notifications for a dispose or a departed writer, and a
|
|
255
|
+
read that assumes otherwise raises on the first peer that joins a lane and
|
|
256
|
+
leaves. Until 0.2.1 this library made that assumption in one place, which
|
|
257
|
+
reached two call sites, and the first real deployment found it within a
|
|
258
|
+
minute. [`docs/failability.md`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/docs/failability.md) §3 has the tracebacks.
|
|
259
|
+
|
|
260
|
+
This adapter still does not *act* on disposal: it owns one key, never disposes
|
|
261
|
+
it, and reports going quiet as `UNOBSERVED` — a weaker statement than "this
|
|
262
|
+
robot is gone". Not acting on an event is not the same as not receiving one.
|
|
263
|
+
|
|
264
|
+
## Tests
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
python3 -m pytest test/ -q
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Everything runs with no ROS installed — that is a gate
|
|
271
|
+
(`test/test_no_ros_needed.py`), not a habit, since one module-scope
|
|
272
|
+
`import rclpy` would take the whole suite offline. The DDS-backed tests need
|
|
273
|
+
`cyclonedds` and skip with a reason without it.
|
|
274
|
+
|
|
275
|
+
| document | what is in it |
|
|
276
|
+
|---|---|
|
|
277
|
+
| [`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 |
|
|
278
|
+
| [`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) | two things for the specification rather than worked around |
|
|
280
|
+
| [`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
|
+
| [`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
|
+
|
|
283
|
+
## Licence
|
|
284
|
+
|
|
285
|
+
MIT. See [`LICENSE`](https://github.com/OpenArCloud/spatialdds-ros/blob/main/LICENSE).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://relaxng.org/ns/structure/1.0"?>
|
|
3
|
+
<package format="3">
|
|
4
|
+
<name>spatialdds_ros</name>
|
|
5
|
+
<version>0.3.0</version>
|
|
6
|
+
<description>
|
|
7
|
+
Makes a ROS 2 robot a SpatialDDS participant: its pose published as a
|
|
8
|
+
spatial.owm entity on scene-scoped topics, goto commands consumed from
|
|
9
|
+
ModelCommand and dispatched to nav2, and its presence announced — on the
|
|
10
|
+
same DDS bus ROS already runs on. A type adapter on a shared bus, not a
|
|
11
|
+
protocol gateway.
|
|
12
|
+
</description>
|
|
13
|
+
<maintainer email="fiberhog@gmail.com">James Jackson</maintainer>
|
|
14
|
+
<license>MIT</license>
|
|
15
|
+
|
|
16
|
+
<buildtool_depend>ament_python</buildtool_depend>
|
|
17
|
+
|
|
18
|
+
<exec_depend>rclpy</exec_depend>
|
|
19
|
+
<exec_depend>geometry_msgs</exec_depend>
|
|
20
|
+
<exec_depend>nav_msgs</exec_depend>
|
|
21
|
+
<exec_depend>nav2_msgs</exec_depend>
|
|
22
|
+
<exec_depend>tf2_ros</exec_depend>
|
|
23
|
+
|
|
24
|
+
<test_depend>python3-pytest</test_depend>
|
|
25
|
+
|
|
26
|
+
<export>
|
|
27
|
+
<build_type>ament_python</build_type>
|
|
28
|
+
</export>
|
|
29
|
+
</package>
|
|
File without changes
|