rosotacom-dev 2.5.dev66__tar.gz → 2.5.dev69__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.
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/PKG-INFO +9 -1
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/README.md +8 -0
- rosotacom_dev-2.5.dev69/docs/ota-pcap.md +150 -0
- rosotacom_dev-2.5.dev69/src/rosotacom/resources/ws/ros2src/com_py/com_py/ota_pcap.py +460 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/status_overview.py +124 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/content/base/session_plugin_base.yaml +12 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/creation/generate_session_files.py +45 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom_dev.egg-info/PKG-INFO +9 -1
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom_dev.egg-info/SOURCES.txt +4 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom_dev.egg-info/scm_file_list.json +4 -0
- rosotacom_dev-2.5.dev69/src/rosotacom_dev.egg-info/scm_version.json +8 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/terminology.md +54 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_generated_topics_have_a_producer.py +3 -1
- rosotacom_dev-2.5.dev69/tests/unit/test_ota_pcap.py +357 -0
- rosotacom_dev-2.5.dev69/tests/unit/test_terminology_stage_ownership.py +103 -0
- rosotacom_dev-2.5.dev66/src/rosotacom_dev.egg-info/scm_version.json +0 -8
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.devcontainer/devcontainer.json.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.devcontainer/re_generate_devcontainer_json.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/CODEOWNERS +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/documentation-audit.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/implementation-cleanup.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/quality-workflow.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/redesign.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/release-process.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/repository-diagnosis.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/test-ci-audit.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/ISSUE_TEMPLATE/work-item.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/actions/setup-just/action.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/dependabot.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/benchmark-calibrate.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/benchmark-gate.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/dev-release.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/image-scan.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/nightly-e2e.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/pr-lightweight.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/pr-merge-gate.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.github/workflows/release.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/.pre-commit-config.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/AGENTS.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/CONCURRENCY.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/CONTRIBUTING.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/DEVELOPMENT_PRINCIPLES.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/LICENSE.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/MANIFEST.in +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/budgets.jsonl +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/calibration/README.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/calibration/capacity-size-tight-cyclone.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/calibration/probe-loss-gop-tight-cyclone.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/calibration/probe-loss-nominal-cyclone.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/calibration/probe-loss-tight-cyclone.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/calibration/probe-loss-tight-fastdds.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/calibration/probe-loss-tight-zenoh.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/codecov.yml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/deployment-configuration.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/benchmark-ab.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/benchmark-bag-as-load.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/ci.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/ffmpeg-keyframes.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/README.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/a-transport-element-doubles-every-fastdds-sample.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/cyclone-fragment-equal-to-max-drops-fragmented-samples.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/cyclone-spdp-discovery-bursts.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/delay-alone-no-loss.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/fastdds-datagram-cap-drops-large-samples.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/graph-derived-types-block-a-non-graph-transport.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/head-of-line-irregularity.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/jitter-loss-bandwidth-latency.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/keyframe-loss-kills-the-next-deltas.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/lifespan-is-enforced-by-one-reader-and-not-the-other.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/lighter-message-loss.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/one-domain-two-cyclone-configs-stop-interoperating.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/oversubscription-queues-not-losses.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/reorder-becomes-reader-loss.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/repro/decode_chain_repro.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/repro/reorder_drop_repro.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/sender-rows-make-a-dead-link-look-loss-free.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/findings/zero-loss-boundaries-18kb-20hz.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/forensics-report.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/geo-link-map.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/github-repository-settings.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/link-bandwidth.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/link-trace.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/owner-runbook.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/performance-bands.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/playbooks/README.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/quality-model.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/release-notes/TEMPLATE.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/release-notes/v2.1.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/release-notes/v2.2.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/release-notes/v2.3.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/release-notes/v2.4.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/release.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/0001-expectation-driven-test-suite.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/0002-expectation-concepts.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/0003-metric-backbone.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/0004-network-profiles.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/0005-benchmark-genres-and-ci.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/0006-dynamic-qos.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/0007-regression-gate.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/rfcs/README.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/testing.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/docs/work-items.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/install.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/justfile +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/media/control_center.png +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/media/echo_test.png +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/media/forensics-report-example.png +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/media/host.png +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/media/vehicle.png +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/pyproject.toml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/requirements.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/rosotacom.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/session-configuration.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/setup.cfg +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/__init__.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/__main__.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/anonymize.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/bag_ground_truth.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/benched_set.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/benchmark.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/benchmark_delta.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/bundle_check.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/cli.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/cli_benchmark.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/deployment.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/ffmpeg_packet.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/forensics.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/geomap.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/image_cache.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/netem_dist.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/network_preflight.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/network_profiles.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/network_shaper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/plots.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/py.typed +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/benched-set.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/.gitignore +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/README.md +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/__init__.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/bags/8_drop_reference/metadata.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/deployment.example.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/dist/field-20260817.dist +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/profiles.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/ros2docker.lyrical.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/rosotacom.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scenarios/2_native_chatter/scenario-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/1_heartbeat/run_machine_a.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/1_heartbeat/run_machine_b.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.lyrical.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_a/wait_for_echo_topic.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.lyrical.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external_session.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/foxglove.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/metadata.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external_session.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/foxglove.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/metadata.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external_session.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external_session.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external_session.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external.ros2docker.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external_session.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_communication.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_external.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/10_restamp/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/11_trickle/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/12_content_integrity/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/13_link_latency/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/14_remote_assist_anonymized/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/15_remote_assist_anonymized_costmap/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/16_remote_assist_anonymized_camera/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/17_synthetic_camera_quality/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/1_heartbeat/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/1_heartbeat_status/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/2_native_chatter/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/3_comp_occ_grid/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/4_comp_occ_grid_zen/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/5_sized_payload/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/6_sized_payload_zen/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/7_latched_static/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/8_drop/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/9_throttle/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/bench_1_1_capacity/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/bench_1_2_load_sweep/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/bench_1_3_ramp/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/bench_1_4_recovery/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/examples/sessions/bench_1_5_camera/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ros2docker.json.example +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ros2docker.lyrical.json.example +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/.vscode/c_cpp_properties.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/.vscode/settings.json +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_easy_mode.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_initial_peer_list.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v2.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v3.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v4.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/archive/fastdds_v5.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/create_zenoh_json5.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/cyclonedds_local_participants.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/cyclonedds_minimal.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/cyclonedds_scoped.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/cyclonedds_tuned.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/fastdds_easy_mode.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/fastdds_local.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/fastdds_tuned.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/fastdds_unicast.xml.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/get_ota_xml.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/utils.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ota_configs/zenoh.json5.template +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/.gitkeep +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_cpp/CMakeLists.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_cpp/COLCON_IGNORE +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_cpp/LICENSE.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_cpp/package.xml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_cpp/src/hz_monitor.cpp +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_cpp/src/universal_compressor.cpp +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_cpp/src/universal_decompressor.cpp +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_msgs/CMakeLists.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_msgs/LICENSE.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/CompressedData.msg +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/EchoHeartbeat.msg +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/OtaStamped.msg +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_msgs/msg/SizedPayload.msg +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_msgs/package.xml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/LICENSE.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/__init__.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/base_bridge_relay.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/bridge_in.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/bridge_out.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/ffmpeg_flags.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/heartbeat_echo.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/hz_monitor.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/image_bandwidth_reducer.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/image_pixel_capper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/lanelet_saver.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/latch_relay.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/costmap_remapper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/marker_remapper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/speed_test.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/tf_remapper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/traffic_monitor.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/legacy/wifi_network_analysis_1.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/link_bytes.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/link_trace.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/local_global_frame_bridge.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/localization_map_split_reduce_saver.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/msg_transform.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/pair_management.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/playout_pacer.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/playout_pacer_core.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/pub_sub_pair.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/qos.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/relay_in.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/relay_out.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/restamp.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/sized_publisher.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency_core.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/status_overview_core.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/tf_map_to_cart_saver.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/topic_monitor.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/topic_resolution.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/trickle.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_compressor.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_decompressor.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_unwrapper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_wrapper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/package.xml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/resource/com_py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/setup.cfg +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/setup.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/test/test_copyright.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/test/test_flake8.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/ros2src/com_py/test/test_pep257.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/content/network/up_connectivity.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/content/network/wifi_network_analysis_2.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/creation/anonymize_bag.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/creation/catmux_log_setup.sh +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/creation/create_session_yaml.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/creation/run_session.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/resources/ws/session/creation/strip_ansi.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/status_eval.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/stream_stats.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/trace_profiles.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/transit.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom/video_quality.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom_dev.egg-info/dependency_links.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom_dev.egg-info/entry_points.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom_dev.egg-info/requires.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/src/rosotacom_dev.egg-info/top_level.txt +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_benched_set_registry.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_findings.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_lyrical_runtime_compat.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_markdown_links.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_ota_source_bundle.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_package_metadata.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_packaged_resources.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_profile_covers_generated_commands.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_pytest_policy.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_readme_examples.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_security_maintenance_config.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_session_configuration_doc.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_session_template_substitution.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/contract/test_workflow_contracts.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/conftest.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_anonymize_e2e.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_benchmark_ab.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_benchmark_capacity.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_benchmark_replay.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_parallel_smoke.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_smoke.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_timeline_stepping.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/e2e/test_video_quality_e2e.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/sessions/generate_rmw_matrix.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-local_zenoh-ros2dds-ota/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-ota/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/sessions/rmw_matrix/1_heartbeat_cyclone-ota-tuned/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/sessions/rmw_matrix/1_heartbeat_fastdds/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/sessions/rmw_matrix/1_heartbeat_fastdds-default/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/sessions/rmw_matrix/1_heartbeat_zen-endpoints/session-definition.yaml +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_anonymize.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_bag_ground_truth.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_benched_set.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_benchmark.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_benchmark_delta.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_bundle_check.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_catmux_pane_logging.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_cli.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_cli_benchmark.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_concurrency.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_container_shaping.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_deployment.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_ffmpeg_flags.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_ffmpeg_packet.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_field_profiles.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_forensics.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_geomap.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_image_cache.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_link_bytes.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_link_trace.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_netem_dist.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_network_preflight.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_network_profiles.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_network_shaper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_ota_link_configs.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_ota_unwrapper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_ota_wrapper.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_ota_xml.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_playout_pacer.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_plots.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_pub_sub_pair_types.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_qos_fallback_logging.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_relay_optional_parameters.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_ros_node_entity_ownership.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_session_pipeline.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_stage_latency.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_status_eval.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_status_overview.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_status_overview_observer.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_stream_stats.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_trace_profiles.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/tests/unit/test_transit.py +0 -0
- {rosotacom_dev-2.5.dev66 → rosotacom_dev-2.5.dev69}/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.
|
|
3
|
+
Version: 2.5.dev69
|
|
4
4
|
Summary: Host CLI for ROS OTA communication sessions.
|
|
5
5
|
Author: develNor
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -684,6 +684,14 @@ the [link trace recorder](docs/link-trace.md). It writes
|
|
|
684
684
|
`link_trace.jsonl` under the same status directory with `/proc/net/dev` counter
|
|
685
685
|
deltas, echo-heartbeat RTT/loss provenance, and an optional modem-metrics hook.
|
|
686
686
|
|
|
687
|
+
Those counters localise loss to a *direction* and no further. When the question
|
|
688
|
+
is which fragment was lost, whether a retransmit followed, or how one sample's
|
|
689
|
+
sub-messages were spaced, enable the [header-only OTA capture](docs/ota-pcap.md)
|
|
690
|
+
next to them — `shared.ota_pcap.enabled`, off by default, roughly 1 % of payload
|
|
691
|
+
volume, and every packet labelled uplink or downlink. It needs no capability,
|
|
692
|
+
no image change and no extra container — only the passwordless sudo the
|
|
693
|
+
container already has, and only until its socket is open.
|
|
694
|
+
|
|
687
695
|
Convert a recorded link trace into a profiles-file entry when you want to replay
|
|
688
696
|
the same drive conditions in a repeatable benchmark:
|
|
689
697
|
|
|
@@ -639,6 +639,14 @@ the [link trace recorder](docs/link-trace.md). It writes
|
|
|
639
639
|
`link_trace.jsonl` under the same status directory with `/proc/net/dev` counter
|
|
640
640
|
deltas, echo-heartbeat RTT/loss provenance, and an optional modem-metrics hook.
|
|
641
641
|
|
|
642
|
+
Those counters localise loss to a *direction* and no further. When the question
|
|
643
|
+
is which fragment was lost, whether a retransmit followed, or how one sample's
|
|
644
|
+
sub-messages were spaced, enable the [header-only OTA capture](docs/ota-pcap.md)
|
|
645
|
+
next to them — `shared.ota_pcap.enabled`, off by default, roughly 1 % of payload
|
|
646
|
+
volume, and every packet labelled uplink or downlink. It needs no capability,
|
|
647
|
+
no image change and no extra container — only the passwordless sudo the
|
|
648
|
+
container already has, and only until its socket is open.
|
|
649
|
+
|
|
642
650
|
Convert a recorded link trace into a profiles-file entry when you want to replay
|
|
643
651
|
the same drive conditions in a repeatable benchmark:
|
|
644
652
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Header-Only OTA Capture
|
|
2
|
+
|
|
3
|
+
A passive, header-only packet capture of the OTA link, written beside the
|
|
4
|
+
[link trace](link-trace.md) by the same node:
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
session-instances/.../logs/<peer>/status/ota.pcap
|
|
8
|
+
session-instances/.../logs/<peer>/status/ota.json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Enable it in a session definition:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
shared:
|
|
15
|
+
use_status_overview: true
|
|
16
|
+
ota_pcap:
|
|
17
|
+
enabled: true
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Everything else is optional:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
ota_pcap:
|
|
24
|
+
enabled: true
|
|
25
|
+
snaplen: 96 # bytes kept per packet; headers, no payload
|
|
26
|
+
max_mb: 2000 # ceiling for one capture file
|
|
27
|
+
peer_filter: true # keep only packets to or from the remote peer
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Off by default, and off is exactly the session that existed before: a
|
|
31
|
+
definition without the block renders the same peer files it always did, which
|
|
32
|
+
a test asserts by rendering both and diffing them.
|
|
33
|
+
|
|
34
|
+
## What it answers that the link trace cannot
|
|
35
|
+
|
|
36
|
+
`link_trace.jsonl` counts `/proc/net/dev` bytes per direction once a second.
|
|
37
|
+
That localises loss to a **direction** and no further. It cannot say which
|
|
38
|
+
fragment of which sample was lost, whether a retransmit followed, or how one
|
|
39
|
+
sample's sub-messages were spaced on the wire.
|
|
40
|
+
|
|
41
|
+
The capture answers those, at roughly 1 % of payload volume. Use the two
|
|
42
|
+
together: the trace is the cheap continuous record, the capture is what you
|
|
43
|
+
turn on for a measurement run.
|
|
44
|
+
|
|
45
|
+
## What the file is
|
|
46
|
+
|
|
47
|
+
`LINKTYPE_LINUX_SLL` (113), microsecond `pcap`, snaplen 96 by default. Every
|
|
48
|
+
reader takes it — Wireshark, `tshark`, `scapy`, `capinfos`.
|
|
49
|
+
|
|
50
|
+
The link type is the important choice. The capture uses a `SOCK_DGRAM` packet
|
|
51
|
+
socket, so one code path works on both link types the fleet has: a VPN `tun`
|
|
52
|
+
device is layer 3 and has no link-layer header at all, while a control-centre
|
|
53
|
+
LAN port is Ethernet. The kernel hands `SOCK_DGRAM` the network-layer packet
|
|
54
|
+
either way, plus the `sockaddr_ll` that `LINKTYPE_LINUX_SLL` is defined over.
|
|
55
|
+
|
|
56
|
+
That pseudo-header carries the field the analysis is for:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# uplink (this host sent it) against downlink, per packet
|
|
60
|
+
tshark -r ota.pcap -T fields -e sll.pkttype | sort | uniq -c
|
|
61
|
+
# 4 = PACKET_OUTGOING, 0 = PACKET_HOST
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A `LINKTYPE_RAW` capture would have thrown that away.
|
|
65
|
+
|
|
66
|
+
Timestamps come from the kernel (`SO_TIMESTAMPNS`), not from the moment python
|
|
67
|
+
reached the packet.
|
|
68
|
+
|
|
69
|
+
## The sidecar, and the number that matters
|
|
70
|
+
|
|
71
|
+
`ota.json` is written when the capture stops:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"interface": "tun0",
|
|
76
|
+
"snaplen": 96,
|
|
77
|
+
"linktype": "LINKTYPE_LINUX_SLL",
|
|
78
|
+
"filter": "udp and host 10.254.0.37",
|
|
79
|
+
"promiscuous": false,
|
|
80
|
+
"packets": 2434,
|
|
81
|
+
"bytes": 272498,
|
|
82
|
+
"kernel_drops": 0,
|
|
83
|
+
"kernel_seen": 4638,
|
|
84
|
+
"first_timestamp": 1787297717.008177,
|
|
85
|
+
"last_timestamp": 1787297733.10892,
|
|
86
|
+
"stopped_because": "signal"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**`kernel_drops` is a gap in the file, not on the link.** A capture that fell
|
|
91
|
+
behind looks exactly like radio loss to anyone reading the pcap months later,
|
|
92
|
+
so subtract it before reading any loss out of the capture. Zero is the normal
|
|
93
|
+
result on a link of a few Mbit/s; a non-zero value means this process, not the
|
|
94
|
+
radio.
|
|
95
|
+
|
|
96
|
+
## Which interface, and the refusal that matters
|
|
97
|
+
|
|
98
|
+
The interface is not configured. It is resolved from the peer's own OTA
|
|
99
|
+
address by `com_py.link_bytes.resolve_link_interface` — the same resolution the
|
|
100
|
+
status snapshot's `link` block uses, including its refusal to report a loopback
|
|
101
|
+
interface for a non-loopback address. That refusal is issue #267: sampling `lo`
|
|
102
|
+
on a machine that also runs an `-a` recorder reads about 28 Gbit/s next to a
|
|
103
|
+
6 Mbit/s link, and it survived a field session and an offline analysis before
|
|
104
|
+
anyone doubted the number. A capture on the wrong device would be the same
|
|
105
|
+
mistake in a bigger file.
|
|
106
|
+
|
|
107
|
+
If no interface resolves, the capture is skipped with a warning and the session
|
|
108
|
+
runs on. `shared.ota_pcap.enabled` therefore requires
|
|
109
|
+
`shared.use_status_overview: true`, which is where that resolution lives.
|
|
110
|
+
|
|
111
|
+
## Privilege, containers, and what the option costs
|
|
112
|
+
|
|
113
|
+
No image change, no rebuild, no added capability and no extra container in
|
|
114
|
+
`docker ps`. What it does need is `sudo`, and only until the socket is open.
|
|
115
|
+
|
|
116
|
+
ros2docker's entrypoint runs this container's processes as `containeruser`,
|
|
117
|
+
whose `CapEff` is `0000000000000000`. A non-root process cannot open an
|
|
118
|
+
`AF_PACKET` socket however the container was started, so `--cap-add NET_RAW`
|
|
119
|
+
would buy nothing. `containeruser` does have passwordless sudo here — the NET
|
|
120
|
+
pane's `sudo iftop` already depends on it — so the capture is launched through
|
|
121
|
+
it, and `--drop-to` hands the process back to `containeruser` the moment the
|
|
122
|
+
socket exists. The pcap therefore belongs to the same user as
|
|
123
|
+
`link_trace.jsonl` beside it, rather than to root.
|
|
124
|
+
|
|
125
|
+
Two earlier attempts at this assumed root from a `docker run` that bypassed the
|
|
126
|
+
entrypoint, and were wrong in the same way both times. If you change this,
|
|
127
|
+
check the **running** container:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
docker exec <com-container> bash -lc 'id; grep CapEff /proc/self/status'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`CAP_NET_ADMIN` is *not* taken and is not needed: it buys promiscuous mode,
|
|
134
|
+
which this capture deliberately does not use. Everything of interest is
|
|
135
|
+
addressed to or from this host, and promiscuous mode on a control-centre LAN
|
|
136
|
+
port would collect other people's traffic.
|
|
137
|
+
|
|
138
|
+
The capture runs as a separate process rather than inside the status node's
|
|
139
|
+
thread, so a capture that misbehaves cannot reach the snapshot an operator is
|
|
140
|
+
watching. It is stopped with `SIGINT` and waited for, because that is what
|
|
141
|
+
writes the sidecar JSON.
|
|
142
|
+
|
|
143
|
+
## Cost
|
|
144
|
+
|
|
145
|
+
Header-only at 96 bytes: about 6 MB per minute at 500 packets/s, so a two-hour
|
|
146
|
+
run is well under a gigabyte. `max_mb` bounds a mistake — a capture pointed at
|
|
147
|
+
a busy LAN port — rather than shaping a run; when it is reached the capture
|
|
148
|
+
stops and says so in `stopped_because`.
|
|
149
|
+
|
|
150
|
+
Nothing is transmitted, so the capture costs no link capacity.
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""Header-only packet capture of the OTA link, beside the link trace.
|
|
2
|
+
|
|
3
|
+
Runs inside the communication container for the lifetime of one peer, started
|
|
4
|
+
by `status_overview` when a session definition asks for it:
|
|
5
|
+
|
|
6
|
+
shared:
|
|
7
|
+
use_status_overview: true
|
|
8
|
+
ota_pcap:
|
|
9
|
+
enabled: true
|
|
10
|
+
|
|
11
|
+
python3 -m com_py.ota_pcap --iface tun0 --out .../ota.pcap [--peer 10.254.0.37]
|
|
12
|
+
|
|
13
|
+
WHAT IT ANSWERS THAT `link_trace.jsonl` CANNOT
|
|
14
|
+
----------------------------------------------
|
|
15
|
+
The link trace counts `/proc/net/dev` bytes per direction once a second, which
|
|
16
|
+
localises loss to a *direction* and no further. It cannot say which fragment of
|
|
17
|
+
which sample was lost, whether a retransmit followed, or how one sample's
|
|
18
|
+
sub-messages were spaced on the wire. Those are the questions a header-only
|
|
19
|
+
capture answers, at roughly 1 % of payload volume.
|
|
20
|
+
|
|
21
|
+
It is passive and local: nothing is transmitted, so it costs no link capacity,
|
|
22
|
+
and the interface is never put into promiscuous mode — everything of interest
|
|
23
|
+
is addressed to or from this host, and promiscuous mode on a control-centre LAN
|
|
24
|
+
port would collect other people's traffic. `CAP_NET_ADMIN`, which the
|
|
25
|
+
originating issue assumed was needed, buys only that.
|
|
26
|
+
|
|
27
|
+
WHY IT LIVES HERE AND NOT NEXT TO A RECORDER
|
|
28
|
+
--------------------------------------------
|
|
29
|
+
The first implementation of this sat beside the recording tool in the
|
|
30
|
+
downstream project, and had to rebuild what this container already knows:
|
|
31
|
+
which interface carries the link, and to which peer. `link_bytes` answers both
|
|
32
|
+
from the peer's own OTA address — including the refusal that matters most, a
|
|
33
|
+
loopback interface for a non-loopback address, which once read 28 Gbit/s next
|
|
34
|
+
to a 6 Mbit/s link and survived a field session (#267). A capture that has to
|
|
35
|
+
re-derive link knowledge is in the wrong place; this one asks the same
|
|
36
|
+
resolution the status snapshot's own link block uses.
|
|
37
|
+
|
|
38
|
+
WHAT IT NEEDS, MEASURED RATHER THAN ASSUMED
|
|
39
|
+
-------------------------------------------
|
|
40
|
+
`sudo`, and only until the socket is open. ros2docker's entrypoint runs this
|
|
41
|
+
container's processes as `containeruser`, whose `CapEff` is
|
|
42
|
+
0000000000000000 — a non-root process cannot open an `AF_PACKET` socket
|
|
43
|
+
however the container was started, so `--cap-add` buys nothing. `containeruser`
|
|
44
|
+
does have passwordless sudo here, which the NET pane's `sudo iftop` already
|
|
45
|
+
relies on, so the capture is launched through it and `--drop-to` hands the
|
|
46
|
+
process back to `containeruser` the moment the socket exists. The pcap in
|
|
47
|
+
`logs/<peer>/status/` therefore belongs to the same user as `link_trace.jsonl`
|
|
48
|
+
beside it.
|
|
49
|
+
|
|
50
|
+
That is the whole cost: no image change, no rebuild, no added capability and no
|
|
51
|
+
extra container. Two earlier attempts at this assumed root from a `docker run`
|
|
52
|
+
that bypassed the entrypoint, and both were wrong in the same way — check the
|
|
53
|
+
*running* container, not the image.
|
|
54
|
+
|
|
55
|
+
THE FILE
|
|
56
|
+
--------
|
|
57
|
+
`SOCK_DGRAM` rather than `SOCK_RAW`, which is what makes one code path work on
|
|
58
|
+
both link types the fleet uses: a VPN `tun` device is layer 3 and has no
|
|
59
|
+
link-layer header at all, while a LAN port is Ethernet. The kernel hands a
|
|
60
|
+
`SOCK_DGRAM` packet socket the network-layer packet either way, plus the
|
|
61
|
+
`sockaddr_ll` that says what the link layer was.
|
|
62
|
+
|
|
63
|
+
That is exactly the input `LINKTYPE_LINUX_SLL` (113) is defined over, so the
|
|
64
|
+
16-byte SLL pseudo-header is synthesized from the address tuple and the file
|
|
65
|
+
reads in Wireshark, tshark and scapy without a hint. It carries the field the
|
|
66
|
+
analysis is actually for: `pkttype` distinguishes PACKET_OUTGOING from
|
|
67
|
+
PACKET_HOST, so **every packet in the file is labelled uplink or downlink** —
|
|
68
|
+
which is the direction question the per-second counters answer only coarsely,
|
|
69
|
+
and the one a direction-blind `LINKTYPE_RAW` capture would have thrown away.
|
|
70
|
+
|
|
71
|
+
Timestamps come from the kernel (`SO_TIMESTAMPNS`), not from the moment python
|
|
72
|
+
got round to the packet, and are written at microsecond resolution — the
|
|
73
|
+
classic `0xa1b2c3d4` magic, because every reader takes it and a millisecond
|
|
74
|
+
phenomenon does not need nanoseconds.
|
|
75
|
+
|
|
76
|
+
HONESTY ABOUT WHAT IT MISSED
|
|
77
|
+
----------------------------
|
|
78
|
+
A capture that quietly drops packets is worse than none: the analysis would
|
|
79
|
+
read the gap as radio loss. So `PACKET_STATISTICS` is read from the kernel and
|
|
80
|
+
the sidecar `.json` reports `kernel_drops` next to `packets`. A non-zero drop
|
|
81
|
+
count means this process fell behind, not that the link did.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
from __future__ import annotations
|
|
85
|
+
|
|
86
|
+
import argparse
|
|
87
|
+
import json
|
|
88
|
+
import os
|
|
89
|
+
import signal
|
|
90
|
+
import socket
|
|
91
|
+
import struct
|
|
92
|
+
import sys
|
|
93
|
+
import time
|
|
94
|
+
from pathlib import Path
|
|
95
|
+
|
|
96
|
+
#: Capture every ethertype; the protocol filter is applied below, on the
|
|
97
|
+
#: network-layer header the kernel hands a SOCK_DGRAM packet socket.
|
|
98
|
+
ETH_P_ALL = 0x0003
|
|
99
|
+
|
|
100
|
+
ETH_P_IP = 0x0800
|
|
101
|
+
ETH_P_IPV6 = 0x86DD
|
|
102
|
+
IPPROTO_UDP = 17
|
|
103
|
+
|
|
104
|
+
#: linux/if_packet.h. Only the outgoing one is load-bearing — it is what makes
|
|
105
|
+
#: a packet uplink rather than downlink — but all five are written through so a
|
|
106
|
+
#: reader sees what the kernel saw.
|
|
107
|
+
PACKET_OUTGOING = 4
|
|
108
|
+
|
|
109
|
+
SOL_PACKET = 263
|
|
110
|
+
PACKET_STATISTICS = 6
|
|
111
|
+
|
|
112
|
+
#: CPython does not export these on every build — 3.12 in the recorder image
|
|
113
|
+
#: has neither, which cost the first capture its kernel timestamps and left it
|
|
114
|
+
#: stamping packets when python got round to them. The numbers are the
|
|
115
|
+
#: asm-generic ones every architecture the fleet runs uses, and `SCM_` shares
|
|
116
|
+
#: the `SO_` value by definition (linux/socket.h).
|
|
117
|
+
SO_TIMESTAMPNS = getattr(socket, "SO_TIMESTAMPNS", 35)
|
|
118
|
+
SCM_TIMESTAMPNS = getattr(socket, "SCM_TIMESTAMPNS", 35)
|
|
119
|
+
|
|
120
|
+
#: pcap, classic microsecond magic (LE). Every reader takes it.
|
|
121
|
+
PCAP_MAGIC = 0xA1B2C3D4
|
|
122
|
+
LINKTYPE_LINUX_SLL = 113
|
|
123
|
+
|
|
124
|
+
#: `-s 96` in the issue: enough for the link, network, UDP and RTPS sub-message
|
|
125
|
+
#: headers, and short of any payload.
|
|
126
|
+
DEFAULT_SNAPLEN = 96
|
|
127
|
+
|
|
128
|
+
#: Not part of the recorder's own disk budget, and deliberately far above what
|
|
129
|
+
#: a drive produces: header-only at 500 packets/s is ~6 MB per minute, so a
|
|
130
|
+
#: two-hour drive is well under a gigabyte. It exists to bound a mistake — a
|
|
131
|
+
#: capture pointed at a busy control-centre LAN port — not to shape a drive.
|
|
132
|
+
DEFAULT_MAX_MB = 2000
|
|
133
|
+
|
|
134
|
+
#: 4 MiB of kernel socket buffer. The default is a few hundred kilobytes, which
|
|
135
|
+
#: is where `kernel_drops` comes from on a burst.
|
|
136
|
+
RCVBUF_BYTES = 4 << 20
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def log(message: str) -> None:
|
|
140
|
+
print(f"[ota_pcap] {message}", flush=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------ the file
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def pcap_header(snaplen: int) -> bytes:
|
|
147
|
+
return struct.pack(
|
|
148
|
+
"<IHHiIII",
|
|
149
|
+
PCAP_MAGIC,
|
|
150
|
+
2, # version_major
|
|
151
|
+
4, # version_minor
|
|
152
|
+
0, # thiszone: timestamps are UTC
|
|
153
|
+
0, # sigfigs, as every writer leaves it
|
|
154
|
+
snaplen,
|
|
155
|
+
LINKTYPE_LINUX_SLL,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sll_header(pkttype: int, hatype: int, addr: bytes, protocol: int) -> bytes:
|
|
160
|
+
"""The 16-byte pseudo-header LINKTYPE_LINUX_SLL is defined over.
|
|
161
|
+
|
|
162
|
+
Big-endian, unlike the pcap headers around it — that is the format, not an
|
|
163
|
+
oversight. `addr` is padded to the fixed 8 bytes and its true length is
|
|
164
|
+
written next to it, which is how a reader tells a 6-byte MAC from the empty
|
|
165
|
+
address a layer-3 tunnel has.
|
|
166
|
+
"""
|
|
167
|
+
address = (addr or b"")[:8]
|
|
168
|
+
return struct.pack(
|
|
169
|
+
"!HHH8sH",
|
|
170
|
+
pkttype,
|
|
171
|
+
hatype,
|
|
172
|
+
len(address),
|
|
173
|
+
address.ljust(8, b"\0"),
|
|
174
|
+
protocol,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def record_header(seconds: int, micros: int, incl_len: int, orig_len: int) -> bytes:
|
|
179
|
+
return struct.pack("<IIII", seconds, micros, incl_len, orig_len)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --------------------------------------------------------------- the filter
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def is_udp(protocol: int, packet: bytes) -> bool:
|
|
186
|
+
"""UDP, judged from the network-layer header the packet socket handed us.
|
|
187
|
+
|
|
188
|
+
Both branches read a fixed offset of the header the kernel guarantees is
|
|
189
|
+
there, so a truncated or malformed packet is answered `False` rather than
|
|
190
|
+
raising inside the capture loop.
|
|
191
|
+
|
|
192
|
+
IPv4 fragments after the first carry no UDP header, and are still matched:
|
|
193
|
+
the protocol field is in the IP header of every fragment, which is what
|
|
194
|
+
keeps a fragmented sample whole in the file.
|
|
195
|
+
"""
|
|
196
|
+
if protocol == ETH_P_IP:
|
|
197
|
+
return len(packet) > 9 and packet[9] == IPPROTO_UDP
|
|
198
|
+
if protocol == ETH_P_IPV6:
|
|
199
|
+
# Next Header. An extension-header chain would hide the UDP behind it;
|
|
200
|
+
# nothing on this link uses one, and guessing past it would be the kind
|
|
201
|
+
# of cleverness that silently drops traffic.
|
|
202
|
+
return len(packet) > 6 and packet[6] == IPPROTO_UDP
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def addresses_of(protocol: int, packet: bytes) -> tuple[str, str] | None:
|
|
207
|
+
"""Source and destination, for the optional peer filter."""
|
|
208
|
+
try:
|
|
209
|
+
if protocol == ETH_P_IP and len(packet) >= 20:
|
|
210
|
+
return (
|
|
211
|
+
socket.inet_ntop(socket.AF_INET, packet[12:16]),
|
|
212
|
+
socket.inet_ntop(socket.AF_INET, packet[16:20]),
|
|
213
|
+
)
|
|
214
|
+
if protocol == ETH_P_IPV6 and len(packet) >= 40:
|
|
215
|
+
return (
|
|
216
|
+
socket.inet_ntop(socket.AF_INET6, packet[8:24]),
|
|
217
|
+
socket.inet_ntop(socket.AF_INET6, packet[24:40]),
|
|
218
|
+
)
|
|
219
|
+
except (ValueError, OSError):
|
|
220
|
+
return None
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def wanted(protocol: int, packet: bytes, peer: str | None) -> bool:
|
|
225
|
+
if not is_udp(protocol, packet):
|
|
226
|
+
return False
|
|
227
|
+
if peer is None:
|
|
228
|
+
return True
|
|
229
|
+
pair = addresses_of(protocol, packet)
|
|
230
|
+
return bool(pair) and peer in pair
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------- capturing
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def open_socket(iface: str) -> socket.socket:
|
|
237
|
+
"""A non-promiscuous packet socket, bound to one interface.
|
|
238
|
+
|
|
239
|
+
`SOCK_DGRAM` normalises the two link types the fleet has — see the module
|
|
240
|
+
header. No `PACKET_ADD_MEMBERSHIP`, so the interface is never put into
|
|
241
|
+
promiscuous mode and only this host's own traffic is seen.
|
|
242
|
+
"""
|
|
243
|
+
sock = socket.socket(socket.AF_PACKET, socket.SOCK_DGRAM, socket.htons(ETH_P_ALL))
|
|
244
|
+
try:
|
|
245
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, RCVBUF_BYTES)
|
|
246
|
+
except OSError: # a smaller buffer is a worse capture, not a broken one
|
|
247
|
+
pass
|
|
248
|
+
try:
|
|
249
|
+
sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1)
|
|
250
|
+
except OSError:
|
|
251
|
+
log("WARN the kernel refused SO_TIMESTAMPNS — timestamps are taken in "
|
|
252
|
+
"userspace, so they carry this process's scheduling delay")
|
|
253
|
+
sock.bind((iface, ETH_P_ALL))
|
|
254
|
+
return sock
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def drop_privileges(spec: str | None) -> str | None:
|
|
258
|
+
"""Hand the process back to the user that started it.
|
|
259
|
+
|
|
260
|
+
The capture is launched through `sudo` — see the module header — because a
|
|
261
|
+
non-root process cannot open an `AF_PACKET` socket. Nothing after that call
|
|
262
|
+
needs root, and the artifacts in `logs/<peer>/status/` belong to
|
|
263
|
+
`containeruser` like `link_trace.jsonl` beside them: a root-owned file in
|
|
264
|
+
that directory is a `sudo rm` waiting to happen months later, and
|
|
265
|
+
`gather_drive.py` would copy it without noticing.
|
|
266
|
+
|
|
267
|
+
`spec` is "uid:gid". An argument rather than an environment variable
|
|
268
|
+
because sudo does not pass the environment through. Returns what it did, or
|
|
269
|
+
None when there was nothing to do.
|
|
270
|
+
"""
|
|
271
|
+
if not spec:
|
|
272
|
+
return None
|
|
273
|
+
try:
|
|
274
|
+
uid_text, _, gid_text = spec.partition(":")
|
|
275
|
+
uid, gid = int(uid_text), int(gid_text or uid_text)
|
|
276
|
+
except ValueError:
|
|
277
|
+
log(f"WARN --drop-to {spec!r} is not uid:gid — staying as I am")
|
|
278
|
+
return None
|
|
279
|
+
if os.geteuid() != 0:
|
|
280
|
+
return None
|
|
281
|
+
try:
|
|
282
|
+
os.setgroups([])
|
|
283
|
+
os.setgid(gid)
|
|
284
|
+
os.setuid(uid)
|
|
285
|
+
except OSError as error:
|
|
286
|
+
log(f"WARN could not drop to {uid}:{gid} ({error}) — the file will be root's")
|
|
287
|
+
return None
|
|
288
|
+
return f"{uid}:{gid}"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def kernel_timestamp(ancdata) -> tuple[int, int] | None:
|
|
292
|
+
"""(seconds, microseconds) out of SCM_TIMESTAMPNS, if the kernel sent it."""
|
|
293
|
+
for level, kind, data in ancdata:
|
|
294
|
+
if level == socket.SOL_SOCKET and kind == SCM_TIMESTAMPNS:
|
|
295
|
+
if len(data) >= 16:
|
|
296
|
+
seconds, nanos = struct.unpack("qq", data[:16])
|
|
297
|
+
return int(seconds), int(nanos) // 1000
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def packet_statistics(sock: socket.socket) -> tuple[int, int]:
|
|
302
|
+
"""(seen, dropped) since the last call — reading resets the counters."""
|
|
303
|
+
try:
|
|
304
|
+
raw = sock.getsockopt(SOL_PACKET, PACKET_STATISTICS, 8)
|
|
305
|
+
except OSError:
|
|
306
|
+
return 0, 0
|
|
307
|
+
seen, dropped = struct.unpack("II", raw)
|
|
308
|
+
return int(seen), int(dropped)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class Capture:
|
|
312
|
+
"""One pcap file, and the counts needed to trust it."""
|
|
313
|
+
|
|
314
|
+
def __init__(self, iface: str, out: Path, *, snaplen: int = DEFAULT_SNAPLEN,
|
|
315
|
+
peer: str | None = None, max_bytes: int = DEFAULT_MAX_MB * 1000 * 1000,
|
|
316
|
+
drop_to: str | None = None):
|
|
317
|
+
self.drop_to = drop_to
|
|
318
|
+
self.iface = iface
|
|
319
|
+
self.out = out
|
|
320
|
+
self.snaplen = snaplen
|
|
321
|
+
self.peer = peer
|
|
322
|
+
self.max_bytes = max_bytes
|
|
323
|
+
self.packets = 0
|
|
324
|
+
self.bytes_written = 0
|
|
325
|
+
self.kernel_drops = 0
|
|
326
|
+
self.kernel_seen = 0
|
|
327
|
+
self.first_ts: float | None = None
|
|
328
|
+
self.last_ts: float | None = None
|
|
329
|
+
self.stopped_because = "signal"
|
|
330
|
+
|
|
331
|
+
def sidecar(self) -> dict:
|
|
332
|
+
"""What the file is, and what it is missing. Written next to the pcap."""
|
|
333
|
+
return {
|
|
334
|
+
"interface": self.iface,
|
|
335
|
+
"snaplen": self.snaplen,
|
|
336
|
+
"linktype": "LINKTYPE_LINUX_SLL",
|
|
337
|
+
"filter": "udp" + (f" and host {self.peer}" if self.peer else ""),
|
|
338
|
+
"promiscuous": False,
|
|
339
|
+
"packets": self.packets,
|
|
340
|
+
"bytes": self.bytes_written,
|
|
341
|
+
# Packets the kernel dropped because this process fell behind. A
|
|
342
|
+
# non-zero value here is a gap in the FILE, not in the link, and
|
|
343
|
+
# any loss analysis has to subtract it rather than report it.
|
|
344
|
+
"kernel_drops": self.kernel_drops,
|
|
345
|
+
"kernel_seen": self.kernel_seen,
|
|
346
|
+
"first_timestamp": self.first_ts,
|
|
347
|
+
"last_timestamp": self.last_ts,
|
|
348
|
+
"stopped_because": self.stopped_because,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def run(self, stop: list) -> None:
|
|
352
|
+
sock = open_socket(self.iface)
|
|
353
|
+
# Root was needed for that one call and for nothing after it.
|
|
354
|
+
dropped = drop_privileges(self.drop_to)
|
|
355
|
+
if dropped:
|
|
356
|
+
log(f"privileges dropped to {dropped}; the socket stays open")
|
|
357
|
+
packet_statistics(sock) # zero the counters; the run owns them from here
|
|
358
|
+
ancillary = socket.CMSG_SPACE(16)
|
|
359
|
+
self.out.parent.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
with open(self.out, "wb", buffering=1 << 20) as handle:
|
|
361
|
+
handle.write(pcap_header(self.snaplen))
|
|
362
|
+
self.bytes_written = 24
|
|
363
|
+
sock.settimeout(0.5)
|
|
364
|
+
while not stop:
|
|
365
|
+
try:
|
|
366
|
+
packet, ancdata, _flags, address = sock.recvmsg(65535, ancillary)
|
|
367
|
+
except socket.timeout:
|
|
368
|
+
continue
|
|
369
|
+
except OSError as error:
|
|
370
|
+
log(f"WARN capture socket: {error}")
|
|
371
|
+
self.stopped_because = f"socket error: {error}"
|
|
372
|
+
break
|
|
373
|
+
# (ifname, protocol, pkttype, hatype, addr) — everything the
|
|
374
|
+
# SLL header needs, which is why SOCK_DGRAM is enough.
|
|
375
|
+
_name, protocol, pkttype, hatype, addr = address
|
|
376
|
+
if not wanted(protocol, packet, self.peer):
|
|
377
|
+
continue
|
|
378
|
+
stamp = kernel_timestamp(ancdata)
|
|
379
|
+
if stamp is None:
|
|
380
|
+
now = time.time()
|
|
381
|
+
stamp = (int(now), int((now % 1) * 1_000_000))
|
|
382
|
+
body = sll_header(pkttype, hatype, addr, protocol) + packet
|
|
383
|
+
orig_len = len(body)
|
|
384
|
+
clipped = body[: self.snaplen]
|
|
385
|
+
handle.write(record_header(stamp[0], stamp[1], len(clipped), orig_len))
|
|
386
|
+
handle.write(clipped)
|
|
387
|
+
self.packets += 1
|
|
388
|
+
self.bytes_written += 16 + len(clipped)
|
|
389
|
+
seconds = stamp[0] + stamp[1] / 1_000_000
|
|
390
|
+
if self.first_ts is None:
|
|
391
|
+
self.first_ts = seconds
|
|
392
|
+
self.last_ts = seconds
|
|
393
|
+
if self.bytes_written >= self.max_bytes:
|
|
394
|
+
log(f"stopping at the {self.max_bytes // 1000 // 1000} MB ceiling "
|
|
395
|
+
f"after {self.packets} packets — the recording is untouched")
|
|
396
|
+
self.stopped_because = "size ceiling"
|
|
397
|
+
break
|
|
398
|
+
if self.packets % 20000 == 0:
|
|
399
|
+
self._collect(sock)
|
|
400
|
+
self._collect(sock)
|
|
401
|
+
sock.close()
|
|
402
|
+
|
|
403
|
+
def _collect(self, sock: socket.socket) -> None:
|
|
404
|
+
seen, dropped = packet_statistics(sock)
|
|
405
|
+
self.kernel_seen += seen
|
|
406
|
+
self.kernel_drops += dropped
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# --------------------------------------------------------------------- main
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def main(argv: list[str] | None = None) -> int:
|
|
413
|
+
parser = argparse.ArgumentParser(
|
|
414
|
+
description="Header-only UDP capture on one interface, written as pcap.")
|
|
415
|
+
parser.add_argument("--iface", required=True, help="interface to capture on")
|
|
416
|
+
parser.add_argument("--out", required=True, type=Path, help="pcap file to write")
|
|
417
|
+
parser.add_argument("--snaplen", type=int, default=DEFAULT_SNAPLEN,
|
|
418
|
+
help=f"bytes kept per packet (default {DEFAULT_SNAPLEN})")
|
|
419
|
+
parser.add_argument("--peer", help="keep only packets to or from this address")
|
|
420
|
+
parser.add_argument("--max-mb", type=int, default=DEFAULT_MAX_MB,
|
|
421
|
+
help=f"stop after this many MB (default {DEFAULT_MAX_MB})")
|
|
422
|
+
parser.add_argument("--drop-to", metavar="UID:GID",
|
|
423
|
+
help="give root back once the socket is open, so the pcap "
|
|
424
|
+
"belongs to the user that started the session")
|
|
425
|
+
args = parser.parse_args(argv)
|
|
426
|
+
|
|
427
|
+
capture = Capture(args.iface, args.out, snaplen=args.snaplen, peer=args.peer,
|
|
428
|
+
max_bytes=args.max_mb * 1000 * 1000, drop_to=args.drop_to)
|
|
429
|
+
|
|
430
|
+
stop: list = []
|
|
431
|
+
for name in (signal.SIGINT, signal.SIGTERM):
|
|
432
|
+
signal.signal(name, lambda *_: stop.append(True))
|
|
433
|
+
|
|
434
|
+
log(f"{args.iface} -> {args.out} (-s {args.snaplen}, udp"
|
|
435
|
+
f"{f' and host {args.peer}' if args.peer else ''})")
|
|
436
|
+
try:
|
|
437
|
+
capture.run(stop)
|
|
438
|
+
except PermissionError:
|
|
439
|
+
log("FAIL no permission to open a packet socket. This is meant to run "
|
|
440
|
+
"through `sudo` from inside the communication container, where "
|
|
441
|
+
"containeruser has passwordless sudo; a non-root process cannot "
|
|
442
|
+
"open AF_PACKET whatever capabilities the container was given.")
|
|
443
|
+
return 3
|
|
444
|
+
except OSError as error:
|
|
445
|
+
log(f"FAIL {error}")
|
|
446
|
+
return 3
|
|
447
|
+
|
|
448
|
+
sidecar = capture.sidecar()
|
|
449
|
+
args.out.with_suffix(".json").write_text(
|
|
450
|
+
json.dumps(sidecar, indent=2) + "\n", encoding="utf-8")
|
|
451
|
+
log(f"{sidecar['packets']} packets, {sidecar['bytes'] / 1e6:.1f} MB, "
|
|
452
|
+
f"{sidecar['kernel_drops']} dropped by the kernel")
|
|
453
|
+
if sidecar["kernel_drops"]:
|
|
454
|
+
log("WARN the kernel dropped packets: that is a gap in THIS FILE, not "
|
|
455
|
+
"on the link. Subtract it before reading any loss out of the pcap.")
|
|
456
|
+
return 0
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
sys.exit(main())
|