rosotacom-dev 2.5.dev48__tar.gz → 2.5.dev50__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.
Files changed (364) hide show
  1. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/PKG-INFO +1 -1
  2. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/0004-network-profiles.md +18 -0
  3. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/session-configuration.md +29 -7
  4. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/cli.py +155 -8
  5. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/cli_benchmark.py +30 -5
  6. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/content/base/session_plugin_base.yaml +66 -4
  7. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/creation/generate_session_files.py +72 -7
  8. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/PKG-INFO +1 -1
  9. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/SOURCES.txt +1 -0
  10. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/scm_file_list.json +1 -0
  11. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/scm_version.json +2 -2
  12. rosotacom_dev-2.5.dev50/tests/unit/test_container_shaping.py +67 -0
  13. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_session_pipeline.py +73 -0
  14. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.devcontainer/devcontainer.json.template +0 -0
  15. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.devcontainer/re_generate_devcontainer_json.py +0 -0
  16. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/CODEOWNERS +0 -0
  17. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/documentation-audit.md +0 -0
  18. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/implementation-cleanup.md +0 -0
  19. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/quality-workflow.md +0 -0
  20. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/redesign.md +0 -0
  21. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/release-process.md +0 -0
  22. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/repository-diagnosis.md +0 -0
  23. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/test-ci-audit.md +0 -0
  24. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/ISSUE_TEMPLATE/work-item.md +0 -0
  25. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  26. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/actions/setup-just/action.yml +0 -0
  27. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/dependabot.yml +0 -0
  28. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/benchmark-calibrate.yml +0 -0
  29. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/benchmark-gate.yml +0 -0
  30. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/dev-release.yml +0 -0
  31. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/image-scan.yml +0 -0
  32. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/nightly-e2e.yml +0 -0
  33. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/pr-lightweight.yml +0 -0
  34. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/pr-merge-gate.yml +0 -0
  35. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.github/workflows/release.yml +0 -0
  36. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/.pre-commit-config.yaml +0 -0
  37. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/AGENTS.md +0 -0
  38. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/CONCURRENCY.md +0 -0
  39. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/CONTRIBUTING.md +0 -0
  40. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/DEVELOPMENT_PRINCIPLES.md +0 -0
  41. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/LICENSE.txt +0 -0
  42. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/MANIFEST.in +0 -0
  43. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/README.md +0 -0
  44. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/budgets.jsonl +0 -0
  45. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/calibration/README.md +0 -0
  46. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/calibration/capacity-size-tight-cyclone.json +0 -0
  47. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/calibration/probe-loss-gop-tight-cyclone.json +0 -0
  48. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/calibration/probe-loss-nominal-cyclone.json +0 -0
  49. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/calibration/probe-loss-tight-cyclone.json +0 -0
  50. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/calibration/probe-loss-tight-fastdds.json +0 -0
  51. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/calibration/probe-loss-tight-zenoh.json +0 -0
  52. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/codecov.yml +0 -0
  53. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/deployment-configuration.md +0 -0
  54. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/benchmark-ab.md +0 -0
  55. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/benchmark-bag-as-load.md +0 -0
  56. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/ci.md +0 -0
  57. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/ffmpeg-keyframes.md +0 -0
  58. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/README.md +0 -0
  59. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/cyclone-spdp-discovery-bursts.md +0 -0
  60. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/delay-alone-no-loss.md +0 -0
  61. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/head-of-line-irregularity.md +0 -0
  62. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/jitter-loss-bandwidth-latency.md +0 -0
  63. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/lighter-message-loss.md +0 -0
  64. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/reorder-becomes-reader-loss.md +0 -0
  65. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/repro/reorder_drop_repro.py +0 -0
  66. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/findings/zero-loss-boundaries-18kb-20hz.md +0 -0
  67. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/forensics-report.md +0 -0
  68. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/geo-link-map.md +0 -0
  69. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/github-repository-settings.md +0 -0
  70. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/link-bandwidth.md +0 -0
  71. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/link-trace.md +0 -0
  72. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/owner-runbook.md +0 -0
  73. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/performance-bands.md +0 -0
  74. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/playbooks/README.md +0 -0
  75. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/quality-model.md +0 -0
  76. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/release-notes/TEMPLATE.md +0 -0
  77. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/release-notes/v2.1.md +0 -0
  78. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/release-notes/v2.2.md +0 -0
  79. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/release-notes/v2.3.md +0 -0
  80. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/release-notes/v2.4.md +0 -0
  81. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/release.md +0 -0
  82. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/0001-expectation-driven-test-suite.md +0 -0
  83. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/0002-expectation-concepts.md +0 -0
  84. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/0003-metric-backbone.md +0 -0
  85. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/0005-benchmark-genres-and-ci.md +0 -0
  86. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/0006-dynamic-qos.md +0 -0
  87. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/0007-regression-gate.md +0 -0
  88. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/rfcs/README.md +0 -0
  89. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/testing.md +0 -0
  90. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/docs/work-items.md +0 -0
  91. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/install.sh +0 -0
  92. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/justfile +0 -0
  93. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/media/control_center.png +0 -0
  94. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/media/echo_test.png +0 -0
  95. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/media/forensics-report-example.png +0 -0
  96. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/media/host.png +0 -0
  97. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/media/vehicle.png +0 -0
  98. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/pyproject.toml +0 -0
  99. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/requirements.txt +0 -0
  100. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/rosotacom.yaml +0 -0
  101. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/setup.cfg +0 -0
  102. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/__init__.py +0 -0
  103. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/__main__.py +0 -0
  104. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/anonymize.py +0 -0
  105. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/bag_ground_truth.py +0 -0
  106. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/benched_set.py +0 -0
  107. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/benchmark.py +0 -0
  108. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/benchmark_delta.py +0 -0
  109. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/bundle_check.py +0 -0
  110. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/deployment.py +0 -0
  111. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/ffmpeg_packet.py +0 -0
  112. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/forensics.py +0 -0
  113. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/geomap.py +0 -0
  114. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/image_cache.py +0 -0
  115. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/netem_dist.py +0 -0
  116. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/network_profiles.py +0 -0
  117. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/network_shaper.py +0 -0
  118. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/plots.py +0 -0
  119. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/py.typed +0 -0
  120. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/benched-set.yaml +0 -0
  121. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/.gitignore +0 -0
  122. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/README.md +0 -0
  123. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/__init__.py +0 -0
  124. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/bags/8_drop_reference/metadata.yaml +0 -0
  125. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/deployment.example.yaml +0 -0
  126. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/dist/field-20260817.dist +0 -0
  127. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/profiles.yaml +0 -0
  128. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/ros2docker.json +0 -0
  129. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/rosotacom.yaml +0 -0
  130. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scenarios/2_native_chatter/scenario-definition.yaml +0 -0
  131. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/1_heartbeat/run_machine_a.sh +0 -0
  132. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/1_heartbeat/run_machine_b.sh +0 -0
  133. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.ros2docker.json +0 -0
  134. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_communication.sh +0 -0
  135. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_external.py +0 -0
  136. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/wait_for_echo_topic.sh +0 -0
  137. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.ros2docker.json +0 -0
  138. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_communication.sh +0 -0
  139. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_external.py +0 -0
  140. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external.ros2docker.json +0 -0
  141. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external_session.yaml +0 -0
  142. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/foxglove.json +0 -0
  143. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_communication.sh +0 -0
  144. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_external.py +0 -0
  145. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
  146. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/metadata.yaml +0 -0
  147. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/external.ros2docker.json +0 -0
  148. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_communication.sh +0 -0
  149. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_external.py +0 -0
  150. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external.ros2docker.json +0 -0
  151. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external_session.yaml +0 -0
  152. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/foxglove.json +0 -0
  153. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_communication.sh +0 -0
  154. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_external.py +0 -0
  155. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
  156. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/metadata.yaml +0 -0
  157. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/external.ros2docker.json +0 -0
  158. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_communication.sh +0 -0
  159. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_external.py +0 -0
  160. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external.ros2docker.json +0 -0
  161. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external_session.yaml +0 -0
  162. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_communication.sh +0 -0
  163. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_external.py +0 -0
  164. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external.ros2docker.json +0 -0
  165. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external_session.yaml +0 -0
  166. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_communication.sh +0 -0
  167. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_external.py +0 -0
  168. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external.ros2docker.json +0 -0
  169. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external_session.yaml +0 -0
  170. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_communication.sh +0 -0
  171. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_external.py +0 -0
  172. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external.ros2docker.json +0 -0
  173. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external_session.yaml +0 -0
  174. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_communication.sh +0 -0
  175. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_external.py +0 -0
  176. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/10_restamp/session-definition.yaml +0 -0
  177. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/11_trickle/session-definition.yaml +0 -0
  178. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/12_content_integrity/session-definition.yaml +0 -0
  179. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/13_link_latency/session-definition.yaml +0 -0
  180. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/14_remote_assist_anonymized/session-definition.yaml +0 -0
  181. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/15_remote_assist_anonymized_costmap/session-definition.yaml +0 -0
  182. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/16_remote_assist_anonymized_camera/session-definition.yaml +0 -0
  183. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/17_synthetic_camera_quality/session-definition.yaml +0 -0
  184. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/1_heartbeat/session-definition.yaml +0 -0
  185. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/1_heartbeat_status/session-definition.yaml +0 -0
  186. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/2_native_chatter/session-definition.yaml +0 -0
  187. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/3_comp_occ_grid/session-definition.yaml +0 -0
  188. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/4_comp_occ_grid_zen/session-definition.yaml +0 -0
  189. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/5_sized_payload/session-definition.yaml +0 -0
  190. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/6_sized_payload_zen/session-definition.yaml +0 -0
  191. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/7_latched_static/session-definition.yaml +0 -0
  192. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/8_drop/session-definition.yaml +0 -0
  193. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/9_throttle/session-definition.yaml +0 -0
  194. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/bench_1_1_capacity/session-definition.yaml +0 -0
  195. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/bench_1_2_load_sweep/session-definition.yaml +0 -0
  196. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/bench_1_3_ramp/session-definition.yaml +0 -0
  197. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/bench_1_4_recovery/session-definition.yaml +0 -0
  198. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/examples/sessions/bench_1_5_camera/session-definition.yaml +0 -0
  199. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ros2docker.json.example +0 -0
  200. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/.vscode/c_cpp_properties.json +0 -0
  201. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/.vscode/settings.json +0 -0
  202. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_easy_mode.xml.template +0 -0
  203. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_initial_peer_list.xml.template +0 -0
  204. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v2.xml.template +0 -0
  205. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v3.xml.template +0 -0
  206. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v4.xml.template +0 -0
  207. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v5.xml.template +0 -0
  208. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/create_zenoh_json5.py +0 -0
  209. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/cyclonedds_local_participants.xml.template +0 -0
  210. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/cyclonedds_minimal.xml.template +0 -0
  211. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/cyclonedds_tuned.xml.template +0 -0
  212. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/fastdds_unicast.xml.template +0 -0
  213. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/get_ota_xml.py +0 -0
  214. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/utils.py +0 -0
  215. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ota_configs/zenoh.json5.template +0 -0
  216. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/.gitkeep +0 -0
  217. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_cpp/CMakeLists.txt +0 -0
  218. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_cpp/COLCON_IGNORE +0 -0
  219. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_cpp/LICENSE.txt +0 -0
  220. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_cpp/package.xml +0 -0
  221. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_cpp/src/hz_monitor.cpp +0 -0
  222. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_cpp/src/universal_compressor.cpp +0 -0
  223. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_cpp/src/universal_decompressor.cpp +0 -0
  224. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_msgs/CMakeLists.txt +0 -0
  225. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_msgs/LICENSE.txt +0 -0
  226. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/CompressedData.msg +0 -0
  227. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/EchoHeartbeat.msg +0 -0
  228. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/OtaStamped.msg +0 -0
  229. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/SizedPayload.msg +0 -0
  230. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_msgs/package.xml +0 -0
  231. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/LICENSE.txt +0 -0
  232. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/__init__.py +0 -0
  233. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/base_bridge_relay.py +0 -0
  234. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/bridge_in.py +0 -0
  235. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/bridge_out.py +0 -0
  236. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/ffmpeg_flags.py +0 -0
  237. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/heartbeat_echo.py +0 -0
  238. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/hz_monitor.py +0 -0
  239. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/image_bandwidth_reducer.py +0 -0
  240. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/image_pixel_capper.py +0 -0
  241. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/lanelet_saver.py +0 -0
  242. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/latch_relay.py +0 -0
  243. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/costmap_remapper.py +0 -0
  244. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/marker_remapper.py +0 -0
  245. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/speed_test.py +0 -0
  246. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/tf_remapper.py +0 -0
  247. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/traffic_monitor.py +0 -0
  248. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/wifi_network_analysis_1.py +0 -0
  249. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/link_bytes.py +0 -0
  250. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/link_trace.py +0 -0
  251. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/local_global_frame_bridge.py +0 -0
  252. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/localization_map_split_reduce_saver.py +0 -0
  253. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/msg_transform.py +0 -0
  254. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/pair_management.py +0 -0
  255. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/playout_pacer.py +0 -0
  256. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/playout_pacer_core.py +0 -0
  257. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/pub_sub_pair.py +0 -0
  258. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/qos.py +0 -0
  259. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/relay_in.py +0 -0
  260. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/relay_out.py +0 -0
  261. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/restamp.py +0 -0
  262. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/sized_publisher.py +0 -0
  263. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency.py +0 -0
  264. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency_core.py +0 -0
  265. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/status_overview.py +0 -0
  266. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/status_overview_core.py +0 -0
  267. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/tf_map_to_cart_saver.py +0 -0
  268. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/topic_monitor.py +0 -0
  269. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/topic_resolution.py +0 -0
  270. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/trickle.py +0 -0
  271. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_compressor.py +0 -0
  272. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_decompressor.py +0 -0
  273. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_unwrapper.py +0 -0
  274. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_wrapper.py +0 -0
  275. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/package.xml +0 -0
  276. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/resource/com_py +0 -0
  277. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/setup.cfg +0 -0
  278. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/setup.py +0 -0
  279. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/test/test_copyright.py +0 -0
  280. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/test/test_flake8.py +0 -0
  281. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/ros2src/com_py/test/test_pep257.py +0 -0
  282. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/content/network/up_connectivity.py +0 -0
  283. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/content/network/wifi_network_analysis_2.py +0 -0
  284. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/creation/anonymize_bag.py +0 -0
  285. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/creation/catmux_log_setup.sh +0 -0
  286. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/creation/create_session_yaml.py +0 -0
  287. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/creation/run_session.py +0 -0
  288. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py +0 -0
  289. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/resources/ws/session/creation/strip_ansi.py +0 -0
  290. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/status_eval.py +0 -0
  291. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/stream_stats.py +0 -0
  292. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/trace_profiles.py +0 -0
  293. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/transit.py +0 -0
  294. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom/video_quality.py +0 -0
  295. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/dependency_links.txt +0 -0
  296. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/entry_points.txt +0 -0
  297. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/requires.txt +0 -0
  298. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/src/rosotacom_dev.egg-info/top_level.txt +0 -0
  299. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/terminology.md +0 -0
  300. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_benched_set_registry.py +0 -0
  301. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_findings.py +0 -0
  302. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_markdown_links.py +0 -0
  303. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_ota_source_bundle.py +0 -0
  304. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_package_metadata.py +0 -0
  305. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_packaged_resources.py +0 -0
  306. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_profile_covers_generated_commands.py +0 -0
  307. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_pytest_policy.py +0 -0
  308. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_readme_examples.py +0 -0
  309. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_security_maintenance_config.py +0 -0
  310. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_session_configuration_doc.py +0 -0
  311. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_session_template_substitution.py +0 -0
  312. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/contract/test_workflow_contracts.py +0 -0
  313. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/conftest.py +0 -0
  314. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_anonymize_e2e.py +0 -0
  315. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_benchmark_ab.py +0 -0
  316. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_benchmark_capacity.py +0 -0
  317. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_benchmark_replay.py +0 -0
  318. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_parallel_smoke.py +0 -0
  319. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_smoke.py +0 -0
  320. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_timeline_stepping.py +0 -0
  321. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/e2e/test_video_quality_e2e.py +0 -0
  322. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/sessions/generate_rmw_matrix.py +0 -0
  323. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-local_zenoh-ros2dds-ota/session-definition.yaml +0 -0
  324. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-ota/session-definition.yaml +0 -0
  325. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-ota-tuned/session-definition.yaml +0 -0
  326. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/sessions/rmw_matrix/1_heartbeat_fastdds/session-definition.yaml +0 -0
  327. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/sessions/rmw_matrix/1_heartbeat_zen-endpoints/session-definition.yaml +0 -0
  328. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_anonymize.py +0 -0
  329. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_bag_ground_truth.py +0 -0
  330. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_benched_set.py +0 -0
  331. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_benchmark.py +0 -0
  332. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_benchmark_delta.py +0 -0
  333. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_bundle_check.py +0 -0
  334. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_catmux_pane_logging.py +0 -0
  335. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_cli.py +0 -0
  336. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_cli_benchmark.py +0 -0
  337. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_concurrency.py +0 -0
  338. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_deployment.py +0 -0
  339. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_ffmpeg_flags.py +0 -0
  340. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_ffmpeg_packet.py +0 -0
  341. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_field_profiles.py +0 -0
  342. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_forensics.py +0 -0
  343. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_geomap.py +0 -0
  344. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_image_cache.py +0 -0
  345. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_link_bytes.py +0 -0
  346. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_link_trace.py +0 -0
  347. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_netem_dist.py +0 -0
  348. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_network_profiles.py +0 -0
  349. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_network_shaper.py +0 -0
  350. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_ota_unwrapper.py +0 -0
  351. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_ota_wrapper.py +0 -0
  352. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_ota_xml.py +0 -0
  353. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_playout_pacer.py +0 -0
  354. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_plots.py +0 -0
  355. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_qos_fallback_logging.py +0 -0
  356. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_relay_optional_parameters.py +0 -0
  357. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_stage_latency.py +0 -0
  358. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_status_eval.py +0 -0
  359. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_status_overview.py +0 -0
  360. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_status_overview_observer.py +0 -0
  361. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_stream_stats.py +0 -0
  362. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_trace_profiles.py +0 -0
  363. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_transit.py +0 -0
  364. {rosotacom_dev-2.5.dev48 → rosotacom_dev-2.5.dev50}/tests/unit/test_video_quality.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosotacom-dev
3
- Version: 2.5.dev48
3
+ Version: 2.5.dev50
4
4
  Summary: Host CLI for ROS OTA communication sessions.
5
5
  Author: develNor
6
6
  License-Expression: BSD-3-Clause
@@ -266,6 +266,17 @@ risk — a stuck `qdisc` silently corrupts every later result on the machine.
266
266
  and outage steps, plus static distillation from a full trace or window
267
267
  (`trace_profiles.convert_trace_to_profile_yaml`). Passive throughput is omitted
268
268
  as `rate` unless the sample is marked saturated/probed/capacity-like.
269
+ - [x] Shape without host sudo (issue #279): `--sudo-mode container` runs every
270
+ tc/ip argv in a short-lived `docker run --rm --network host --cap-add
271
+ NET_ADMIN` container on the peer, so docker-group membership is the whole
272
+ privilege. `--network host` keeps the target identical to the sudo modes
273
+ (the host netns is the netns the OTA com containers use); the safety
274
+ watchdog detaches as `docker run -d` under a fixed name and replaces a
275
+ stale predecessor at arm time; the image is `--shaping-image` or resolved
276
+ on the peer (running `rosotacom_` container's image, else the newest local
277
+ `ros-communication*` image, else an actionable exit-69). The lab benchmark
278
+ path needed none of this — it has always armed tc inside the peer
279
+ containers' own netns (`--cap-add NET_ADMIN` on the isolated network).
269
280
  - [ ] Confirm the `/proc/net/dev` link sampler (RFC 0003 / `link_bytes.py`) reports
270
281
  post-shaping wire bytes so link-overhead stays meaningful under a profile. *(Bench
271
282
  check.)*
@@ -342,6 +353,13 @@ off during implementation). Notes whether automation is feasible; privileged
342
353
  load generated YAML through `load_profiles_file`, and feed the resulting
343
354
  timeline into dry-run `ProfileShaper` command generation
344
355
  (`tests/unit/test_trace_profiles.py`). Done.
356
+ - [x] **Sudo-free container shaping** (`--sudo-mode container`) — host unit tests
357
+ pin the generated shell: host-netns + NET_ADMIN on every tc/ip run, the
358
+ watchdog's `docker run -d` detachment and stale-predecessor replacement, the
359
+ on-peer image resolution with its actionable failure, and that the watchdog
360
+ name stays out of the `rosotacom_` conflict scan
361
+ (`tests/unit/test_container_shaping.py`). Arming it against a live remote pair
362
+ stays a bench check (same standing as the sudo modes' live half).
345
363
  - [ ] **Emulated-profile gate (rung 2)** — an example session run under one
346
364
  canonical profile in the per-promotion CI smoke, asserting a conditional bound
347
365
  that differs from the unshaped run. Automatable in the smoke matrix.
@@ -568,13 +568,39 @@ number being reported.
568
568
  - `bit_rate` (int)
569
569
  - `encoder_av_options` (string)
570
570
 
571
- #### Receiver-side playout pacing (manual node)
571
+ #### Receiver-side playout pacing
572
572
 
573
573
  A receiver that hands every packet to the decoder the moment it arrives turns
574
574
  network delay jitter into display stutter. `com_py playout_pacer` re-times a
575
575
  received packet stream to `stamp + budget` (budget adaptive above the fastest
576
576
  observed path, clock-offset-proof; late packets pass through immediately, order
577
- always preserved — the decoder chain sees every packet):
577
+ always preserved — the decoder chain sees every packet). On the 2026-08-17 CCNG
578
+ field trace this removes ~95 % of delay-caused >200 ms display gaps at ~100 ms
579
+ median added age (adaptive mode).
580
+
581
+ Declaratively, add a `playout` block to the transport:
582
+
583
+ ```yaml
584
+ transport:
585
+ type: "ffmpeg"
586
+ gop_size: 3
587
+ remote_republish: compressed
588
+ playout: # receiver-side; requires remote_republish
589
+ adaptive: true # default true; false uses target_ms as a fixed budget
590
+ target_ms: 350 # fixed budget when adaptive: false
591
+ min_ms: 100 # adaptive clamp above the observed delay floor
592
+ max_ms: 800
593
+ ```
594
+
595
+ The receiving peer then runs the pacer in its own `PACE` window against the
596
+ arrived encoded stream, and the reverse republish decodes `<encoded>/paced`
597
+ instead of the raw arrival — the delivered topic name is unchanged, which is
598
+ why `playout` requires `remote_republish`: without a republish the pacer's
599
+ `/paced` suffix would rename what the receiving application subscribes to.
600
+ The sender's own preview decode (`local_republish`) is never paced — it reads
601
+ the local copy of what was just encoded, so there is no jitter to absorb.
602
+
603
+ The node also runs standalone against any packet stream:
578
604
 
579
605
  ```bash
580
606
  ros2 run com_py playout_pacer --ros-args \
@@ -583,11 +609,7 @@ ros2 run com_py playout_pacer --ros-args \
583
609
  ```
584
610
 
585
611
  It republishes on `<topic>/paced` plus `.../paced/budget_ms` and
586
- `.../paced/queue_depth` debug topics; point the reverse republish (or any
587
- decoder) at the paced name. On the 2026-08-17 CCNG field trace this removes
588
- ~95 % of delay-caused >200 ms display gaps at ~100 ms median added age
589
- (adaptive mode). Declarative wiring as a `transport.playout` block is tracked
590
- in issue #284.
612
+ `.../paced/queue_depth` debug topics.
591
613
 
592
614
  `type: foxglove` supports (CompressedVideo):
593
615
  - `gop_size` (int)
@@ -121,7 +121,15 @@ EXTERNAL_SESSION_CONTAINER_DIR = "/session/current"
121
121
  # `anonymize_bag.py` two thousand lines below already does it this way.
122
122
  RUN_SESSION_CONTAINER_ARGV = ("python3", "/ws/session/creation/run_session.py")
123
123
  DEFAULT_SMOKE_SESSION = "1_heartbeat"
124
- OTA_SUDO_MODES = ("passwordless", "askpass")
124
+ # How profile shaping obtains tc/ip privileges on a peer. `passwordless` and
125
+ # `askpass` are the two sudo shapes. `container` needs no sudo at all: every
126
+ # tc/ip argv runs in a short-lived `docker run --rm --network host --cap-add
127
+ # NET_ADMIN` container on the peer, so docker-group membership is the whole
128
+ # privilege — the mode that makes profiles runnable on a machine that has
129
+ # Docker but no passwordless sudo (issue #279). `--network host` puts tc in
130
+ # the host netns, which is the netns the OTA com containers use, so it shapes
131
+ # exactly the interface sudo would.
132
+ OTA_SUDO_MODES = ("passwordless", "askpass", "container")
125
133
 
126
134
  # How ota-smoke puts rosotacom on each peer.
127
135
  #
@@ -2663,6 +2671,59 @@ def _sudo_command(argv: Sequence[str], *, askpass: bool = False) -> str:
2663
2671
  return f"{mode} {shlex.join(list(argv))}"
2664
2672
 
2665
2673
 
2674
+ #: Deterministic name for the container-mode safety watchdog on a peer. Fixed
2675
+ #: (not per-run) on purpose: arming replaces a stale predecessor with
2676
+ #: `docker rm -f`, so a crashed run's watchdog cannot accumulate — and the
2677
+ #: dash spelling keeps it out of the `rosotacom_` conflict scan, whose signal
2678
+ #: for a live run is the qdisc itself, not the sleeping watchdog.
2679
+ _SHAPING_WATCHDOG_NAME = "rosotacom-shaping-watchdog"
2680
+
2681
+
2682
+ def _shaping_image_resolution(image: str | None) -> str:
2683
+ """Shell preamble that leaves the shaping image in ``$img`` on the peer.
2684
+
2685
+ An explicit image wins. Otherwise resolve on the peer itself, because the
2686
+ scoped image name (`<base>-<install_id>`) is a property of the peer's own
2687
+ install that the orchestrator cannot know before that install ran: prefer
2688
+ the image of a running `rosotacom_` container, fall back to the newest
2689
+ local `ros-communication*` image. Exit 69 with an actionable message when
2690
+ neither exists (a machine that never ran rosotacom): shaping needs *an*
2691
+ image with iproute2, and the com image is the one guaranteed relevant."""
2692
+ if image:
2693
+ return f"img={shlex.quote(image)}; "
2694
+ return (
2695
+ "img=\"$(docker ps --filter name=rosotacom_ --format '{{.Image}}' | head -n1)\"; "
2696
+ '[ -n "$img" ] || img="$(docker image ls --format \'{{.Repository}}:{{.Tag}}\''
2697
+ " | grep -m1 '^ros-communication' || true)\"; "
2698
+ '[ -n "$img" ] || { echo "no shaping image on this peer: run rosotacom here once'
2699
+ ' (any smoke/start builds the com image) or pass --shaping-image" >&2; exit 69; }; '
2700
+ )
2701
+
2702
+
2703
+ def _container_shaping_command(argv: Sequence[str], *, image: str | None, watchdog: bool = False) -> str:
2704
+ """One privileged tc/ip argv as a short-lived NET_ADMIN container run.
2705
+
2706
+ ``--network host`` is the point: the ephemeral container shares the host
2707
+ netns, so ``tc`` acts on the same interface sudo would — but the privilege
2708
+ comes from docker-group membership instead of a sudoers entry. A watchdog
2709
+ argv (``sh -c 'sleep …; tc …'``) runs detached (``-d``) under the fixed
2710
+ name above, replacing any stale predecessor; docker itself keeps it alive
2711
+ across an orchestrator crash, which is the fail-safe RFC 0004 demands."""
2712
+ argv = list(argv)
2713
+ resolve = _shaping_image_resolution(image)
2714
+ run = "docker run --rm --network host --cap-add NET_ADMIN"
2715
+ if watchdog:
2716
+ run = (
2717
+ f"docker rm -f {_SHAPING_WATCHDOG_NAME} >/dev/null 2>&1 || true; "
2718
+ f"docker run -d --rm --name {_SHAPING_WATCHDOG_NAME} --network host --cap-add NET_ADMIN"
2719
+ )
2720
+ if len(argv) >= 3 and argv[0] == "sh" and argv[1] == "-c":
2721
+ tail = f'--entrypoint sh "$img" -c {shlex.quote(str(argv[2]))}'
2722
+ else:
2723
+ tail = f'--entrypoint {shlex.quote(argv[0])} "$img" {shlex.join(argv[1:])}'
2724
+ return f"{resolve}{run} {tail}"
2725
+
2726
+
2666
2727
  def _passwordless_watchdog_command(argv: Sequence[str]) -> str:
2667
2728
  argv = list(argv)
2668
2729
  if len(argv) >= 3 and argv[0] == "sh" and argv[1] == "-c":
@@ -2744,6 +2805,7 @@ def _ota_preflight(
2744
2805
  require_network_shaping_sudo: bool = False,
2745
2806
  sudo_mode: str = OTA_DEFAULT_SUDO_MODE,
2746
2807
  sudo_passwords: Mapping[str, str] | None = None,
2808
+ shaping_image: str | None = None,
2747
2809
  check_conflicts: bool = True,
2748
2810
  ) -> None:
2749
2811
  sudo_mode = _validate_ota_sudo_mode(sudo_mode)
@@ -2771,6 +2833,27 @@ def _ota_preflight(
2771
2833
  )
2772
2834
  _ota_run(peer, "docker ps >/dev/null", label=f"{peer.name}: docker access", dry_run=dry_run, batch=True)
2773
2835
  if require_network_shaping_sudo:
2836
+ if sudo_mode == "container":
2837
+ # tc/ip live in the shaping image, not on the host, and no sudo
2838
+ # is involved: the one thing to prove is that a NET_ADMIN
2839
+ # host-netns container can run and sees the tools. This also
2840
+ # fails early — with the actionable exit-69 message — on a
2841
+ # machine that has no candidate image yet.
2842
+ _ota_run(
2843
+ peer,
2844
+ _container_shaping_command(
2845
+ [
2846
+ "sh",
2847
+ "-c",
2848
+ "command -v tc >/dev/null && command -v ip >/dev/null && tc qdisc show >/dev/null",
2849
+ ],
2850
+ image=shaping_image,
2851
+ ),
2852
+ label=f"{peer.name}: containerized network shaping (docker run, NET_ADMIN, tc in image)",
2853
+ dry_run=dry_run,
2854
+ batch=True,
2855
+ )
2856
+ continue
2774
2857
  _ota_run(
2775
2858
  peer,
2776
2859
  "command -v tc >/dev/null 2>&1 && command -v ip >/dev/null 2>&1",
@@ -3241,13 +3324,21 @@ def _ota_resolve_interfaces(peer: OtaSmokePeer, peer_addr: str, *, dry_run: bool
3241
3324
 
3242
3325
 
3243
3326
  def _peer_command_runner(
3244
- peer: OtaSmokePeer, *, dry_run: bool, sudo_password: str | None = None
3327
+ peer: OtaSmokePeer,
3328
+ *,
3329
+ dry_run: bool,
3330
+ sudo_password: str | None = None,
3331
+ sudo_mode: str = OTA_DEFAULT_SUDO_MODE,
3332
+ shaping_image: str | None = None,
3245
3333
  ) -> Callable[[Sequence[str]], None]:
3246
3334
  """A CommandRunner that runs one privileged argv on ``peer`` via the SSH path."""
3247
3335
 
3248
3336
  def run(argv: Sequence[str]) -> None:
3249
3337
  label = f"{peer.name}: tc/netem"
3250
- if sudo_password is None:
3338
+ if sudo_mode == "container":
3339
+ script = _container_shaping_command(argv, image=shaping_image)
3340
+ _ota_run(peer, script, label=label, dry_run=dry_run)
3341
+ elif sudo_password is None:
3251
3342
  script = _sudo_command(argv)
3252
3343
  _ota_run(peer, script, label=label, dry_run=dry_run)
3253
3344
  else:
@@ -3265,12 +3356,23 @@ def _peer_command_runner(
3265
3356
 
3266
3357
 
3267
3358
  def _peer_watchdog_launcher(
3268
- peer: OtaSmokePeer, *, dry_run: bool, sudo_password: str | None = None
3359
+ peer: OtaSmokePeer,
3360
+ *,
3361
+ dry_run: bool,
3362
+ sudo_password: str | None = None,
3363
+ sudo_mode: str = OTA_DEFAULT_SUDO_MODE,
3364
+ shaping_image: str | None = None,
3269
3365
  ) -> Callable[[Sequence[str]], None]:
3270
3366
  """Launch the safety-watchdog argv detached on ``peer`` so it survives a crash."""
3271
3367
 
3272
3368
  def launch(argv: Sequence[str]) -> None:
3273
- if sudo_password is None:
3369
+ if sudo_mode == "container":
3370
+ # `docker run -d` is the detachment: the daemon owns the sleeping
3371
+ # watchdog, so it survives the SSH session, the orchestrator, and
3372
+ # every rosotacom container on the machine.
3373
+ detached = _container_shaping_command(argv, image=shaping_image, watchdog=True)
3374
+ _ota_run(peer, detached, label=f"{peer.name}: profile safety watchdog", dry_run=dry_run, check=False)
3375
+ elif sudo_password is None:
3274
3376
  detached = _passwordless_watchdog_command(argv)
3275
3377
  _ota_run(peer, detached, label=f"{peer.name}: profile safety watchdog", dry_run=dry_run, check=False)
3276
3378
  else:
@@ -3297,6 +3399,8 @@ def _ota_arm_profile(
3297
3399
  *,
3298
3400
  dry_run: bool,
3299
3401
  sudo_passwords: Mapping[str, str] | None = None,
3402
+ sudo_mode: str = OTA_DEFAULT_SUDO_MODE,
3403
+ shaping_image: str | None = None,
3300
3404
  ) -> list[Any]:
3301
3405
  """Arm the static profile per direction on every peer's OTA egress (RFC 0004),
3302
3406
  returning the ``ProfileShaper`` handles to revert in the run's ``finally``."""
@@ -3320,10 +3424,22 @@ def _ota_arm_profile(
3320
3424
  sudo_password = (sudo_passwords or {}).get(peer_name)
3321
3425
  shaper = ProfileShaper(
3322
3426
  ota_iface,
3323
- _peer_command_runner(peer, dry_run=dry_run, sudo_password=sudo_password),
3427
+ _peer_command_runner(
3428
+ peer,
3429
+ dry_run=dry_run,
3430
+ sudo_password=sudo_password,
3431
+ sudo_mode=sudo_mode,
3432
+ shaping_image=shaping_image,
3433
+ ),
3324
3434
  control_interface=control_iface,
3325
3435
  safety_max_duration_s=_PROFILE_SAFETY_MAX_S,
3326
- watchdog_launcher=_peer_watchdog_launcher(peer, dry_run=dry_run, sudo_password=sudo_password),
3436
+ watchdog_launcher=_peer_watchdog_launcher(
3437
+ peer,
3438
+ dry_run=dry_run,
3439
+ sudo_password=sudo_password,
3440
+ sudo_mode=sudo_mode,
3441
+ shaping_image=shaping_image,
3442
+ ),
3327
3443
  )
3328
3444
  # Register before arming so a mid-arm failure is still reverted in `finally`.
3329
3445
  shapers.append(shaper)
@@ -4085,7 +4201,20 @@ def _start_noninteractive_ota_smoke(args: argparse.Namespace) -> int:
4085
4201
  errors: list[str] = []
4086
4202
  try:
4087
4203
  if profile is not None:
4088
- shapers = _ota_arm_profile(plan, profile, directions, dry_run=dry_run)
4204
+ shapers = _ota_arm_profile(
4205
+ plan,
4206
+ profile,
4207
+ directions,
4208
+ dry_run=dry_run,
4209
+ sudo_passwords=_ota_network_sudo_passwords(
4210
+ plan,
4211
+ sudo_mode=getattr(args, "sudo_mode", OTA_DEFAULT_SUDO_MODE),
4212
+ require_network_shaping_sudo=True,
4213
+ dry_run=dry_run,
4214
+ ),
4215
+ sudo_mode=getattr(args, "sudo_mode", OTA_DEFAULT_SUDO_MODE),
4216
+ shaping_image=getattr(args, "shaping_image", None),
4217
+ )
4089
4218
  _ota_start_peers(
4090
4219
  target,
4091
4220
  plan,
@@ -9039,6 +9168,24 @@ def main(argv: list[str] | None = None) -> int:
9039
9168
  action="store_true",
9040
9169
  help="Keep staged source and project files after cleanup.",
9041
9170
  )
9171
+ ota_smoke_parser.add_argument(
9172
+ "--sudo-mode",
9173
+ choices=OTA_SUDO_MODES,
9174
+ default=OTA_DEFAULT_SUDO_MODE,
9175
+ help=(
9176
+ "How profile shaping obtains tc/ip privileges on the peers "
9177
+ "(passwordless: require sudo -n; askpass: prompt locally per peer and feed sudo -S; "
9178
+ "container: no sudo -- run each tc/ip in a short-lived NET_ADMIN host-netns container, "
9179
+ "for peers with Docker but no passwordless sudo)."
9180
+ ),
9181
+ )
9182
+ ota_smoke_parser.add_argument(
9183
+ "--shaping-image",
9184
+ help=(
9185
+ "Image for --sudo-mode container (must contain tc/ip). Default: resolve on the peer -- "
9186
+ "the image of a running rosotacom_ container, else the newest local ros-communication* image."
9187
+ ),
9188
+ )
9042
9189
  ota_smoke_parser.add_argument("--skip-preflight", action="store_true", help="Skip SSH/Docker readiness checks.")
9043
9190
  ota_smoke_parser.add_argument(
9044
9191
  "--skip-conflict-check",
@@ -4981,8 +4981,17 @@ def _add_benchmark_common_args(parser: argparse.ArgumentParser, *, ota_benchmark
4981
4981
  choices=OTA_SUDO_MODES,
4982
4982
  default=OTA_DEFAULT_SUDO_MODE,
4983
4983
  help=(
4984
- "How OTA benchmark profile shaping obtains sudo for tc/ip "
4985
- "(passwordless: require sudo -n; askpass: prompt locally per peer and feed sudo -S)."
4984
+ "How OTA benchmark profile shaping obtains tc/ip privileges "
4985
+ "(passwordless: require sudo -n; askpass: prompt locally per peer and feed sudo -S; "
4986
+ "container: no sudo -- run each tc/ip in a short-lived NET_ADMIN host-netns container, "
4987
+ "for peers with Docker but no passwordless sudo)."
4988
+ ),
4989
+ )
4990
+ parser.add_argument(
4991
+ "--shaping-image",
4992
+ help=(
4993
+ "Image for --sudo-mode container (must contain tc/ip). Default: resolve on the peer -- "
4994
+ "the image of a running rosotacom_ container, else the newest local ros-communication* image."
4986
4995
  ),
4987
4996
  )
4988
4997
  parser.add_argument("--interactive", action="store_true", help="Open a tmux operator view for this benchmark run.")
@@ -5148,6 +5157,7 @@ def _make_live_run_point(args: argparse.Namespace, session_name: str) -> RunPoin
5148
5157
  profile=profile,
5149
5158
  benchmark_stepping=True,
5150
5159
  sudo_mode=getattr(args, "sudo_mode", "passwordless"),
5160
+ shaping_image=getattr(args, "shaping_image", None),
5151
5161
  )
5152
5162
 
5153
5163
  runtime, plan, target = _resolve_ota_smoke_context(smoke_args)
@@ -5170,6 +5180,7 @@ def _make_live_run_point(args: argparse.Namespace, session_name: str) -> RunPoin
5170
5180
  require_network_shaping_sudo=profile_obj is not None,
5171
5181
  sudo_mode=smoke_args.sudo_mode,
5172
5182
  sudo_passwords=sudo_passwords,
5183
+ shaping_image=smoke_args.shaping_image,
5173
5184
  check_conflicts=not getattr(args, "skip_conflict_check", False),
5174
5185
  )
5175
5186
  _ota_prepare_hosts(smoke_args, runtime, plan)
@@ -5210,12 +5221,20 @@ def _make_live_run_point(args: argparse.Namespace, session_name: str) -> RunPoin
5210
5221
  shaper = ProfileShaper(
5211
5222
  ota_iface,
5212
5223
  _peer_command_runner(
5213
- peer, dry_run=dry_run, sudo_password=sudo_passwords.get(peer_name)
5224
+ peer,
5225
+ dry_run=dry_run,
5226
+ sudo_password=sudo_passwords.get(peer_name),
5227
+ sudo_mode=smoke_args.sudo_mode,
5228
+ shaping_image=smoke_args.shaping_image,
5214
5229
  ),
5215
5230
  control_interface=control_iface,
5216
5231
  safety_max_duration_s=_PROFILE_SAFETY_MAX_S,
5217
5232
  watchdog_launcher=_peer_watchdog_launcher(
5218
- peer, dry_run=dry_run, sudo_password=sudo_passwords.get(peer_name)
5233
+ peer,
5234
+ dry_run=dry_run,
5235
+ sudo_password=sudo_passwords.get(peer_name),
5236
+ sudo_mode=smoke_args.sudo_mode,
5237
+ shaping_image=smoke_args.shaping_image,
5219
5238
  ),
5220
5239
  )
5221
5240
  shapers.append(shaper)
@@ -5226,7 +5245,13 @@ def _make_live_run_point(args: argparse.Namespace, session_name: str) -> RunPoin
5226
5245
  shaper.arm([])
5227
5246
  else:
5228
5247
  shapers = _ota_arm_profile(
5229
- plan, profile_obj, directions, dry_run=dry_run, sudo_passwords=sudo_passwords
5248
+ plan,
5249
+ profile_obj,
5250
+ directions,
5251
+ dry_run=dry_run,
5252
+ sudo_passwords=sudo_passwords,
5253
+ sudo_mode=smoke_args.sudo_mode,
5254
+ shaping_image=smoke_args.shaping_image,
5230
5255
  )
5231
5256
 
5232
5257
  _ota_start_peers(target, plan, instance.instance_id, dry_run=dry_run)
@@ -232,15 +232,49 @@ parameters:
232
232
  irt_1_topic: # e.g. /sensors/camera/front_wide/image_color
233
233
  irt_1_transport: # e.g. raw
234
234
  irt_1_out_transport: compressed # e.g. raw, compressed – determines the output remap format
235
+ irt_1_paced: # "true" -> decode reads ${irt_1_topic}/paced (playout pacer, PACE window)
235
236
  irt_2_topic: # e.g. /sensors/front_camera/image_raw/compressed
236
237
  irt_2_transport: # e.g. raw
237
238
  irt_2_out_transport: compressed # e.g. raw, compressed – determines the output remap format
239
+ irt_2_paced: # "true" -> decode reads ${irt_2_topic}/paced
238
240
  irt_3_topic: # e.g. /sensor/front_camera/image_color/compressed
239
241
  irt_3_transport: # e.g. raw
240
242
  irt_3_out_transport: compressed # e.g. raw, compressed – determines the output remap format
243
+ irt_3_paced: # "true" -> decode reads ${irt_3_topic}/paced
241
244
  irt_4_topic: # e.g. /sensor/front_camera/image_color/compressed
242
245
  irt_4_transport: # e.g. raw
243
246
  irt_4_out_transport: compressed # e.g. raw, compressed – determines the output remap format
247
+ irt_4_paced: # "true" -> decode reads ${irt_4_topic}/paced
248
+
249
+ # Receiver-side playout pacing (transport.playout in the session definition):
250
+ # re-times the arrived encoded stream on '<topic>/paced' so the decode sees a
251
+ # steady cadence instead of the network's delay jitter. Never reorders, never
252
+ # drops; late packets pass through immediately.
253
+ pace: false
254
+ pace_1_topic: # the arrived encoded stream, e.g. /camera/image/ffmpeg
255
+ pace_1_msg_type: # e.g. ffmpeg_image_transport_msgs/msg/FFMPEGPacket
256
+ pace_1_adaptive: # "true" | "false"
257
+ pace_1_target_ms: # fixed budget when adaptive is "false"
258
+ pace_1_min_ms: # adaptive clamp above the observed delay floor
259
+ pace_1_max_ms:
260
+ pace_2_topic:
261
+ pace_2_msg_type:
262
+ pace_2_adaptive:
263
+ pace_2_target_ms:
264
+ pace_2_min_ms:
265
+ pace_2_max_ms:
266
+ pace_3_topic:
267
+ pace_3_msg_type:
268
+ pace_3_adaptive:
269
+ pace_3_target_ms:
270
+ pace_3_min_ms:
271
+ pace_3_max_ms:
272
+ pace_4_topic:
273
+ pace_4_msg_type:
274
+ pace_4_adaptive:
275
+ pace_4_target_ms:
276
+ pace_4_min_ms:
277
+ pace_4_max_ms:
244
278
 
245
279
  common:
246
280
  before_commands:
@@ -623,6 +657,30 @@ windows:
623
657
  - args+=(--remap "${in_remap}:=${it_4_topic}" --remap "out/${it_4_transport}:=${it_4_topic}/${outgoing_suffix}")
624
658
  - echo ${args[@]}
625
659
  - ${args[@]}
660
+ - name: PACE
661
+ if: pace
662
+ layout: even-vertical
663
+ splits:
664
+ - commands:
665
+ - if [ '${pace_1_topic}' = "None" ]; then exit; fi
666
+ - args=(ros2 run com_py playout_pacer --ros-args -p topic:=${pace_1_topic} -p msg_type:=${pace_1_msg_type} -p adaptive:=${pace_1_adaptive} -p target_ms:=${pace_1_target_ms} -p min_ms:=${pace_1_min_ms} -p max_ms:=${pace_1_max_ms} -p qos_config_file:='${qos_config_file}')
667
+ - echo ${args[@]}
668
+ - ${args[@]}
669
+ - commands:
670
+ - if [ '${pace_2_topic}' = "None" ]; then exit; fi
671
+ - args=(ros2 run com_py playout_pacer --ros-args -p topic:=${pace_2_topic} -p msg_type:=${pace_2_msg_type} -p adaptive:=${pace_2_adaptive} -p target_ms:=${pace_2_target_ms} -p min_ms:=${pace_2_min_ms} -p max_ms:=${pace_2_max_ms} -p qos_config_file:='${qos_config_file}')
672
+ - echo ${args[@]}
673
+ - ${args[@]}
674
+ - commands:
675
+ - if [ '${pace_3_topic}' = "None" ]; then exit; fi
676
+ - args=(ros2 run com_py playout_pacer --ros-args -p topic:=${pace_3_topic} -p msg_type:=${pace_3_msg_type} -p adaptive:=${pace_3_adaptive} -p target_ms:=${pace_3_target_ms} -p min_ms:=${pace_3_min_ms} -p max_ms:=${pace_3_max_ms} -p qos_config_file:='${qos_config_file}')
677
+ - echo ${args[@]}
678
+ - ${args[@]}
679
+ - commands:
680
+ - if [ '${pace_4_topic}' = "None" ]; then exit; fi
681
+ - args=(ros2 run com_py playout_pacer --ros-args -p topic:=${pace_4_topic} -p msg_type:=${pace_4_msg_type} -p adaptive:=${pace_4_adaptive} -p target_ms:=${pace_4_target_ms} -p min_ms:=${pace_4_min_ms} -p max_ms:=${pace_4_max_ms} -p qos_config_file:='${qos_config_file}')
682
+ - echo ${args[@]}
683
+ - ${args[@]}
626
684
  - name: IRT
627
685
  if: irt
628
686
  layout: even-vertical
@@ -630,28 +688,32 @@ windows:
630
688
  - commands:
631
689
  - if [ '${irt_1_topic}' = "None" ]; then exit; fi
632
690
  - if [[ "${irt_1_out_transport}" == "raw" ]]; then out_remap="out"; else out_remap="out/${irt_1_out_transport}"; fi
691
+ - if [[ "${irt_1_paced}" != "None" ]]; then irt_in_topic="${irt_1_topic}/paced"; else irt_in_topic="${irt_1_topic}"; fi
633
692
  - args=(ros2 run image_transport republish --ros-args -r __node:=irt_republish1 -p in_transport:=${irt_1_transport} -p out_transport:=${irt_1_out_transport}
634
- --remap in/${irt_1_transport}:=${irt_1_topic} --remap ${out_remap}:=${irt_1_topic}/${irt_1_out_transport})
693
+ --remap in/${irt_1_transport}:=${irt_in_topic} --remap ${out_remap}:=${irt_1_topic}/${irt_1_out_transport})
635
694
  - echo ${args[@]}
636
695
  - ${args[@]}
637
696
  - commands:
638
697
  - if [ '${irt_2_topic}' = "None" ]; then exit; fi
639
698
  - if [[ "${irt_2_out_transport}" == "raw" ]]; then out_remap="out"; else out_remap="out/${irt_2_out_transport}"; fi
699
+ - if [[ "${irt_2_paced}" != "None" ]]; then irt_in_topic="${irt_2_topic}/paced"; else irt_in_topic="${irt_2_topic}"; fi
640
700
  - args=(ros2 run image_transport republish --ros-args -r __node:=irt_republish2 -p in_transport:=${irt_2_transport} -p out_transport:=${irt_2_out_transport}
641
- --remap in/${irt_2_transport}:=${irt_2_topic} --remap ${out_remap}:=${irt_2_topic}/${irt_2_out_transport})
701
+ --remap in/${irt_2_transport}:=${irt_in_topic} --remap ${out_remap}:=${irt_2_topic}/${irt_2_out_transport})
642
702
  - echo ${args[@]}
643
703
  - ${args[@]}
644
704
  - commands:
645
705
  - if [ '${irt_3_topic}' = "None" ]; then exit; fi
646
706
  - if [[ "${irt_3_out_transport}" == "raw" ]]; then out_remap="out"; else out_remap="out/${irt_3_out_transport}"; fi
707
+ - if [[ "${irt_3_paced}" != "None" ]]; then irt_in_topic="${irt_3_topic}/paced"; else irt_in_topic="${irt_3_topic}"; fi
647
708
  - args=(ros2 run image_transport republish --ros-args -r __node:=irt_republish3 -p in_transport:=${irt_3_transport} -p out_transport:=${irt_3_out_transport}
648
- --remap in/${irt_3_transport}:=${irt_3_topic} --remap ${out_remap}:=${irt_3_topic}/${irt_3_out_transport})
709
+ --remap in/${irt_3_transport}:=${irt_in_topic} --remap ${out_remap}:=${irt_3_topic}/${irt_3_out_transport})
649
710
  - echo ${args[@]}
650
711
  - ${args[@]}
651
712
  - commands:
652
713
  - if [ '${irt_4_topic}' = "None" ]; then exit; fi
653
714
  - if [[ "${irt_4_out_transport}" == "raw" ]]; then out_remap="out"; else out_remap="out/${irt_4_out_transport}"; fi
715
+ - if [[ "${irt_4_paced}" != "None" ]]; then irt_in_topic="${irt_4_topic}/paced"; else irt_in_topic="${irt_4_topic}"; fi
654
716
  - args=(ros2 run image_transport republish --ros-args -r __node:=irt_republish4 -p in_transport:=${irt_4_transport} -p out_transport:=${irt_4_out_transport}
655
- --remap in/${irt_4_transport}:=${irt_4_topic} --remap ${out_remap}:=${irt_4_topic}/${irt_4_out_transport})
717
+ --remap in/${irt_4_transport}:=${irt_in_topic} --remap ${out_remap}:=${irt_4_topic}/${irt_4_out_transport})
656
718
  - echo ${args[@]}
657
719
  - ${args[@]}
@@ -92,6 +92,13 @@ class TransportSpec:
92
92
  #: image_transport itself, or one that only forwards or records packets,
93
93
  #: does not want a second copy of every frame.
94
94
  remote_republish: Optional[str] = None
95
+ #: Receiver-side playout pacing (issue #284): re-time the arrived packet
96
+ #: stream to smooth network delay jitter before the reverse republish
97
+ #: decodes it. Keys: adaptive / target_ms / min_ms / max_ms (validated in
98
+ #: the parser; defaults live in the playout_pacer node). Requires
99
+ #: `remote_republish`, because the pacer publishes on `<encoded>/paced`
100
+ #: and only the republish remap keeps the delivered topic name unchanged.
101
+ playout: Optional[Dict[str, Any]] = None
95
102
 
96
103
 
97
104
  @dataclass
@@ -1692,12 +1699,45 @@ def _compute_pipeline(
1692
1699
  )
1693
1700
  return str(value)
1694
1701
 
1695
- params = {k: v for k, v in t.items() if k not in ("type", *republish_keys)}
1702
+ remote_republish = _republish("remote_republish")
1703
+
1704
+ playout: Optional[Dict[str, Any]] = None
1705
+ playout_cfg = t.get("playout")
1706
+ if playout_cfg is not None:
1707
+ if not isinstance(playout_cfg, dict):
1708
+ raise ValueError(f"transport.playout must be a mapping for topic '{entry.base}'")
1709
+ allowed = {"adaptive", "target_ms", "min_ms", "max_ms"}
1710
+ unknown = sorted(set(playout_cfg) - allowed)
1711
+ if unknown:
1712
+ raise ValueError(
1713
+ f"transport.playout for topic '{entry.base}' has unknown keys {unknown}; "
1714
+ f"allowed: {sorted(allowed)}"
1715
+ )
1716
+ if remote_republish is None:
1717
+ raise ValueError(
1718
+ f"transport.playout for topic '{entry.base}' requires remote_republish: "
1719
+ "the pacer publishes on '<encoded>/paced', and only the reverse republish's "
1720
+ "input remap keeps the delivered topic name unchanged. Without a republish "
1721
+ "the receiver would have to subscribe a renamed topic."
1722
+ )
1723
+ for key in ("target_ms", "min_ms", "max_ms"):
1724
+ if key in playout_cfg and (not isinstance(playout_cfg[key], (int, float)) or playout_cfg[key] <= 0):
1725
+ raise ValueError(f"transport.playout.{key} must be a positive number for topic '{entry.base}'")
1726
+ min_ms = float(playout_cfg.get("min_ms", 100.0))
1727
+ max_ms = float(playout_cfg.get("max_ms", 800.0))
1728
+ if max_ms < min_ms:
1729
+ raise ValueError(f"transport.playout.max_ms must be >= min_ms for topic '{entry.base}'")
1730
+ if "adaptive" in playout_cfg and not isinstance(playout_cfg["adaptive"], bool):
1731
+ raise ValueError(f"transport.playout.adaptive must be a boolean for topic '{entry.base}'")
1732
+ playout = dict(playout_cfg)
1733
+
1734
+ params = {k: v for k, v in t.items() if k not in ("type", "playout", *republish_keys)}
1696
1735
  transport = TransportSpec(
1697
1736
  type=ttype,
1698
1737
  params=params,
1699
1738
  local_republish=_republish("local_republish"),
1700
- remote_republish=_republish("remote_republish"),
1739
+ remote_republish=remote_republish,
1740
+ playout=playout,
1701
1741
  )
1702
1742
 
1703
1743
  topic = entry.base
@@ -3201,21 +3241,46 @@ def func(
3201
3241
  # it encoded and (b) the decode of what arrived. Both run on *this* peer;
3202
3242
  # what differs is which copy they decode and which transport the session
3203
3243
  # asked each of them to publish in.
3204
- irt_all: List[Tuple[str, str, str]] = []
3244
+ # The sender's own preview decode is never paced: it reads the local
3245
+ # copy of what it just encoded, so there is no network jitter to
3246
+ # absorb. Playout pacing exists on the receiving side only.
3247
+ irt_all: List[Tuple[str, str, str, Optional[Dict[str, Any]]]] = []
3205
3248
  for t, tspec in irt_items_local:
3206
3249
  assert tspec is not None
3207
- irt_all.append((t, tspec.type, str(tspec.local_republish)))
3208
- irt_all.extend((topic, tspec.type, str(tspec.remote_republish)) for topic, tspec in irt_items_remote)
3250
+ irt_all.append((t, tspec.type, str(tspec.local_republish), None))
3251
+ irt_all.extend(
3252
+ (topic, tspec.type, str(tspec.remote_republish), tspec.playout) for topic, tspec in irt_items_remote
3253
+ )
3209
3254
  _assert_max("irt", irt_all, 4)
3210
3255
  if irt_all:
3211
3256
  items2 = [("irt", True)]
3212
- for i, (topic, _ttype, _out_transport) in enumerate(irt_all, 1):
3257
+ for i, (topic, _ttype, _out_transport, _playout) in enumerate(irt_all, 1):
3213
3258
  items2.append((f"irt_{i}_topic", topic))
3214
- for i, (_topic, ttype, out_transport) in enumerate(irt_all, 1):
3259
+ for i, (_topic, ttype, out_transport, playout) in enumerate(irt_all, 1):
3215
3260
  items2.append((f"irt_{i}_transport", ttype))
3216
3261
  items2.append((f"irt_{i}_out_transport", out_transport))
3262
+ if playout is not None:
3263
+ # Points the decode's input remap at '<topic>/paced'; the
3264
+ # pacer itself runs in the PACE window below.
3265
+ items2.append((f"irt_{i}_paced", "true"))
3217
3266
  blocks.append(PluginBlock("irt", items2))
3218
3267
 
3268
+ paced_entries = [
3269
+ (i, topic, ttype, playout)
3270
+ for i, (topic, ttype, _out_transport, playout) in enumerate(irt_all, 1)
3271
+ if playout is not None
3272
+ ]
3273
+ if paced_entries:
3274
+ items_pace: List[Tuple[str, Any]] = [("pace", True)]
3275
+ for i, topic, ttype, playout in paced_entries:
3276
+ items_pace.append((f"pace_{i}_topic", topic))
3277
+ items_pace.append((f"pace_{i}_msg_type", TRANSPORT_OUTPUT_TYPES[ttype]))
3278
+ items_pace.append((f"pace_{i}_adaptive", "true" if playout.get("adaptive", True) else "false"))
3279
+ items_pace.append((f"pace_{i}_target_ms", float(playout.get("target_ms", 350.0))))
3280
+ items_pace.append((f"pace_{i}_min_ms", float(playout.get("min_ms", 100.0))))
3281
+ items_pace.append((f"pace_{i}_max_ms", float(playout.get("max_ms", 800.0))))
3282
+ blocks.append(PluginBlock("pace", items_pace))
3283
+
3219
3284
  if nor_items:
3220
3285
  items2 = [("nor", True)]
3221
3286
  nor_prefix = f"/{peer_name[remote]}" if bool(inbound_cfg.get("keep_source_prefix", False)) else ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosotacom-dev
3
- Version: 2.5.dev48
3
+ Version: 2.5.dev50
4
4
  Summary: Host CLI for ROS OTA communication sessions.
5
5
  Author: develNor
6
6
  License-Expression: BSD-3-Clause