flightopsDocumentation
Guides

Capture a talker and open a report

7 min readdocs/tutorials/capture-to-report.md (source repository)

This walkthrough starts a harmless ROS 2 talker under Flightops, records only its /chatter topic, inspects the closed recording, queries a short window, and opens the saved result as a desktop HTML report. Allow about 15 minutes after installation. No AI connection or robot hardware is needed.

1. Prepare the two terminals

Use Ubuntu 22.04 with ROS 2 Humble or Ubuntu 24.04 with ROS 2 Jazzy. Install the matching ROS desktop packages, including demo_nodes_cpp, rosbag2, and rosbag2_storage_mcap. Install the Flightops beta and activate this device with flightops license activate. The install puts flightops and splashdown in ~/.local/bin. Use a laptop or desktop terminal at least 120 columns wide.

In terminal A, source the ROS distribution you installed:

bash
# Ubuntu 22.04: source /opt/ros/humble/setup.bash
source /opt/ros/jazzy/setup.bash
export PATH="$HOME/.local/bin:$PATH"
export ROS_DOMAIN_ID=181 ROS_LOCALHOST_ONLY=1
ros2 pkg prefix demo_nodes_cpp
ros2 pkg prefix rosbag2_storage_mcap
command -v flightops
command -v splashdown
flightops doctor

On Ubuntu 22.04, run the commented Humble source command instead of the Jazzy command. Both package checks should print a path, and the two commands should resolve under ~/.local/bin. doctor should report your ROS distribution and domain 181. Choose another unused domain from 1–232 if 181 is occupied, and use it in both terminals. Keep the same ROS setup, domain, localhost setting, and workspace overlays throughout; the recording policy is bound to that environment.

In terminal B, source the same ROS distribution, then set the same environment variables:

bash
# Ubuntu 22.04: source /opt/ros/humble/setup.bash
source /opt/ros/jazzy/setup.bash
export PATH="$HOME/.local/bin:$PATH"
export ROS_DOMAIN_ID=181 ROS_LOCALHOST_ONLY=1

Terminal B is ready for later inspection while Flightops occupies terminal A. The demo creates a new project and evidence directory in your home directory. Run this in terminal A:

bash
demo_dir="$HOME/flightops-capture-demo-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$demo_dir"
cat > "$demo_dir/talker.launch.py" <<'PY'
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(package='demo_nodes_cpp', executable='talker',
             name='capture_demo_talker', output='screen')
    ])
PY
printf '%s\n' "$demo_dir" > "$HOME/.flightops-capture-demo-path"

The last file lets terminal B find this run. The launch file only publishes demo text. It does not command an actuator.

2. Enable a bounded recording and launch

In terminal A, enable an exact-topic policy for this project and domain:

bash
flightops record --project "$demo_dir" enable --topics /chatter \
  --max-duration-s 60 --max-session-bytes 67108864 \
  --max-segment-bytes 33554432
flightops record --project "$demo_dir" status
flightops record list > "$demo_dir/recordings-before.json"
flightops --project "$demo_dir" launch "$demo_dir/talker.launch.py"

The policy is enabled for later eligible launches in this exact scope until you disable it. The 60-second duration and byte limits bound capture; the recorder also keeps a free-disk reserve. These are sampled stop thresholds, not hard disk quotas. In Flightops, press L to see the talker log and look for repeated Publishing: lines. Press ? for terminal help. After roughly 10–15 seconds, close the log view and press q to quit Flightops. Quitting stops this owned launch and closes its recording. A startup interval before the first recorded message may still be unknown.

Back at the shell in terminal A, disable this demonstration's standing policy so a later launch does not capture again:

bash
flightops record --project "$demo_dir" disable
flightops record list > "$demo_dir/recordings-after.json"

The policy change does not erase the recording. It remains in Flightops' local state directory.

3. Find and inspect the closed MCAP

In terminal B, read the run directory and identify the one new recording session created by the launch:

bash
demo_dir="$(cat "$HOME/.flightops-capture-demo-path")"
python3 - "$demo_dir/recordings-before.json" "$demo_dir/recordings-after.json" \
  "$demo_dir/session-id.txt" <<'PY'
import json
import sys
from pathlib import Path

before = {item['id'] for item in json.loads(Path(sys.argv[1]).read_text())}
new = [item['id'] for item in json.loads(Path(sys.argv[2]).read_text())
       if item['id'] not in before]
if len(new) != 1:
    raise SystemExit(f'Expected one new recording; found {len(new)}. Inspect record list.')
Path(sys.argv[3]).write_text(new[0] + '\n')
print('New recording session:', new[0])
PY
session_id="$(cat "$demo_dir/session-id.txt")"
flightops record show "$session_id" > "$demo_dir/recording.json"
flightops record view -o "$demo_dir/recordings.html"

Open recordings.html in a desktop browser to browse the catalog, or read recording.json for the exact session and segment. The segment should say state: finalized, include a sha256, a nonzero message_count, and a .mcap path. A failed, interrupted, or zero-message result is useful evidence of a capture problem, but cannot support the query below. The catalog's availability: catalogued means the file has not been rechecked since closure; the query will read it.

4. Save a bounded result JSON

Still in terminal B, run this small script. It selects the first finalized segment with messages and queries its recorded log-time span for /chatter. The script refuses a span over the query's five-minute limit. It saves the actual splashdown result in window.json:

bash
python3 - "$demo_dir/recording.json" "$demo_dir/window.json" <<'PY'
import json
import subprocess
import sys
from pathlib import Path

record = json.loads(Path(sys.argv[1]).read_text())
segment = next((item for item in record['segments']
                if item['state'] == 'finalized'
                and item['message_count'] > 0
                and item['first_log_ns'] is not None
                and item['last_log_ns'] is not None), None)
if segment is None:
    raise SystemExit('No finalized segment with messages; inspect recording.json')
start = segment['first_log_ns']
end = segment['last_log_ns'] + 1
if end - start > 300_000_000_000:
    raise SystemExit('Recording exceeds the five-minute query limit')
path = Path(segment['path'])
if not path.is_file():
    raise SystemExit(f'MCAP is missing: {path}')
command = ['splashdown', 'query', path.name, '--root', str(path.parent),
           '--topic', '/chatter', '--start-ns', str(start),
           '--end-ns', str(end), '--reduction', 'count']
with Path(sys.argv[2]).open('w') as output:
    subprocess.run(command, check=True, stdout=output)
print('Saved:', sys.argv[2])
PY
python3 -m json.tool "$demo_dir/window.json" | less

Press q to leave less. A usable result has schema flightops.evidence-window/0, outcome: observed, complete: true, and a positive measurements.message_count. The count is for the selected recorded window, not a claim that every published message was captured. Keep the JSON, MCAP, and catalog metadata together when retaining evidence.

5. Open and reopen the report

In terminal B:

bash
flightops results "$demo_dir/window.json" -o "$demo_dir/results.html" --open

The command prints the report path. If the browser does not open, open results.html directly in a desktop browser. Look for the /chatter topic, the count and window bounds, and the result's completeness status. The HTML report is generated from the saved JSON; it does not need ROS or a running talker. Later, in a new terminal, run:

bash
demo_dir="$(cat "$HOME/.flightops-capture-demo-path")"
flightops results "$demo_dir/window.json" \
  -o "$demo_dir/results-reopened.html" --open

Use a new output filename because Flightops never overwrites an explicit -o path. Reopening does not restart the launch or record anything new.

If the recording is absent, check ros2 pkg prefix rosbag2_storage_mcap, flightops doctor, and the status/error in recording.json. Check that terminal A used the same project, domain, ROS setup, and middleware as the enabled policy. If the query reports missing_topic or empty_window, inspect the segment's message count and topic selection; a valid closed MCAP alone does not prove /chatter payload. If splashdown is missing, check that ~/.local/bin is on your PATH and run flightops --verify. The managed-recording reference explains policy scope and capture limits.