Skip to content

Commit 3bb24e3

Browse files
feat: allow selecting topics by message type in ring buffer config
- topic_filter.py: extend find_matching_config with optional type_name parameter; entries with a 'type' key are matched by exact message type, entries with a 'pattern' key by name glob, both keys AND-ed together - ringbuffer_node.py: pass type_names[0] to find_matching_config so type-based config entries are evaluated during topic discovery - example_config.yaml: add geometry_msgs/msg/Twist type-selector example - test_topic_filter.py: 11 new tests covering type-only, combined, and edge-case matching scenarios Co-authored-by: marc-hanheide <1153084+marc-hanheide@users.noreply.github.com>
1 parent c89cc1a commit 3bb24e3

4 files changed

Lines changed: 131 additions & 8 deletions

File tree

src/rosbag_blackbox/config/example_config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
# Pattern syntax (zenoh-style):
88
# * matches exactly ONE path segment (e.g. /camera/* → /camera/left)
99
# ** matches ANY depth of namespace (e.g. /camera/** → /camera/left/image_raw)
10+
#
11+
# Topic selectors (at least one must be provided per entry):
12+
# pattern: <glob> – match by topic NAME using the syntax above
13+
# type: <string> – match by message TYPE (exact, e.g. geometry_msgs/msg/Twist)
14+
# Both keys may be combined; in that case BOTH conditions must hold.
1015

1116
# ── Ring-buffer settings ────────────────────────────────────────────────────
1217
buffer:
@@ -32,6 +37,14 @@ topics:
3237
durability: volatile
3338
depth: 1
3439

40+
# ── All Twist messages, whatever topic they appear on ──────────────────
41+
- type: "geometry_msgs/msg/Twist"
42+
max_frequency: 10.0
43+
qos:
44+
reliability: best_effort
45+
durability: volatile
46+
depth: 10
47+
3548
# ── TF / TF-static (reliable, higher depth) ────────────────────────────
3649
- pattern: "/tf"
3750
qos:

src/rosbag_blackbox/rosbag_blackbox/ringbuffer_node.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ def _discover_topics(self) -> None:
243243
# If topic configs are defined, only subscribe to matches;
244244
# otherwise record everything.
245245
if self._topic_cfgs:
246-
topic_cfg = find_matching_config(topic_name, self._topic_cfgs)
246+
topic_cfg = find_matching_config(
247+
topic_name, self._topic_cfgs, type_names[0]
248+
)
247249
if topic_cfg is None:
248250
continue
249251
else:

src/rosbag_blackbox/rosbag_blackbox/topic_filter.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424
matches_pattern('/camera/left/image', '/camera/*') -> False
2525
matches_pattern('/camera/left/image', '/camera/**') -> True
2626
matches_pattern('/tf', '/**') -> True
27+
28+
Topic config entries support two selectors that can be used independently or in combination:
29+
``pattern`` – Zenoh-style glob matched against the topic *name*.
30+
``type`` – Exact message type string matched against the topic *type*
31+
(e.g. ``'geometry_msgs/msg/Twist'``).
32+
33+
At least one of ``pattern`` or ``type`` must be present in each entry.
34+
When both are present both conditions must hold simultaneously.
2735
"""
2836

2937
from typing import List, Optional
@@ -79,16 +87,45 @@ def matches_pattern(topic: str, pattern: str) -> bool:
7987
return _match_parts(topic_parts, pattern_parts)
8088

8189

82-
def find_matching_config(topic: str, topic_configs: list) -> Optional[dict]:
83-
"""Return the first config entry whose ``pattern`` matches *topic*.
90+
def find_matching_config(
91+
topic: str,
92+
topic_configs: list,
93+
type_name: str = '',
94+
) -> Optional[dict]:
95+
"""Return the first config entry whose selector matches *topic*.
96+
97+
Entries are evaluated in list order; the first match wins.
98+
99+
Each config entry must supply **at least one** of:
100+
101+
* ``pattern`` – Zenoh-style glob matched against the topic *name*.
102+
* ``type`` – Exact message type string matched against *type_name*
103+
(e.g. ``'geometry_msgs/msg/Twist'``).
84104
85-
Patterns are evaluated in list order; the first match wins.
105+
When both keys are present **both** conditions must hold simultaneously.
106+
Entries that contain neither key are skipped.
86107
87-
:param topic: ROS2 topic name.
88-
:param topic_configs: List of dicts, each containing a ``'pattern'`` key.
108+
:param topic: ROS2 topic name (e.g. ``'/cmd_vel'``).
109+
:param topic_configs: List of dicts, each containing ``'pattern'``
110+
and/or ``'type'`` keys.
111+
:param type_name: ROS2 message type of the topic
112+
(e.g. ``'geometry_msgs/msg/Twist'``).
113+
When empty, entries that require type matching
114+
are skipped.
89115
:returns: Matched config dict, or ``None`` if nothing matches.
90116
"""
91117
for cfg in topic_configs:
92-
if matches_pattern(topic, cfg.get('pattern', '')):
93-
return cfg
118+
has_pattern = 'pattern' in cfg
119+
has_type = 'type' in cfg
120+
121+
if not has_pattern and not has_type:
122+
continue
123+
124+
if has_pattern and not matches_pattern(topic, cfg['pattern']):
125+
continue
126+
127+
if has_type and (not type_name or type_name != cfg['type']):
128+
continue
129+
130+
return cfg
94131
return None

src/rosbag_blackbox/test/test_topic_filter.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,74 @@ def test_no_match(self):
103103

104104
def test_empty_configs(self):
105105
assert find_matching_config('/tf', []) is None
106+
107+
108+
# ---------------------------------------------------------------------------
109+
# find_matching_config – type-based and combined selectors
110+
# ---------------------------------------------------------------------------
111+
112+
class TestFindMatchingConfigByType:
113+
114+
_CONFIGS = [
115+
{'type': 'geometry_msgs/msg/Twist'},
116+
{'pattern': '/tf', 'qos': {'depth': 100}},
117+
{'type': 'std_msgs/msg/String', 'pattern': '/chatter'},
118+
]
119+
120+
def test_type_only_match(self):
121+
# Any topic that publishes Twist should match the first entry.
122+
cfg = find_matching_config('/cmd_vel', self._CONFIGS, 'geometry_msgs/msg/Twist')
123+
assert cfg is not None
124+
assert cfg['type'] == 'geometry_msgs/msg/Twist'
125+
126+
def test_type_only_different_topic_name(self):
127+
# A different topic name publishing Twist also matches.
128+
cfg = find_matching_config('/robot/cmd_vel', self._CONFIGS, 'geometry_msgs/msg/Twist')
129+
assert cfg is not None
130+
assert cfg['type'] == 'geometry_msgs/msg/Twist'
131+
132+
def test_type_only_wrong_type(self):
133+
# The Twist-only entry should NOT match a String topic.
134+
# The pattern-only /tf entry should NOT match /cmd_vel.
135+
# The combined entry requires /chatter, so /cmd_vel also misses.
136+
cfg = find_matching_config('/cmd_vel', self._CONFIGS, 'std_msgs/msg/String')
137+
assert cfg is None
138+
139+
def test_type_no_type_name_provided(self):
140+
# When type_name is not provided, type-only configs are skipped.
141+
cfg = find_matching_config('/cmd_vel', self._CONFIGS)
142+
assert cfg is None
143+
144+
def test_pattern_config_unaffected_by_type(self):
145+
# Pattern-only configs still match regardless of type_name.
146+
cfg = find_matching_config('/tf', self._CONFIGS, 'tf2_msgs/msg/TFMessage')
147+
assert cfg is not None
148+
assert cfg['qos']['depth'] == 100
149+
150+
def test_pattern_config_with_no_type_arg(self):
151+
# Pattern-only configs still work when type_name is omitted.
152+
cfg = find_matching_config('/tf', self._CONFIGS)
153+
assert cfg is not None
154+
assert cfg['qos']['depth'] == 100
155+
156+
def test_combined_both_match(self):
157+
# Entry requires BOTH /chatter pattern AND String type.
158+
cfg = find_matching_config('/chatter', self._CONFIGS, 'std_msgs/msg/String')
159+
assert cfg is not None
160+
assert cfg['type'] == 'std_msgs/msg/String'
161+
162+
def test_combined_pattern_no_match(self):
163+
# Type matches but topic name doesn't match the pattern.
164+
cfg = find_matching_config('/wrong_topic', self._CONFIGS, 'std_msgs/msg/String')
165+
assert cfg is None
166+
167+
def test_combined_type_no_match(self):
168+
# Pattern matches (/chatter) but type is wrong.
169+
cfg = find_matching_config('/chatter', self._CONFIGS, 'geometry_msgs/msg/Pose')
170+
assert cfg is None
171+
172+
def test_entry_without_either_key_is_skipped(self):
173+
configs = [{'max_frequency': 5.0}, {'pattern': '/tf'}]
174+
cfg = find_matching_config('/tf', configs)
175+
assert cfg is not None
176+
assert 'pattern' in cfg

0 commit comments

Comments
 (0)