rosotacom-dev 2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rosotacom/__init__.py +32 -0
- rosotacom/__main__.py +8 -0
- rosotacom/anonymize.py +360 -0
- rosotacom/bag_ground_truth.py +395 -0
- rosotacom/benched_set.py +492 -0
- rosotacom/benchmark.py +1842 -0
- rosotacom/bundle_check.py +517 -0
- rosotacom/cli.py +8637 -0
- rosotacom/cli_benchmark.py +8211 -0
- rosotacom/deployment.py +196 -0
- rosotacom/ffmpeg_packet.py +123 -0
- rosotacom/forensics.py +1172 -0
- rosotacom/geomap.py +848 -0
- rosotacom/network_profiles.py +563 -0
- rosotacom/network_shaper.py +127 -0
- rosotacom/plots.py +609 -0
- rosotacom/py.typed +0 -0
- rosotacom/resources/benched-set.yaml +317 -0
- rosotacom/resources/examples/.gitignore +1 -0
- rosotacom/resources/examples/README.md +163 -0
- rosotacom/resources/examples/__init__.py +1 -0
- rosotacom/resources/examples/bags/8_drop_reference/metadata.yaml +17 -0
- rosotacom/resources/examples/deployment.example.yaml +14 -0
- rosotacom/resources/examples/profiles.yaml +85 -0
- rosotacom/resources/examples/ros2docker.json +33 -0
- rosotacom/resources/examples/rosotacom.yaml +12 -0
- rosotacom/resources/examples/scenarios/2_native_chatter/scenario-definition.yaml +10 -0
- rosotacom/resources/examples/scripts/1_heartbeat/run_machine_a.sh +9 -0
- rosotacom/resources/examples/scripts/1_heartbeat/run_machine_b.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.ros2docker.json +21 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/wait_for_echo_topic.sh +10 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.ros2docker.json +19 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external.ros2docker.json +12 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external_session.yaml +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/foxglove.json +89 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/metadata.yaml +44 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/external.ros2docker.json +13 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external.ros2docker.json +12 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external_session.yaml +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/foxglove.json +89 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/metadata.yaml +44 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/external.ros2docker.json +13 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external.ros2docker.json +20 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external.ros2docker.json +20 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external.ros2docker.json +18 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external.ros2docker.json +18 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/sessions/10_restamp/session-definition.yaml +39 -0
- rosotacom/resources/examples/sessions/11_trickle/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/12_content_integrity/session-definition.yaml +33 -0
- rosotacom/resources/examples/sessions/13_link_latency/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/14_remote_assist_anonymized/session-definition.yaml +211 -0
- rosotacom/resources/examples/sessions/15_remote_assist_anonymized_costmap/session-definition.yaml +59 -0
- rosotacom/resources/examples/sessions/16_remote_assist_anonymized_camera/session-definition.yaml +61 -0
- rosotacom/resources/examples/sessions/17_synthetic_camera_quality/session-definition.yaml +57 -0
- rosotacom/resources/examples/sessions/1_heartbeat/session-definition.yaml +26 -0
- rosotacom/resources/examples/sessions/1_heartbeat_status/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/2_native_chatter/session-definition.yaml +34 -0
- rosotacom/resources/examples/sessions/3_comp_occ_grid/session-definition.yaml +43 -0
- rosotacom/resources/examples/sessions/4_comp_occ_grid_zen/session-definition.yaml +43 -0
- rosotacom/resources/examples/sessions/5_sized_payload/session-definition.yaml +39 -0
- rosotacom/resources/examples/sessions/6_sized_payload_zen/session-definition.yaml +50 -0
- rosotacom/resources/examples/sessions/7_latched_static/session-definition.yaml +49 -0
- rosotacom/resources/examples/sessions/8_drop/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/9_throttle/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/bench_1_1_capacity/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/bench_1_2_load_sweep/session-definition.yaml +51 -0
- rosotacom/resources/examples/sessions/bench_1_3_ramp/session-definition.yaml +41 -0
- rosotacom/resources/examples/sessions/bench_1_4_recovery/session-definition.yaml +47 -0
- rosotacom/resources/examples/sessions/bench_1_5_camera/session-definition.yaml +45 -0
- rosotacom/resources/ros2docker.json.example +33 -0
- rosotacom/resources/ws/.vscode/c_cpp_properties.json +20 -0
- rosotacom/resources/ws/.vscode/settings.json +8 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_easy_mode.xml.template +32 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_initial_peer_list.xml.template +21 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v2.xml.template +17 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v3.xml.template +48 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v4.xml.template +95 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v5.xml.template +64 -0
- rosotacom/resources/ws/ota_configs/create_zenoh_json5.py +56 -0
- rosotacom/resources/ws/ota_configs/cyclonedds_minimal.xml.template +9 -0
- rosotacom/resources/ws/ota_configs/cyclonedds_tuned.xml.template +33 -0
- rosotacom/resources/ws/ota_configs/fastdds_unicast.xml.template +71 -0
- rosotacom/resources/ws/ota_configs/get_ota_xml.py +214 -0
- rosotacom/resources/ws/ota_configs/utils.py +74 -0
- rosotacom/resources/ws/ota_configs/zenoh.json5.template +32 -0
- rosotacom/resources/ws/ros2src/.gitkeep +0 -0
- rosotacom/resources/ws/ros2src/com_cpp/CMakeLists.txt +67 -0
- rosotacom/resources/ws/ros2src/com_cpp/COLCON_IGNORE +0 -0
- rosotacom/resources/ws/ros2src/com_cpp/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_cpp/package.xml +37 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/hz_monitor.cpp +192 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/universal_compressor.cpp +283 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/universal_decompressor.cpp +306 -0
- rosotacom/resources/ws/ros2src/com_msgs/CMakeLists.txt +22 -0
- rosotacom/resources/ws/ros2src/com_msgs/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/CompressedData.msg +3 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/EchoHeartbeat.msg +10 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/OtaStamped.msg +4 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/SizedPayload.msg +4 -0
- rosotacom/resources/ws/ros2src/com_msgs/package.xml +25 -0
- rosotacom/resources/ws/ros2src/com_py/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/__init__.py +0 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/base_bridge_relay.py +106 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/bridge_in.py +76 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/bridge_out.py +70 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/heartbeat_echo.py +221 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/hz_monitor.py +178 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/image_bandwidth_reducer.py +218 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/image_pixel_capper.py +370 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/lanelet_saver.py +80 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/latch_relay.py +205 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/costmap_remapper.py +73 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/marker_remapper.py +93 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/speed_test.py +107 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/tf_remapper.py +156 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/traffic_monitor.py +91 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/wifi_network_analysis_1.py +213 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/link_bytes.py +177 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/link_trace.py +186 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/local_global_frame_bridge.py +465 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/localization_map_split_reduce_saver.py +258 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/msg_transform.py +277 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/pair_management.py +87 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/pub_sub_pair.py +145 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/qos.py +254 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/relay_in.py +78 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/relay_out.py +75 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/restamp.py +191 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/sized_publisher.py +244 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency.py +64 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency_core.py +51 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/status_overview.py +476 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/status_overview_core.py +989 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/tf_map_to_cart_saver.py +99 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/topic_monitor.py +327 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/topic_resolution.py +171 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/trickle.py +183 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_compressor.py +653 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_decompressor.py +372 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_unwrapper.py +191 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_wrapper.py +154 -0
- rosotacom/resources/ws/ros2src/com_py/package.xml +34 -0
- rosotacom/resources/ws/ros2src/com_py/resource/com_py +0 -0
- rosotacom/resources/ws/ros2src/com_py/setup.cfg +6 -0
- rosotacom/resources/ws/ros2src/com_py/setup.py +55 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_copyright.py +25 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_flake8.py +25 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_pep257.py +23 -0
- rosotacom/resources/ws/session/content/base/session_plugin_base.yaml +633 -0
- rosotacom/resources/ws/session/content/network/up_connectivity.py +364 -0
- rosotacom/resources/ws/session/content/network/wifi_network_analysis_2.py +215 -0
- rosotacom/resources/ws/session/creation/anonymize_bag.py +156 -0
- rosotacom/resources/ws/session/creation/catmux_log_setup.sh +65 -0
- rosotacom/resources/ws/session/creation/create_session_yaml.py +145 -0
- rosotacom/resources/ws/session/creation/generate_session_files.py +3217 -0
- rosotacom/resources/ws/session/creation/run_session.py +447 -0
- rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py +54 -0
- rosotacom/resources/ws/session/creation/strip_ansi.py +42 -0
- rosotacom/status_eval.py +475 -0
- rosotacom/stream_stats.py +650 -0
- rosotacom/trace_profiles.py +561 -0
- rosotacom/transit.py +249 -0
- rosotacom/video_quality.py +906 -0
- rosotacom_dev-2.2.dist-info/METADATA +874 -0
- rosotacom_dev-2.2.dist-info/RECORD +195 -0
- rosotacom_dev-2.2.dist-info/WHEEL +5 -0
- rosotacom_dev-2.2.dist-info/entry_points.txt +4 -0
- rosotacom_dev-2.2.dist-info/licenses/LICENSE.txt +25 -0
- rosotacom_dev-2.2.dist-info/top_level.txt +1 -0
rosotacom/deployment.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Deployment host configuration and peer binding resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
VALUE_PREFIX = "value:"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class DeploymentHost:
|
|
17
|
+
name: str
|
|
18
|
+
address: str
|
|
19
|
+
ssh: str | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class DeploymentConfig:
|
|
24
|
+
path: Path
|
|
25
|
+
hosts: dict[str, DeploymentHost]
|
|
26
|
+
values: dict[str, Any]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class PeerBinding:
|
|
31
|
+
peer: str
|
|
32
|
+
address: str
|
|
33
|
+
ssh: str | None
|
|
34
|
+
host: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_mapping(path: Path) -> dict[str, Any]:
|
|
38
|
+
loaded = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
39
|
+
if not isinstance(loaded, dict):
|
|
40
|
+
raise RuntimeError(f"Deployment file must contain a mapping: {path}")
|
|
41
|
+
return loaded
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_deployment(path: Path | None) -> DeploymentConfig | None:
|
|
45
|
+
if path is None:
|
|
46
|
+
return None
|
|
47
|
+
raw = _load_mapping(path)
|
|
48
|
+
unknown = sorted(set(raw) - {"hosts", "values"})
|
|
49
|
+
if unknown:
|
|
50
|
+
raise RuntimeError(f"Unsupported deployment keys {unknown}; allowed keys are ['hosts', 'values'].")
|
|
51
|
+
|
|
52
|
+
hosts_raw = raw.get("hosts")
|
|
53
|
+
values = raw.get("values")
|
|
54
|
+
if hosts_raw is None:
|
|
55
|
+
hosts_raw = {}
|
|
56
|
+
if values is None:
|
|
57
|
+
values = {}
|
|
58
|
+
if not isinstance(hosts_raw, dict):
|
|
59
|
+
raise RuntimeError("deployment.hosts must be a mapping.")
|
|
60
|
+
if not isinstance(values, dict):
|
|
61
|
+
raise RuntimeError("deployment.values must be a mapping.")
|
|
62
|
+
|
|
63
|
+
hosts: dict[str, DeploymentHost] = {}
|
|
64
|
+
for raw_name, raw_host in hosts_raw.items():
|
|
65
|
+
name = str(raw_name).strip()
|
|
66
|
+
if not name:
|
|
67
|
+
raise RuntimeError("deployment host names must be non-empty.")
|
|
68
|
+
if not isinstance(raw_host, dict):
|
|
69
|
+
raise RuntimeError(f"deployment.hosts.{name} must be a mapping.")
|
|
70
|
+
extra = sorted(set(raw_host) - {"address", "ssh"})
|
|
71
|
+
if extra:
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
f"Unsupported deployment.hosts.{name} keys {extra}; allowed keys are ['address', 'ssh']."
|
|
74
|
+
)
|
|
75
|
+
address = raw_host.get("address")
|
|
76
|
+
if not isinstance(address, str) or not address.strip():
|
|
77
|
+
raise RuntimeError(f"deployment.hosts.{name}.address must be a non-empty string.")
|
|
78
|
+
ssh = raw_host.get("ssh")
|
|
79
|
+
if ssh is not None and (not isinstance(ssh, str) or not ssh.strip()):
|
|
80
|
+
raise RuntimeError(f"deployment.hosts.{name}.ssh must be a non-empty string or null.")
|
|
81
|
+
hosts[name] = DeploymentHost(name=name, address=address.strip(), ssh=ssh.strip() if ssh else None)
|
|
82
|
+
|
|
83
|
+
return DeploymentConfig(path=path, hosts=hosts, values=copy.deepcopy(values))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_assignments(raw_values: list[str] | None, *, option: str) -> dict[str, str]:
|
|
87
|
+
result: dict[str, str] = {}
|
|
88
|
+
for raw in raw_values or []:
|
|
89
|
+
key, separator, value = (raw or "").partition("=")
|
|
90
|
+
key = key.strip()
|
|
91
|
+
value = value.strip()
|
|
92
|
+
if separator != "=" or not key or not value:
|
|
93
|
+
raise RuntimeError(f"{option} must use '<peer>=<value>', for example '{option} a=machine-a'.")
|
|
94
|
+
if key in result:
|
|
95
|
+
raise RuntimeError(f"Duplicate {option} assignment for peer '{key}'.")
|
|
96
|
+
result[key] = value
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def resolve_value(expression: str, deployment: DeploymentConfig | None) -> str:
|
|
101
|
+
value = str(expression).strip()
|
|
102
|
+
if not value:
|
|
103
|
+
raise RuntimeError("Deployment values must be non-empty strings.")
|
|
104
|
+
if not value.startswith(VALUE_PREFIX):
|
|
105
|
+
return value
|
|
106
|
+
key = value[len(VALUE_PREFIX) :].strip()
|
|
107
|
+
if not key:
|
|
108
|
+
raise RuntimeError("value: references must include a key.")
|
|
109
|
+
if deployment is None:
|
|
110
|
+
raise RuntimeError(f"Cannot resolve '{value}' because no deployment file is configured.")
|
|
111
|
+
|
|
112
|
+
current: Any = deployment.values
|
|
113
|
+
for part in key.split("."):
|
|
114
|
+
if not isinstance(current, dict) or part not in current:
|
|
115
|
+
raise RuntimeError(f"Deployment value '{key}' was not found in {deployment.path}.")
|
|
116
|
+
current = current[part]
|
|
117
|
+
if isinstance(current, (dict, list)) or current is None:
|
|
118
|
+
raise RuntimeError(f"Deployment value '{key}' must resolve to a scalar.")
|
|
119
|
+
return str(current)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_peer_bindings(
|
|
123
|
+
cfg: dict[str, Any],
|
|
124
|
+
deployment: DeploymentConfig | None,
|
|
125
|
+
*,
|
|
126
|
+
peer_assignments: dict[str, str] | None = None,
|
|
127
|
+
address_overrides: dict[str, str] | None = None,
|
|
128
|
+
ssh_overrides: dict[str, str] | None = None,
|
|
129
|
+
require_addresses: bool = True,
|
|
130
|
+
) -> dict[str, PeerBinding]:
|
|
131
|
+
peers = cfg.get("peers")
|
|
132
|
+
if not isinstance(peers, dict) or not peers:
|
|
133
|
+
raise RuntimeError("Session config must define a non-empty peers mapping.")
|
|
134
|
+
|
|
135
|
+
peer_assignments = peer_assignments or {}
|
|
136
|
+
address_overrides = address_overrides or {}
|
|
137
|
+
ssh_overrides = ssh_overrides or {}
|
|
138
|
+
known = set(str(peer) for peer in peers)
|
|
139
|
+
for option, assignments in (
|
|
140
|
+
("--peer", peer_assignments),
|
|
141
|
+
("--peer-address", address_overrides),
|
|
142
|
+
("--peer-ssh", ssh_overrides),
|
|
143
|
+
):
|
|
144
|
+
unknown = sorted(set(assignments) - known)
|
|
145
|
+
if unknown:
|
|
146
|
+
raise RuntimeError(f"{option} references unknown peer(s) {unknown}. Known peers: {sorted(known)}")
|
|
147
|
+
|
|
148
|
+
bindings: dict[str, PeerBinding] = {}
|
|
149
|
+
missing: list[str] = []
|
|
150
|
+
for raw_peer, raw_peer_cfg in peers.items():
|
|
151
|
+
peer = str(raw_peer)
|
|
152
|
+
peer_cfg = raw_peer_cfg or {}
|
|
153
|
+
if not isinstance(peer_cfg, dict):
|
|
154
|
+
raise RuntimeError(f"peers.{peer} must be a mapping.")
|
|
155
|
+
host_name = peer_assignments.get(peer)
|
|
156
|
+
if host_name is None:
|
|
157
|
+
configured_host = peer_cfg.get("host")
|
|
158
|
+
host_name = str(configured_host).strip() if configured_host is not None else None
|
|
159
|
+
|
|
160
|
+
address: str | None = None
|
|
161
|
+
ssh: str | None = None
|
|
162
|
+
if host_name:
|
|
163
|
+
if deployment is None:
|
|
164
|
+
if peer in peer_assignments or peer not in address_overrides:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"Peer '{peer}' selects deployment host '{host_name}', but no deployment file is configured."
|
|
167
|
+
)
|
|
168
|
+
host_name = None
|
|
169
|
+
else:
|
|
170
|
+
try:
|
|
171
|
+
host = deployment.hosts[host_name]
|
|
172
|
+
except KeyError as exc:
|
|
173
|
+
raise RuntimeError(
|
|
174
|
+
f"Unknown deployment host '{host_name}' for peer '{peer}'. "
|
|
175
|
+
f"Known hosts: {sorted(deployment.hosts)}"
|
|
176
|
+
) from exc
|
|
177
|
+
address = resolve_value(host.address, deployment)
|
|
178
|
+
ssh = host.ssh
|
|
179
|
+
|
|
180
|
+
if peer in address_overrides:
|
|
181
|
+
address = resolve_value(address_overrides[peer], deployment)
|
|
182
|
+
if peer in ssh_overrides:
|
|
183
|
+
ssh_value = ssh_overrides[peer]
|
|
184
|
+
ssh = None if ssh_value == "local" else ssh_value
|
|
185
|
+
|
|
186
|
+
if address is None:
|
|
187
|
+
missing.append(peer)
|
|
188
|
+
continue
|
|
189
|
+
bindings[peer] = PeerBinding(peer=peer, address=address, ssh=ssh, host=host_name)
|
|
190
|
+
|
|
191
|
+
if missing and require_addresses:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
"Missing deployment address for peer(s) "
|
|
194
|
+
f"{missing}. Use --peer PEER=HOST with a deployment file, or --peer-address PEER=ADDRESS."
|
|
195
|
+
)
|
|
196
|
+
return bindings
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Keyframe identification for ffmpeg camera streams (FFMPEGPacket).
|
|
2
|
+
|
|
3
|
+
An ffmpeg-transported camera stream is a sequence of
|
|
4
|
+
``ffmpeg_image_transport_msgs/msg/FFMPEGPacket`` messages with a periodic GOP
|
|
5
|
+
structure: one keyframe (I-frame, self-contained, large) followed by
|
|
6
|
+
``gop_size - 1`` delta frames (P-frames, tiny). The two classes differ in size
|
|
7
|
+
by one to two orders of magnitude, so distinguishing them is essential for
|
|
8
|
+
network analysis -- the keyframes are the bursts a link actually has to absorb.
|
|
9
|
+
|
|
10
|
+
Primary algorithm: the packet's ``flags`` field mirrors libav's packet flags;
|
|
11
|
+
bit 0 is ``AV_PKT_FLAG_KEY`` -- set exactly on keyframes. ``rosotacom
|
|
12
|
+
anonymize`` preserves ``flags`` (and ``pts``) when it zeroes payloads, so this
|
|
13
|
+
works on live streams, recorded handoff traces, and anonymized replay bags
|
|
14
|
+
alike. See docs/ffmpeg-keyframes.md.
|
|
15
|
+
|
|
16
|
+
This module parses the serialized CDR bytes directly (as stored in mcap
|
|
17
|
+
recordings or delivered by raw subscriptions), so offline tooling needs no ROS
|
|
18
|
+
runtime. Only little-endian CDR (the ROS 2 default on all supported platforms)
|
|
19
|
+
is handled.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import struct
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from statistics import median
|
|
27
|
+
|
|
28
|
+
AV_PKT_FLAG_KEY = 0x1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class FFMPEGPacketInfo:
|
|
33
|
+
"""Structure of one serialized FFMPEGPacket, without its payload bytes."""
|
|
34
|
+
|
|
35
|
+
stamp_sec: int
|
|
36
|
+
stamp_nanosec: int
|
|
37
|
+
frame_id: str
|
|
38
|
+
width: int
|
|
39
|
+
height: int
|
|
40
|
+
encoding: str
|
|
41
|
+
pts: int
|
|
42
|
+
flags: int
|
|
43
|
+
payload_size: int
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_keyframe(self) -> bool:
|
|
47
|
+
return bool(self.flags & AV_PKT_FLAG_KEY)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _align(offset: int, alignment: int) -> int:
|
|
51
|
+
return (offset + alignment - 1) // alignment * alignment
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_ffmpeg_packet(cdr: bytes) -> FFMPEGPacketInfo:
|
|
55
|
+
"""Parse a serialized ``FFMPEGPacket`` (CDR bytes as stored in a bag).
|
|
56
|
+
|
|
57
|
+
Field order per the message definition: ``std_msgs/Header header``,
|
|
58
|
+
``int32 width``, ``int32 height``, ``string encoding``, ``uint64 pts``,
|
|
59
|
+
``uint8 flags``, ``bool is_bigendian``, ``uint8[] data``. CDR alignment is
|
|
60
|
+
relative to the payload start, after the 4-byte encapsulation header.
|
|
61
|
+
"""
|
|
62
|
+
if len(cdr) < 4 or cdr[1] != 0x01:
|
|
63
|
+
raise ValueError("expected little-endian CDR encapsulation (0x00 0x01 ...)")
|
|
64
|
+
body = cdr[4:]
|
|
65
|
+
offset = 0
|
|
66
|
+
|
|
67
|
+
stamp_sec, stamp_nanosec = struct.unpack_from("<iI", body, offset)
|
|
68
|
+
offset += 8
|
|
69
|
+
|
|
70
|
+
offset = _align(offset, 4)
|
|
71
|
+
(str_len,) = struct.unpack_from("<I", body, offset)
|
|
72
|
+
offset += 4
|
|
73
|
+
frame_id = body[offset : offset + str_len - 1].decode("utf-8", errors="replace")
|
|
74
|
+
offset += str_len
|
|
75
|
+
|
|
76
|
+
offset = _align(offset, 4)
|
|
77
|
+
width, height = struct.unpack_from("<ii", body, offset)
|
|
78
|
+
offset += 8
|
|
79
|
+
|
|
80
|
+
offset = _align(offset, 4)
|
|
81
|
+
(str_len,) = struct.unpack_from("<I", body, offset)
|
|
82
|
+
offset += 4
|
|
83
|
+
encoding = body[offset : offset + str_len - 1].decode("utf-8", errors="replace")
|
|
84
|
+
offset += str_len
|
|
85
|
+
|
|
86
|
+
offset = _align(offset, 8)
|
|
87
|
+
(pts,) = struct.unpack_from("<Q", body, offset)
|
|
88
|
+
offset += 8
|
|
89
|
+
|
|
90
|
+
flags = body[offset]
|
|
91
|
+
offset += 2 # flags + is_bigendian
|
|
92
|
+
|
|
93
|
+
offset = _align(offset, 4)
|
|
94
|
+
(payload_size,) = struct.unpack_from("<I", body, offset)
|
|
95
|
+
|
|
96
|
+
return FFMPEGPacketInfo(
|
|
97
|
+
stamp_sec=stamp_sec,
|
|
98
|
+
stamp_nanosec=stamp_nanosec,
|
|
99
|
+
frame_id=frame_id,
|
|
100
|
+
width=width,
|
|
101
|
+
height=height,
|
|
102
|
+
encoding=encoding,
|
|
103
|
+
pts=pts,
|
|
104
|
+
flags=flags,
|
|
105
|
+
payload_size=payload_size,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def keyframes_by_size(sizes: list[int] | list[float], *, min_ratio: float = 3.0) -> list[bool]:
|
|
110
|
+
"""Size-bimodality fallback when ``flags`` is unavailable or was zeroed.
|
|
111
|
+
|
|
112
|
+
Marks a frame as keyframe iff its size exceeds ``min_ratio`` times the
|
|
113
|
+
stream's median frame size. Works because delta frames dominate the stream
|
|
114
|
+
(a GOP of N has N-1 of them), pinning the median to the delta-frame mode,
|
|
115
|
+
while keyframes are typically 5-100x larger. Misclassifies only when the
|
|
116
|
+
encoder emits unusually large delta frames (heavy motion) or near-empty
|
|
117
|
+
keyframes (static scene at very low bitrate) -- prefer ``flags`` whenever
|
|
118
|
+
present.
|
|
119
|
+
"""
|
|
120
|
+
if not sizes:
|
|
121
|
+
return []
|
|
122
|
+
threshold = min_ratio * median(sizes)
|
|
123
|
+
return [size > threshold for size in sizes]
|