rosotacom-dev 2.2__py3-none-any.whl

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.
Files changed (195) hide show
  1. rosotacom/__init__.py +32 -0
  2. rosotacom/__main__.py +8 -0
  3. rosotacom/anonymize.py +360 -0
  4. rosotacom/bag_ground_truth.py +395 -0
  5. rosotacom/benched_set.py +492 -0
  6. rosotacom/benchmark.py +1842 -0
  7. rosotacom/bundle_check.py +517 -0
  8. rosotacom/cli.py +8637 -0
  9. rosotacom/cli_benchmark.py +8211 -0
  10. rosotacom/deployment.py +196 -0
  11. rosotacom/ffmpeg_packet.py +123 -0
  12. rosotacom/forensics.py +1172 -0
  13. rosotacom/geomap.py +848 -0
  14. rosotacom/network_profiles.py +563 -0
  15. rosotacom/network_shaper.py +127 -0
  16. rosotacom/plots.py +609 -0
  17. rosotacom/py.typed +0 -0
  18. rosotacom/resources/benched-set.yaml +317 -0
  19. rosotacom/resources/examples/.gitignore +1 -0
  20. rosotacom/resources/examples/README.md +163 -0
  21. rosotacom/resources/examples/__init__.py +1 -0
  22. rosotacom/resources/examples/bags/8_drop_reference/metadata.yaml +17 -0
  23. rosotacom/resources/examples/deployment.example.yaml +14 -0
  24. rosotacom/resources/examples/profiles.yaml +85 -0
  25. rosotacom/resources/examples/ros2docker.json +33 -0
  26. rosotacom/resources/examples/rosotacom.yaml +12 -0
  27. rosotacom/resources/examples/scenarios/2_native_chatter/scenario-definition.yaml +10 -0
  28. rosotacom/resources/examples/scripts/1_heartbeat/run_machine_a.sh +9 -0
  29. rosotacom/resources/examples/scripts/1_heartbeat/run_machine_b.sh +9 -0
  30. rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.ros2docker.json +21 -0
  31. rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_communication.sh +9 -0
  32. rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_external.py +15 -0
  33. rosotacom/resources/examples/scripts/2_native_chatter/machine_a/wait_for_echo_topic.sh +10 -0
  34. rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.ros2docker.json +19 -0
  35. rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_communication.sh +9 -0
  36. rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_external.py +15 -0
  37. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external.ros2docker.json +12 -0
  38. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external_session.yaml +9 -0
  39. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/foxglove.json +89 -0
  40. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_communication.sh +9 -0
  41. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_external.py +15 -0
  42. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
  43. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/metadata.yaml +44 -0
  44. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/external.ros2docker.json +13 -0
  45. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_communication.sh +9 -0
  46. rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_external.py +15 -0
  47. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external.ros2docker.json +12 -0
  48. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external_session.yaml +9 -0
  49. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/foxglove.json +89 -0
  50. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_communication.sh +9 -0
  51. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_external.py +15 -0
  52. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
  53. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/metadata.yaml +44 -0
  54. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/external.ros2docker.json +13 -0
  55. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_communication.sh +9 -0
  56. rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_external.py +15 -0
  57. rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external.ros2docker.json +20 -0
  58. rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external_session.yaml +22 -0
  59. rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_communication.sh +9 -0
  60. rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_external.py +15 -0
  61. rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external.ros2docker.json +20 -0
  62. rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external_session.yaml +22 -0
  63. rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_communication.sh +9 -0
  64. rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_external.py +15 -0
  65. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external.ros2docker.json +18 -0
  66. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external_session.yaml +22 -0
  67. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_communication.sh +9 -0
  68. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_external.py +15 -0
  69. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external.ros2docker.json +18 -0
  70. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external_session.yaml +22 -0
  71. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_communication.sh +9 -0
  72. rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_external.py +15 -0
  73. rosotacom/resources/examples/sessions/10_restamp/session-definition.yaml +39 -0
  74. rosotacom/resources/examples/sessions/11_trickle/session-definition.yaml +42 -0
  75. rosotacom/resources/examples/sessions/12_content_integrity/session-definition.yaml +33 -0
  76. rosotacom/resources/examples/sessions/13_link_latency/session-definition.yaml +42 -0
  77. rosotacom/resources/examples/sessions/14_remote_assist_anonymized/session-definition.yaml +211 -0
  78. rosotacom/resources/examples/sessions/15_remote_assist_anonymized_costmap/session-definition.yaml +59 -0
  79. rosotacom/resources/examples/sessions/16_remote_assist_anonymized_camera/session-definition.yaml +61 -0
  80. rosotacom/resources/examples/sessions/17_synthetic_camera_quality/session-definition.yaml +57 -0
  81. rosotacom/resources/examples/sessions/1_heartbeat/session-definition.yaml +26 -0
  82. rosotacom/resources/examples/sessions/1_heartbeat_status/session-definition.yaml +37 -0
  83. rosotacom/resources/examples/sessions/2_native_chatter/session-definition.yaml +34 -0
  84. rosotacom/resources/examples/sessions/3_comp_occ_grid/session-definition.yaml +43 -0
  85. rosotacom/resources/examples/sessions/4_comp_occ_grid_zen/session-definition.yaml +43 -0
  86. rosotacom/resources/examples/sessions/5_sized_payload/session-definition.yaml +39 -0
  87. rosotacom/resources/examples/sessions/6_sized_payload_zen/session-definition.yaml +50 -0
  88. rosotacom/resources/examples/sessions/7_latched_static/session-definition.yaml +49 -0
  89. rosotacom/resources/examples/sessions/8_drop/session-definition.yaml +37 -0
  90. rosotacom/resources/examples/sessions/9_throttle/session-definition.yaml +37 -0
  91. rosotacom/resources/examples/sessions/bench_1_1_capacity/session-definition.yaml +42 -0
  92. rosotacom/resources/examples/sessions/bench_1_2_load_sweep/session-definition.yaml +51 -0
  93. rosotacom/resources/examples/sessions/bench_1_3_ramp/session-definition.yaml +41 -0
  94. rosotacom/resources/examples/sessions/bench_1_4_recovery/session-definition.yaml +47 -0
  95. rosotacom/resources/examples/sessions/bench_1_5_camera/session-definition.yaml +45 -0
  96. rosotacom/resources/ros2docker.json.example +33 -0
  97. rosotacom/resources/ws/.vscode/c_cpp_properties.json +20 -0
  98. rosotacom/resources/ws/.vscode/settings.json +8 -0
  99. rosotacom/resources/ws/ota_configs/archive/fastdds_easy_mode.xml.template +32 -0
  100. rosotacom/resources/ws/ota_configs/archive/fastdds_initial_peer_list.xml.template +21 -0
  101. rosotacom/resources/ws/ota_configs/archive/fastdds_v2.xml.template +17 -0
  102. rosotacom/resources/ws/ota_configs/archive/fastdds_v3.xml.template +48 -0
  103. rosotacom/resources/ws/ota_configs/archive/fastdds_v4.xml.template +95 -0
  104. rosotacom/resources/ws/ota_configs/archive/fastdds_v5.xml.template +64 -0
  105. rosotacom/resources/ws/ota_configs/create_zenoh_json5.py +56 -0
  106. rosotacom/resources/ws/ota_configs/cyclonedds_minimal.xml.template +9 -0
  107. rosotacom/resources/ws/ota_configs/cyclonedds_tuned.xml.template +33 -0
  108. rosotacom/resources/ws/ota_configs/fastdds_unicast.xml.template +71 -0
  109. rosotacom/resources/ws/ota_configs/get_ota_xml.py +214 -0
  110. rosotacom/resources/ws/ota_configs/utils.py +74 -0
  111. rosotacom/resources/ws/ota_configs/zenoh.json5.template +32 -0
  112. rosotacom/resources/ws/ros2src/.gitkeep +0 -0
  113. rosotacom/resources/ws/ros2src/com_cpp/CMakeLists.txt +67 -0
  114. rosotacom/resources/ws/ros2src/com_cpp/COLCON_IGNORE +0 -0
  115. rosotacom/resources/ws/ros2src/com_cpp/LICENSE.txt +25 -0
  116. rosotacom/resources/ws/ros2src/com_cpp/package.xml +37 -0
  117. rosotacom/resources/ws/ros2src/com_cpp/src/hz_monitor.cpp +192 -0
  118. rosotacom/resources/ws/ros2src/com_cpp/src/universal_compressor.cpp +283 -0
  119. rosotacom/resources/ws/ros2src/com_cpp/src/universal_decompressor.cpp +306 -0
  120. rosotacom/resources/ws/ros2src/com_msgs/CMakeLists.txt +22 -0
  121. rosotacom/resources/ws/ros2src/com_msgs/LICENSE.txt +25 -0
  122. rosotacom/resources/ws/ros2src/com_msgs/msg/CompressedData.msg +3 -0
  123. rosotacom/resources/ws/ros2src/com_msgs/msg/EchoHeartbeat.msg +10 -0
  124. rosotacom/resources/ws/ros2src/com_msgs/msg/OtaStamped.msg +4 -0
  125. rosotacom/resources/ws/ros2src/com_msgs/msg/SizedPayload.msg +4 -0
  126. rosotacom/resources/ws/ros2src/com_msgs/package.xml +25 -0
  127. rosotacom/resources/ws/ros2src/com_py/LICENSE.txt +25 -0
  128. rosotacom/resources/ws/ros2src/com_py/com_py/__init__.py +0 -0
  129. rosotacom/resources/ws/ros2src/com_py/com_py/base_bridge_relay.py +106 -0
  130. rosotacom/resources/ws/ros2src/com_py/com_py/bridge_in.py +76 -0
  131. rosotacom/resources/ws/ros2src/com_py/com_py/bridge_out.py +70 -0
  132. rosotacom/resources/ws/ros2src/com_py/com_py/heartbeat_echo.py +221 -0
  133. rosotacom/resources/ws/ros2src/com_py/com_py/hz_monitor.py +178 -0
  134. rosotacom/resources/ws/ros2src/com_py/com_py/image_bandwidth_reducer.py +218 -0
  135. rosotacom/resources/ws/ros2src/com_py/com_py/image_pixel_capper.py +370 -0
  136. rosotacom/resources/ws/ros2src/com_py/com_py/lanelet_saver.py +80 -0
  137. rosotacom/resources/ws/ros2src/com_py/com_py/latch_relay.py +205 -0
  138. rosotacom/resources/ws/ros2src/com_py/com_py/legacy/costmap_remapper.py +73 -0
  139. rosotacom/resources/ws/ros2src/com_py/com_py/legacy/marker_remapper.py +93 -0
  140. rosotacom/resources/ws/ros2src/com_py/com_py/legacy/speed_test.py +107 -0
  141. rosotacom/resources/ws/ros2src/com_py/com_py/legacy/tf_remapper.py +156 -0
  142. rosotacom/resources/ws/ros2src/com_py/com_py/legacy/traffic_monitor.py +91 -0
  143. rosotacom/resources/ws/ros2src/com_py/com_py/legacy/wifi_network_analysis_1.py +213 -0
  144. rosotacom/resources/ws/ros2src/com_py/com_py/link_bytes.py +177 -0
  145. rosotacom/resources/ws/ros2src/com_py/com_py/link_trace.py +186 -0
  146. rosotacom/resources/ws/ros2src/com_py/com_py/local_global_frame_bridge.py +465 -0
  147. rosotacom/resources/ws/ros2src/com_py/com_py/localization_map_split_reduce_saver.py +258 -0
  148. rosotacom/resources/ws/ros2src/com_py/com_py/msg_transform.py +277 -0
  149. rosotacom/resources/ws/ros2src/com_py/com_py/pair_management.py +87 -0
  150. rosotacom/resources/ws/ros2src/com_py/com_py/pub_sub_pair.py +145 -0
  151. rosotacom/resources/ws/ros2src/com_py/com_py/qos.py +254 -0
  152. rosotacom/resources/ws/ros2src/com_py/com_py/relay_in.py +78 -0
  153. rosotacom/resources/ws/ros2src/com_py/com_py/relay_out.py +75 -0
  154. rosotacom/resources/ws/ros2src/com_py/com_py/restamp.py +191 -0
  155. rosotacom/resources/ws/ros2src/com_py/com_py/sized_publisher.py +244 -0
  156. rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency.py +64 -0
  157. rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency_core.py +51 -0
  158. rosotacom/resources/ws/ros2src/com_py/com_py/status_overview.py +476 -0
  159. rosotacom/resources/ws/ros2src/com_py/com_py/status_overview_core.py +989 -0
  160. rosotacom/resources/ws/ros2src/com_py/com_py/tf_map_to_cart_saver.py +99 -0
  161. rosotacom/resources/ws/ros2src/com_py/com_py/topic_monitor.py +327 -0
  162. rosotacom/resources/ws/ros2src/com_py/com_py/topic_resolution.py +171 -0
  163. rosotacom/resources/ws/ros2src/com_py/com_py/trickle.py +183 -0
  164. rosotacom/resources/ws/ros2src/com_py/com_py/universal_compressor.py +653 -0
  165. rosotacom/resources/ws/ros2src/com_py/com_py/universal_decompressor.py +372 -0
  166. rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_unwrapper.py +191 -0
  167. rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_wrapper.py +154 -0
  168. rosotacom/resources/ws/ros2src/com_py/package.xml +34 -0
  169. rosotacom/resources/ws/ros2src/com_py/resource/com_py +0 -0
  170. rosotacom/resources/ws/ros2src/com_py/setup.cfg +6 -0
  171. rosotacom/resources/ws/ros2src/com_py/setup.py +55 -0
  172. rosotacom/resources/ws/ros2src/com_py/test/test_copyright.py +25 -0
  173. rosotacom/resources/ws/ros2src/com_py/test/test_flake8.py +25 -0
  174. rosotacom/resources/ws/ros2src/com_py/test/test_pep257.py +23 -0
  175. rosotacom/resources/ws/session/content/base/session_plugin_base.yaml +633 -0
  176. rosotacom/resources/ws/session/content/network/up_connectivity.py +364 -0
  177. rosotacom/resources/ws/session/content/network/wifi_network_analysis_2.py +215 -0
  178. rosotacom/resources/ws/session/creation/anonymize_bag.py +156 -0
  179. rosotacom/resources/ws/session/creation/catmux_log_setup.sh +65 -0
  180. rosotacom/resources/ws/session/creation/create_session_yaml.py +145 -0
  181. rosotacom/resources/ws/session/creation/generate_session_files.py +3217 -0
  182. rosotacom/resources/ws/session/creation/run_session.py +447 -0
  183. rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py +54 -0
  184. rosotacom/resources/ws/session/creation/strip_ansi.py +42 -0
  185. rosotacom/status_eval.py +475 -0
  186. rosotacom/stream_stats.py +650 -0
  187. rosotacom/trace_profiles.py +561 -0
  188. rosotacom/transit.py +249 -0
  189. rosotacom/video_quality.py +906 -0
  190. rosotacom_dev-2.2.dist-info/METADATA +874 -0
  191. rosotacom_dev-2.2.dist-info/RECORD +195 -0
  192. rosotacom_dev-2.2.dist-info/WHEEL +5 -0
  193. rosotacom_dev-2.2.dist-info/entry_points.txt +4 -0
  194. rosotacom_dev-2.2.dist-info/licenses/LICENSE.txt +25 -0
  195. rosotacom_dev-2.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,874 @@
1
+ Metadata-Version: 2.4
2
+ Name: rosotacom-dev
3
+ Version: 2.2
4
+ Summary: Host CLI for ROS OTA communication sessions.
5
+ Author: develNor
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/develNor/rosotacom
8
+ Project-URL: Repository, https://github.com/develNor/rosotacom
9
+ Project-URL: Issues, https://github.com/develNor/rosotacom/issues
10
+ Keywords: ros2,docker,teleoperation,communication,cli
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Topic :: System :: Networking
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE.txt
26
+ Requires-Dist: argcomplete<4,>=3.6
27
+ Requires-Dist: mcap<2,>=1.4
28
+ Requires-Dist: Pillow<13,>=10
29
+ Requires-Dist: PyYAML>=6
30
+ Requires-Dist: ros2docker<0.2,>=0.1.4
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.2; extra == "dev"
33
+ Requires-Dist: check-wheel-contents>=0.6; extra == "dev"
34
+ Requires-Dist: mypy>=1.14; extra == "dev"
35
+ Requires-Dist: pre-commit>=4.0; extra == "dev"
36
+ Requires-Dist: pytest>=8.3; extra == "dev"
37
+ Requires-Dist: pytest-cov>=6.0; extra == "dev"
38
+ Requires-Dist: ruff==0.15.17; extra == "dev"
39
+ Requires-Dist: shellcheck-py>=0.10.0; extra == "dev"
40
+ Requires-Dist: twine>=6.0; extra == "dev"
41
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
42
+ Provides-Extra: plots
43
+ Requires-Dist: matplotlib>=3.8; extra == "plots"
44
+ Dynamic: license-file
45
+
46
+ # ROS Communication DevContainer
47
+
48
+ [![CI](https://github.com/develNor/ros_communication_devcontainer/actions/workflows/pr-merge-gate.yml/badge.svg?branch=develop&event=push)](https://github.com/develNor/ros_communication_devcontainer/actions/workflows/pr-merge-gate.yml)
49
+ [![Coverage](https://codecov.io/gh/develNor/ros_communication_devcontainer/branch/develop/graph/badge.svg)](https://codecov.io/gh/develNor/ros_communication_devcontainer)
50
+ [![PyPI](https://img.shields.io/pypi/v/rosotacom.svg)](https://pypi.org/project/rosotacom/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/rosotacom.svg)](https://pypi.org/project/rosotacom/)
52
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/develNor/ros_communication_devcontainer/blob/HEAD/LICENSE.txt)
53
+ [![Nightly E2E](https://github.com/develNor/ros_communication_devcontainer/actions/workflows/nightly-e2e.yml/badge.svg?branch=develop)](https://github.com/develNor/ros_communication_devcontainer/actions/workflows/nightly-e2e.yml)
54
+
55
+ The ROS Communication DevContainer is a Docker-based solution designed to streamline the bidirectional synchronization of ROS2 topics between two Linux machines. It provides built-in compression and routing capabilities for over-the-air (OTA) data transfer: selected topics are remapped into an OTA namespace and transmitted either via direct DDS (CycloneDDS) or through a Zenoh router. When desired, the session can also place local application nodes and OTA-facing bridge nodes into separate ROS 2 domain IDs and automatically generate a standard ROS 2 `domain_bridge` configuration for the `/com/...` boundary. This project aligns with the publication *“Scalable Remote Operation for Autonomous Vehicles: Integration of Cooperative Perception and Open Source Communication.”*
56
+
57
+ <details>
58
+ <summary>Key Features</summary>
59
+
60
+ - **Minimal Dependencies**: Only Docker is needed to get started, simplifying the setup process.
61
+ - **Isolation**: Operates in a separate Docker container, ensuring minimal impact on existing ROS setups.
62
+ - **Centralized Configuration Management**: All configurations are stored and managed in this repository.
63
+ - **Compression**: Built-in compression capabilities for efficient data transfer.
64
+ - **QoS Configuration**: Flexible Quality of Service settings for optimized communication.
65
+
66
+ </details>
67
+
68
+ ## Getting Started
69
+
70
+ ### Prerequisites
71
+
72
+ - Docker installed on all machines
73
+ - Git for configuration management
74
+ - Python 3 with virtual environment support (`python3-venv` on Debian/Ubuntu;
75
+ for versioned Python packages this may be named like `python3.14-venv`)
76
+ - `ros2docker` v0.1.4 or newer. The local installer below installs the pinned
77
+ supported range into this checkout's virtual environment.
78
+ - `tmux` for the optional `rosotacom scenario` orchestration commands
79
+ - Machines connected to the same network (VPN or local WLAN)
80
+
81
+ ### Convenience CLI: `rosotacom`
82
+
83
+ This repository's main entrypoint for starting ROS communication sessions is
84
+ the checkout-local `rosotacom` command. Each checkout owns its own `.venv`,
85
+ so multiple rosotacom versions can coexist without global symlink drift.
86
+
87
+ #### Install
88
+
89
+ Use a released version, or develop from a checkout — both with off-the-shelf
90
+ tooling, nothing rosotacom-specific:
91
+
92
+ ```bash
93
+ # released version (recommended for users)
94
+ pipx install rosotacom-dev # isolated, on PATH everywhere
95
+
96
+ <!-- The distribution is rosotacom-dev; the import name and the CLI stay
97
+ `rosotacom`. This fork publishes under its own name so that `rosotacom`
98
+ on PyPI stays reserved for the FZI upstream. -->
99
+ # or: pip install --user rosotacom
100
+
101
+ # from a source checkout (developers)
102
+ cd /path/to/ros_communication_devcontainer
103
+ ./install.sh # builds a checkout-local .venv
104
+ source .venv/bin/activate # use it in this terminal
105
+ # ./install.sh --global-symlink # optional: put it on PATH via ~/.local/bin
106
+
107
+ rosotacom --version
108
+ python -m rosotacom --version
109
+ rosotacom doctor
110
+ ```
111
+
112
+ For a source checkout, that single `source .venv/bin/activate` command also
113
+ enables completion. Nothing needs to be added to your shell configuration.
114
+
115
+ For a pipx/global installation, add one line once to your shell startup file:
116
+
117
+ ```bash
118
+ # zsh: add this line to ~/.zshrc
119
+ eval "$(rosotacom completion zsh)"
120
+
121
+ # bash: add this line to ~/.bashrc
122
+ eval "$(rosotacom completion bash)"
123
+ ```
124
+
125
+ Every new terminal then loads completion automatically; you do not run the
126
+ command manually in each terminal. After reloading the shell,
127
+ `rosotacom <TAB><TAB>` lists commands,
128
+ `rosotacom smoke <TAB><TAB>` lists sessions from the active project, and a
129
+ prefix such as `rosotacom smoke 1<TAB>` expands to `1_heartbeat`. The same
130
+ session completion is available for `start`, `stop`, `status`, `test`, and the
131
+ probe commands; absolute and relative session-directory paths still complete
132
+ normally. `--identity <TAB><TAB>` lists the peer identities from the selected
133
+ session or scenario. `--peer <TAB><TAB>` completes logical peer keys, and
134
+ `--peer a=<TAB><TAB>` completes named hosts from the active deployment file.
135
+ `--peer-address <TAB><TAB>` completes logical peer keys for raw overrides.
136
+ Scenario `attach` and `stop` complete only active scenarios and identities.
137
+ Running `rosotacom completion` without a shell argument infers bash or zsh from
138
+ `$SHELL`.
139
+
140
+ Completion is not pinned to the version that registered it: each Tab press runs
141
+ the `rosotacom` currently selected by `PATH`. Activating another checkout's
142
+ virtual environment therefore switches both the command and its completion,
143
+ and `deactivate` switches back without a reset step. A pipx-suffixed executable
144
+ can be registered independently with, for example,
145
+ `eval "$(rosotacom@2.2.0 completion zsh)"`.
146
+
147
+ **Multiple versions / try-without-disturbing.** Because each checkout owns its
148
+ own `.venv` and `./install.sh` never touches your PATH on its own, you can keep a
149
+ pinned/production version in use while smoke-testing another in one terminal:
150
+
151
+ ```bash
152
+ cd ~/checkouts/rosotacom-newest # a second checkout (or git worktree)
153
+ ./install.sh # builds .venv here; your other rosotacom is untouched
154
+ source .venv/bin/activate # this terminal now runs the new commit
155
+ rosotacom smoke # built-in example, zero config
156
+ deactivate # back to your usual rosotacom
157
+ ```
158
+
159
+ For coexisting *released* versions, `pipx install rosotacom-dev==2.2.0 --suffix=@2.2.0`
160
+ gives you a `rosotacom@2.2.0` alongside the default — standard pipx, no bespoke
161
+ version manager.
162
+
163
+ ### Basic Setup
164
+
165
+ `rosotacom` uses five layers of configuration/runtime state:
166
+
167
+ - A **project setup** file (`rosotacom.yaml`) points to host-local resources such as `ros2docker.json`, static `sessions/`, ignored `session-instances/`, and an optional deployment file.
168
+ - A **session config** defines the communication behavior for one run: logical peers, topics, QoS, processing, and transport choices.
169
+ - An optional **scenario config** composes one communication session with identity-specific local application containers.
170
+ - An optional **deployment config** maps named physical hosts to reachable addresses and optional SSH targets.
171
+ - A **session instance** stores one concrete run: generated config, catmux pane logs, smoke debug output, and future rosbags.
172
+
173
+ `rosotacom` resolves the active `rosotacom.yaml` by scope (first wins), using the
174
+ `shell` / `local` / `global` vocabulary from `pyenv`:
175
+
176
+ 1. `--project <path>` (alias of `--rosotacom-config`) on the command
177
+ 2. **shell** — `ROSOTACOM_CONFIG` in the environment (needs `eval`, since only
178
+ your shell can change its own environment)
179
+ 3. **local** — the nearest `rosotacom.yaml` discovered upward from the cwd
180
+ 4. **global** — a machine-wide default (`rosotacom config set project ... --global`)
181
+ 5. a built-in example project, so a fresh install runs with zero setup
182
+
183
+ The built-in example is used **in place** (read-only) and writes nothing to
184
+ `$HOME`: its generated session output goes to a per-user tmpfs dir under
185
+ `$XDG_RUNTIME_DIR` (printed on `start`/`smoke`), so it is ephemeral. For
186
+ persistent, editable projects, create one with `rosotacom examples create`.
187
+
188
+ So the simplest workflow is just to be in a project directory (the **local**
189
+ scope — no command needed, just have the file):
190
+
191
+ ```bash
192
+ rosotacom examples create ./rosotacom_examples
193
+ cd ./rosotacom_examples
194
+ rosotacom doctor # picks up ./rosotacom.yaml automatically
195
+ ```
196
+
197
+ Inspect or pin the selection with `rosotacom config`:
198
+
199
+ ```bash
200
+ rosotacom config show # every scope + the winner
201
+ rosotacom config set project ./rosotacom.yaml --global # machine-wide default
202
+ eval "$(rosotacom config set project ./rosotacom.yaml --shell)" # this terminal only
203
+ ```
204
+
205
+ The copied packaged example project uses this layout:
206
+
207
+ ```text
208
+ rosotacom.yaml
209
+ ros2docker.json
210
+ deployment.example.yaml
211
+ sessions/
212
+ scenarios/
213
+ session-instances/
214
+ scripts/
215
+ ```
216
+
217
+ `session_configs_dir` and `scenario_configs_dir` are ordered YAML lists. Put
218
+ project-local directories first and shared/example directories later when one
219
+ project should discover both.
220
+
221
+ See the [example project README](src/rosotacom/resources/examples/README.md)
222
+ for the copyable example layout, the
223
+ [session configuration reference](session-configuration.md) for the session
224
+ schema, the [deployment configuration reference](deployment-configuration.md) for
225
+ the machine-specific schema and peer-binding precedence,
226
+ [terminology.md](terminology.md) for the project vocabulary, and
227
+ [CONCURRENCY.md](CONCURRENCY.md) for what can run in parallel and how
228
+ conflicting runs are detected and aborted.
229
+
230
+ The normal examples intentionally contain no machine addresses. `rosotacom
231
+ smoke` injects isolated Docker-network addresses, while manual and OTA runs
232
+ select physical hosts explicitly.
233
+
234
+ Write or edit session configs under `sessions/<name>/`:
235
+
236
+ - `session-definition.yaml` for a self-contained session
237
+ - `session-parametrization.yaml` for a template plus parameters
238
+
239
+ Run `rosotacom` on each peer with the same active setup but a different identity:
240
+
241
+ ```bash
242
+ # on peer "a"
243
+ rosotacom start 1_heartbeat --identity a \
244
+ --peer-address a=10.0.0.10 --peer-address b=10.0.0.11
245
+
246
+ # on peer "b"
247
+ rosotacom start 1_heartbeat --identity b \
248
+ --peer-address a=10.0.0.10 --peer-address b=10.0.0.11
249
+ ```
250
+
251
+ `rosotacom` reads the static session input and creates generated files under `session-instances/<date>/<session>_<timestamp>_<id>/config/`, including per-peer plugin/session specs, topic lists, optional QoS, and optional `domain_bridge.yaml`. Catmux pane output is logged under the same instance in `logs/<peer>/catmux/`.
252
+
253
+ ### Complete use cases with scenarios
254
+
255
+ A session intentionally describes only the communication contract. When a use
256
+ case also needs a local ROS application, rosbag, or ROS command, define a
257
+ scenario above it:
258
+
259
+ ```yaml
260
+ schema_version: 1
261
+ session: 2_native_chatter
262
+
263
+ applications:
264
+ a:
265
+ - name: native_application
266
+ ros2docker_config: ../../scripts/2_native_chatter/machine_a/external.ros2docker.json
267
+ b:
268
+ - name: native_application
269
+ ros2docker_config: ../../scripts/2_native_chatter/machine_b/external.ros2docker.json
270
+ ```
271
+
272
+ Start one identity's communication and local application together:
273
+
274
+ ```bash
275
+ rosotacom scenario start 2_native_chatter --identity a
276
+ rosotacom scenario list
277
+ rosotacom scenario attach
278
+ rosotacom scenario stop
279
+ ```
280
+
281
+ `scenario list` shows both configured scenarios and currently active
282
+ scenario/identity pairs. `attach` and `stop` infer omitted values when exactly
283
+ one active choice exists; otherwise their completions and error messages show
284
+ the eligible active choices.
285
+
286
+ The outer view uses an isolated host tmux server with one full window for
287
+ communication and one full window per local application. Its prefix remains
288
+ `Ctrl-b`; switch windows with `Ctrl-b n`/`Ctrl-b p`, and send the inner
289
+ container-side catmux prefix with `Ctrl-b Ctrl-b`. Detaching from the outer
290
+ tmux does not stop the use case. `scenario stop` owns cleanup of the application
291
+ containers, communication container, and outer tmux session.
292
+
293
+ ### Safe Replay Testing with Anonymized Rosbags
294
+
295
+ To test scenarios involving proprietary data safely, first record the processed
296
+ outbound handoff topics that the session pipeline actually forwards into the OTA
297
+ path. `rosotacom anonymize` consumes that processed trace bag and the matching
298
+ session/scenario definition. It:
299
+
300
+ 1. Resolves the session pipeline and selects the outbound handoff topics that feed
301
+ `/com/out/...` and `/ota/...`.
302
+ 2. Fails closed if the input bag only contains raw source topics and is missing
303
+ the processed handoff topics.
304
+ 3. Renames the handoff topics to generic names like `/topic1`, `/topic2`, etc.
305
+ 4. Replaces message contents with mock payloads while preserving message shape,
306
+ timestamps, payload lengths, bag QoS metadata, and playback QoS overrides.
307
+ For ffmpeg camera packets it additionally preserves the keyframe flag and
308
+ `pts`, keeping the GOP burst structure analyzable — see
309
+ [docs/ffmpeg-keyframes.md](docs/ffmpeg-keyframes.md).
310
+ 5. Generates a self-contained `rosotacom` project containing the anonymized bag,
311
+ processed-topic session definition, replay scenario, QoS overrides, and an
312
+ anonymization manifest.
313
+
314
+ ```bash
315
+ rosotacom anonymize /path/to/processed_handoff_bag \
316
+ -s 3_comp_occ_grid \
317
+ -o ./anonymized_project
318
+ ```
319
+
320
+ ## Usage Examples
321
+
322
+ Create the example project first (cwd discovery wires it automatically once you
323
+ `cd` in):
324
+
325
+ ```bash
326
+ rosotacom examples create ./rosotacom_examples
327
+ cd ./rosotacom_examples
328
+ ```
329
+
330
+ Run the local heartbeat smoke test:
331
+
332
+ ```bash
333
+ rosotacom smoke
334
+ ```
335
+
336
+ To smoke-test one specific configured session locally, pass its name:
337
+
338
+ ```bash
339
+ rosotacom smoke 1_heartbeat
340
+ ```
341
+
342
+ The smoke test verifies both directions through the communication path: it waits
343
+ for `/com/in/a/heartbeat_a` and `/heartbeat_a` in peer `b`, plus
344
+ `/com/in/b/heartbeat_b` and `/heartbeat_b` in peer `a`. For each checked topic it
345
+ also reports a `SMOKE_METRIC` line with the received rate (`hz`) and end-to-end
346
+ latency (`delay_s`) so rate and latency regressions are visible. It prints the
347
+ `session-instances/...` artifact path so failures (and the per-peer
348
+ `logs/<peer>/catmux/...` pane output) can be inspected after the containers stop.
349
+
350
+ For local debugging, run the same end-to-end idea interactively:
351
+
352
+ ```bash
353
+ rosotacom smoke 2_native_chatter --interactive
354
+ rosotacom smoke --interactive --list
355
+ rosotacom smoke 2_native_chatter --interactive --stop
356
+ ```
357
+
358
+ Interactive smoke starts all peers on one host, opens an isolated outer tmux
359
+ session, and keeps full windows for each peer's communication container. If the
360
+ target is a scenario, it also opens one window per local application container;
361
+ each application shares its peer communication container's isolated network
362
+ namespace so local ROS discovery behaves like colocated processes. A
363
+ `verification` window runs the local checks/status view while you inspect the
364
+ system. Its upper pane keeps the verification log; its lower pane watches live
365
+ status. The outer prefix is `Ctrl-b`; use `Ctrl-b n`/`Ctrl-b p` for windows and
366
+ `Ctrl-b Ctrl-b` for the inner catmux session.
367
+
368
+ `rosotacom smoke TARGET --interactive` accepts either a session or a scenario.
369
+ When both names exist, interactive smoke treats `TARGET` as a scenario unless
370
+ you pass `--target-type session`. Non-interactive `rosotacom smoke TARGET`
371
+ keeps its existing session-only behavior.
372
+
373
+ Local smoke runs of different targets are parallel-safe: every run gets
374
+ instance-scoped container names and its own isolated Docker network. Starting a
375
+ run that cannot coexist with an active one (same smoke target, same session
376
+ identity, any benchmark) aborts immediately with the conflicting containers and
377
+ the stop command. `rosotacom ps` shows what is active in this workspace;
378
+ [CONCURRENCY.md](CONCURRENCY.md) documents the boundaries.
379
+
380
+ For a real two-host OTA check with no deployment file, provide both addresses
381
+ and only the SSH target needed to reach the remote peer:
382
+
383
+ ```bash
384
+ rosotacom ota-smoke 2_native_chatter \
385
+ --peer-address a=10.0.0.10 \
386
+ --peer-address b=10.0.0.11 \
387
+ --peer-ssh b=robot-b
388
+ ```
389
+
390
+ For reusable host names, reference one deployment file from `rosotacom.yaml`:
391
+
392
+ ```yaml
393
+ # rosotacom.yaml
394
+ deployment: deployment.yaml
395
+ ```
396
+
397
+ ```yaml
398
+ # deployment.yaml
399
+ hosts:
400
+ workstation:
401
+ address: 10.0.0.10
402
+ ssh: null
403
+ robot:
404
+ address: 10.0.0.11
405
+ ssh: robot-b
406
+ ```
407
+
408
+ ```bash
409
+ rosotacom ota-smoke 2_native_chatter \
410
+ --peer a=workstation \
411
+ --peer b=robot
412
+ ```
413
+
414
+ `ota-smoke` accepts sessions and scenarios. It automatically stages and
415
+ installs the currently selected rosotacom version, stages the active project,
416
+ runs delivery and isolation checks, collects artifacts, stops the run, and
417
+ removes the remote workdir. Add `--interactive` for a local control tmux with
418
+ one attachable communication/catmux window per peer with a live status pane below
419
+ each one. Scenario targets also get one native application-container window per
420
+ scenario app. Stop an interactive run with `rosotacom ota-smoke TARGET
421
+ --interactive --stop`; pass the same peer/deployment arguments if the local tmux
422
+ metadata is not available. Use `--keep-running` to leave components up,
423
+ `--keep-workdir` to retain staged files, or `--reuse` to reuse an existing
424
+ installation.
425
+
426
+ ### Live status / debugging overview
427
+
428
+ Enable a continuously-updated, per-topic pipeline overview by setting
429
+ `shared.use_status_overview: true` in the session definition (see the
430
+ ready-made `1_heartbeat_status` example). For every configured topic it tracks
431
+ where the topic currently is in the communication pipeline (furthest stage
432
+ reached and the first stage that is missing/broken), plus last-message age, Hz,
433
+ mean size, latency, exact wrapped-topic sequence loss/reordering, and echo-derived
434
+ RTT/clock offset.
435
+
436
+ The running session writes, under
437
+ `session-instances/.../logs/<peer>/status/`:
438
+
439
+ - `status.json` — machine-readable snapshot (source of truth) for tools/agents,
440
+ refreshed on a short interval and on every state transition,
441
+ - `status.txt` — a human-rendered table, and
442
+ - `events.jsonl` — state transitions plus per-`(topic, seq)` transit records for
443
+ wrapped topics (delivered/lost/reordered, section latency, size, inter-arrival,
444
+ and jitter).
445
+
446
+ For fixed-interval network-condition samples alongside the same session, enable
447
+ the [link trace recorder](docs/link-trace.md). It writes
448
+ `link_trace.jsonl` under the same status directory with `/proc/net/dev` counter
449
+ deltas, echo-heartbeat RTT/loss provenance, and an optional modem-metrics hook.
450
+
451
+ Convert a recorded link trace into a profiles-file entry when you want to replay
452
+ the same drive conditions in a repeatable benchmark:
453
+
454
+ ```bash
455
+ rosotacom profile from-trace session-instances/.../logs/a/status/link_trace.jsonl \
456
+ --mode timeline --name drive_replay --out generated-profiles.yaml
457
+ rosotacom benchmark recovery --profiles-file generated-profiles.yaml --profile drive_replay
458
+ ```
459
+
460
+ Timeline mode emits piecewise-constant RFC 0004 segments, turns long sample gaps
461
+ into `outage: reconnect`, and turns sustained 100% probe loss into
462
+ `outage: catchup`. Static mode distills one profile with median valid rate, p90
463
+ one-way delay, delay-spread jitter, and mean loss; constrain it with
464
+ `--window START:END` when only part of a drive should calibrate the profile. RTT
465
+ is mapped to symmetric one-way delay. Passive `/proc/net/dev` throughput is only
466
+ a lower-bound observation, so `rate` is emitted only when a sample is marked as
467
+ saturated/probed/capacity-like; otherwise the converter omits rate instead of
468
+ fabricating capacity. The generated YAML carries a provenance comment with the
469
+ source path, SHA-256, parameters, and these caveats.
470
+
471
+ Render an offline route map when a run also has GPS or pose samples:
472
+
473
+ ```bash
474
+ rosotacom geomap --bag session-instances/.../rosbags/a/native --gps-topic /fix \
475
+ --trace session-instances/.../logs/a/status/link_trace.jsonl \
476
+ --metric observed_tx_kbps \
477
+ --out-csv artifacts/geo-link-quality.csv \
478
+ --out-html artifacts/geo-link-quality.html
479
+ ```
480
+
481
+ `geomap` writes georeferenced CSV samples, an HTML report, and a sibling
482
+ `.route.png` route image. The timestamp offset between trace/event time and
483
+ bag/GPS time is explicit; see [docs/geo-link-map.md](docs/geo-link-map.md) for
484
+ the alignment model, CSV fixture workflow, and supported metrics.
485
+
486
+ Read it from the host with the `status` command:
487
+
488
+ ```bash
489
+ rosotacom status 1_heartbeat_status # human-readable table
490
+ rosotacom status 1_heartbeat_status --json # machine-readable, for tools/agents
491
+ rosotacom status 1_heartbeat_status --watch # live refresh
492
+ ```
493
+
494
+ Join one or more peer timelines after a run:
495
+
496
+ ```bash
497
+ rosotacom metrics session-instances/.../logs/a/status/events.jsonl \
498
+ session-instances/.../logs/b/status/events.jsonl
499
+ ```
500
+
501
+ Use `--records` for joined per-message rows instead of the loss and p50/p95
502
+ summary. Wrapped-topic corrected latency requires `shared.use_heartbeat: true`;
503
+ `status.json.clock_sync` reports the selected minimum-RTT sample, peer offset,
504
+ sample age, and symmetric-path assumption. Uncorrected OTA delay remains a
505
+ separate field.
506
+
507
+ Compare how a stream changes across recorded stages — for example native
508
+ pre-processing, processed handoff, and post-OTA transit rows:
509
+
510
+ ```bash
511
+ rosotacom stream-stats \
512
+ --bag pre=logs/b/metrics/stages_20260702_172900:/sensors/camera/front_medium/resized/image_rect_color \
513
+ --bag handoff=logs/b/metrics/stages_20260702_172900:/sensors/camera/front_medium/resized/image_rect_color/compressed/restamped/drop1of2/ffmpeg \
514
+ --events post=logs/b/status/events.jsonl:/sensors/camera/front_medium/resized/image_rect_color \
515
+ --out stream-stats
516
+ ```
517
+
518
+ The command emits a side-by-side Markdown table plus `stream-stats.json`, with
519
+ per-stage size distributions, observed rate, interval regularity, and FFMPEG
520
+ GOP/keyframe shape when available. MCAP bag sources use the Python `mcap`
521
+ reader; other rosbag2 storage backends require `rosbag2_py`. `events`
522
+ sources work from the recorded RFC 0003 transit rows. See
523
+ [docs/ffmpeg-keyframes.md](docs/ffmpeg-keyframes.md) for the GOP methodology
524
+ and example camera interpretation.
525
+
526
+ Turn a whole recorded instance into an explanation of *where* and *how*
527
+ delivery degraded — loss bursts, latency excursions, and rate collapses with
528
+ exact boundaries, joined with link-trace/profile/keyframe context:
529
+
530
+ ```bash
531
+ rosotacom report session-instances/<day>/<instance>
532
+ ```
533
+
534
+ It writes `report/report.json`, `report/report.md`, and per-stream figures with
535
+ the events marked (needs the `[plots]` extra). See
536
+ [docs/forensics-report.md](docs/forensics-report.md) for the detection
537
+ semantics and the correlation-not-causation caveat.
538
+
539
+ For opt-in local per-step latency, record generated local stage topics:
540
+
541
+ ```yaml
542
+ shared:
543
+ use_status_overview: true
544
+ metric_backbone:
545
+ record_stages: true
546
+ ```
547
+
548
+ The MCAP is written under `logs/<peer>/metrics/`. Analyze an ordered pipeline
549
+ with `ros2 run com_py stage_latency BAG TOPIC...`.
550
+
551
+ For decoded camera stages, compute offline image quality with PSNR/SSIM:
552
+
553
+ ```bash
554
+ rosotacom videoquality \
555
+ session-instances/.../logs/b/metrics/stages_... \
556
+ session-instances/.../logs/a/metrics/stages_... \
557
+ --ref-topic /camera/image \
558
+ --degraded-topic /camera/image/ffmpeg/raw \
559
+ --out videoquality.json \
560
+ --plot videoquality.png \
561
+ --min-mean-psnr 30 \
562
+ --min-mean-ssim 0.90 \
563
+ --max-loss-pct 0
564
+ ```
565
+
566
+ Use the outbound pre-transport image topic as the reference (`native` when no
567
+ preprocessing runs; otherwise the generated topic immediately before
568
+ `transport`, such as a restamped/drop output) and the receiver-side decoded
569
+ `native_in` stage as the degraded stream. The reference topic may be decoded
570
+ `sensor_msgs/msg/Image` or JPEG/PNG `sensor_msgs/msg/CompressedImage`; the
571
+ receiver-side ffmpeg comparison topic should be the decoded `/raw` reverse
572
+ transport output. Lost frames are reported as delivery loss and are never
573
+ averaged into PSNR/SSIM. Encoded `ffmpeg_image_transport_msgs/msg/FFMPEGPacket`
574
+ topics carry packet bytes, not pixels, so select the decoded `/raw` topic when
575
+ computing image quality. The same command also accepts JSON frame manifests for
576
+ host-only checks:
577
+
578
+ ```bash
579
+ rosotacom videoquality --make-synthetic /tmp/rosotacom-quality
580
+ rosotacom videoquality \
581
+ /tmp/rosotacom-quality/reference-frames.json \
582
+ /tmp/rosotacom-quality/degraded-frames.json \
583
+ --min-mean-psnr 20 --max-loss-pct 0
584
+ ```
585
+
586
+ After a recording run, validate that a session instance still contains the
587
+ artifacts needed for later analysis:
588
+
589
+ ```bash
590
+ rosotacom bundle check session-instances/.../drive_run --peer a --peer b \
591
+ --file config/resolved-session.yaml \
592
+ --bag rosbags/a/native
593
+ ```
594
+
595
+ `bundle check` validates each expected peer's `status.json` and `events.jsonl`,
596
+ requires transit rows in the event streams, and validates expected rosbag2 bags
597
+ from their `metadata.yaml` message counts. Extra expected artifacts can be passed
598
+ as repeated `--file`, `--optional-file`, `--bag`, and `--optional-bag` flags, or
599
+ via a generic YAML manifest:
600
+
601
+ ```yaml
602
+ schema_version: 1
603
+ peers: [a, b]
604
+ required_files:
605
+ - config/resolved-session.yaml
606
+ required_bags:
607
+ - path: rosbags/a/native
608
+ label: native bag
609
+ optional_files:
610
+ - traces/link.jsonl
611
+ ```
612
+
613
+ Phase 1 samples local-domain stages directly. OTA stages are graph-only and
614
+ their activity is inferred from adjacent local flow, so the status overview
615
+ never creates an additional OTA payload subscription. Combine both peers' files
616
+ for the full end-to-end picture; cross-peer confirmation is reserved for a later
617
+ phase.
618
+
619
+ Run the CI single-machine smoke matrix locally:
620
+
621
+ ```bash
622
+ just test-e2e-smoke
623
+ ```
624
+
625
+ Run the heartbeat example manually:
626
+
627
+ ```bash
628
+ ./scripts/1_heartbeat/run_machine_a.sh
629
+ ./scripts/1_heartbeat/run_machine_b.sh
630
+ ```
631
+
632
+ Example 2 is the pilot for one-command scenario orchestration:
633
+
634
+ ```bash
635
+ rosotacom scenario start 2_native_chatter --identity a
636
+ ```
637
+
638
+ The existing `run_external.py` and `run_communication.sh` helpers remain
639
+ available as a compatibility fallback. Examples 3–6 continue to use those
640
+ helpers for now.
641
+
642
+ The `sessions/` directory contains curated built-in session definitions:
643
+
644
+ - `1_heartbeat`: minimal heartbeat exchange
645
+ - `1_heartbeat_status`: heartbeat exchange with the live status overview enabled
646
+ - `2_native_chatter`: bridge `/chatter` from `machine_b` to `machine_a`
647
+ - `3_comp_occ_grid`: compressed occupancy grid over DDS
648
+ - `4_comp_occ_grid_zen`: compressed occupancy grid through Zenoh
649
+ - `5_sized_payload`: sized payload test over DDS
650
+ - `6_sized_payload_zen`: sized payload test through Zenoh
651
+ - `14_remote_assist_anonymized`: generic processed replay shape for the
652
+ remote-assist OTA contract; run it with
653
+ `rosotacom smoke 14_remote_assist_anonymized`
654
+ - `15_remote_assist_anonymized_costmap`: single-stream cut of example 14 —
655
+ only `/topic5`, the anonymized compressed occupancy-grid stream; run it with
656
+ `rosotacom smoke 15_remote_assist_anonymized_costmap`
657
+ - `16_remote_assist_anonymized_camera`: single-stream cut of example 14 —
658
+ only `/topic9`, the anonymized ffmpeg camera stream; run it with
659
+ `rosotacom smoke 16_remote_assist_anonymized_camera`
660
+ - `17_synthetic_camera_quality`: synthetic raw camera stream encoded through
661
+ ffmpeg and reverse-republished as decoded `/raw` frames for offline
662
+ `rosotacom videoquality` checks
663
+
664
+ Transport-combination coverage lives under `tests/sessions/rmw_matrix` and is
665
+ generated from `tests/sessions/generate_rmw_matrix.py`.
666
+
667
+ `rosotacom benchmark` uses the packaged `bench_1_*` sessions for local or OTA
668
+ benchmark probes. Benchmark sessions default to Cyclone DDS; pass `--rmw
669
+ fastdds`, `--rmw cyclone`, or another supported session RMW value to pin a run.
670
+ Each run writes a self-contained `result.json` under its benchmark artifact
671
+ directory with the selected RMW, configured load and offered bandwidth, profile
672
+ shaping context, thresholds, verdict, per-topic loss/latency/jitter metrics, and
673
+ the runner fingerprint. Deterministic runs gate against the committed two-sided
674
+ performance bands in `budgets.jsonl` via `rosotacom benchmark compare`, and
675
+ bands move only through `rosotacom benchmark ratchet` — see
676
+ [docs/performance-bands.md](docs/performance-bands.md) for the band schema,
677
+ verdicts, and the ratchet workflow (RFC 0007).
678
+ For live benchmark probes, `--duration` is the shaped publish window; after that
679
+ rosotacom stops synthetic publishers and waits `--drain-s` seconds before
680
+ tearing down shaping, so delayed in-flight messages are not miscounted as loss.
681
+ Cyclone DDS benchmark sessions keep the default tuned OTA
682
+ `SPDPInterval=30s`. On tight rate-limited profiles, this periodic discovery
683
+ metatraffic can share the shaped link with payload traffic and appear as regular
684
+ p99/max latency spikes. rosotacom records a `cyclonedds_spdp` diagnostic in
685
+ `result.json` and prints a warning when the probe duration and offered/shaped
686
+ bandwidth make that effect plausible. Keep the default for end-to-end DDS
687
+ behavior. For payload-only characterization where discovery bursts would mask the
688
+ question under test, pass `--cyclone-spdp-interval 150s` or another longer
689
+ positive `ms`/`s` duration. Making SPDP too frequent improves stale-peer and
690
+ reconnect detection cadence but contaminates tight-link latency more often;
691
+ making it too lax quiets short probes but hides discovery/liveliness overhead and
692
+ delays detection of changed peer state. The living record for this quirk is the
693
+ [SPDP discovery-burst finding](docs/findings/cyclone-spdp-discovery-bursts.md).
694
+ Use `benchmark probe` when you want a fixed payload/rate under one profile
695
+ instead of a breakpoint search. It writes the normal `result.json` plus
696
+ `time-bins.jsonl` with per-second loss, delivered Hz, payload bandwidth, and
697
+ p50/p95 latency/jitter bins; by default it also renders `probe-timeseries.png`
698
+ when the optional plotting dependency is installed:
699
+
700
+ ```bash
701
+ rosotacom benchmark probe --profile cellular-4g-typical --size 18000 --rate-hz 20 --duration 20 --repeats 1
702
+ rosotacom benchmark probe --profile cellular-4g-typical --size-pattern 1x20KB+1x0KB --rate-hz 10 --duration 20 --repeats 1
703
+ rosotacom benchmark probe --profile cellular-4g-typical --size 18000 --rate-hz 20 --duration 20 --cyclone-spdp-interval 150s
704
+ rosotacom benchmark plot time-bins.jsonl --type probe
705
+ ```
706
+
707
+ Use `benchmark requirements` when you want rosotacom to search for a tight
708
+ network profile for a stream and quality target. The driver starts from ideal
709
+ conditions, uses a geometric boundary search for bandwidth, linear boundary
710
+ searches for latency-like axes, and finishes with an extra refinement pass around
711
+ the final candidate. By default jitter and loss are searched as a coupled
712
+ practical tradeoff curve; pass `--loss-coupling independent` if you want network
713
+ loss treated as its own axis. For an exact `--max-loss 0` target, rosotacom
714
+ automatically keeps shaped network loss at `0%` and searches jitter separately,
715
+ so the reproducible command encodes the same policy used for the measurement. If
716
+ the zero-loss boundary is noisy, repeat each candidate and require a pass count;
717
+ for example, `--probe-repeats 10 --probe-min-passes 9 --bad-lossy-count 10`
718
+ defines a good case as at least nine clean repeats and records bad-case neighbors
719
+ where all ten repeats lose messages. Add `--netem-seed <n>` when you want the
720
+ generated jitter/loss draw to be replayable across repeated boundary searches.
721
+ For local Docker benchmarks, rosotacom copies the host `tc` binary into seeded
722
+ benchmark containers when the container distro `tc` is too old for
723
+ `netem seed`; the host `tc` must support `seed SEED`. For a practical good-case
724
+ reference, `--jitter-guard-ratio 0.10` records the tight jitter boundary but
725
+ uses 10% less jitter for later axes and the final profile;
726
+ `--bandwidth-guard-ratio 0.10` does the symmetric thing for bandwidth by using
727
+ 10% more bandwidth after finding the lower bound. The chosen profile, target
728
+ slack, repeat statistics, all probes, and the nearby failing neighbors are written to
729
+ `result.json`, `requirements.jsonl`, and `generated-profiles.yaml`:
730
+
731
+ ```bash
732
+ rosotacom benchmark requirements --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-loss 5 --max-latency-ms 250
733
+ rosotacom benchmark requirements --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-loss 0 --max-latency-ms 250
734
+ rosotacom benchmark requirements --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-loss 0 --max-latency-ms 250 --latency-base-ms 30 --downlink-mode lan --probe-repeats 10 --probe-min-passes 9 --bad-lossy-count 10 --jitter-guard-ratio 0.10 --bandwidth-guard-ratio 0.10 --netem-seed 424242
735
+ rosotacom benchmark requirements --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-loss 0 --max-latency-ms 250 --latency-base-ms 30 --downlink-mode lan --axes jitter,bandwidth --bandwidth-high-factor 8 --bandwidth-low-factor 1 --jitter-high-ms 40 --search-iterations 7 --final-refine-iterations 0 --search-rounds 1 --min-duration 20 --min-messages 100 --probe-repeats 1 --bandwidth-probe-repeats 1 --netem-seed 424242 --jitter-guard-ratio 0.20 --bandwidth-guard-ratio 0.20
736
+ ```
737
+
738
+ Use `benchmark loss-boundaries` when the question is specifically “where does
739
+ the first ROS 2 loss appear?” It searches bandwidth and jitter as discrete axes:
740
+ bandwidth reports the lowest loss-free rate and the adjacent lower bad rate at
741
+ `--bandwidth-step`; jitter reports the highest fully clean value, the first
742
+ non-clean neighbor, the lowest fully lossy value, and the mixed zone between
743
+ them at `--jitter-step-ms`. Seedless runs are the best empirical estimate for a
744
+ host/link, a single `--netem-seed` is useful for replay/debugging one random
745
+ jitter sequence, and `--netem-seeds 101,102,...` is the reproducible compromise
746
+ when you want several replayable jitter draws instead of overfitting one seed:
747
+
748
+ ```bash
749
+ rosotacom benchmark loss-boundaries --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-latency-ms 250 --latency-base-ms 30 --downlink-mode lan --bandwidth-low 2.8mbit --bandwidth-high 4mbit --bandwidth-step 0.1mbit --jitter-low-ms 0 --jitter-high-ms 40 --jitter-step-ms 1 --min-duration 20 --min-messages 100 --probe-repeats 10
750
+ rosotacom benchmark loss-boundaries --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-latency-ms 250 --latency-base-ms 30 --downlink-mode lan --bandwidth-low 2.8mbit --bandwidth-high 4mbit --bandwidth-step 0.1mbit --jitter-low-ms 0 --jitter-high-ms 40 --jitter-step-ms 1 --min-duration 20 --min-messages 100 --probe-repeats 1 --netem-seeds 101,102,103,104,105,106,107,108,109,110
751
+ ```
752
+
753
+ Both `requirements` and `loss-boundaries` also accept a **bag replay as the
754
+ load** instead of the synthetic `sized_publisher` stream: pass `--target
755
+ <session>` (a two-peer replay session such as `15_remote_assist_anonymized_costmap`)
756
+ and, optionally, `--bag <metadata.yaml>` for per-topic completeness ground truth.
757
+ A probe point is then one full replay run under the candidate profile, and the
758
+ oracle judges the **whole contract** — every carried topic must clear the
759
+ loss/latency bound and the `--oracle-min-completeness` floor — aggregated to a
760
+ run verdict that names any failing topics. This answers "which network could
761
+ carry *this bag*?" rather than "…this synthetic stream?". Because a probe now
762
+ costs a full bag loop, keep the iteration budget small. See
763
+ [docs/benchmark-bag-as-load.md](docs/benchmark-bag-as-load.md) for the oracle,
764
+ the local-vs-OTA execution split, and cost guidance:
765
+
766
+ ```bash
767
+ rosotacom benchmark loss-boundaries --target 15_remote_assist_anonymized_costmap --target-type session --axes bandwidth --bandwidth-low 0.5mbit --bandwidth-high 2mbit --bandwidth-step 0.5mbit --rate-hz 10 --min-duration 20 --min-messages 1 --probe-repeats 1 --good-clean-count 1 --bad-lossy-count 1 --max-latency-ms 1000
768
+ ```
769
+
770
+ Use `benchmark ab` when the question is a **tuning** one: "does candidate config
771
+ B beat baseline config A on the same load and profile?" Each `--baseline` and
772
+ `--candidate label=…` is a whole session config (a directory or a
773
+ `session-definition.yaml`) that shares the synthetic `a_to_b` load and differs
774
+ only in the pipeline knobs under test (QoS reliability/depth/lifespan,
775
+ compression, ffmpeg gop/bitrate/crf, … — knobs that keep the delivered topic
776
+ name, unlike `throttle_hz`/`drop` which republish to a renamed topic; see the
777
+ doc). The driver runs them interleaved with a rotating start over `--repeats`,
778
+ then classifies every candidate against the baseline per topic and metric as
779
+ **IMPROVED / WITHIN / REGRESSED** — the same verdict language the regression gate
780
+ uses — with a two-sided tolerance band (`--rel-tolerance`, `--abs-tolerance`).
781
+ The overall verdict fails if any watched metric regressed; the repeat spread
782
+ (min/median/max) travels with every cell so small-N effects are read honestly.
783
+ It writes `result.json`, a markdown table (`ab.md`), the per-config YAML diffs,
784
+ and `ab.jsonl`. See [docs/benchmark-ab.md](docs/benchmark-ab.md) for the config
785
+ model, interleaving, and the statistical-power note:
786
+
787
+ ```bash
788
+ rosotacom benchmark ab --profile cellular-4g-degraded --baseline configs/gop30 --candidate gop15=configs/gop15 --size 18000 --rate-hz 20 --duration 20 --repeats 3
789
+ ```
790
+
791
+ Use `rosotacom ota-benchmark` for the same probes over deployment peers, without
792
+ having to name a target:
793
+
794
+ ```bash
795
+ rosotacom ota-benchmark probe --profile cellular-4g-degraded --size 18000 --rate-hz 20 --duration 20 --repeats 1 --peer a=seat_tks --peer b=majestic_tks
796
+ rosotacom ota-benchmark capacity --profile cellular-4g-degraded --knob size --low 1 --high 1 --max-loss 30 --max-latency-ms 1000 --duration 10 --repeats 1 --peer a=seat_tks --peer b=majestic_tks
797
+ rosotacom ota-benchmark requirements --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-loss 5 --max-latency-ms 250 --peer a=seat_tks --peer b=majestic_tks
798
+ rosotacom ota-benchmark loss-boundaries --rate-hz 20 --size 18000 --qos-reliability best_effort --qos-depth 1 --max-latency-ms 250 --latency-base-ms 30 --downlink-mode lan --bandwidth-low 2.8mbit --bandwidth-high 4mbit --bandwidth-step 0.1mbit --jitter-high-ms 40 --jitter-step-ms 1 --probe-repeats 10 --peer a=seat_tks --peer b=majestic_tks
799
+ ```
800
+
801
+ OTA benchmarks default to the benchmark session for the selected genre. Pass
802
+ `--target` and `--target-type` only when you deliberately want to benchmark a
803
+ project-specific session or scenario instead. Add `--interactive` to open a tmux
804
+ operator view with a high-level run window, one fullscreen attachable catmux
805
+ window per local peer, a network window split into qdisc status and tc/netem
806
+ command logs, and a results window that prints the final result once. Shaped OTA
807
+ benchmark profiles need sudo on the remote peers for the generated `tc`/`ip`
808
+ commands. The default `--sudo-mode passwordless` checks `sudo -n` and is the right
809
+ mode for unattended runs. For attended operator runs, pass `--sudo-mode askpass`
810
+ to prompt locally once per peer and feed the password to remote `sudo -S` over SSH
811
+ stdin without printing or storing it.
812
+
813
+ ## Development
814
+
815
+ For contributor setup, local checks, PR workflow, CI, and releases, see
816
+ [CONTRIBUTING.md](CONTRIBUTING.md). CI behavior is summarized in
817
+ [docs/ci.md](docs/ci.md), releases in [docs/release.md](docs/release.md), and
818
+ issue-driven work tracking in [docs/work-items.md](docs/work-items.md).
819
+
820
+ The test and measurement architecture is recorded in the
821
+ [design RFCs](docs/rfcs/README.md), with measured public effects in the
822
+ [findings ledger](docs/findings/README.md); see [docs/testing.md](docs/testing.md)
823
+ for the test taxonomy. The diagnosis-first quality workflow is described in
824
+ [docs/quality-model.md](docs/quality-model.md), with owner-facing entrypoints in
825
+ [docs/owner-runbook.md](docs/owner-runbook.md). Reusable maintainer procedures
826
+ live in [docs/playbooks/README.md](docs/playbooks/README.md). Quality rules live
827
+ in [DEVELOPMENT_PRINCIPLES.md](DEVELOPMENT_PRINCIPLES.md).
828
+
829
+ ## Choosing the Transport Layer: CycloneDDS or Zenoh
830
+
831
+ - **Use CycloneDDS** when all machines share the **same `ROS_DOMAIN_ID`**.
832
+ This is the simplest and most direct configuration.
833
+
834
+ - **Use Zenoh** when peers cannot rely on one shared DDS domain, or when you want to split local application nodes and OTA-facing bridge nodes into different ROS 2 domains on each peer.
835
+ In that split-domain setup this repository generates a standard ROS 2 `domain_bridge` for `/com/...` topics locally, while Zenoh carries the `/ota/...` traffic between peers.
836
+
837
+ ## Position in the OTA Communication Landscape
838
+
839
+ This repository fits into a broader set of ROS-based OTA communication approaches:
840
+
841
+ - **Direct ROS 2 DDS Communication**
842
+ Native DDS (CycloneDDS, Fast DDS), often with custom configuration for constrained or long-range links.
843
+ The examples in this repository use CycloneDDS to illustrate this approach.
844
+
845
+ - **ROS 2 over Router-like Backbones**
846
+ Some RMW have their own DDS Routers such as [eProsima/DDS-Router](https://github.com/eProsima/DDS-Router).
847
+ Example 4 uses Zenoh to act as a lightweight router layer.
848
+
849
+ - **MQTT-based Approaches**
850
+ Common in cloud/IoT scenarios. Example:
851
+ [ika-rwth-aachen/mqtt_client](https://github.com/ika-rwth-aachen/mqtt_client)
852
+
853
+ - **Custom TCP/UDP Teleoperation Stacks**
854
+ Some frameworks implement their manual tcp/udp transportion layers. Example:
855
+ [TUMFTM/teleoperated_driving](https://github.com/TUMFTM/teleoperated_driving)
856
+
857
+ ## How to Cite
858
+
859
+ If you wish to cite the ROS Communication DevContainer in your work, please use the following citation:
860
+
861
+ ```latex
862
+ @InProceedings{gontscharow_scalable,
863
+ author = {Gontscharow, Martin and Doll Jens and Schotschneider, Albert and Bogdoll, Daniel and Orf Stefan and Jestram Johannes and Zofka, Marc and Z\"{o}llner, J. Marius},
864
+ title = {{Scalable Remote Operation for Autonomous Vehicles: Integration of Cooperative Perception and Open Source Communication}},
865
+ booktitle = {2024 IEEE Intelligent Vehicles Symposium (IV)},
866
+ year = {2024}
867
+ }
868
+ ```
869
+ ## Acknowledgements
870
+ The research leading to these results was conducted within
871
+ the project ÖV-LeitmotiF-KI and was funded by the German
872
+ Federal Ministry for Digital and Transport (BMDV), grant number 45AVF3004A-G.
873
+ Responsibility for the information and views set out in this
874
+ publication lies entirely with the authors.