PingPongRealWorldRL / scripts /motion_switcher_guard.py
KurisiTOMA's picture
chore: publish H2 real-world RL system
d8290eb verified
Raw History Blame Contribute Delete
4 kB
#!/usr/bin/env python3
"""Inspect or explicitly release Unitree high-level motion control for H2.
The default operation is read-only. Releasing a motion mode is intentionally
double-gated because it changes robot state and must remain an operator action.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
import time
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from unitree_sdk2py.comm.motion_switcher.motion_switcher_client import MotionSwitcherClient
from unitree_sdk2py.core.channel import ChannelFactoryInitialize
from runtime.motion_attestation import (
ATTESTATION_PATH,
motion_switcher_released,
write_attestation,
)
RELEASE_ACK = "RELEASE-H2-HIGH-LEVEL-CONTROL"
def _mode(client: MotionSwitcherClient) -> tuple[int, dict]:
status, result = client.CheckMode()
motion_switcher_released(status, result)
return int(status), result
def _released(result: dict) -> bool:
return motion_switcher_released(0, result)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--network-interface", required=True)
parser.add_argument("--release", action="store_true", help="Release the active high-level mode.")
parser.add_argument(
"--write-attestation",
action="store_true",
help="After a successful CheckMode, write the short-lived token required by the runner.",
)
parser.add_argument(
"--ack-release",
default="",
help=f"Required with --release; exact value: {RELEASE_ACK}",
)
args = parser.parse_args()
# A new check always invalidates any earlier token before contacting the
# robot, so a failed/active result cannot leave an old pass consumable.
try:
ATTESTATION_PATH.unlink()
except FileNotFoundError:
pass
interface = str(args.network_interface)
state_path = Path("/sys/class/net") / interface / "operstate"
if not state_path.is_file():
raise RuntimeError(f"network interface does not exist: {interface}")
state = state_path.read_text(encoding="utf-8").strip()
if state != "up":
raise RuntimeError(f"network interface {interface} is not UP (operstate={state!r})")
if args.release and args.ack_release != RELEASE_ACK:
raise RuntimeError(f"--release requires --ack-release {RELEASE_ACK}")
ChannelFactoryInitialize(0, interface)
client = MotionSwitcherClient()
client.SetTimeout(5.0)
client.Init()
_, result = _mode(client)
print("[MOTION MODE] " + json.dumps(result, sort_keys=True, ensure_ascii=False))
if not args.release:
if not _released(result):
raise SystemExit(
"active high-level motion mode detected; inspect it, then rerun with the explicit release gate"
)
print("[MOTION MODE PASS] no active high-level motion mode")
if args.write_attestation:
write_attestation(interface)
print(f"[MOTION ATTESTATION] wrote owner-only token {ATTESTATION_PATH}")
return
for attempt in range(1, 4):
if _released(result):
break
status, _ = client.ReleaseMode()
if status != 0:
raise RuntimeError(f"MotionSwitcher ReleaseMode failed on attempt {attempt}: status={status}")
time.sleep(1.0)
_, result = _mode(client)
print(
f"[MOTION MODE] post-release attempt={attempt} "
+ json.dumps(result, sort_keys=True, ensure_ascii=False)
)
if not _released(result):
raise RuntimeError("high-level motion mode remains active after three release attempts")
print("[MOTION MODE PASS] high-level motion control is released")
if args.write_attestation:
write_attestation(interface)
print(f"[MOTION ATTESTATION] wrote owner-only token {ATTESTATION_PATH}")
if __name__ == "__main__":
main()