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
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# -- BEGIN LICENSE BLOCK ----------------------------------------------
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
|
6
|
+
#
|
|
7
|
+
# * Redistributions of source code must retain the above copyright
|
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
11
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
12
|
+
# documentation and/or other materials provided with the distribution.
|
|
13
|
+
#
|
|
14
|
+
# * Neither the name of the {copyright_holder} nor the names of its
|
|
15
|
+
# contributors may be used to endorse or promote products derived from
|
|
16
|
+
# this software without specific prior written permission.
|
|
17
|
+
#
|
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
21
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
22
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
23
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
24
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
25
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
26
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
27
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
28
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
+
# -- END LICENSE BLOCK ------------------------------------------------
|
|
30
|
+
#
|
|
31
|
+
# ---------------------------------------------------------------------
|
|
32
|
+
# !\file
|
|
33
|
+
#
|
|
34
|
+
# \author Martin Gontscharow <gontscharow@fzi.de>
|
|
35
|
+
# \date 2024-11-13
|
|
36
|
+
#
|
|
37
|
+
#
|
|
38
|
+
# ---------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
from array import array
|
|
41
|
+
import bz2
|
|
42
|
+
import re
|
|
43
|
+
import time
|
|
44
|
+
from threading import Lock
|
|
45
|
+
import zlib
|
|
46
|
+
|
|
47
|
+
import lz4.frame as lz4
|
|
48
|
+
import rclpy
|
|
49
|
+
import yaml
|
|
50
|
+
from rclpy.node import Node
|
|
51
|
+
from rclpy.qos import ReliabilityPolicy
|
|
52
|
+
from rclpy.serialization import serialize_message
|
|
53
|
+
from rclpy.executors import ExternalShutdownException
|
|
54
|
+
from rosidl_runtime_py.utilities import get_message
|
|
55
|
+
|
|
56
|
+
from com_py.qos import get_topic_qos, load_qos_config
|
|
57
|
+
# import zstandard as zstd
|
|
58
|
+
# zstandard is not available on the Ubuntu 26.04 / ROS 2 Lyrical setup.
|
|
59
|
+
# Reject zstd explicitly when a user configures it.
|
|
60
|
+
|
|
61
|
+
# Adjust the import below to your custom package and message name:
|
|
62
|
+
# e.g. from com_cpp.msg import CompressedData
|
|
63
|
+
from com_msgs.msg import CompressedData
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def format_size(size: int) -> str:
|
|
67
|
+
"""Format a byte size into a human-readable string."""
|
|
68
|
+
if size < 1024:
|
|
69
|
+
return f"{size} B"
|
|
70
|
+
elif size < 1048576:
|
|
71
|
+
return f"{size / 1024:.1f} KB"
|
|
72
|
+
elif size < 1073741824:
|
|
73
|
+
return f"{size / 1048576:.1f} MB"
|
|
74
|
+
else:
|
|
75
|
+
return f"{size / 1073741824:.1f} GB"
|
|
76
|
+
|
|
77
|
+
def compress_data(data: bytes, algorithm: str) -> bytes:
|
|
78
|
+
"""
|
|
79
|
+
Compress raw bytes according to the chosen algorithm.
|
|
80
|
+
Recognized: 'bz2', 'zlib', 'lz4'.
|
|
81
|
+
"""
|
|
82
|
+
if algorithm == 'bz2':
|
|
83
|
+
return bz2.compress(data)
|
|
84
|
+
elif algorithm == 'zlib':
|
|
85
|
+
return zlib.compress(data)
|
|
86
|
+
elif algorithm == 'lz4':
|
|
87
|
+
return lz4.compress(data)
|
|
88
|
+
elif algorithm == 'zstd':
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"zstd compression is not supported on this Ubuntu 26.04 / ROS 2 Lyrical setup. "
|
|
91
|
+
"Choose bz2, zlib, or lz4 instead."
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError(f"Unsupported compression algorithm: {algorithm}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class UniversalCompressorNode(Node):
|
|
98
|
+
"""
|
|
99
|
+
A ROS 2 node that dynamically searches for topics matching user-defined regex
|
|
100
|
+
and publishes a compressed version of their messages to a new topic.
|
|
101
|
+
|
|
102
|
+
Configuration is read from a YAML file specified via the 'config_file' parameter.
|
|
103
|
+
The YAML has a 'compression:' list of dictionaries, each specifying:
|
|
104
|
+
- topic_regex
|
|
105
|
+
- algorithm (optional, default="bz2")
|
|
106
|
+
- add_suffix (optional, default="/{algorithm}")
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
super().__init__('universal_compressor')
|
|
111
|
+
|
|
112
|
+
# Use the node's default callback group for timers and subscriptions.
|
|
113
|
+
# On this Lyrical/Fast DDS setup, dynamically created entities in
|
|
114
|
+
# custom callback groups were visible in the graph but did not keep
|
|
115
|
+
# dispatching reliably.
|
|
116
|
+
|
|
117
|
+
# 1) Declare + get parameter for the config file
|
|
118
|
+
self.declare_parameter('config_file', 'compression_config.yaml')
|
|
119
|
+
config_file = self.get_parameter('config_file').value
|
|
120
|
+
|
|
121
|
+
# Default compression algorithm (used when a YAML rule does not specify one)
|
|
122
|
+
self.declare_parameter('default_algorithm', 'bz2')
|
|
123
|
+
self.default_algorithm = self.get_parameter('default_algorithm').value
|
|
124
|
+
|
|
125
|
+
self.get_logger().info(
|
|
126
|
+
f"[universal_compressor] Starting with config_file='{config_file}'"
|
|
127
|
+
)
|
|
128
|
+
self.get_logger().info(
|
|
129
|
+
f"[universal_compressor] default_algorithm='{self.default_algorithm}'"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self.declare_parameter('stats_log_period_s', 10.0)
|
|
133
|
+
self.stats_log_period_s = float(
|
|
134
|
+
self.get_parameter('stats_log_period_s').value
|
|
135
|
+
)
|
|
136
|
+
self.declare_parameter('slow_callback_warn_ms', 1000.0)
|
|
137
|
+
self.slow_callback_warn_ms = float(
|
|
138
|
+
self.get_parameter('slow_callback_warn_ms').value
|
|
139
|
+
)
|
|
140
|
+
self.declare_parameter('no_input_warn_period_s', 15.0)
|
|
141
|
+
self.no_input_warn_period_s = float(
|
|
142
|
+
self.get_parameter('no_input_warn_period_s').value
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# 3) Simple set to track topics we've already subscribed to
|
|
146
|
+
self.subscribed_topics = set()
|
|
147
|
+
# Keep our own references without touching rclpy.Node private fields.
|
|
148
|
+
self._owned_subscriptions = []
|
|
149
|
+
self._owned_publishers = []
|
|
150
|
+
self._owned_output_topics = set()
|
|
151
|
+
self._topic_stats = {}
|
|
152
|
+
self._stats_lock = Lock()
|
|
153
|
+
# 4) QoS config + roles (configured via qos.py YAML)
|
|
154
|
+
self.qos_config_file = self.declare_parameter('qos_config_file', '').value
|
|
155
|
+
self.sub_role = 'compressor_sub'
|
|
156
|
+
self.pub_role = 'compressor_pub'
|
|
157
|
+
self.qos_config = load_qos_config(self.get_logger(), self.qos_config_file)
|
|
158
|
+
|
|
159
|
+
# 5) Load YAML config
|
|
160
|
+
self.config = self.load_config(config_file)
|
|
161
|
+
compression_rules = self.config.get('compression', [])
|
|
162
|
+
if isinstance(compression_rules, list):
|
|
163
|
+
self.get_logger().info(
|
|
164
|
+
f"[universal_compressor] Loaded {len(compression_rules)} "
|
|
165
|
+
f"compression rule(s). "
|
|
166
|
+
f"stats_log_period_s={self.stats_log_period_s:.1f}, "
|
|
167
|
+
f"slow_callback_warn_ms={self.slow_callback_warn_ms:.1f}"
|
|
168
|
+
)
|
|
169
|
+
self.get_logger().info(
|
|
170
|
+
f"[universal_compressor] Node initialized with config file: {config_file}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# 6) Set up a timer to periodically re-check the graph for new topics
|
|
174
|
+
self.timer = self.create_timer(
|
|
175
|
+
5.0,
|
|
176
|
+
self.check_and_subscribe,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Discovery is timer-driven once the executor is spinning. Calling
|
|
180
|
+
# get_topic_names_and_types() before spin can leave this node with
|
|
181
|
+
# graph entities that are visible but never dispatch callbacks on the
|
|
182
|
+
# Lyrical/Fast DDS setup used here.
|
|
183
|
+
|
|
184
|
+
def load_config(self, filename: str) -> dict:
|
|
185
|
+
"""Load compression configuration from a YAML file into a Python dict."""
|
|
186
|
+
try:
|
|
187
|
+
with open(filename, 'r') as file:
|
|
188
|
+
data = yaml.safe_load(file)
|
|
189
|
+
self.get_logger().debug(f"Configuration loaded: {data}")
|
|
190
|
+
return data or {}
|
|
191
|
+
except Exception as e:
|
|
192
|
+
self.get_logger().error(f"Failed to load config from '{filename}': {e}")
|
|
193
|
+
return {}
|
|
194
|
+
|
|
195
|
+
def check_and_subscribe(self):
|
|
196
|
+
"""
|
|
197
|
+
Checks the ROS graph for topics, matches them against config['compression'] entries,
|
|
198
|
+
and sets up new subscribers (with corresponding publishers) if not already subscribed.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
self._check_and_subscribe_impl()
|
|
202
|
+
except Exception as e:
|
|
203
|
+
self.get_logger().error(
|
|
204
|
+
f"[universal_compressor] Discovery error: {type(e).__name__}: {e}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _check_and_subscribe_impl(self):
|
|
208
|
+
self._warn_about_inactive_subscriptions()
|
|
209
|
+
|
|
210
|
+
if 'compression' not in self.config:
|
|
211
|
+
self.get_logger().warning(
|
|
212
|
+
"[universal_compressor] No 'compression' section in config."
|
|
213
|
+
)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# 1) Gather all active topics and their types
|
|
217
|
+
# e.g. [("/foo", ["std_msgs/msg/String"]), ...]
|
|
218
|
+
graph_start = time.monotonic()
|
|
219
|
+
all_topics = self.get_topic_names_and_types()
|
|
220
|
+
graph_ms = (time.monotonic() - graph_start) * 1000.0
|
|
221
|
+
if graph_ms > self.slow_callback_warn_ms:
|
|
222
|
+
self.get_logger().warning(
|
|
223
|
+
f"[universal_compressor] ROS graph lookup took "
|
|
224
|
+
f"{graph_ms:.1f} ms "
|
|
225
|
+
f"for {len(all_topics)} topic(s)."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# 2) Build a dictionary: topic_name -> first_type
|
|
229
|
+
topic_map = {}
|
|
230
|
+
for (tname, ttypes) in all_topics:
|
|
231
|
+
if len(ttypes) > 0:
|
|
232
|
+
topic_map[tname] = ttypes[0]
|
|
233
|
+
|
|
234
|
+
# 3) For each rule in config['compression'], check regex
|
|
235
|
+
for item in self.config['compression']:
|
|
236
|
+
topic_pattern = item.get('topic_regex', '')
|
|
237
|
+
# Default to node parameter if no algorithm provided:
|
|
238
|
+
algorithm = item.get('algorithm', self.default_algorithm)
|
|
239
|
+
add_suffix = item.get('add_suffix', f'/{algorithm}')
|
|
240
|
+
|
|
241
|
+
# Pre-compile the regex for efficiency if you like:
|
|
242
|
+
try:
|
|
243
|
+
rx = re.compile(topic_pattern)
|
|
244
|
+
except re.error as e:
|
|
245
|
+
self.get_logger().error(
|
|
246
|
+
f"[universal_compressor] Invalid "
|
|
247
|
+
f"topic_regex='{topic_pattern}': {e}"
|
|
248
|
+
)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
match_count = 0
|
|
252
|
+
|
|
253
|
+
for tname, type_name in topic_map.items():
|
|
254
|
+
if rx.search(tname):
|
|
255
|
+
match_count += 1
|
|
256
|
+
# Avoid recursively compressing our own compressed output topics.
|
|
257
|
+
if tname in self._owned_output_topics:
|
|
258
|
+
continue
|
|
259
|
+
if type_name == 'com_msgs/msg/CompressedData':
|
|
260
|
+
continue
|
|
261
|
+
if add_suffix and tname.endswith(add_suffix):
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# e.g. matched => /costmap/costmap.
|
|
265
|
+
out_topic = tname + add_suffix
|
|
266
|
+
subscription_key = (tname, out_topic)
|
|
267
|
+
|
|
268
|
+
if subscription_key not in self.subscribed_topics:
|
|
269
|
+
msg_class = self.get_message_class(type_name)
|
|
270
|
+
if msg_class is None:
|
|
271
|
+
# If we can't load or parse the type, skip
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# QoS from roles.
|
|
275
|
+
sub_qos = get_topic_qos(
|
|
276
|
+
self.get_logger(),
|
|
277
|
+
self.qos_config,
|
|
278
|
+
tname,
|
|
279
|
+
self.sub_role,
|
|
280
|
+
)
|
|
281
|
+
sub_qos = self._adapt_sub_qos_to_publishers(tname, sub_qos)
|
|
282
|
+
pub_qos = get_topic_qos(
|
|
283
|
+
self.get_logger(),
|
|
284
|
+
self.qos_config,
|
|
285
|
+
tname,
|
|
286
|
+
self.pub_role,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Create publisher with CompressedData
|
|
290
|
+
pub = self.create_publisher(
|
|
291
|
+
CompressedData,
|
|
292
|
+
out_topic,
|
|
293
|
+
pub_qos,
|
|
294
|
+
)
|
|
295
|
+
self._owned_publishers.append(pub)
|
|
296
|
+
self._owned_output_topics.add(out_topic)
|
|
297
|
+
|
|
298
|
+
# Create subscription to the original message
|
|
299
|
+
# We'll pass a small lambda capturing the arguments
|
|
300
|
+
# so we know which publisher and algorithm to use.
|
|
301
|
+
# Pass 'tname' so logs identify the source topic.
|
|
302
|
+
sub = self.create_subscription(
|
|
303
|
+
msg_class,
|
|
304
|
+
tname,
|
|
305
|
+
self.make_compression_callback(
|
|
306
|
+
pub,
|
|
307
|
+
algorithm,
|
|
308
|
+
tname,
|
|
309
|
+
type_name,
|
|
310
|
+
),
|
|
311
|
+
sub_qos,
|
|
312
|
+
)
|
|
313
|
+
self._owned_subscriptions.append(sub)
|
|
314
|
+
|
|
315
|
+
self.subscribed_topics.add(subscription_key)
|
|
316
|
+
self._init_topic_stats(
|
|
317
|
+
tname, out_topic, algorithm, type_name
|
|
318
|
+
)
|
|
319
|
+
self.get_logger().info(
|
|
320
|
+
f"[universal_compressor] Subscribed to '{tname}' "
|
|
321
|
+
f"(type={type_name}) => publishing compressed "
|
|
322
|
+
f"to '{out_topic}' "
|
|
323
|
+
f"with algorithm='{algorithm}'"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if match_count == 0:
|
|
327
|
+
self.get_logger().debug(
|
|
328
|
+
f"[universal_compressor] Rule topic_regex='{topic_pattern}' "
|
|
329
|
+
f"matched no topics "
|
|
330
|
+
f"among {len(topic_map)} discovered topic(s)."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
self.get_logger().info(
|
|
334
|
+
f"[universal_compressor] Discovery pass complete; "
|
|
335
|
+
f"subscriptions={len(self.subscribed_topics)}"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _qos_reliability_name(self, qos) -> str:
|
|
339
|
+
try:
|
|
340
|
+
return qos.reliability.name
|
|
341
|
+
except Exception:
|
|
342
|
+
return str(getattr(qos, 'reliability', 'unknown'))
|
|
343
|
+
|
|
344
|
+
def _adapt_sub_qos_to_publishers(self, topic: str, sub_qos):
|
|
345
|
+
"""
|
|
346
|
+
Make the compressor subscription compatible with discovered publishers.
|
|
347
|
+
|
|
348
|
+
A RELIABLE subscription cannot receive from a BEST_EFFORT publisher. The
|
|
349
|
+
graph still shows both endpoints, which looks like a silent hang. For a
|
|
350
|
+
transparent compressor, prefer receiving data over enforcing reliability.
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
publishers = self.get_publishers_info_by_topic(topic)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
self.get_logger().warning(
|
|
356
|
+
f"[universal_compressor] Could not inspect publishers for "
|
|
357
|
+
f"'{topic}' QoS: {e}. Using configured subscriber QoS "
|
|
358
|
+
f"reliability={self._qos_reliability_name(sub_qos)}."
|
|
359
|
+
)
|
|
360
|
+
return sub_qos
|
|
361
|
+
|
|
362
|
+
if not publishers:
|
|
363
|
+
self.get_logger().info(
|
|
364
|
+
f"[universal_compressor] No publisher QoS info available yet "
|
|
365
|
+
f"for '{topic}'. Using configured subscriber QoS "
|
|
366
|
+
f"reliability={self._qos_reliability_name(sub_qos)}."
|
|
367
|
+
)
|
|
368
|
+
return sub_qos
|
|
369
|
+
|
|
370
|
+
offered = []
|
|
371
|
+
saw_best_effort = False
|
|
372
|
+
for info in publishers:
|
|
373
|
+
qos = getattr(info, 'qos_profile', None)
|
|
374
|
+
if qos is None:
|
|
375
|
+
continue
|
|
376
|
+
reliability = self._qos_reliability_name(qos)
|
|
377
|
+
offered.append(reliability)
|
|
378
|
+
if getattr(qos, 'reliability', None) == ReliabilityPolicy.BEST_EFFORT:
|
|
379
|
+
saw_best_effort = True
|
|
380
|
+
|
|
381
|
+
before = self._qos_reliability_name(sub_qos)
|
|
382
|
+
if saw_best_effort and sub_qos.reliability != ReliabilityPolicy.BEST_EFFORT:
|
|
383
|
+
sub_qos.reliability = ReliabilityPolicy.BEST_EFFORT
|
|
384
|
+
self.get_logger().warning(
|
|
385
|
+
f"[universal_compressor] '{topic}' has BEST_EFFORT publisher(s); "
|
|
386
|
+
f"using BEST_EFFORT compressor_sub QoS instead of {before}. "
|
|
387
|
+
f"Offered publisher reliabilities={offered}."
|
|
388
|
+
)
|
|
389
|
+
else:
|
|
390
|
+
self.get_logger().info(
|
|
391
|
+
f"[universal_compressor] '{topic}' compressor_sub QoS "
|
|
392
|
+
f"reliability={self._qos_reliability_name(sub_qos)}; "
|
|
393
|
+
f"offered publisher reliabilities={offered}."
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return sub_qos
|
|
397
|
+
|
|
398
|
+
def get_message_class(self, ros1_style_path: str):
|
|
399
|
+
"""
|
|
400
|
+
Convert 'package_name/msg/MessageName' into a Python class object.
|
|
401
|
+
E.g. 'std_msgs/msg/String' -> <class 'std_msgs.msg._string.String'>
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
return get_message(ros1_style_path)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
self.get_logger().error(
|
|
407
|
+
f"Could not load message class '{ros1_style_path}': {e}"
|
|
408
|
+
)
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
def _init_topic_stats(
|
|
412
|
+
self,
|
|
413
|
+
topic: str,
|
|
414
|
+
out_topic: str,
|
|
415
|
+
algorithm: str,
|
|
416
|
+
msg_type: str,
|
|
417
|
+
):
|
|
418
|
+
now = time.monotonic()
|
|
419
|
+
with self._stats_lock:
|
|
420
|
+
self._topic_stats.setdefault(topic, {
|
|
421
|
+
'out_topic': out_topic,
|
|
422
|
+
'algorithm': algorithm,
|
|
423
|
+
'msg_type': msg_type,
|
|
424
|
+
'received': 0,
|
|
425
|
+
'published': 0,
|
|
426
|
+
'errors': 0,
|
|
427
|
+
'created_time': now,
|
|
428
|
+
'last_no_input_warn_time': now,
|
|
429
|
+
'last_log_time': now,
|
|
430
|
+
'last_rx_time': None,
|
|
431
|
+
'last_publish_time': None,
|
|
432
|
+
'total_original_bytes': 0,
|
|
433
|
+
'total_compressed_bytes': 0,
|
|
434
|
+
'total_compress_ms': 0.0,
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
def _record_receive(self, topic: str) -> int:
|
|
438
|
+
now = time.monotonic()
|
|
439
|
+
with self._stats_lock:
|
|
440
|
+
stats = self._topic_stats.setdefault(topic, {
|
|
441
|
+
'out_topic': '',
|
|
442
|
+
'algorithm': '',
|
|
443
|
+
'msg_type': '',
|
|
444
|
+
'received': 0,
|
|
445
|
+
'published': 0,
|
|
446
|
+
'errors': 0,
|
|
447
|
+
'created_time': now,
|
|
448
|
+
'last_no_input_warn_time': now,
|
|
449
|
+
'last_log_time': now,
|
|
450
|
+
'last_rx_time': None,
|
|
451
|
+
'last_publish_time': None,
|
|
452
|
+
'total_original_bytes': 0,
|
|
453
|
+
'total_compressed_bytes': 0,
|
|
454
|
+
'total_compress_ms': 0.0,
|
|
455
|
+
})
|
|
456
|
+
stats['received'] += 1
|
|
457
|
+
stats['last_rx_time'] = now
|
|
458
|
+
return stats['received']
|
|
459
|
+
|
|
460
|
+
def _warn_about_inactive_subscriptions(self):
|
|
461
|
+
now = time.monotonic()
|
|
462
|
+
warnings = []
|
|
463
|
+
with self._stats_lock:
|
|
464
|
+
for topic, stats in self._topic_stats.items():
|
|
465
|
+
if stats['received'] > 0:
|
|
466
|
+
continue
|
|
467
|
+
age_s = now - stats['created_time']
|
|
468
|
+
since_warn_s = now - stats['last_no_input_warn_time']
|
|
469
|
+
if age_s < self.no_input_warn_period_s:
|
|
470
|
+
continue
|
|
471
|
+
if since_warn_s < self.no_input_warn_period_s:
|
|
472
|
+
continue
|
|
473
|
+
stats['last_no_input_warn_time'] = now
|
|
474
|
+
warnings.append((
|
|
475
|
+
topic,
|
|
476
|
+
stats['out_topic'],
|
|
477
|
+
stats['algorithm'],
|
|
478
|
+
stats['msg_type'],
|
|
479
|
+
age_s,
|
|
480
|
+
))
|
|
481
|
+
|
|
482
|
+
for topic, out_topic, algorithm, msg_type, age_s in warnings:
|
|
483
|
+
self.get_logger().warning(
|
|
484
|
+
f"[{topic}] Subscribed {age_s:.1f}s ago but no input "
|
|
485
|
+
f"callbacks have fired yet. If the topic is publishing, "
|
|
486
|
+
f"check subscriber QoS role='{self.sub_role}', type='{msg_type}', "
|
|
487
|
+
f"algorithm='{algorithm}', out='{out_topic}'."
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def _record_publish(
|
|
491
|
+
self,
|
|
492
|
+
topic: str,
|
|
493
|
+
original_size: int,
|
|
494
|
+
compressed_size: int,
|
|
495
|
+
compress_ms: float,
|
|
496
|
+
):
|
|
497
|
+
now = time.monotonic()
|
|
498
|
+
with self._stats_lock:
|
|
499
|
+
stats = self._topic_stats[topic]
|
|
500
|
+
stats['published'] += 1
|
|
501
|
+
stats['last_publish_time'] = now
|
|
502
|
+
stats['total_original_bytes'] += original_size
|
|
503
|
+
stats['total_compressed_bytes'] += compressed_size
|
|
504
|
+
stats['total_compress_ms'] += compress_ms
|
|
505
|
+
|
|
506
|
+
due = (now - stats['last_log_time']) >= self.stats_log_period_s
|
|
507
|
+
if not due and stats['published'] != 1:
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
window_s = max(now - stats['last_log_time'], 1e-9)
|
|
511
|
+
total_compressed = max(stats['total_compressed_bytes'], 1)
|
|
512
|
+
ratio = (
|
|
513
|
+
float(stats['total_original_bytes']) / float(total_compressed)
|
|
514
|
+
)
|
|
515
|
+
avg_ms = stats['total_compress_ms'] / max(stats['published'], 1)
|
|
516
|
+
hz = stats['published'] / window_s if due else 0.0
|
|
517
|
+
stats['last_log_time'] = now
|
|
518
|
+
|
|
519
|
+
if due:
|
|
520
|
+
return (
|
|
521
|
+
f"[{topic}] compressed stats: rx={stats['received']}, "
|
|
522
|
+
f"pub={stats['published']}, errors={stats['errors']}, "
|
|
523
|
+
f"avg_time={avg_ms:.1f} ms, ratio={ratio:.2f}, "
|
|
524
|
+
f"recent_pub_hz={hz:.2f}, out='{stats['out_topic']}'"
|
|
525
|
+
)
|
|
526
|
+
return (
|
|
527
|
+
f"[{topic}] first message compressed: "
|
|
528
|
+
f"original={format_size(original_size)}, "
|
|
529
|
+
f"compressed={format_size(compressed_size)}, ratio={ratio:.2f}, "
|
|
530
|
+
f"time={compress_ms:.1f} ms "
|
|
531
|
+
f"(algo={stats['algorithm']}, type='{stats['msg_type']}')"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
def _record_error(self, topic: str):
|
|
535
|
+
with self._stats_lock:
|
|
536
|
+
stats = self._topic_stats.get(topic)
|
|
537
|
+
if stats is not None:
|
|
538
|
+
stats['errors'] += 1
|
|
539
|
+
|
|
540
|
+
def make_compression_callback(
|
|
541
|
+
self,
|
|
542
|
+
publisher,
|
|
543
|
+
algorithm: str,
|
|
544
|
+
original_topic: str,
|
|
545
|
+
msg_type_str: str,
|
|
546
|
+
):
|
|
547
|
+
def callback(msg):
|
|
548
|
+
self.compression_callback(
|
|
549
|
+
msg,
|
|
550
|
+
publisher,
|
|
551
|
+
algorithm,
|
|
552
|
+
original_topic,
|
|
553
|
+
msg_type_str,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
return callback
|
|
557
|
+
|
|
558
|
+
def compression_callback(
|
|
559
|
+
self,
|
|
560
|
+
msg,
|
|
561
|
+
publisher,
|
|
562
|
+
algorithm,
|
|
563
|
+
original_topic,
|
|
564
|
+
msg_type_str: str,
|
|
565
|
+
):
|
|
566
|
+
"""
|
|
567
|
+
Callback that:
|
|
568
|
+
1) Serializes the incoming message
|
|
569
|
+
2) Compresses the bytes
|
|
570
|
+
3) Publishes them as 'com_msgs/msg/CompressedData'
|
|
571
|
+
"""
|
|
572
|
+
callback_start = time.monotonic()
|
|
573
|
+
try:
|
|
574
|
+
receive_count = self._record_receive(original_topic)
|
|
575
|
+
if receive_count == 1:
|
|
576
|
+
self.get_logger().info(
|
|
577
|
+
f"[{original_topic}] First input message received; "
|
|
578
|
+
f"serializing and compressing "
|
|
579
|
+
f"type='{msg_type_str}' with algorithm='{algorithm}'."
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# 1) Serialize the message into raw bytes
|
|
583
|
+
serialize_start = time.monotonic()
|
|
584
|
+
serialized = serialize_message(msg)
|
|
585
|
+
serialize_ms = (time.monotonic() - serialize_start) * 1000.0
|
|
586
|
+
original_size = len(serialized)
|
|
587
|
+
if serialize_ms > self.slow_callback_warn_ms:
|
|
588
|
+
self.get_logger().warning(
|
|
589
|
+
f"[{original_topic}] Serialization took "
|
|
590
|
+
f"{serialize_ms:.1f} ms "
|
|
591
|
+
f"for {format_size(original_size)}."
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# 2) Compress
|
|
595
|
+
start_time = time.monotonic()
|
|
596
|
+
compressed_bytes = compress_data(serialized, algorithm)
|
|
597
|
+
elapsed_ms = (time.monotonic() - start_time) * 1000.0
|
|
598
|
+
|
|
599
|
+
compressed_size = len(compressed_bytes)
|
|
600
|
+
if elapsed_ms > self.slow_callback_warn_ms:
|
|
601
|
+
self.get_logger().warning(
|
|
602
|
+
f"[{original_topic}] Compression took "
|
|
603
|
+
f"{elapsed_ms:.1f} ms "
|
|
604
|
+
f"for {format_size(original_size)} -> "
|
|
605
|
+
f"{format_size(compressed_size)} "
|
|
606
|
+
f"(algo={algorithm})."
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# 3) Publish as custom CompressedData.
|
|
610
|
+
out_msg = CompressedData()
|
|
611
|
+
out_msg.header.stamp = self.get_clock().now().to_msg()
|
|
612
|
+
out_msg.msg_type = msg_type_str
|
|
613
|
+
# Avoid list(compressed_bytes): it expands each byte into a Python
|
|
614
|
+
# int and can make large messages look like a hung compressor.
|
|
615
|
+
out_msg.data = array('B', compressed_bytes)
|
|
616
|
+
|
|
617
|
+
publisher.publish(out_msg)
|
|
618
|
+
|
|
619
|
+
callback_ms = (time.monotonic() - callback_start) * 1000.0
|
|
620
|
+
if callback_ms > self.slow_callback_warn_ms:
|
|
621
|
+
self.get_logger().warning(
|
|
622
|
+
f"[{original_topic}] Full compression callback took "
|
|
623
|
+
f"{callback_ms:.1f} ms "
|
|
624
|
+
f"(serialize={serialize_ms:.1f} ms, "
|
|
625
|
+
f"compress={elapsed_ms:.1f} ms, "
|
|
626
|
+
f"publish/assign="
|
|
627
|
+
f"{(callback_ms - serialize_ms - elapsed_ms):.1f} ms)."
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
stats_log = self._record_publish(
|
|
631
|
+
original_topic, original_size, compressed_size, elapsed_ms
|
|
632
|
+
)
|
|
633
|
+
if stats_log:
|
|
634
|
+
self.get_logger().info(stats_log)
|
|
635
|
+
|
|
636
|
+
except Exception as e:
|
|
637
|
+
self._record_error(original_topic)
|
|
638
|
+
self.get_logger().error(f"[{original_topic}] Compression error: {e}")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def main(args=None):
|
|
642
|
+
rclpy.init(args=args)
|
|
643
|
+
node = UniversalCompressorNode()
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
rclpy.spin(node)
|
|
647
|
+
except ExternalShutdownException:
|
|
648
|
+
pass
|
|
649
|
+
except KeyboardInterrupt:
|
|
650
|
+
pass
|
|
651
|
+
finally:
|
|
652
|
+
node.destroy_node()
|
|
653
|
+
rclpy.shutdown()
|