rosotacom-dev 2.5.dev56__tar.gz → 2.5.dev58__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 (380) hide show
  1. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/PKG-INFO +1 -1
  2. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/session-configuration.md +29 -2
  3. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/playout_pacer.py +17 -3
  4. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/playout_pacer_core.py +16 -3
  5. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/status_overview.py +6 -0
  6. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/status_overview_core.py +68 -4
  7. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/creation/generate_session_files.py +16 -10
  8. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/status_eval.py +11 -2
  9. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/PKG-INFO +1 -1
  10. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/scm_version.json +2 -2
  11. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_generated_topics_have_a_producer.py +30 -4
  12. rosotacom_dev-2.5.dev58/tests/unit/test_playout_pacer.py +167 -0
  13. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_session_pipeline.py +51 -0
  14. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_status_eval.py +41 -0
  15. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_status_overview.py +124 -0
  16. rosotacom_dev-2.5.dev56/tests/unit/test_playout_pacer.py +0 -81
  17. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.devcontainer/devcontainer.json.template +0 -0
  18. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.devcontainer/re_generate_devcontainer_json.py +0 -0
  19. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/CODEOWNERS +0 -0
  20. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/documentation-audit.md +0 -0
  21. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/implementation-cleanup.md +0 -0
  22. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/quality-workflow.md +0 -0
  23. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/redesign.md +0 -0
  24. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/release-process.md +0 -0
  25. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/repository-diagnosis.md +0 -0
  26. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/test-ci-audit.md +0 -0
  27. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/ISSUE_TEMPLATE/work-item.md +0 -0
  28. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/actions/setup-just/action.yml +0 -0
  30. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/dependabot.yml +0 -0
  31. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/benchmark-calibrate.yml +0 -0
  32. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/benchmark-gate.yml +0 -0
  33. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/dev-release.yml +0 -0
  34. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/image-scan.yml +0 -0
  35. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/nightly-e2e.yml +0 -0
  36. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/pr-lightweight.yml +0 -0
  37. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/pr-merge-gate.yml +0 -0
  38. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.github/workflows/release.yml +0 -0
  39. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/.pre-commit-config.yaml +0 -0
  40. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/AGENTS.md +0 -0
  41. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/CONCURRENCY.md +0 -0
  42. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/CONTRIBUTING.md +0 -0
  43. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/DEVELOPMENT_PRINCIPLES.md +0 -0
  44. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/LICENSE.txt +0 -0
  45. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/MANIFEST.in +0 -0
  46. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/README.md +0 -0
  47. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/budgets.jsonl +0 -0
  48. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/calibration/README.md +0 -0
  49. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/calibration/capacity-size-tight-cyclone.json +0 -0
  50. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/calibration/probe-loss-gop-tight-cyclone.json +0 -0
  51. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/calibration/probe-loss-nominal-cyclone.json +0 -0
  52. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/calibration/probe-loss-tight-cyclone.json +0 -0
  53. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/calibration/probe-loss-tight-fastdds.json +0 -0
  54. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/calibration/probe-loss-tight-zenoh.json +0 -0
  55. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/codecov.yml +0 -0
  56. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/deployment-configuration.md +0 -0
  57. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/benchmark-ab.md +0 -0
  58. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/benchmark-bag-as-load.md +0 -0
  59. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/ci.md +0 -0
  60. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/ffmpeg-keyframes.md +0 -0
  61. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/README.md +0 -0
  62. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/cyclone-spdp-discovery-bursts.md +0 -0
  63. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/delay-alone-no-loss.md +0 -0
  64. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/fastdds-datagram-cap-drops-large-samples.md +0 -0
  65. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/graph-derived-types-block-a-non-graph-transport.md +0 -0
  66. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/head-of-line-irregularity.md +0 -0
  67. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/jitter-loss-bandwidth-latency.md +0 -0
  68. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/keyframe-loss-kills-the-next-deltas.md +0 -0
  69. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/lighter-message-loss.md +0 -0
  70. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/oversubscription-queues-not-losses.md +0 -0
  71. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/reorder-becomes-reader-loss.md +0 -0
  72. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/repro/decode_chain_repro.py +0 -0
  73. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/repro/reorder_drop_repro.py +0 -0
  74. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/sender-rows-make-a-dead-link-look-loss-free.md +0 -0
  75. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/findings/zero-loss-boundaries-18kb-20hz.md +0 -0
  76. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/forensics-report.md +0 -0
  77. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/geo-link-map.md +0 -0
  78. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/github-repository-settings.md +0 -0
  79. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/link-bandwidth.md +0 -0
  80. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/link-trace.md +0 -0
  81. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/owner-runbook.md +0 -0
  82. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/performance-bands.md +0 -0
  83. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/playbooks/README.md +0 -0
  84. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/quality-model.md +0 -0
  85. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/release-notes/TEMPLATE.md +0 -0
  86. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/release-notes/v2.1.md +0 -0
  87. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/release-notes/v2.2.md +0 -0
  88. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/release-notes/v2.3.md +0 -0
  89. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/release-notes/v2.4.md +0 -0
  90. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/release.md +0 -0
  91. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/0001-expectation-driven-test-suite.md +0 -0
  92. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/0002-expectation-concepts.md +0 -0
  93. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/0003-metric-backbone.md +0 -0
  94. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/0004-network-profiles.md +0 -0
  95. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/0005-benchmark-genres-and-ci.md +0 -0
  96. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/0006-dynamic-qos.md +0 -0
  97. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/0007-regression-gate.md +0 -0
  98. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/rfcs/README.md +0 -0
  99. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/testing.md +0 -0
  100. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/docs/work-items.md +0 -0
  101. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/install.sh +0 -0
  102. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/justfile +0 -0
  103. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/media/control_center.png +0 -0
  104. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/media/echo_test.png +0 -0
  105. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/media/forensics-report-example.png +0 -0
  106. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/media/host.png +0 -0
  107. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/media/vehicle.png +0 -0
  108. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/pyproject.toml +0 -0
  109. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/requirements.txt +0 -0
  110. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/rosotacom.yaml +0 -0
  111. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/setup.cfg +0 -0
  112. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/__init__.py +0 -0
  113. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/__main__.py +0 -0
  114. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/anonymize.py +0 -0
  115. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/bag_ground_truth.py +0 -0
  116. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/benched_set.py +0 -0
  117. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/benchmark.py +0 -0
  118. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/benchmark_delta.py +0 -0
  119. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/bundle_check.py +0 -0
  120. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/cli.py +0 -0
  121. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/cli_benchmark.py +0 -0
  122. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/deployment.py +0 -0
  123. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/ffmpeg_packet.py +0 -0
  124. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/forensics.py +0 -0
  125. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/geomap.py +0 -0
  126. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/image_cache.py +0 -0
  127. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/netem_dist.py +0 -0
  128. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/network_preflight.py +0 -0
  129. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/network_profiles.py +0 -0
  130. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/network_shaper.py +0 -0
  131. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/plots.py +0 -0
  132. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/py.typed +0 -0
  133. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/benched-set.yaml +0 -0
  134. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/.gitignore +0 -0
  135. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/README.md +0 -0
  136. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/__init__.py +0 -0
  137. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/bags/8_drop_reference/metadata.yaml +0 -0
  138. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/deployment.example.yaml +0 -0
  139. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/dist/field-20260817.dist +0 -0
  140. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/profiles.yaml +0 -0
  141. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/ros2docker.json +0 -0
  142. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/rosotacom.yaml +0 -0
  143. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scenarios/2_native_chatter/scenario-definition.yaml +0 -0
  144. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/1_heartbeat/run_machine_a.sh +0 -0
  145. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/1_heartbeat/run_machine_b.sh +0 -0
  146. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.ros2docker.json +0 -0
  147. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_communication.sh +0 -0
  148. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_external.py +0 -0
  149. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/wait_for_echo_topic.sh +0 -0
  150. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.ros2docker.json +0 -0
  151. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_communication.sh +0 -0
  152. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_external.py +0 -0
  153. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external.ros2docker.json +0 -0
  154. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external_session.yaml +0 -0
  155. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/foxglove.json +0 -0
  156. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_communication.sh +0 -0
  157. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_external.py +0 -0
  158. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
  159. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/metadata.yaml +0 -0
  160. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/external.ros2docker.json +0 -0
  161. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_communication.sh +0 -0
  162. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_external.py +0 -0
  163. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external.ros2docker.json +0 -0
  164. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external_session.yaml +0 -0
  165. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/foxglove.json +0 -0
  166. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_communication.sh +0 -0
  167. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_external.py +0 -0
  168. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
  169. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/metadata.yaml +0 -0
  170. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/external.ros2docker.json +0 -0
  171. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_communication.sh +0 -0
  172. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_external.py +0 -0
  173. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external.ros2docker.json +0 -0
  174. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external_session.yaml +0 -0
  175. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_communication.sh +0 -0
  176. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_external.py +0 -0
  177. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external.ros2docker.json +0 -0
  178. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external_session.yaml +0 -0
  179. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_communication.sh +0 -0
  180. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_external.py +0 -0
  181. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external.ros2docker.json +0 -0
  182. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external_session.yaml +0 -0
  183. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_communication.sh +0 -0
  184. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_external.py +0 -0
  185. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external.ros2docker.json +0 -0
  186. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external_session.yaml +0 -0
  187. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_communication.sh +0 -0
  188. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_external.py +0 -0
  189. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/10_restamp/session-definition.yaml +0 -0
  190. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/11_trickle/session-definition.yaml +0 -0
  191. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/12_content_integrity/session-definition.yaml +0 -0
  192. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/13_link_latency/session-definition.yaml +0 -0
  193. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/14_remote_assist_anonymized/session-definition.yaml +0 -0
  194. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/15_remote_assist_anonymized_costmap/session-definition.yaml +0 -0
  195. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/16_remote_assist_anonymized_camera/session-definition.yaml +0 -0
  196. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/17_synthetic_camera_quality/session-definition.yaml +0 -0
  197. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/1_heartbeat/session-definition.yaml +0 -0
  198. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/1_heartbeat_status/session-definition.yaml +0 -0
  199. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/2_native_chatter/session-definition.yaml +0 -0
  200. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/3_comp_occ_grid/session-definition.yaml +0 -0
  201. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/4_comp_occ_grid_zen/session-definition.yaml +0 -0
  202. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/5_sized_payload/session-definition.yaml +0 -0
  203. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/6_sized_payload_zen/session-definition.yaml +0 -0
  204. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/7_latched_static/session-definition.yaml +0 -0
  205. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/8_drop/session-definition.yaml +0 -0
  206. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/9_throttle/session-definition.yaml +0 -0
  207. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/bench_1_1_capacity/session-definition.yaml +0 -0
  208. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/bench_1_2_load_sweep/session-definition.yaml +0 -0
  209. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/bench_1_3_ramp/session-definition.yaml +0 -0
  210. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/bench_1_4_recovery/session-definition.yaml +0 -0
  211. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/examples/sessions/bench_1_5_camera/session-definition.yaml +0 -0
  212. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ros2docker.json.example +0 -0
  213. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/.vscode/c_cpp_properties.json +0 -0
  214. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/.vscode/settings.json +0 -0
  215. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_easy_mode.xml.template +0 -0
  216. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_initial_peer_list.xml.template +0 -0
  217. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v2.xml.template +0 -0
  218. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v3.xml.template +0 -0
  219. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v4.xml.template +0 -0
  220. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v5.xml.template +0 -0
  221. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/create_zenoh_json5.py +0 -0
  222. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/cyclonedds_local_participants.xml.template +0 -0
  223. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/cyclonedds_minimal.xml.template +0 -0
  224. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/cyclonedds_tuned.xml.template +0 -0
  225. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/fastdds_easy_mode.xml.template +0 -0
  226. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/fastdds_local.xml.template +0 -0
  227. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/fastdds_tuned.xml.template +0 -0
  228. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/fastdds_unicast.xml.template +0 -0
  229. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/get_ota_xml.py +0 -0
  230. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/utils.py +0 -0
  231. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ota_configs/zenoh.json5.template +0 -0
  232. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/.gitkeep +0 -0
  233. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_cpp/CMakeLists.txt +0 -0
  234. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_cpp/COLCON_IGNORE +0 -0
  235. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_cpp/LICENSE.txt +0 -0
  236. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_cpp/package.xml +0 -0
  237. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_cpp/src/hz_monitor.cpp +0 -0
  238. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_cpp/src/universal_compressor.cpp +0 -0
  239. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_cpp/src/universal_decompressor.cpp +0 -0
  240. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_msgs/CMakeLists.txt +0 -0
  241. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_msgs/LICENSE.txt +0 -0
  242. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/CompressedData.msg +0 -0
  243. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/EchoHeartbeat.msg +0 -0
  244. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/OtaStamped.msg +0 -0
  245. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/SizedPayload.msg +0 -0
  246. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_msgs/package.xml +0 -0
  247. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/LICENSE.txt +0 -0
  248. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/__init__.py +0 -0
  249. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/base_bridge_relay.py +0 -0
  250. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/bridge_in.py +0 -0
  251. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/bridge_out.py +0 -0
  252. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/ffmpeg_flags.py +0 -0
  253. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/heartbeat_echo.py +0 -0
  254. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/hz_monitor.py +0 -0
  255. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/image_bandwidth_reducer.py +0 -0
  256. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/image_pixel_capper.py +0 -0
  257. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/lanelet_saver.py +0 -0
  258. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/latch_relay.py +0 -0
  259. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/costmap_remapper.py +0 -0
  260. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/marker_remapper.py +0 -0
  261. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/speed_test.py +0 -0
  262. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/tf_remapper.py +0 -0
  263. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/traffic_monitor.py +0 -0
  264. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/wifi_network_analysis_1.py +0 -0
  265. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/link_bytes.py +0 -0
  266. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/link_trace.py +0 -0
  267. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/local_global_frame_bridge.py +0 -0
  268. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/localization_map_split_reduce_saver.py +0 -0
  269. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/msg_transform.py +0 -0
  270. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/pair_management.py +0 -0
  271. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/pub_sub_pair.py +0 -0
  272. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/qos.py +0 -0
  273. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/relay_in.py +0 -0
  274. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/relay_out.py +0 -0
  275. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/restamp.py +0 -0
  276. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/sized_publisher.py +0 -0
  277. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency.py +0 -0
  278. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency_core.py +0 -0
  279. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/tf_map_to_cart_saver.py +0 -0
  280. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/topic_monitor.py +0 -0
  281. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/topic_resolution.py +0 -0
  282. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/trickle.py +0 -0
  283. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_compressor.py +0 -0
  284. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_decompressor.py +0 -0
  285. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_unwrapper.py +0 -0
  286. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_wrapper.py +0 -0
  287. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/package.xml +0 -0
  288. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/resource/com_py +0 -0
  289. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/setup.cfg +0 -0
  290. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/setup.py +0 -0
  291. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/test/test_copyright.py +0 -0
  292. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/test/test_flake8.py +0 -0
  293. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/ros2src/com_py/test/test_pep257.py +0 -0
  294. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/content/base/session_plugin_base.yaml +0 -0
  295. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/content/network/up_connectivity.py +0 -0
  296. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/content/network/wifi_network_analysis_2.py +0 -0
  297. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/creation/anonymize_bag.py +0 -0
  298. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/creation/catmux_log_setup.sh +0 -0
  299. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/creation/create_session_yaml.py +0 -0
  300. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/creation/run_session.py +0 -0
  301. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py +0 -0
  302. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/resources/ws/session/creation/strip_ansi.py +0 -0
  303. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/stream_stats.py +0 -0
  304. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/trace_profiles.py +0 -0
  305. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/transit.py +0 -0
  306. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom/video_quality.py +0 -0
  307. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/SOURCES.txt +0 -0
  308. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/dependency_links.txt +0 -0
  309. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/entry_points.txt +0 -0
  310. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/requires.txt +0 -0
  311. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/scm_file_list.json +0 -0
  312. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/src/rosotacom_dev.egg-info/top_level.txt +0 -0
  313. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/terminology.md +0 -0
  314. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_benched_set_registry.py +0 -0
  315. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_findings.py +0 -0
  316. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_markdown_links.py +0 -0
  317. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_ota_source_bundle.py +0 -0
  318. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_package_metadata.py +0 -0
  319. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_packaged_resources.py +0 -0
  320. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_profile_covers_generated_commands.py +0 -0
  321. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_pytest_policy.py +0 -0
  322. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_readme_examples.py +0 -0
  323. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_security_maintenance_config.py +0 -0
  324. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_session_configuration_doc.py +0 -0
  325. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_session_template_substitution.py +0 -0
  326. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/contract/test_workflow_contracts.py +0 -0
  327. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/conftest.py +0 -0
  328. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_anonymize_e2e.py +0 -0
  329. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_benchmark_ab.py +0 -0
  330. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_benchmark_capacity.py +0 -0
  331. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_benchmark_replay.py +0 -0
  332. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_parallel_smoke.py +0 -0
  333. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_smoke.py +0 -0
  334. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_timeline_stepping.py +0 -0
  335. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/e2e/test_video_quality_e2e.py +0 -0
  336. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/sessions/generate_rmw_matrix.py +0 -0
  337. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-local_zenoh-ros2dds-ota/session-definition.yaml +0 -0
  338. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-ota/session-definition.yaml +0 -0
  339. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-ota-tuned/session-definition.yaml +0 -0
  340. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/sessions/rmw_matrix/1_heartbeat_fastdds/session-definition.yaml +0 -0
  341. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/sessions/rmw_matrix/1_heartbeat_fastdds-default/session-definition.yaml +0 -0
  342. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/sessions/rmw_matrix/1_heartbeat_zen-endpoints/session-definition.yaml +0 -0
  343. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_anonymize.py +0 -0
  344. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_bag_ground_truth.py +0 -0
  345. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_benched_set.py +0 -0
  346. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_benchmark.py +0 -0
  347. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_benchmark_delta.py +0 -0
  348. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_bundle_check.py +0 -0
  349. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_catmux_pane_logging.py +0 -0
  350. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_cli.py +0 -0
  351. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_cli_benchmark.py +0 -0
  352. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_concurrency.py +0 -0
  353. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_container_shaping.py +0 -0
  354. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_deployment.py +0 -0
  355. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_ffmpeg_flags.py +0 -0
  356. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_ffmpeg_packet.py +0 -0
  357. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_field_profiles.py +0 -0
  358. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_forensics.py +0 -0
  359. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_geomap.py +0 -0
  360. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_image_cache.py +0 -0
  361. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_link_bytes.py +0 -0
  362. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_link_trace.py +0 -0
  363. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_netem_dist.py +0 -0
  364. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_network_preflight.py +0 -0
  365. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_network_profiles.py +0 -0
  366. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_network_shaper.py +0 -0
  367. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_ota_link_configs.py +0 -0
  368. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_ota_unwrapper.py +0 -0
  369. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_ota_wrapper.py +0 -0
  370. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_ota_xml.py +0 -0
  371. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_plots.py +0 -0
  372. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_pub_sub_pair_types.py +0 -0
  373. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_qos_fallback_logging.py +0 -0
  374. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_relay_optional_parameters.py +0 -0
  375. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_stage_latency.py +0 -0
  376. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_status_overview_observer.py +0 -0
  377. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_stream_stats.py +0 -0
  378. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_trace_profiles.py +0 -0
  379. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/tests/unit/test_transit.py +0 -0
  380. {rosotacom_dev-2.5.dev56 → rosotacom_dev-2.5.dev58}/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.dev56
3
+ Version: 2.5.dev58
4
4
  Summary: Host CLI for ROS OTA communication sessions.
5
5
  Author: develNor
6
6
  License-Expression: BSD-3-Clause
@@ -720,8 +720,35 @@ ros2 run com_py playout_pacer --ros-args \
720
720
  -p target_ms:=350.0 -p adaptive:=true
721
721
  ```
722
722
 
723
- It republishes on `<topic>/paced` plus `.../paced/budget_ms` and
724
- `.../paced/queue_depth` debug topics.
723
+ It republishes on `<topic>/paced` plus three topics next to it:
724
+ `.../paced/budget_ms` and `.../paced/queue_depth` at 1 Hz, and
725
+ `.../paced/hold_ms` with every release, carrying how long *that* message was
726
+ actually held.
727
+
728
+ ##### What the status overview does with it
729
+
730
+ A paced stage's age is the link plus a delay this session asked for, and one
731
+ number cannot be judged against a threshold meant for the link. So the
732
+ generator names the pacer's report on the stage it explains
733
+ (`paced_hold_topic` in `pipeline_spec.yaml`), and `status_overview` takes the
734
+ hold back out before classifying:
735
+
736
+ ```
737
+ native_in FLOWING .../ffmpeg/compressed 10.0Hz 286ms(link 46+paced 240) ...
738
+ ```
739
+
740
+ `latency_ms` stays the age on screen; `link_latency_ms` and `pacing_hold_ms`
741
+ are the split, and the quality verdict uses the former. Without this the
742
+ default 200 ms bad-threshold — set before anything on a link was held on
743
+ purpose — marks every healthy paced stream BAD, which is how an operator learns
744
+ to stop reading the panel.
745
+
746
+ It is the *applied* hold that is subtracted, never the configured budget: a
747
+ message that arrived later than its release time passes straight through, is
748
+ reported as held for 0 ms, and still flags. A budget-based correction would
749
+ credit that message a buffer it never received and quietly stop reporting a slow
750
+ link. Before the pacer has reported at all, nothing is subtracted — neither zero
751
+ nor the budget is a safe guess.
725
752
 
726
753
  `type: foxglove` supports (CompressedVideo):
727
754
  - `gop_size` (int)
@@ -8,7 +8,16 @@ Inserted between the OTA unwrapper and a decoder chain it turns network jitter
8
8
  into a constant, bounded age instead of visible stutter.
9
9
 
10
10
  Debug topics next to the output: ``.../paced/budget_ms`` (current delay
11
- budget) and ``.../paced/queue_depth``.
11
+ budget) and ``.../paced/queue_depth``, both at 1 Hz, and ``.../paced/hold_ms``
12
+ — published with *every* release, carrying how long that message was actually
13
+ held.
14
+
15
+ ``hold_ms`` exists so the status overview can tell the link's contribution from
16
+ this node's. Deliberately the applied hold rather than the budget: a message
17
+ that arrived later than its release time passes straight through and is held
18
+ for 0 ms, so a genuinely slow link cannot be credited with a buffer it never
19
+ used. Subtracting the *budget* would do exactly that, and would quietly stop
20
+ reporting the problem it exists to make visible.
12
21
  """
13
22
 
14
23
  from __future__ import annotations
@@ -62,6 +71,7 @@ class PlayoutPacerNode(Node):
62
71
  self._pub = self.create_publisher(msg_cls, out_topic, qos)
63
72
  self._budget_pub = self.create_publisher(Float64, out_topic + "/budget_ms", 10)
64
73
  self._depth_pub = self.create_publisher(Int32, out_topic + "/queue_depth", 10)
74
+ self._hold_pub = self.create_publisher(Float64, out_topic + "/hold_ms", 10)
65
75
  self.create_subscription(msg_cls, topic, self._on_message, qos)
66
76
  # 2 ms release tick: bounds added jitter to well below frame cadence.
67
77
  self.create_timer(0.002, self._release_due)
@@ -72,12 +82,16 @@ class PlayoutPacerNode(Node):
72
82
  now = time.time()
73
83
  stamp = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
74
84
  release = self.schedule.on_message(stamp, now)
75
- self._queue.append((release, msg))
85
+ self._queue.append((release, msg, now))
76
86
 
77
87
  def _release_due(self) -> None:
78
88
  now = time.time()
79
89
  while self._queue and self._queue[0][0] <= now:
80
- self._pub.publish(self._queue.popleft()[1])
90
+ release, msg, arrival = self._queue.popleft()
91
+ self._pub.publish(msg)
92
+ # What this node added, not what it was allowed to add. Clamped
93
+ # because a late message's release time is already in the past.
94
+ self._hold_pub.publish(Float64(data=max(0.0, (release - arrival) * 1000.0)))
81
95
 
82
96
  def _publish_debug(self) -> None:
83
97
  self._budget_pub.publish(Float64(data=float(self.schedule.budget_ms)))
@@ -32,7 +32,10 @@ class PacerConfig:
32
32
  adaptive: bool = True
33
33
  percentile: float = 0.95 # of (arrival - stamp), window-local
34
34
  margin_ms: float = 40.0 # headroom above the tracked percentile
35
- window: int = 400 # samples (~40 s at 10 Hz)
35
+ #: Samples (~40 s at 10 Hz). Bounds both the tracked percentile and the
36
+ #: delay floor, so how fast the schedule re-anchors after the path changes
37
+ #: is this number: a longer window is steadier and slower to recover.
38
+ window: int = 400
36
39
 
37
40
  def __post_init__(self) -> None:
38
41
  if not 0.0 < self.percentile < 1.0:
@@ -72,8 +75,18 @@ class PlayoutSchedule:
72
75
  """
73
76
  d = arrival_s - stamp_s
74
77
  self._d.append(d)
75
- if self._d_floor is None or d < self._d_floor:
76
- self._d_floor = d
78
+ # The minimum over the *window*, not over all time. An all-time floor
79
+ # can only fall, so any upward shift in `arrival - stamp` -- a clock
80
+ # step, an NTP correction, a route change, a looping replay -- leaves
81
+ # every later message past `stamp + floor + budget`, and the pacer
82
+ # degrades to a pass-through it can never come back from. Found on the
83
+ # fleet e2e, where a bag loop moved the delay by the bag's length and
84
+ # the centre's camera ran unpaced for the rest of the run with every
85
+ # panel healthy. Windowed, the same shift re-anchors within one window.
86
+ #
87
+ # O(window) per message: 400 floats at 10 Hz. A monotonic deque would
88
+ # make it O(1) and is not worth the machinery at these rates.
89
+ self._d_floor = min(self._d)
77
90
  release = stamp_s + self._d_floor + self.budget_ms / 1000.0
78
91
  if release < arrival_s: # already late: pass through immediately
79
92
  release = arrival_s
@@ -71,6 +71,7 @@ from com_py.ffmpeg_flags import FFMPEG_PACKET_TYPE, parse_keyframe_flag
71
71
  from com_py.link_bytes import LinkByteSampler, resolve_link_interface
72
72
  from com_py.link_trace import LinkTraceRecorder
73
73
  from com_py.status_overview_core import (
74
+ SCALAR_MSG_TYPE,
74
75
  STARTUP_GRACE_S,
75
76
  ClockOffsetEstimator,
76
77
  StageObservation,
@@ -309,6 +310,11 @@ class StageObserver(Node):
309
310
  "t_com_out": now_ros_s,
310
311
  "keyframe": keyframe,
311
312
  }
313
+ elif obs.type_str == SCALAR_MSG_TYPE:
314
+ # Not a stage: a number this node reads to interpret one. The
315
+ # pacing hold arrives here (`.../paced/hold_ms`); it carries no
316
+ # header, so there is no latency to record, only the value.
317
+ obs.last_value = float(msg.data)
312
318
  else:
313
319
  header = getattr(msg, "header", None)
314
320
  stamp = getattr(header, "stamp", None) if header is not None else None
@@ -61,6 +61,12 @@ GOOD = "GOOD"
61
61
  DEGRADED = "DEGRADED"
62
62
  BAD = "BAD"
63
63
 
64
+ #: A stage whose age includes a delay this session asked for names the topic on
65
+ #: which the node responsible reports what it actually held. Set by the
66
+ #: generator for a decode that reads a playout pacer's output.
67
+ PACED_HOLD_KEY = "paced_hold_topic"
68
+ SCALAR_MSG_TYPE = "std_msgs/msg/Float64"
69
+
64
70
  # Topic-level rollup
65
71
  OK = "OK"
66
72
  PARTIAL = "PARTIAL"
@@ -371,6 +377,9 @@ class StageObservation:
371
377
  last_recv_wall: Optional[float] = None
372
378
  last_delay_s: Optional[float] = None
373
379
  last_raw_delay_s: Optional[float] = None
380
+ #: Payload of the last message, for the scalar topics this node reads as
381
+ #: numbers rather than observes as stages (the pacing hold).
382
+ last_value: Optional[float] = None
374
383
  last_rtt_s: Optional[float] = None
375
384
  last_clock_offset_s: Optional[float] = None
376
385
  # (t_mono, size_bytes, delay_s_or_None)
@@ -673,6 +682,21 @@ class StatusAggregator:
673
682
  self._startup_pending: Optional[Tuple[float, set]] = None
674
683
 
675
684
  # -- observation lookup --
685
+ def _pacing_hold_ms(self, stage: Dict[str, Any]) -> Optional[float]:
686
+ """How long this session deliberately held the last message of a stage.
687
+
688
+ None whenever the answer is not actually known -- no pacer named, or the
689
+ pacer has not reported yet. Guessing zero would read a paced picture as
690
+ a slow link; guessing the configured budget would do the opposite and
691
+ credit a hold that a late message never received.
692
+ """
693
+ hold_topic = stage.get(PACED_HOLD_KEY)
694
+ if not hold_topic:
695
+ return None
696
+ observer = self._observers.get("local")
697
+ observation = observer.observations.get(str(hold_topic)) if observer else None
698
+ return None if observation is None else observation.last_value
699
+
676
700
  def _obs_for(self, stage: Dict[str, Any]) -> Optional[StageObservation]:
677
701
  domain = stage.get("domain", "local")
678
702
  observer = self._observers.get(domain) or self._observers.get("local")
@@ -714,6 +738,9 @@ class StatusAggregator:
714
738
  "observation": "graph" if obs is not None and obs.graph_only else "payload",
715
739
  "inferred_from": None,
716
740
  "latched": str((expect or {}).get("mode", "")).strip().lower() == "latched",
741
+ # Split of `latency_ms` for a stage this session delays on purpose.
742
+ "pacing_hold_ms": None,
743
+ "link_latency_ms": None,
717
744
  }
718
745
  if obs is None:
719
746
  return result
@@ -727,6 +754,10 @@ class StatusAggregator:
727
754
  result["age_s"] = round(m["age_s"], 3) if m["age_s"] is not None else None
728
755
  if m["last_delay_s"] is not None:
729
756
  result["latency_ms"] = round(m["last_delay_s"] * 1000.0, 1)
757
+ hold_ms = self._pacing_hold_ms(stage)
758
+ if hold_ms is not None:
759
+ result["pacing_hold_ms"] = round(hold_ms, 1)
760
+ result["link_latency_ms"] = round(max(0.0, result["latency_ms"] - hold_ms), 1)
730
761
  if m["last_raw_delay_s"] is not None:
731
762
  result["latency_uncorrected_ms"] = round(m["last_raw_delay_s"] * 1000.0, 1)
732
763
  if m["last_rtt_s"] is not None:
@@ -759,7 +790,9 @@ class StatusAggregator:
759
790
  return result
760
791
 
761
792
  result["state"] = FLOWING
762
- quality, reason = self._classify_quality(m, expected_hz, expect)
793
+ quality, reason = self._classify_quality(
794
+ m, expected_hz, expect, pacing_hold_ms=result["pacing_hold_ms"]
795
+ )
763
796
  result["quality"] = quality
764
797
  result["quality_reason"] = reason
765
798
  return result
@@ -823,7 +856,12 @@ class StatusAggregator:
823
856
  self._copy_inferred_activity(stage_result, source)
824
857
 
825
858
  def _classify_quality(
826
- self, m: Dict[str, Any], expected_hz: Optional[float], expect: Optional[Dict[str, Any]] = None
859
+ self,
860
+ m: Dict[str, Any],
861
+ expected_hz: Optional[float],
862
+ expect: Optional[Dict[str, Any]] = None,
863
+ *,
864
+ pacing_hold_ms: Optional[float] = None,
827
865
  ) -> Tuple[str, Optional[str]]:
828
866
  expect = expect or {}
829
867
  hz_exp = expect.get("hz") or {}
@@ -833,8 +871,17 @@ class StatusAggregator:
833
871
  # Latency: a declared `expect.latency_ms.max` overrides the global bad
834
872
  # threshold; the good band stays the default so a tight contract still
835
873
  # surfaces a DEGRADED before BAD where it makes sense.
874
+ #
875
+ # A stage the session delays on purpose is judged on what is left after
876
+ # that delay is taken back out. Otherwise a threshold calibrated for a
877
+ # link measures a buffer, which reads as a fault on a healthy stream and
878
+ # teaches an operator to stop believing the panel. The subtracted value
879
+ # is the hold actually applied, so a late message -- which the pacer
880
+ # passes straight through -- is credited nothing and still flags.
836
881
  bad_lat = float(lat_exp["max"]) if "max" in lat_exp else self._delay_bad_ms
837
882
  delay_ms = (m["last_delay_s"] * 1000.0) if m["last_delay_s"] is not None else None
883
+ if delay_ms is not None and pacing_hold_ms is not None:
884
+ delay_ms = max(0.0, delay_ms - pacing_hold_ms)
838
885
  if delay_ms is not None:
839
886
  if delay_ms >= bad_lat:
840
887
  return BAD, "latency"
@@ -1142,7 +1189,16 @@ class StatusAggregator:
1142
1189
  if st["state"] in (FLOWING, STALE):
1143
1190
  parts = [f"{st['hz']:.1f}Hz"]
1144
1191
  if st["latency_ms"] is not None:
1145
- parts.append(f"{st['latency_ms']:.0f}ms")
1192
+ # A paced stage reads as one big number unless the split
1193
+ # is on the same line: what the link cost, and what this
1194
+ # session chose to add on top of it.
1195
+ if st.get("pacing_hold_ms") is not None:
1196
+ parts.append(
1197
+ f"{st['latency_ms']:.0f}ms"
1198
+ f"(link {st['link_latency_ms']:.0f}+paced {st['pacing_hold_ms']:.0f})"
1199
+ )
1200
+ else:
1201
+ parts.append(f"{st['latency_ms']:.0f}ms")
1146
1202
  elif st["latency_uncorrected_ms"] is not None:
1147
1203
  parts.append(f"{st['latency_uncorrected_ms']:.0f}ms(raw)")
1148
1204
  if st["loss_pct"] is not None:
@@ -1298,7 +1354,12 @@ class StatusAggregator:
1298
1354
 
1299
1355
 
1300
1356
  def collect_stage_topics(spec: Dict[str, Any]) -> Dict[str, Dict[str, Optional[str]]]:
1301
- """Return {domain: {topic: type_hint}} from the pipeline spec."""
1357
+ """Return {domain: {topic: type_hint}} from the pipeline spec.
1358
+
1359
+ Includes the pacing-hold topics a stage names: they are not stages of the
1360
+ link -- nothing is delivered on them -- but the aggregator has to read them
1361
+ to tell this session's deliberate delay from the network's.
1362
+ """
1302
1363
  by_domain: Dict[str, Dict[str, Optional[str]]] = {"local": {}, "ota": {}}
1303
1364
  for topic_spec in spec.get("topics", []):
1304
1365
  type_hint = topic_spec.get("type")
@@ -1307,6 +1368,9 @@ def collect_stage_topics(spec: Dict[str, Any]) -> Dict[str, Dict[str, Optional[s
1307
1368
  by_domain.setdefault(domain, {})
1308
1369
  existing = by_domain[domain].get(stage["topic"])
1309
1370
  by_domain[domain][stage["topic"]] = existing or stage.get("type") or type_hint
1371
+ hold_topic = stage.get(PACED_HOLD_KEY)
1372
+ if hold_topic:
1373
+ by_domain["local"].setdefault(str(hold_topic), SCALAR_MSG_TYPE)
1310
1374
  return by_domain
1311
1375
 
1312
1376
 
@@ -1666,17 +1666,23 @@ def _build_status_pipeline_spec(
1666
1666
  if postprocessed != final:
1667
1667
  native_in_type = base_type
1668
1668
  transport = p.get("transport")
1669
+ native_in: Dict[str, Any] = {
1670
+ "stage": "native_in",
1671
+ "topic": _relay_in_local_topic(postprocessed),
1672
+ "type": native_in_type,
1673
+ "domain": "local",
1674
+ "produced_by": "postprocessing",
1675
+ }
1669
1676
  if isinstance(transport, TransportSpec) and transport.remote_republish:
1670
- native_in_type = REPUBLISH_OUTPUT_TYPES[transport.remote_republish]
1671
- stages.append(
1672
- {
1673
- "stage": "native_in",
1674
- "topic": _relay_in_local_topic(postprocessed),
1675
- "type": native_in_type,
1676
- "domain": "local",
1677
- "produced_by": "postprocessing",
1678
- }
1679
- )
1677
+ native_in["type"] = REPUBLISH_OUTPUT_TYPES[transport.remote_republish]
1678
+ if transport.playout is not None and playout_republish(transport.playout) != "unpaced":
1679
+ # This stage's age is the link plus a hold this session
1680
+ # asked for. The pacer reports the hold it applied; naming
1681
+ # the topic here is what lets the status separate the two
1682
+ # instead of reading a deliberate buffer as a fault.
1683
+ paced = _relay_in_local_topic(str(p.get("irt_in"))) + "/paced"
1684
+ native_in["paced_hold_topic"] = f"{paced}/hold_ms"
1685
+ stages.append(native_in)
1680
1686
 
1681
1687
  topics.append(
1682
1688
  {
@@ -265,10 +265,19 @@ def _check_expect(
265
265
  failures.append(f"[{peer}] {base}: latency_ms.stage '{stage_name}' not in pipeline")
266
266
  return failures
267
267
  lat_stage = named
268
- lat = lat_stage.get("latency_ms")
268
+ # A stage the session delays on purpose reports the split, and `expect` is
269
+ # about what the *link* delivers -- the buffer is already bounded by the
270
+ # `playout` block that asked for it. The live status classifies on the same
271
+ # number, and the two must not disagree about one contract: that is how a
272
+ # session ends up passing `rosotacom test` while its own panel reads BAD.
273
+ lat = lat_stage.get("link_latency_ms")
274
+ measured = "link latency"
275
+ if lat is None:
276
+ lat = lat_stage.get("latency_ms")
277
+ measured = "latency"
269
278
  if "max" in lat_exp and (lat is None or lat > lat_exp["max"]):
270
279
  where = f" at '{stage_name}'" if stage_name else ""
271
- failures.append(f"[{peer}] {base}: latency {lat}ms{where} > expected max {lat_exp['max']}ms")
280
+ failures.append(f"[{peer}] {base}: {measured} {lat}ms{where} > expected max {lat_exp['max']}ms")
272
281
 
273
282
  loss_exp = expect.get("loss_pct") or {}
274
283
  if "max" in loss_exp:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosotacom-dev
3
- Version: 2.5.dev56
3
+ Version: 2.5.dev58
4
4
  Summary: Host CLI for ROS OTA communication sessions.
5
5
  Author: develNor
6
6
  License-Expression: BSD-3-Clause
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "tag": "2.4",
3
- "distance": 56,
4
- "node": "g18a62c4bee965c3d64b7eaa70544f6bb7f078a98",
3
+ "distance": 58,
4
+ "node": "ge746c2a24a5de807925ed1cc3b61921a542c2ac2",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-08-19"
@@ -218,7 +218,7 @@ def touches(params: dict, peer_dir: Path) -> Touches:
218
218
  for index in _slots(params, "pace_{}_topic", limit=4):
219
219
  topic = _one(params, f"pace_{index}_topic")
220
220
  result.through(str(topic), f"{topic}/paced")
221
- result.writes.update({f"{topic}/paced/budget_ms", f"{topic}/paced/queue_depth"})
221
+ result.writes.update({f"{topic}/paced/budget_ms", f"{topic}/paced/queue_depth", f"{topic}/paced/hold_ms"})
222
222
 
223
223
  # NOR: relays a prefixed, suffixed name back onto the bare base, so a local
224
224
  # consumer (a TF tree) finds the name it expects.
@@ -245,14 +245,35 @@ def touches(params: dict, peer_dir: Path) -> Touches:
245
245
 
246
246
  # Observability: reads only, never republishes.
247
247
  result.reads.update(_many(params, "metric_stage_topics"))
248
+ # STAT: the status overview reads the pacing hold a stage names, to tell
249
+ # this session's deliberate delay from the network's. It is configured out
250
+ # of the pipeline spec rather than out of these parameters, but it is a read
251
+ # by a node this peer starts, so it belongs here -- a hold topic no pacer
252
+ # publishes would leave the status silently subtracting nothing.
253
+ result.reads.update(paced_hold_topics(peer_dir))
248
254
 
249
255
  return result
250
256
 
251
257
 
258
+ def _spec(peer_dir: Path) -> dict:
259
+ path = peer_dir / "pipeline_spec.yaml"
260
+ if not path.is_file():
261
+ return {}
262
+ return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
263
+
264
+
265
+ def _stages(peer_dir: Path) -> Iterator[dict]:
266
+ for topic in _spec(peer_dir).get("topics", []):
267
+ yield from topic.get("stages", [])
268
+
269
+
270
+ def paced_hold_topics(peer_dir: Path) -> set[str]:
271
+ return {stage["paced_hold_topic"] for stage in _stages(peer_dir) if stage.get("paced_hold_topic")}
272
+
273
+
252
274
  def spec_topics(peer_dir: Path) -> set[str]:
253
275
  """Every topic this peer's pipeline spec models, at any stage."""
254
- spec = yaml.safe_load((peer_dir / "pipeline_spec.yaml").read_text(encoding="utf-8"))
255
- return {stage["topic"] for topic in spec.get("topics", []) for stage in topic.get("stages", [])}
276
+ return {stage["topic"] for stage in _stages(peer_dir)}
256
277
 
257
278
 
258
279
  # ---------------------------------------------------------------------------
@@ -352,7 +373,12 @@ def test_the_composed_names_are_the_ones_the_template_composes() -> None:
352
373
  # reads that name, and `touches` claims those two writes, so all three have
353
374
  # to agree -- they are three files apart and nothing else compares them.
354
375
  pacer = PACER_PY.read_text(encoding="utf-8")
355
- for composed in ('declare_parameter("topic_suffix", "/paced")', '+ "/budget_ms"', '+ "/queue_depth"'):
376
+ for composed in (
377
+ 'declare_parameter("topic_suffix", "/paced")',
378
+ '+ "/budget_ms"',
379
+ '+ "/queue_depth"',
380
+ '+ "/hold_ms"',
381
+ ):
356
382
  assert composed in pacer, f"the pacer no longer composes {composed}"
357
383
 
358
384
 
@@ -0,0 +1,167 @@
1
+ """Playout pacer core — schedule properties on synthetic arrival traces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ REPO_ROOT = Path(__file__).resolve().parents[2]
12
+ WS_PY = REPO_ROOT / "src" / "rosotacom" / "resources" / "ws" / "ros2src" / "com_py"
13
+ sys.path.insert(0, str(WS_PY))
14
+
15
+ from com_py.playout_pacer_core import PacerConfig, PlayoutSchedule # noqa: E402
16
+
17
+
18
+ def _run(schedule: PlayoutSchedule, stamps, delays):
19
+ releases = []
20
+ for stamp, d in zip(stamps, delays, strict=True):
21
+ releases.append(schedule.on_message(stamp, stamp + d))
22
+ return releases
23
+
24
+
25
+ def test_jittered_arrivals_release_on_a_regular_grid() -> None:
26
+ # 10 Hz stream, delay jitter far above the frame period: released cadence
27
+ # must be the sender cadence, not the arrival cadence.
28
+ rng = random.Random(1)
29
+ stamps = [i / 10 for i in range(300)]
30
+ delays = [0.030 + rng.random() * 0.120 for _ in stamps] # 30-150 ms
31
+ schedule = PlayoutSchedule(PacerConfig(adaptive=True, min_ms=100.0, max_ms=800.0))
32
+ releases = _run(schedule, stamps, delays)
33
+ gaps = [b - a for a, b in zip(releases[50:], releases[51:], strict=False)]
34
+ assert max(gaps) < 0.150 # no visible stutter (>150 ms) left
35
+ assert sum(1 for g in gaps if abs(g - 0.1) < 0.01) / len(gaps) > 0.95
36
+
37
+
38
+ def test_late_message_passes_through_immediately_and_order_is_kept() -> None:
39
+ schedule = PlayoutSchedule(PacerConfig(adaptive=False, target_ms=200.0))
40
+ stamps = [i / 10 for i in range(50)]
41
+ delays = [0.030] * 50
42
+ delays[25] = 0.700 # one message far beyond any budget
43
+ releases = _run(schedule, stamps, delays)
44
+ # Late message is released at its arrival, not held further...
45
+ assert releases[25] == pytest.approx(stamps[25] + 0.700)
46
+ # ...and ordering is never violated.
47
+ assert all(b >= a for a, b in zip(releases, releases[1:], strict=False))
48
+
49
+
50
+ def test_budget_is_clock_offset_proof() -> None:
51
+ # Same jitter, sender clock shifted by -3 s: the budget must not explode.
52
+ rng = random.Random(2)
53
+ delays = [0.030 + rng.random() * 0.060 for _ in range(200)]
54
+ plain = PlayoutSchedule()
55
+ shifted = PlayoutSchedule()
56
+ for i, d in enumerate(delays):
57
+ plain.on_message(i / 10, i / 10 + d)
58
+ shifted.on_message(i / 10 - 3.0, i / 10 + d) # stamp 3 s in the "past"
59
+ assert shifted.budget_ms == pytest.approx(plain.budget_ms, rel=0.05)
60
+ assert shifted.budget_ms < 800.0
61
+
62
+
63
+ def test_adaptive_budget_tracks_jitter_and_respects_clamps() -> None:
64
+ calm = PlayoutSchedule(PacerConfig(min_ms=100.0, max_ms=800.0, margin_ms=40.0))
65
+ rough = PlayoutSchedule(PacerConfig(min_ms=100.0, max_ms=800.0, margin_ms=40.0))
66
+ rng = random.Random(3)
67
+ for i in range(300):
68
+ calm.on_message(i / 10, i / 10 + 0.030 + rng.random() * 0.010)
69
+ rough.on_message(i / 10, i / 10 + 0.030 + rng.random() * 0.400)
70
+ assert calm.budget_ms == pytest.approx(100.0) # clamped at min
71
+ assert rough.budget_ms > 300.0
72
+ assert rough.budget_ms <= 800.0
73
+
74
+
75
+ def test_config_validation() -> None:
76
+ with pytest.raises(ValueError):
77
+ PacerConfig(percentile=1.5)
78
+ with pytest.raises(ValueError):
79
+ PacerConfig(min_ms=500.0, max_ms=100.0)
80
+ with pytest.raises(ValueError):
81
+ PacerConfig(window=3)
82
+
83
+
84
+ # The hold the node reports (#301).
85
+ #
86
+ # `.../paced/hold_ms` carries what this node actually added, so the status
87
+ # overview can take it back out of a decoded stage's age and judge what is left
88
+ # -- the link. The node computes `max(0, release - arrival)`; both halves of
89
+ # that clamp are properties of the schedule, so they are asserted here rather
90
+ # than in three lines of publishing glue.
91
+
92
+
93
+ def _hold_ms(schedule: PlayoutSchedule, stamp: float, arrival: float) -> float:
94
+ """What the node publishes for a message: the delay it applied, never negative."""
95
+ return max(0.0, (schedule.on_message(stamp, arrival) - arrival) * 1000.0)
96
+
97
+
98
+ def test_the_reported_hold_is_what_was_added_not_what_was_allowed() -> None:
99
+ schedule = PlayoutSchedule(PacerConfig(adaptive=False, target_ms=200.0))
100
+ # First message sets the floor at its own delay, so it is released at
101
+ # floor + 200 ms and waits exactly the budget.
102
+ assert _hold_ms(schedule, 0.0, 0.030) == pytest.approx(200.0, abs=1.0)
103
+ # One that arrives 150 ms later than the floor has already spent 150 of the
104
+ # budget on the wire and waits only the remainder.
105
+ assert _hold_ms(schedule, 1.0, 1.180) == pytest.approx(50.0, abs=1.0)
106
+
107
+
108
+ def test_a_late_message_is_reported_as_held_for_nothing() -> None:
109
+ """The clamp: a pass-through must not be credited with a buffer it skipped."""
110
+ schedule = PlayoutSchedule(PacerConfig(adaptive=False, target_ms=200.0))
111
+ _hold_ms(schedule, 0.0, 0.030)
112
+
113
+ assert _hold_ms(schedule, 1.0, 1.400) == 0.0
114
+
115
+
116
+ # The floor has to be able to rise again (#312).
117
+ #
118
+ # Every budget is anchored to `d_floor`, the fastest path observed. Taking that
119
+ # over all time rather than over the window means it can only fall, so one
120
+ # upward shift in `arrival - stamp` puts every later message past its release
121
+ # time and the pacer becomes a pass-through it never recovers from. A clock step
122
+ # on either peer is enough; the fleet e2e's looping bag replay does it every
123
+ # lap, and until #301 the symptom was invisible -- node up, topic flowing,
124
+ # nothing re-timed.
125
+
126
+
127
+ def _hold_p50(schedule: PlayoutSchedule, count: int, stamp0: float, offset: float) -> float:
128
+ holds = []
129
+ for index in range(count):
130
+ stamp = stamp0 + index * 0.1
131
+ # 10 ms most of the time, 50 ms every third message: enough jitter for
132
+ # an adaptive budget to have something to track.
133
+ arrival = stamp + offset + (0.05 if index % 3 == 0 else 0.01)
134
+ holds.append(max(0.0, (schedule.on_message(stamp, arrival) - arrival) * 1000.0))
135
+ return sorted(holds)[len(holds) // 2]
136
+
137
+
138
+ def test_a_delay_step_up_is_re_anchored_within_one_window() -> None:
139
+ schedule = PlayoutSchedule(PacerConfig(adaptive=True, min_ms=100.0, max_ms=300.0))
140
+ window = schedule.config.window
141
+
142
+ assert _hold_p50(schedule, 500, 0.0, 0.20) == pytest.approx(100.0, abs=5.0)
143
+ # The step itself is absorbed, not smoothed: those messages are genuinely
144
+ # late relative to what was known, and pass through.
145
+ _hold_p50(schedule, window, 1000.0, 161.20)
146
+ # One window later the floor describes the new path again.
147
+ assert _hold_p50(schedule, 500, 2000.0, 161.20) == pytest.approx(100.0, abs=5.0)
148
+
149
+
150
+ def test_a_constant_offset_is_still_absorbed_entirely() -> None:
151
+ """The property the floor exists for: no clock synchronization needed."""
152
+ near = PlayoutSchedule(PacerConfig(adaptive=True, min_ms=100.0, max_ms=300.0))
153
+ far = PlayoutSchedule(PacerConfig(adaptive=True, min_ms=100.0, max_ms=300.0))
154
+
155
+ assert _hold_p50(near, 500, 0.0, 0.20) == pytest.approx(_hold_p50(far, 500, 0.0, 3600.20), abs=1.0)
156
+
157
+
158
+ def test_the_step_neither_drops_nor_reorders() -> None:
159
+ schedule = PlayoutSchedule(PacerConfig(adaptive=True, min_ms=100.0, max_ms=300.0))
160
+ releases = []
161
+ for index in range(600):
162
+ stamp = index * 0.1
163
+ offset = 0.20 if index < 300 else 161.20
164
+ releases.append(schedule.on_message(stamp, stamp + offset + 0.01))
165
+
166
+ assert len(releases) == 600
167
+ assert releases == sorted(releases)
@@ -394,3 +394,54 @@ def test_every_mode_delivers_the_same_topic_name(tmp_path: Path) -> None:
394
394
  delivered.add(generator._delivered_topic(unpaced_link, unpaced_link["final"]))
395
395
 
396
396
  assert len(delivered) == 1, delivered
397
+
398
+
399
+ # The status has to be told which pacer belongs to which stage (#301).
400
+ #
401
+ # A decoded stage's age is the link plus a hold this session asked for, and the
402
+ # status overview cannot tell them apart by looking at one number. The generator
403
+ # knows both nodes, so it names the pacer's report on the stage it explains --
404
+ # rather than the status guessing from a topic-name convention.
405
+
406
+
407
+ def _native_in(receiver: str) -> dict:
408
+ spec = yaml.safe_load(receiver)
409
+ for topic in spec["topics"]:
410
+ if topic["direction"] == "inbound":
411
+ for stage in topic["stages"]:
412
+ if stage["stage"] == "native_in":
413
+ return stage
414
+ raise AssertionError("no inbound native_in stage")
415
+
416
+
417
+ def _receiver_spec(tmp_path: Path, playout: dict | None) -> str:
418
+ transport: dict = {"type": "ffmpeg", "remote_republish": "compressed"}
419
+ if playout is not None:
420
+ transport["playout"] = playout
421
+ generator.func(
422
+ session_config_obj=_camera_cfg({"transport": transport, "use_ota_wrapper": True}),
423
+ output_dir=str(tmp_path),
424
+ force=True,
425
+ peer_addresses={"a": "127.0.0.1", "b": "127.0.0.2"},
426
+ )
427
+ return (tmp_path / "a" / "pipeline_spec.yaml").read_text(encoding="utf-8")
428
+
429
+
430
+ def test_a_paced_decode_names_the_pacer_that_explains_its_age(tmp_path: Path) -> None:
431
+ stage = _native_in(_receiver_spec(tmp_path, {"adaptive": True, "max_ms": 300}))
432
+
433
+ assert stage["paced_hold_topic"] == "/camera/image/ffmpeg/paced/hold_ms"
434
+
435
+
436
+ def test_an_unpaced_link_names_no_pacer(tmp_path: Path) -> None:
437
+ """Nothing holds this stream, so nothing may be subtracted from its age."""
438
+ stage = _native_in(_receiver_spec(tmp_path, None))
439
+
440
+ assert "paced_hold_topic" not in stage
441
+
442
+
443
+ def test_a_pacer_kept_off_the_display_path_explains_nothing(tmp_path: Path) -> None:
444
+ """`republish: unpaced` decodes the arrived stream; its age is all link."""
445
+ stage = _native_in(_receiver_spec(tmp_path, {"adaptive": True, "republish": "unpaced"}))
446
+
447
+ assert "paced_hold_topic" not in stage