726 lines
33 KiB
Python
726 lines
33 KiB
Python
"""CLI entrypoint for current sim2real deployment."""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import yaml
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from input_dev.keyboard import KeyboardCommandController
|
|
from interface.real_io import RealIO
|
|
from policy.policy_runner import PolicyRunner
|
|
from safety.runtime_guard import GuardLevel, RuntimeGuard
|
|
from safety.safety_monitor import SafetyLevel, SafetyMonitor
|
|
from startup.pose_initializer import PoseInitFailed, PoseInitializer, STAND_POSE
|
|
from startup.stand_balance import StandBalanceController
|
|
from tools.logger import LogBundle
|
|
from tools.math_utils import get_gravity_orientation
|
|
|
|
JOINT_LABELS = LogBundle.JOINT_LABELS
|
|
|
|
|
|
def make_real_driver_factory():
|
|
def factory(can1_port, can2_port, debug):
|
|
sim2real_root = Path(__file__).resolve().parent
|
|
for path in (
|
|
sim2real_root / "vendored",
|
|
"/home/rc2/work/rcwork/control",
|
|
"/home/rc2/work/rcwork",
|
|
):
|
|
path_str = str(path)
|
|
if path_str not in sys.path and Path(path).exists():
|
|
sys.path.append(path_str)
|
|
from drivers.motor_driver import RobStrideDriver # type: ignore
|
|
|
|
return RobStrideDriver(can1_port, debug), RobStrideDriver(can2_port, debug)
|
|
|
|
return factory
|
|
|
|
|
|
def make_dry_driver_factory():
|
|
class MockMotor:
|
|
def __init__(self):
|
|
class State:
|
|
position = 0.0
|
|
velocity = 0.0
|
|
torque = 0.0
|
|
|
|
self.state = State()
|
|
|
|
class MockDriver:
|
|
def __init__(self, port, debug):
|
|
self.port = port
|
|
self.motors = {}
|
|
|
|
def connect(self): ...
|
|
def disconnect(self): ...
|
|
def add_motor(self, name, motor_id, model): self.motors[name] = MockMotor()
|
|
def enable(self, name): ...
|
|
def disable(self, name): ...
|
|
def clear_warnings(self, name): ...
|
|
def process_messages(self): ...
|
|
def control_mit(self, *args, **kwargs): ...
|
|
|
|
def factory(can1_port, can2_port, debug):
|
|
return MockDriver(can1_port, debug), MockDriver(can2_port, debug)
|
|
|
|
return factory
|
|
|
|
|
|
def _sleep_to(next_exec: float) -> float:
|
|
slack = next_exec - time.perf_counter()
|
|
if slack > 0:
|
|
time.sleep(slack)
|
|
return next_exec + 0.0
|
|
return time.perf_counter()
|
|
|
|
|
|
def build_action_diag(
|
|
*,
|
|
joint_pos: np.ndarray,
|
|
default_pose: np.ndarray,
|
|
raw: np.ndarray,
|
|
scaled: np.ndarray,
|
|
tentative: np.ndarray,
|
|
cmd: np.ndarray,
|
|
zero_command: bool,
|
|
runtime_released: bool,
|
|
release_alpha: float,
|
|
safety_details: dict | None = None,
|
|
) -> dict:
|
|
details = dict(safety_details or {})
|
|
joint_indices = list(details.get("joint_indices", []))
|
|
pos_err = tentative - joint_pos
|
|
leg_offset = tentative[:12] - default_pose[:12]
|
|
diag = {
|
|
"joint_indices": joint_indices,
|
|
"joint_names": [JOINT_LABELS[i] for i in joint_indices if 0 <= i < len(JOINT_LABELS)],
|
|
"cmd": cmd.tolist(),
|
|
"zero_command": bool(zero_command),
|
|
"runtime_released": bool(runtime_released),
|
|
"release_alpha": float(release_alpha),
|
|
"max_raw": float(np.max(np.abs(raw))) if raw.size else 0.0,
|
|
"max_scaled": float(np.max(np.abs(scaled[:12]))) if scaled.size else 0.0,
|
|
"max_target": float(np.max(np.abs(tentative[:12]))) if tentative.size else 0.0,
|
|
}
|
|
if joint_indices:
|
|
primary = int(joint_indices[0])
|
|
diag.update(
|
|
{
|
|
"primary_joint_index": primary,
|
|
"primary_joint_name": JOINT_LABELS[primary],
|
|
"primary_target": float(tentative[primary]),
|
|
"primary_default": float(default_pose[primary]),
|
|
"primary_measured": float(joint_pos[primary]),
|
|
"primary_pos_err": float(pos_err[primary]),
|
|
"primary_raw": float(raw[primary]),
|
|
"primary_scaled": float(scaled[primary]),
|
|
}
|
|
)
|
|
if primary < 12:
|
|
diag["primary_leg_offset"] = float(leg_offset[primary])
|
|
details.update(diag)
|
|
return details
|
|
|
|
|
|
def policy_release_cfg(cfg: dict) -> dict[str, float]:
|
|
policy_cfg = cfg.get("policy", {})
|
|
return {
|
|
"command_hold_s": max(float(policy_cfg.get("release_command_hold_s", 0.12)), 0.0),
|
|
"posture_max_err": max(float(policy_cfg.get("release_posture_max_err", 0.35)), 0.0),
|
|
"target_blend_s": max(float(policy_cfg.get("release_target_blend_s", 0.30)), 1e-3),
|
|
}
|
|
|
|
|
|
def compute_release_metrics(runner: PolicyRunner, state: dict, hold_target: np.ndarray, cmd: np.ndarray) -> dict:
|
|
joint_pos = np.asarray(state["joint_pos"], dtype=np.float32)
|
|
default_pose = np.asarray(runner.default_dof_pos, dtype=np.float32)
|
|
hold_target = np.asarray(hold_target, dtype=np.float32)
|
|
planar_cmd, yaw_cmd = runner.command_activation_metrics(cmd)
|
|
return {
|
|
"planar_cmd": float(planar_cmd),
|
|
"yaw_cmd": float(yaw_cmd),
|
|
"max_hold_err": float(np.max(np.abs(joint_pos[:12] - hold_target[:12]))),
|
|
"max_default_err": float(np.max(np.abs(joint_pos[:12] - default_pose[:12]))),
|
|
"max_hold_default_gap": float(np.max(np.abs(hold_target[:12] - default_pose[:12]))),
|
|
}
|
|
|
|
|
|
def blend_runtime_target(
|
|
runner: PolicyRunner,
|
|
hold_target: np.ndarray,
|
|
policy_target: np.ndarray,
|
|
release_alpha: float,
|
|
target_blend_s: float,
|
|
control_dt: float,
|
|
) -> np.ndarray:
|
|
blend = min(1.0, release_alpha * (runner.command_release_s / max(target_blend_s, control_dt)))
|
|
return ((1.0 - blend) * hold_target + blend * policy_target).astype(np.float32)
|
|
|
|
|
|
def compute_target_error_metrics(
|
|
state: dict,
|
|
hold_target: np.ndarray,
|
|
policy_target: np.ndarray,
|
|
) -> dict[str, float]:
|
|
joint_pos = np.asarray(state["joint_pos"], dtype=np.float32)
|
|
hold_target = np.asarray(hold_target, dtype=np.float32)
|
|
policy_target = np.asarray(policy_target, dtype=np.float32)
|
|
return {
|
|
"hold_target_max_err": float(np.max(np.abs(joint_pos[:12] - hold_target[:12]))),
|
|
"policy_target_max_err": float(np.max(np.abs(joint_pos[:12] - policy_target[:12]))),
|
|
"hold_policy_max_gap": float(np.max(np.abs(hold_target[:12] - policy_target[:12]))),
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--config", default=str(Path(__file__).parent / "config.yaml"))
|
|
parser.add_argument("--policy", default=None)
|
|
parser.add_argument("--dry-run", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
with open(args.config, "r", encoding="utf-8") as file_obj:
|
|
cfg = yaml.safe_load(file_obj)
|
|
|
|
sim2real_root = Path(__file__).resolve().parent
|
|
policy_path = Path(args.policy) if args.policy else sim2real_root / "policies" / "model_rough.pt"
|
|
if not policy_path.exists():
|
|
print(f"[Main] policy not found: {policy_path}")
|
|
sys.exit(1)
|
|
|
|
control_dt = 1.0 / float(cfg["control_freq"])
|
|
driver_factory = make_dry_driver_factory() if args.dry_run else make_real_driver_factory()
|
|
|
|
logger = LogBundle(cfg["log_dir"])
|
|
logger.event(
|
|
"CONFIG_LOADED",
|
|
config_path=args.config,
|
|
policy=str(policy_path),
|
|
dry_run=args.dry_run,
|
|
control_freq=cfg["control_freq"],
|
|
motor_model=cfg["motor_model"],
|
|
)
|
|
|
|
io = RealIO(
|
|
driver_factory=driver_factory,
|
|
motor_model=cfg["motor_model"],
|
|
can1_port=cfg["can1_port"],
|
|
can2_port=cfg["can2_port"],
|
|
imu_lib_path=cfg.get("imu_lib_path"),
|
|
control_dt=control_dt,
|
|
kp_leg=cfg["controller"]["kp_leg"],
|
|
kd_leg=cfg["controller"]["kd_leg"],
|
|
kd_wheel=cfg["controller"]["kd_wheel"],
|
|
debug=cfg.get("debug", False),
|
|
)
|
|
runner = PolicyRunner(
|
|
policy_path,
|
|
enable_zero_cmd_suppression=cfg.get("policy", {}).get("enable_zero_cmd_suppression", True),
|
|
hold_zero_command_pose=cfg.get("policy", {}).get("hold_zero_command_pose", True),
|
|
command_release_s=cfg.get("policy", {}).get("command_release_s", 0.35),
|
|
action_scale=np.asarray(
|
|
cfg.get("policy", {}).get(
|
|
"action_scale",
|
|
[0.125, 0.25, 0.25, 0.125, 0.25, 0.25, 0.125, 0.25, 0.25, 0.125, 0.25, 0.25, 5.0, 5.0, 5.0, 5.0],
|
|
),
|
|
dtype=np.float32,
|
|
),
|
|
zero_cmd_use_yaw_rate=cfg.get("policy", {}).get("zero_cmd_use_yaw_rate", False),
|
|
)
|
|
require_active_command = cfg.get("policy", {}).get("require_active_command_to_release", True)
|
|
keyboard = KeyboardCommandController(
|
|
max_x_vel=cfg["controller"]["max_vx"],
|
|
max_y_vel=cfg["controller"]["max_vy"],
|
|
max_yaw_vel=cfg["controller"]["max_yaw_rate"],
|
|
)
|
|
safety = SafetyMonitor(
|
|
max_target_offset=cfg["safety"]["max_target_offset"],
|
|
max_ang_vel=cfg["safety"]["max_ang_vel"],
|
|
max_tilt_z=cfg["safety"]["max_tilt_z"],
|
|
clip_to_brake=cfg["safety"]["clip_to_brake"],
|
|
)
|
|
safety.reset()
|
|
guard = RuntimeGuard(
|
|
max_ang_vel=cfg["safety"]["max_ang_vel"],
|
|
max_tilt_z=cfg["safety"]["max_tilt_z"],
|
|
imu_age_warn_ms=cfg["safety"].get("imu_age_warn_ms", 60.0),
|
|
imu_age_stop_ms=cfg["safety"].get("imu_age_stop_ms", 200.0),
|
|
)
|
|
initializer = PoseInitializer(
|
|
io,
|
|
control_dt=control_dt,
|
|
transition_time_min=cfg["startup"].get("transition_time_min", 2.0),
|
|
transition_time_max=cfg["startup"].get("transition_time_max", 6.0),
|
|
transition_seconds_per_rad=cfg["startup"].get("transition_seconds_per_rad", 1.5),
|
|
hold_time=cfg["startup"]["hold_time"],
|
|
settle_pos_threshold=cfg["startup"]["settle_pos_threshold"],
|
|
settle_vel_threshold=cfg["startup"]["settle_vel_threshold"],
|
|
timeout_extra=cfg["startup"].get("timeout_extra", 3.0),
|
|
progress_log_interval=cfg["startup"]["progress_log_interval"],
|
|
ramp_kp_time=cfg["startup"].get("ramp_kp_time", 1.0),
|
|
soft_hold_duration=cfg["startup"].get("soft_hold_duration", 1.0),
|
|
max_dev_warn=cfg["startup"].get("max_dev_warn", 1.5),
|
|
max_dev_abort=cfg["startup"].get("max_dev_abort", 3.0),
|
|
)
|
|
initializer.attach(logger=logger, guard=guard, keyboard=keyboard)
|
|
stand_balance = StandBalanceController(cfg.get("stand_balance", {}), control_dt=control_dt)
|
|
|
|
print("\n[Main] connecting hardware...")
|
|
keyboard.start()
|
|
try:
|
|
io.connect()
|
|
logger.event("CAN_IMU_CONNECTED", initial_gravity=io.imu.initial_gravity)
|
|
except Exception as exc:
|
|
logger.event("HARDWARE_CONNECT_FAILED", error=str(exc))
|
|
keyboard.stop()
|
|
logger.close()
|
|
raise
|
|
|
|
try:
|
|
io.enable_motors()
|
|
logger.event("MOTORS_ENABLED")
|
|
time.sleep(0.5)
|
|
|
|
target_pose = initializer.transition_to_stand_from_current(target_pose=STAND_POSE) if cfg["startup"]["enabled"] else STAND_POSE.copy()
|
|
|
|
if stand_balance.enabled:
|
|
logger.event("STAND_BALANCE_BEGIN")
|
|
print("[Main] waiting for stand-balance to settle...")
|
|
stand_balance.reset()
|
|
next_exec = time.perf_counter()
|
|
while True:
|
|
state = io.read_state()
|
|
target_pose = stand_balance.compute_target(state, np.zeros(3, dtype=np.float32))
|
|
io.hold_pose(target_pose, kp_scale=1.0)
|
|
debug = stand_balance.last_debug
|
|
if stand_balance.is_stable():
|
|
logger.event(
|
|
"STAND_BALANCE_STABLE",
|
|
roll_deg=float(np.degrees(debug.roll)),
|
|
pitch_deg=float(np.degrees(debug.pitch)),
|
|
)
|
|
break
|
|
next_exec += control_dt
|
|
next_exec = _sleep_to(next_exec)
|
|
logger.event("STAND_BALANCE_END")
|
|
|
|
if cfg["startup"]["require_user_confirm"]:
|
|
print("[Main] standing complete. Press Enter to release policy control...")
|
|
done = threading.Event()
|
|
|
|
def _wait():
|
|
try:
|
|
input()
|
|
except EOFError:
|
|
pass
|
|
done.set()
|
|
|
|
threading.Thread(target=_wait, daemon=True).start()
|
|
if not initializer.hold_until_user_confirm(target_pose, done):
|
|
raise PoseInitFailed("WAIT_USER interrupted")
|
|
|
|
print("[Main] priming current observation...")
|
|
logger.event("PRIME_BEGIN")
|
|
zero_cmd = np.zeros(3, dtype=np.float32)
|
|
next_exec = time.perf_counter()
|
|
for index in range(1):
|
|
if stand_balance.enabled:
|
|
state = io.read_state()
|
|
target_pose = stand_balance.compute_target(state, zero_cmd)
|
|
io.hold_pose(target_pose, kp_scale=1.0)
|
|
else:
|
|
io.hold_pose(target_pose, kp_scale=1.0)
|
|
state = io.read_state()
|
|
obs = io.get_obs_policy(state, zero_cmd, runner.default_dof_pos, runner.last_actions)
|
|
if index == 0:
|
|
runner.reset(prime_obs=obs)
|
|
logger.state(
|
|
phase="PRIME",
|
|
joint_pos=state["joint_pos"],
|
|
joint_vel=state["joint_vel"],
|
|
joint_torque=state.get("joint_torque", np.zeros(16, dtype=np.float32)),
|
|
target_pose=target_pose,
|
|
raw_action=None,
|
|
gyro=state["imu_gyro"],
|
|
accel=state["imu_accel"],
|
|
quat=state["quat_wxyz"],
|
|
proj_gravity=state["projected_gravity"],
|
|
command=zero_cmd,
|
|
imu_age_ms=float(state["imu_age_ms"]),
|
|
loop_dt_ms=0.0,
|
|
kp_scale=1.0,
|
|
)
|
|
next_exec += control_dt
|
|
next_exec = _sleep_to(next_exec)
|
|
logger.event("PRIME_END")
|
|
|
|
print("[Main] entering 50Hz control loop... (space = estop)")
|
|
logger.event("RUNTIME_BEGIN")
|
|
next_exec = time.perf_counter()
|
|
loop_count = 0
|
|
last_print = next_exec
|
|
log_every = int(cfg.get("log_every", 1))
|
|
recent_dt_ms = []
|
|
runtime_released = not require_active_command
|
|
release_cfg = policy_release_cfg(cfg)
|
|
release_active_time = 0.0
|
|
|
|
while True:
|
|
loop_t0 = time.perf_counter()
|
|
cmd = keyboard.get_command()
|
|
state = io.read_state()
|
|
obs = io.get_obs_policy(state, cmd, runner.default_dof_pos, runner.last_actions)
|
|
zero_command = runner._is_zero_command(cmd, state["imu_gyro"])
|
|
|
|
obs_nan = bool(np.any(np.isnan(obs)) or np.any(np.isinf(obs)))
|
|
if obs_nan:
|
|
logger.event("OBS_NAN", obs_max=float(np.nanmax(obs)))
|
|
io.damping_brake()
|
|
break
|
|
|
|
if not runtime_released and zero_command:
|
|
raw = np.zeros(16, dtype=np.float32)
|
|
scaled = np.zeros(16, dtype=np.float32)
|
|
target_hold = stand_balance.compute_target(state, np.zeros(3, dtype=np.float32)) if stand_balance.enabled else runner.default_dof_pos.copy()
|
|
actual_target = io.hold_pose(target_hold, kp_scale=1.0)
|
|
policy_target = runner.default_dof_pos.copy()
|
|
release_metrics = compute_release_metrics(runner, state, target_hold, cmd)
|
|
target_metrics = compute_target_error_metrics(state, target_hold, policy_target)
|
|
release_active_time = 0.0
|
|
safety_decision = SafetyMonitor().check(
|
|
target_pose=target_hold,
|
|
default_pose=runner.default_dof_pos,
|
|
imu_gyro=state["imu_gyro"],
|
|
projected_gravity=state["projected_gravity"],
|
|
estop_triggered=keyboard.is_estop_triggered(),
|
|
)
|
|
guard_decision = guard.check(
|
|
imu_gyro=state["imu_gyro"],
|
|
projected_gravity=state["projected_gravity"],
|
|
imu_age_ms=float(state["imu_age_ms"]),
|
|
estop_triggered=keyboard.is_estop_triggered(),
|
|
extra_nan_arrays=(target_hold,),
|
|
)
|
|
else:
|
|
target_hold = stand_balance.compute_target(state, np.zeros(3, dtype=np.float32)) if stand_balance.enabled else runner.default_dof_pos.copy()
|
|
release_metrics = compute_release_metrics(runner, state, target_hold, cmd)
|
|
if not runtime_released:
|
|
release_active_time += control_dt if runner.is_command_active(cmd) else 0.0
|
|
active_ready = release_active_time >= release_cfg["command_hold_s"]
|
|
posture_ready = release_metrics["max_hold_err"] <= release_cfg["posture_max_err"]
|
|
if active_ready and posture_ready:
|
|
runtime_released = True
|
|
logger.event(
|
|
"RUNTIME_COMMAND_RELEASED",
|
|
cmd=cmd.tolist(),
|
|
active_hold_s=release_active_time,
|
|
max_hold_err=release_metrics["max_hold_err"],
|
|
max_default_err=release_metrics["max_default_err"],
|
|
max_hold_default_gap=release_metrics["max_hold_default_gap"],
|
|
)
|
|
else:
|
|
reasons = []
|
|
if not active_ready:
|
|
reasons.append(f"cmd_hold<{release_cfg['command_hold_s']:.2f}s")
|
|
if not posture_ready:
|
|
reasons.append(f"hold_err>{release_cfg['posture_max_err']:.3f}")
|
|
logger.event(
|
|
"RUNTIME_RELEASE_BLOCKED",
|
|
reason=",".join(reasons),
|
|
cmd=cmd.tolist(),
|
|
active_hold_s=release_active_time,
|
|
max_hold_err=release_metrics["max_hold_err"],
|
|
max_default_err=release_metrics["max_default_err"],
|
|
max_hold_default_gap=release_metrics["max_hold_default_gap"],
|
|
)
|
|
raw = np.zeros(16, dtype=np.float32)
|
|
scaled = np.zeros(16, dtype=np.float32)
|
|
actual_target = io.hold_pose(target_hold, kp_scale=1.0)
|
|
policy_target = runner.default_dof_pos.copy()
|
|
target_metrics = compute_target_error_metrics(state, target_hold, policy_target)
|
|
safety_decision = SafetyMonitor().check(
|
|
target_pose=target_hold,
|
|
default_pose=runner.default_dof_pos,
|
|
imu_gyro=state["imu_gyro"],
|
|
projected_gravity=state["projected_gravity"],
|
|
estop_triggered=keyboard.is_estop_triggered(),
|
|
)
|
|
guard_decision = guard.check(
|
|
imu_gyro=state["imu_gyro"],
|
|
projected_gravity=state["projected_gravity"],
|
|
imu_age_ms=float(state["imu_age_ms"]),
|
|
estop_triggered=keyboard.is_estop_triggered(),
|
|
extra_nan_arrays=(target_hold,),
|
|
)
|
|
loop_dt_ms = (time.perf_counter() - loop_t0) * 1000.0
|
|
if log_every and (loop_count % log_every == 0):
|
|
motor_diag = state.get("motor_stale", {})
|
|
logger.state(
|
|
phase="RUNTIME",
|
|
joint_pos=state["joint_pos"],
|
|
joint_vel=state["joint_vel"],
|
|
joint_torque=state.get("joint_torque", np.zeros(16, dtype=np.float32)),
|
|
target_pose=actual_target,
|
|
raw_action=raw,
|
|
gyro=state["imu_gyro"],
|
|
accel=state["imu_accel"],
|
|
quat=state["quat_wxyz"],
|
|
proj_gravity=state["projected_gravity"],
|
|
command=cmd,
|
|
imu_age_ms=float(state["imu_age_ms"]),
|
|
loop_dt_ms=loop_dt_ms,
|
|
safety_level=int(safety_decision.level),
|
|
guard_level=int(guard_decision.level),
|
|
holdover=int(motor_diag.get("holdover_this_frame", 0)),
|
|
stale_max=int(motor_diag.get("stale_max", 0)),
|
|
fresh_count=int(motor_diag.get("fresh_count", 16)),
|
|
kp_scale=1.0,
|
|
nan_flag=0,
|
|
kp_leg_cmd=float(io.kp_leg),
|
|
kd_leg_cmd=float(io.kd_leg),
|
|
kd_wheel_cmd=float(io.kd_wheel),
|
|
runtime_release_alpha=0.0,
|
|
runtime_release_hold_s=release_active_time,
|
|
runtime_blend_ratio=0.0,
|
|
hold_target_max_err=target_metrics["hold_target_max_err"],
|
|
policy_target_max_err=target_metrics["policy_target_max_err"],
|
|
hold_policy_max_gap=target_metrics["hold_policy_max_gap"],
|
|
target_source="runtime_hold",
|
|
clip_primary_joint="",
|
|
safety_reason=f"release_blocked:{','.join(reasons)}",
|
|
guard_reason=guard_decision.reason,
|
|
)
|
|
next_exec += control_dt
|
|
next_exec = _sleep_to(next_exec)
|
|
loop_count += 1
|
|
continue
|
|
scaled, raw = runner.step(obs)
|
|
act_nan = bool(np.any(np.isnan(raw)) or np.any(np.isinf(raw)))
|
|
if act_nan:
|
|
logger.event("ACTION_NAN")
|
|
io.damping_brake()
|
|
break
|
|
|
|
policy_target = (scaled + runner.default_dof_pos).astype(np.float32)
|
|
tentative = blend_runtime_target(
|
|
runner,
|
|
target_hold,
|
|
policy_target,
|
|
float(getattr(runner, "_command_release_alpha", 0.0)),
|
|
release_cfg["target_blend_s"],
|
|
control_dt,
|
|
)
|
|
scaled = tentative - runner.default_dof_pos
|
|
target_metrics = compute_target_error_metrics(state, target_hold, policy_target)
|
|
runtime_blend_ratio = min(
|
|
1.0,
|
|
float(getattr(runner, "_command_release_alpha", 0.0))
|
|
* (runner.command_release_s / max(release_cfg["target_blend_s"], control_dt)),
|
|
)
|
|
projected_gravity = get_gravity_orientation(state["quat_wxyz"])
|
|
|
|
guard_decision = guard.check(
|
|
imu_gyro=state["imu_gyro"],
|
|
projected_gravity=projected_gravity,
|
|
imu_age_ms=float(state["imu_age_ms"]),
|
|
estop_triggered=keyboard.is_estop_triggered(),
|
|
extra_nan_arrays=(raw, tentative),
|
|
)
|
|
if guard_decision.level == GuardLevel.STOP:
|
|
logger.event("GUARD_STOP", phase="RUNTIME", reason=guard_decision.reason)
|
|
io.damping_brake()
|
|
break
|
|
|
|
safety_decision = safety.check(
|
|
target_pose=tentative,
|
|
default_pose=runner.default_dof_pos,
|
|
imu_gyro=state["imu_gyro"],
|
|
projected_gravity=projected_gravity,
|
|
estop_triggered=keyboard.is_estop_triggered(),
|
|
)
|
|
if safety_decision.level == SafetyLevel.ESTOP:
|
|
logger.event("SAFETY_ESTOP", reason=safety_decision.message)
|
|
io.damping_brake()
|
|
break
|
|
if safety_decision.level == SafetyLevel.BRAKE:
|
|
safety_diag = build_action_diag(
|
|
joint_pos=state["joint_pos"],
|
|
default_pose=runner.default_dof_pos,
|
|
raw=raw,
|
|
scaled=scaled,
|
|
tentative=tentative,
|
|
cmd=cmd,
|
|
zero_command=zero_command,
|
|
runtime_released=runtime_released,
|
|
release_alpha=float(getattr(runner, "_command_release_alpha", 0.0)),
|
|
safety_details=safety_decision.details,
|
|
)
|
|
logger.event(
|
|
"SAFETY_BRAKE",
|
|
reason=safety_decision.message,
|
|
details=safety_diag,
|
|
primary_joint=safety_diag.get("primary_joint_name"),
|
|
primary_offset=safety_diag.get("primary_leg_offset"),
|
|
primary_target=safety_diag.get("primary_target"),
|
|
primary_measured=safety_diag.get("primary_measured"),
|
|
primary_raw=safety_diag.get("primary_raw"),
|
|
primary_scaled=safety_diag.get("primary_scaled"),
|
|
cmd=cmd.tolist(),
|
|
release_alpha=float(getattr(runner, "_command_release_alpha", 0.0)),
|
|
)
|
|
io.damping_brake()
|
|
break
|
|
if safety_decision.level == SafetyLevel.CLIP and safety_decision.clipped_target is not None:
|
|
scaled = safety_decision.clipped_target - runner.default_dof_pos
|
|
safety_diag = build_action_diag(
|
|
joint_pos=state["joint_pos"],
|
|
default_pose=runner.default_dof_pos,
|
|
raw=raw,
|
|
scaled=scaled,
|
|
tentative=tentative,
|
|
cmd=cmd,
|
|
zero_command=zero_command,
|
|
runtime_released=runtime_released,
|
|
release_alpha=float(getattr(runner, "_command_release_alpha", 0.0)),
|
|
safety_details=safety_decision.details,
|
|
)
|
|
logger.event(
|
|
"SAFETY_CLIP",
|
|
reason=safety_decision.message,
|
|
details=safety_diag,
|
|
primary_joint=safety_diag.get("primary_joint_name"),
|
|
primary_offset=safety_diag.get("primary_leg_offset"),
|
|
primary_target=safety_diag.get("primary_target"),
|
|
primary_measured=safety_diag.get("primary_measured"),
|
|
primary_raw=safety_diag.get("primary_raw"),
|
|
primary_scaled=safety_diag.get("primary_scaled"),
|
|
max_raw=float(np.max(np.abs(raw))),
|
|
cmd=cmd.tolist(),
|
|
release_alpha=float(getattr(runner, "_command_release_alpha", 0.0)),
|
|
)
|
|
|
|
actual_target = io.send_actions(scaled, runner.default_dof_pos)
|
|
loop_dt_ms = (time.perf_counter() - loop_t0) * 1000.0
|
|
|
|
if log_every and (loop_count % log_every == 0):
|
|
motor_diag = state.get("motor_stale", {})
|
|
logger.state(
|
|
phase="RUNTIME",
|
|
joint_pos=state["joint_pos"],
|
|
joint_vel=state["joint_vel"],
|
|
joint_torque=state.get("joint_torque", np.zeros(16, dtype=np.float32)),
|
|
target_pose=actual_target,
|
|
raw_action=raw,
|
|
gyro=state["imu_gyro"],
|
|
accel=state["imu_accel"],
|
|
quat=state["quat_wxyz"],
|
|
proj_gravity=projected_gravity,
|
|
command=cmd,
|
|
imu_age_ms=float(state["imu_age_ms"]),
|
|
loop_dt_ms=loop_dt_ms,
|
|
safety_level=int(safety_decision.level),
|
|
guard_level=int(guard_decision.level),
|
|
holdover=int(motor_diag.get("holdover_this_frame", 0)),
|
|
stale_max=int(motor_diag.get("stale_max", 0)),
|
|
fresh_count=int(motor_diag.get("fresh_count", 16)),
|
|
kp_scale=1.0,
|
|
nan_flag=int(obs_nan or act_nan),
|
|
kp_leg_cmd=float(io.kp_leg),
|
|
kd_leg_cmd=float(io.kd_leg),
|
|
kd_wheel_cmd=float(io.kd_wheel),
|
|
runtime_release_alpha=float(getattr(runner, "_command_release_alpha", 0.0)),
|
|
runtime_release_hold_s=release_active_time,
|
|
runtime_blend_ratio=runtime_blend_ratio,
|
|
hold_target_max_err=target_metrics["hold_target_max_err"],
|
|
policy_target_max_err=target_metrics["policy_target_max_err"],
|
|
hold_policy_max_gap=target_metrics["hold_policy_max_gap"],
|
|
target_source="runtime_blend" if runtime_blend_ratio < 0.999 else "runtime_policy",
|
|
clip_primary_joint=str((safety_decision.details or {}).get("primary_joint_name", "")),
|
|
clip_primary_target=float((safety_decision.details or {}).get("primary_target", 0.0) or 0.0),
|
|
clip_primary_measured=float((safety_decision.details or {}).get("primary_measured", 0.0) or 0.0),
|
|
clip_primary_default=float((safety_decision.details or {}).get("primary_default", 0.0) or 0.0),
|
|
clip_primary_pos_err=float((safety_decision.details or {}).get("primary_pos_err", 0.0) or 0.0),
|
|
clip_primary_raw=float((safety_decision.details or {}).get("primary_raw", 0.0) or 0.0),
|
|
clip_primary_scaled=float((safety_decision.details or {}).get("primary_scaled", 0.0) or 0.0),
|
|
safety_reason=(
|
|
f"{safety_decision.message};zero_cmd={int(zero_command)};"
|
|
f"released={int(runtime_released)};alpha={getattr(runner, '_command_release_alpha', 0.0):.2f};"
|
|
f"max_raw={float(np.max(np.abs(raw))):.2f};"
|
|
f"clip={((safety_decision.details or {}).get('joint_indices', []))}"
|
|
),
|
|
guard_reason=guard_decision.reason,
|
|
)
|
|
|
|
next_exec += control_dt
|
|
slack = next_exec - time.perf_counter()
|
|
if slack > 0:
|
|
coarse = slack - 0.002
|
|
if coarse > 0:
|
|
time.sleep(coarse)
|
|
while time.perf_counter() < next_exec:
|
|
pass
|
|
elif slack < -control_dt:
|
|
logger.event("LOOP_OVERRUN", over_ms=-slack * 1000.0)
|
|
next_exec = time.perf_counter()
|
|
|
|
recent_dt_ms.append(loop_dt_ms)
|
|
if len(recent_dt_ms) > 50:
|
|
recent_dt_ms.pop(0)
|
|
if len(recent_dt_ms) == 50:
|
|
median_dt = float(np.median(recent_dt_ms))
|
|
if median_dt > 22.0:
|
|
logger.event("SLOW_LOOP_TREND", median_dt_ms=median_dt)
|
|
recent_dt_ms.clear()
|
|
|
|
loop_count += 1
|
|
if time.perf_counter() - last_print > 1.0:
|
|
print(
|
|
f"[Loop] cmd=[{cmd[0]:+.2f},{cmd[1]:+.2f},{cmd[2]:+.2f}] "
|
|
f"|raw|={float(np.max(np.abs(raw))):.2f} "
|
|
f"zero={int(zero_command)} rel={int(runtime_released)} "
|
|
f"alpha={getattr(runner, '_command_release_alpha', 0.0):.2f} "
|
|
f"imu_age={state['imu_age_ms']:.1f}ms "
|
|
f"holdover={io.hw.holdover_total} "
|
|
f"safety={int(safety_decision.level)}"
|
|
)
|
|
last_print = time.perf_counter()
|
|
|
|
except PoseInitFailed as exc:
|
|
print(f"[Main] startup aborted: {exc}")
|
|
logger.event("POSE_INIT_FAILED", error=str(exc))
|
|
except KeyboardInterrupt:
|
|
print("\n[Main] Ctrl+C received, stopping...")
|
|
logger.event("KEYBOARD_INTERRUPT")
|
|
except Exception as exc:
|
|
import traceback
|
|
|
|
print(f"\n[Main] exception: {exc}")
|
|
traceback.print_exc()
|
|
logger.event("UNEXPECTED_ERROR", error=str(exc), traceback=traceback.format_exc())
|
|
finally:
|
|
print("[Main] cleaning up...")
|
|
try:
|
|
io.damping_brake()
|
|
time.sleep(0.05)
|
|
logger.event("DAMPING_BRAKE_APPLIED")
|
|
except Exception as exc:
|
|
logger.event("DAMPING_BRAKE_FAILED", error=str(exc))
|
|
try:
|
|
io.disconnect()
|
|
logger.event("HARDWARE_DISCONNECTED")
|
|
finally:
|
|
keyboard.stop()
|
|
logger.close()
|
|
os._exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|