Skip to content

Commit bc04508

Browse files
run_policy: optional per-session rosbag (--task.debug.record-rosbag)
Spawns 'ros2 bag record --all' in its own process group for the lifetime of a run_policy run and SIGINTs it for a clean MCAP close on exit (Ctrl-C, normal completion, or error), then prints the bag's absolute path. Each run gets its own short, self-contained bag, so debugging one run no longer means hunting for a timestamp inside a long fleet-telemetry recording. Wrapped around policy.run() via a SessionRecorder context manager; no-op when the flag is off, and any failure to start recording is logged and swallowed so it can never take down the policy run. Output dir configurable via --task.debug.rosbag-dir (default ~/run_policy_bags).
1 parent 12022b5 commit bc04508

3 files changed

Lines changed: 101 additions & 1 deletion

File tree

src/holosoma_inference/holosoma_inference/config/config_types/task.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ class DebugConfig:
2525
force_zero_action: bool = False
2626
"""Zero out the scaled policy action (robot holds default pose)."""
2727

28+
record_rosbag: bool = False
29+
"""Record a per-session rosbag for the lifetime of this run_policy process.
30+
Spawns ``ros2 bag record --all`` in the background when the policy starts and
31+
stops it on exit (Ctrl-C, normal completion, or error), then prints the bag's
32+
absolute path. Each run gets its own short, self-contained bag — no hunting
33+
for a timestamp inside a long fleet-telemetry recording."""
34+
35+
rosbag_dir: str = "~/run_policy_sessions"
36+
"""Directory for per-session rosbags recorded via ``record_rosbag``."""
37+
2838

2939
@dataclass(frozen=True)
3040
class Ros2DepthConsumerConfig:

src/holosoma_inference/holosoma_inference/run_policy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from holosoma_inference.policies.dual_mode import DualModePolicy, _select_policy_class
2323
from holosoma_inference.utils.config_registry import parse_config
2424
from holosoma_inference.utils.misc import restore_terminal_settings
25+
from holosoma_inference.utils.session_recorder import SessionRecorder
2526

2627

2728
def _print_control_guide(policy_class, use_joystick: bool, dual_mode: bool = False):
@@ -120,7 +121,10 @@ def run_policy(config: InferenceConfig):
120121
logger.info("✅ Policy initialized successfully!")
121122
use_joystick = bool({"joystick", "interface"} & {config.task.velocity_input, config.task.state_input})
122123
_print_control_guide(policy_class, use_joystick, dual_mode=dual_mode)
123-
policy.run()
124+
# Optional per-session rosbag: records ros2 bag record --all for this run
125+
# only, stopping (and printing the bag path) on Ctrl-C / completion / error.
126+
with SessionRecorder.from_debug_config(config.task.debug):
127+
policy.run()
124128
logger.info("✅ Policy execution completed!")
125129

126130
except Exception as e:
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Per-session rosbag recorder for run_policy.
2+
3+
Spawns ``ros2 bag record --all`` in its own process group for the lifetime of a
4+
policy run, then SIGINTs it for a clean MCAP close on exit. Each run_policy
5+
invocation gets its own short, self-contained bag, so debugging a single run no
6+
longer means hunting for a timestamp inside a long fleet-telemetry recording.
7+
8+
Usage::
9+
10+
with SessionRecorder.from_debug_config(cfg.task.debug):
11+
policy.run()
12+
# bag path is logged on exit (and available as the recorder's .bag_path)
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import os
18+
import signal
19+
import subprocess
20+
import time
21+
from pathlib import Path
22+
from typing import TYPE_CHECKING
23+
24+
from loguru import logger
25+
from typing_extensions import Self # typing.Self is 3.11+; robot runs 3.10
26+
27+
if TYPE_CHECKING:
28+
from holosoma_inference.config.config_types.task import DebugConfig
29+
30+
31+
class SessionRecorder:
32+
"""Context manager that records a rosbag for the duration of the block.
33+
34+
No-op when ``enabled`` is False, so callers can wrap unconditionally. Failure
35+
to start the recorder is logged and swallowed — a missing ``ros2`` or a bag
36+
that won't start must never take down the policy run.
37+
"""
38+
39+
def __init__(self, enabled: bool, out_dir: str = "~/run_policy_sessions", storage: str = "mcap"):
40+
self.enabled = enabled
41+
self.out_dir = Path(out_dir).expanduser()
42+
self.storage = storage
43+
self.bag_path: Path | None = None
44+
self._proc: subprocess.Popen | None = None
45+
46+
@classmethod
47+
def from_debug_config(cls, debug: DebugConfig) -> SessionRecorder:
48+
"""Build from a task ``DebugConfig`` (reads ``record_rosbag`` / ``rosbag_dir``)."""
49+
return cls(enabled=debug.record_rosbag, out_dir=debug.rosbag_dir)
50+
51+
def __enter__(self) -> Self:
52+
if not self.enabled:
53+
return self
54+
try:
55+
self.out_dir.mkdir(parents=True, exist_ok=True)
56+
self.bag_path = self.out_dir / time.strftime("run_policy_session_%Y%m%d_%H%M%S")
57+
# fmt: off
58+
cmd = [
59+
"ros2", "bag", "record", "--all",
60+
"--storage", self.storage,
61+
"--disable-keyboard-controls",
62+
"-o", str(self.bag_path),
63+
]
64+
# fmt: on
65+
# setsid: own process group so we can SIGINT only the recorder on exit.
66+
self._proc = subprocess.Popen(cmd, preexec_fn=os.setsid) # noqa: PLW1509
67+
logger.info(f"🎬 Recording session rosbag → {self.bag_path} (ros2 bag record --all)")
68+
except Exception as exc: # never let recording break the run
69+
logger.warning(f"Could not start session rosbag recorder: {exc}")
70+
self._proc = None
71+
self.bag_path = None
72+
return self
73+
74+
def __exit__(self, exc_type, exc, tb) -> None:
75+
if self._proc is None:
76+
return
77+
# SIGINT for a clean MCAP close; SIGKILL if it won't exit.
78+
try:
79+
os.killpg(os.getpgid(self._proc.pid), signal.SIGINT)
80+
self._proc.wait(timeout=15)
81+
except subprocess.TimeoutExpired:
82+
os.killpg(os.getpgid(self._proc.pid), signal.SIGKILL)
83+
except OSError:
84+
pass # recorder already exited
85+
if self.bag_path and self.bag_path.exists():
86+
logger.info(f"💾 Session rosbag saved: {self.bag_path.resolve()}")

0 commit comments

Comments
 (0)