Skip to content

Commit fd14c33

Browse files
Add rosbag_blackbox ROS2 package with ring buffer and snapshot server
Co-authored-by: marc-hanheide <1153084+marc-hanheide@users.noreply.github.com>
1 parent 3ca3363 commit fd14c33

16 files changed

Lines changed: 2246 additions & 0 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ build/
44
bin/
55
lib/
66
install/
7+
*.egg-info/
8+
__pycache__/
9+
*.py[cod]
710
msg_gen/
811
srv_gen/
912
msg/*Action.msg

src/rosbag_blackbox/README.md

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
# rosbag_blackbox
2+
3+
A ROS 2 **Humble** package that provides:
4+
5+
1. **Ring Buffer Node** – continuously records configured topics into an
6+
in-memory ring buffer and saves a *snapshot* (rosbag file) on demand.
7+
2. **Snapshot Server** – a combined **FastAPI** REST + **FastMCP** (Model
8+
Context Protocol) server that lets REST clients and AI agents access and
9+
introspect snapshot files.
10+
11+
---
12+
13+
## Table of Contents
14+
15+
- [Features](#features)
16+
- [Installation](#installation)
17+
- [Quick Start](#quick-start)
18+
- [Configuration Reference](#configuration-reference)
19+
- [Pattern Syntax](#pattern-syntax)
20+
- [QoS Settings](#qos-settings)
21+
- [Latched Topics](#latched-topics)
22+
- [Triggering a Snapshot](#triggering-a-snapshot)
23+
- [Snapshot Server](#snapshot-server)
24+
- [REST Endpoints](#rest-endpoints)
25+
- [MCP Tools](#mcp-tools)
26+
- [Launch File](#launch-file)
27+
- [Node Parameters](#node-parameters)
28+
- [Architecture](#architecture)
29+
30+
---
31+
32+
## Features
33+
34+
| Feature | Details |
35+
|---------|---------|
36+
| **Glob topic matching** | Zenoh-style `*` (one segment) and `**` (any depth) |
37+
| **Per-topic QoS** | `reliability`, `durability`, `depth` fully configurable |
38+
| **Latched topics** | Set `durability: transient_local` per topic |
39+
| **Max frequency** | Discard messages arriving faster than configured Hz |
40+
| **Fixed-duration buffer** | Always keeps the last *N* seconds (default 10 s) |
41+
| **Memory efficiency** | Messages stored as raw serialised bytes; deque-based O(1) expiry |
42+
| **Dynamic type discovery** | Topic types resolved at runtime (like `ros2 bag record`) |
43+
| **On-demand snapshot** | `std_srvs/srv/Trigger` service writes the buffer to a rosbag |
44+
| **Configurable filename** | `{datetime}`, `{date}`, `{time}`, `{timestamp}` placeholders |
45+
| **REST API** | FastAPI server with OpenAPI docs at `/docs` |
46+
| **AI agent tools** | FastMCP server at `/mcp` (streamable-HTTP) |
47+
| **Image support** | `sensor_msgs/Image` and `CompressedImage` → PNG |
48+
49+
---
50+
51+
## Installation
52+
53+
### ROS 2 dependencies
54+
55+
```bash
56+
cd ~/ros2_ws
57+
rosdep install --from-paths src --ignore-src -r -y
58+
colcon build --packages-select rosbag_blackbox
59+
source install/setup.bash
60+
```
61+
62+
### Optional Python dependencies (for the snapshot server)
63+
64+
```bash
65+
pip install fastapi uvicorn fastmcp numpy Pillow
66+
# or
67+
pip install -r src/rosbag_blackbox/requirements.txt
68+
```
69+
70+
The **ring buffer node** works without these dependencies. The snapshot
71+
server prints a warning and exits if FastAPI / uvicorn are missing.
72+
73+
---
74+
75+
## Quick Start
76+
77+
### 1 – Launch both nodes
78+
79+
```bash
80+
ros2 launch rosbag_blackbox rosbag_blackbox.launch.py \
81+
config_file:=/path/to/config.yaml
82+
```
83+
84+
### 2 – Trigger a snapshot
85+
86+
```bash
87+
ros2 service call /rosbag_blackbox/save_snapshot std_srvs/srv/Trigger
88+
```
89+
90+
The response message contains the full path of the new rosbag directory.
91+
92+
### 3 – Query the snapshot via REST
93+
94+
```bash
95+
# List all snapshots
96+
curl http://localhost:8001/snapshots
97+
98+
# Get metadata of a specific snapshot
99+
curl http://localhost:8001/snapshots/snapshot_20240101_120000/metadata
100+
101+
# Get the 5 latest messages on a topic
102+
curl "http://localhost:8001/snapshots/snapshot_20240101_120000/messages?topic=/chatter&limit=5"
103+
```
104+
105+
### 4 – OpenAPI docs
106+
107+
Open `http://localhost:8001/docs` in your browser.
108+
109+
---
110+
111+
## Configuration Reference
112+
113+
Copy `config/example_config.yaml` and adapt it:
114+
115+
```yaml
116+
buffer:
117+
duration_seconds: 10 # keep the last N seconds
118+
119+
snapshot:
120+
directory: /tmp/rosbag_blackbox_snapshots
121+
name_pattern: "snapshot_{datetime}" # see placeholders below
122+
123+
topics:
124+
- pattern: "/camera/**"
125+
max_frequency: 10.0 # Hz; omit or 0 for unlimited
126+
qos:
127+
reliability: best_effort
128+
durability: volatile
129+
depth: 1
130+
131+
- pattern: "/tf_static"
132+
qos:
133+
reliability: reliable
134+
durability: transient_local # latched
135+
depth: 1
136+
137+
- pattern: "/**" # catch-all fallback
138+
max_frequency: 5.0
139+
qos:
140+
reliability: best_effort
141+
durability: volatile
142+
depth: 10
143+
```
144+
145+
**Filename placeholders**
146+
147+
| Placeholder | Example |
148+
|-------------|---------|
149+
| `{datetime}` | `20240101_120000` |
150+
| `{date}` | `20240101` |
151+
| `{time}` | `120000` |
152+
| `{timestamp}` | `1704110400` (Unix epoch) |
153+
154+
### Pattern Syntax
155+
156+
The topic pattern follows **Zenoh selector** conventions:
157+
158+
| Pattern | Matches |
159+
|---------|---------|
160+
| `/camera/*` | `/camera/left`, `/camera/right` |
161+
| `/camera/**` | `/camera/left`, `/camera/left/image_raw`, … |
162+
| `/**` | every topic |
163+
| `/*/scan` | `/lidar/scan`, `/front/scan`, … |
164+
| `/a/**/c` | `/a/c`, `/a/b/c`, `/a/b/d/c`, … |
165+
166+
Rules are evaluated **in order**; the first matching pattern wins.
167+
168+
### QoS Settings
169+
170+
| Key | Values | Default |
171+
|-----|--------|---------|
172+
| `reliability` | `best_effort` \| `reliable` | `best_effort` |
173+
| `durability` | `volatile` \| `transient_local` | `volatile` |
174+
| `depth` | integer | `10` |
175+
176+
### Latched Topics
177+
178+
A **latched** publisher in ROS 2 uses `transient_local` durability. To
179+
receive the cached message when your node (re-)joins, set:
180+
181+
```yaml
182+
qos:
183+
reliability: reliable
184+
durability: transient_local
185+
depth: 1
186+
```
187+
188+
> **Note:** If the publisher uses `volatile` durability and the subscriber
189+
> requests `transient_local`, ROS 2 will refuse the connection. Always match
190+
> the publisher's durability setting.
191+
192+
---
193+
194+
## Triggering a Snapshot
195+
196+
Call the service exposed by the ring-buffer node:
197+
198+
```bash
199+
ros2 service call /rosbag_blackbox/save_snapshot std_srvs/srv/Trigger
200+
```
201+
202+
Or from Python:
203+
204+
```python
205+
import rclpy
206+
from rclpy.node import Node
207+
from std_srvs.srv import Trigger
208+
209+
rclpy.init()
210+
node = Node('snapshot_client')
211+
cli = node.create_client(Trigger, '/rosbag_blackbox/save_snapshot')
212+
cli.wait_for_service()
213+
future = cli.call_async(Trigger.Request())
214+
rclpy.spin_until_future_complete(node, future)
215+
print(future.result().message)
216+
```
217+
218+
---
219+
220+
## Snapshot Server
221+
222+
Start manually (without the launch file):
223+
224+
```bash
225+
ros2 run rosbag_blackbox snapshot_server \
226+
--ros-args -p snapshot_dir:=/tmp/rosbag_blackbox_snapshots \
227+
-p port:=8001
228+
```
229+
230+
### REST Endpoints
231+
232+
| Method | Path | Description |
233+
|--------|------|-------------|
234+
| GET | `/health` | Liveness check |
235+
| GET | `/snapshots` | List all snapshots |
236+
| GET | `/snapshots/latest` | Path of the latest snapshot |
237+
| GET | `/snapshots/{name}/metadata` | Topics, counts, duration |
238+
| GET | `/snapshots/{name}/messages` | Extract messages (filterable) |
239+
| GET | `/snapshots/{name}/image/{topic}` | Latest image as PNG |
240+
241+
**Query parameters for `/messages`**
242+
243+
| Parameter | Type | Description |
244+
|-----------|------|-------------|
245+
| `topic` | string | Filter to a single topic |
246+
| `start` | float | Unix-time lower bound (seconds) |
247+
| `end` | float | Unix-time upper bound (seconds) |
248+
| `limit` | int | Max messages to return (default 100) |
249+
| `keys_only` | bool | Return only field names |
250+
| `truncate_arrays` | int | Truncate arrays to N elements |
251+
252+
### MCP Tools
253+
254+
The FastMCP server at `/mcp` exposes the following tools to AI agents:
255+
256+
| Tool | Description |
257+
|------|-------------|
258+
| `list_snapshots(directory)` | Discover snapshot files |
259+
| `select_snapshot(bag_path)` | Set the active snapshot |
260+
| `get_snapshot_metadata(bag_path)` | Topics, counts, duration |
261+
| `list_topics(bag_path)` | All topics with message counts |
262+
| `search_topics(pattern, bag_path)` | Find topics by substring |
263+
| `get_messages(topic, ...)` | Extract & deserialise messages |
264+
| `get_image(topic, ...)` | Return image as base64-PNG |
265+
266+
All tools default to the **latest** snapshot when `bag_path` is omitted.
267+
268+
**Example agent interaction:**
269+
270+
```
271+
agent → list_snapshots()
272+
[{"path": "/tmp/.../snapshot_20240101_120000", ...}]
273+
274+
agent → get_snapshot_metadata()
275+
← {"topics": [...], "total_messages": 1200, "duration_seconds": 10.0}
276+
277+
agent → list_topics()
278+
[{"name": "/camera/image_raw", "type": "sensor_msgs/msg/Image", ...}]
279+
280+
agent → get_image(topic="/camera/image_raw")
281+
← {"image_base64": "iVBORw0KGgoAAAANS...", "image_format": "PNG"}
282+
283+
agent → get_messages(topic="/tf", max_messages=3, truncate_arrays=5)
284+
← {"messages": [...], "count": 3}
285+
```
286+
287+
---
288+
289+
## Launch File
290+
291+
```bash
292+
ros2 launch rosbag_blackbox rosbag_blackbox.launch.py \
293+
config_file:=/path/to/config.yaml \
294+
snapshot_dir:=/data/snapshots \
295+
buffer_duration:=30.0 \
296+
server_port:=8001
297+
```
298+
299+
**Available arguments**
300+
301+
| Argument | Default | Description |
302+
|----------|---------|-------------|
303+
| `config_file` | `share/…/example_config.yaml` | Path to YAML config |
304+
| `snapshot_dir` | `/tmp/rosbag_blackbox_snapshots` | Snapshot output dir |
305+
| `buffer_duration` | `10.0` | Ring buffer duration (seconds) |
306+
| `server_host` | `0.0.0.0` | Bind host for the API server |
307+
| `server_port` | `8001` | Port for the API server |
308+
309+
---
310+
311+
## Node Parameters
312+
313+
### `ringbuffer_node`
314+
315+
| Parameter | Type | Default | Description |
316+
|-----------|------|---------|-------------|
317+
| `config_file` | string | `''` | Path to YAML config file |
318+
| `buffer_duration` | double | `10.0` | Buffer duration in seconds |
319+
| `snapshot_dir` | string | `/tmp/…` | Snapshot output directory |
320+
| `snapshot_name_pattern` | string | `snapshot_{datetime}` | Filename pattern |
321+
322+
### `snapshot_server`
323+
324+
| Parameter | Type | Default | Description |
325+
|-----------|------|---------|-------------|
326+
| `snapshot_dir` | string | `/tmp/…` | Directory to search for bags |
327+
| `host` | string | `0.0.0.0` | Bind address |
328+
| `port` | int | `8001` | HTTP port |
329+
330+
---
331+
332+
## Architecture
333+
334+
```
335+
┌─────────────────────────────────────────────────────────────┐
336+
│ ROS 2 Humble │
337+
│ │
338+
│ Publishers │
339+
│ │ topic A ──→ ┌──────────────────────────────────┐ │
340+
│ │ topic B ──→ │ rosbag_blackbox node │ │
341+
│ │ topic C ──→ │ │ │
342+
│ │ │ ┌────────────────────────────┐ │ │
343+
│ │ │ │ RingBuffer │ │ │
344+
│ │ │ │ deque[(ts, topic, bytes)] │ │ │
345+
│ │ │ │ purge on every add() │ │ │
346+
│ │ │ └────────────┬───────────────┘ │ │
347+
│ │ │ │ snapshot() │ │
348+
│ │ │ ┌────────────▼──────────────┐ │ │
349+
│ │ │ │ save_snapshot service │ │ │
350+
│ │ │ │ (std_srvs/Trigger) │ │ │
351+
│ │ │ └────────────┬──────────────┘ │ │
352+
│ │ └───────────────┼──────────────────┘ │
353+
│ │ │ writes │
354+
│ │ ▼ │
355+
│ │ ┌────────────────┐ │
356+
│ │ │ rosbag file │ │
357+
│ │ │ (SQLite3) │ │
358+
│ │ └────────┬───────┘ │
359+
│ │ reads │
360+
│ ┌───────────────────────────────▼───────────────────────┐ │
361+
│ │ snapshot_server node │ │
362+
│ │ │ │
363+
│ │ FastAPI ──→ REST endpoints (/snapshots, /image, …) │ │
364+
│ │ FastMCP ──→ /mcp (streamable-HTTP) │ │
365+
│ └────────────────────────────────────────────────────────┘ │
366+
└─────────────────────────────────────────────────────────────┘
367+
▲ ▲
368+
│ HTTP / REST │ MCP
369+
curl / browser AI agent (Claude, etc.)
370+
```

0 commit comments

Comments
 (0)