PingPongRealWorldRL / runtime /planner_protocol.py
KurisiTOMA's picture
chore: publish H2 real-world RL system
d8290eb verified
Raw History Blame Contribute Delete
4.23 kB
"""Versioned H2 planner packet tail shared by predictor and controller."""
from __future__ import annotations
import math
import numpy as np
LEGACY_FIELDS = 20
H2_PLANNER_PROTOCOL_VERSION = 3.0
H2_PLANNER_PACKET_LEN = 27
PROTOCOL_VERSION_INDEX = 20
ROBOT_SOURCE_MONOTONIC_INDEX = 21
BALL_SOURCE_MONOTONIC_INDEX = 22
ROBOT_BASE_ORIGIN_START_INDEX = 23
FRAME_CONVENTION_INDEX = 26
FRAME_CONVENTION_WORLD_X = 1.0
def source_age_s(now_s: float, source_stamp_s: float) -> float:
"""Return a fail-closed same-host monotonic source age."""
now = float(now_s)
stamp = float(source_stamp_s)
if not math.isfinite(now) or not math.isfinite(stamp) or stamp <= 0.0 or stamp > now + 0.01:
return math.inf
return max(0.0, now - stamp)
def build_h2_planner_packet(
*,
robot_pos_world,
robot_quat_xyzw,
time_to_hit_s: float,
hit_pos_robot,
racket_velocity_robot,
command_id: int,
incoming_velocity_robot,
landing_xy_robot,
robot_source_monotonic_s: float,
ball_source_monotonic_s: float,
robot_base_origin_world,
allow_missing_sources: bool = False,
) -> np.ndarray:
"""Build the exact 27-value H2 planner-v3 wire payload.
Both the ROS predictor and the isolated MuJoCo bridge call this function,
preventing a simulation-only packet layout from drifting away from the
real deployment producer.
"""
robot_pos = np.asarray(robot_pos_world, dtype=np.float64).reshape(3)
robot_quat = np.asarray(robot_quat_xyzw, dtype=np.float64).reshape(4)
hit_pos = np.asarray(hit_pos_robot, dtype=np.float64).reshape(3)
racket_velocity = np.asarray(racket_velocity_robot, dtype=np.float64).reshape(3)
incoming_velocity = np.asarray(incoming_velocity_robot, dtype=np.float64).reshape(3)
landing_xy = np.asarray(landing_xy_robot, dtype=np.float64).reshape(2)
origin = np.asarray(robot_base_origin_world, dtype=np.float64).reshape(3)
scalar_values = np.asarray(
[time_to_hit_s, robot_source_monotonic_s, ball_source_monotonic_s],
dtype=np.float64,
)
all_values = np.concatenate(
[robot_pos, robot_quat, scalar_values, hit_pos, racket_velocity,
incoming_velocity, landing_xy, origin]
)
if not np.isfinite(all_values).all():
raise ValueError("H2 planner packet inputs must all be finite")
if isinstance(command_id, bool) or int(command_id) != command_id or int(command_id) < 0:
raise ValueError("H2 planner command_id must be a non-negative integer")
quat_norm = float(np.linalg.norm(robot_quat))
if not 0.5 <= quat_norm <= 1.5:
raise ValueError(f"robot quaternion norm is invalid ({quat_norm:.3f})")
if not allow_missing_sources and (robot_source_monotonic_s <= 0.0 or ball_source_monotonic_s <= 0.0):
raise ValueError("H2 planner source timestamps must be positive monotonic seconds")
if allow_missing_sources and (
robot_source_monotonic_s < 0.0 or ball_source_monotonic_s < 0.0
or (ball_source_monotonic_s == 0.0 and command_id != 0)
):
raise ValueError("simulation missing-source packets use zero stamps and no strike before a ball")
packet = np.empty(H2_PLANNER_PACKET_LEN, dtype=np.float64)
packet[0:3] = robot_pos
packet[3:7] = robot_quat
packet[7] = float(time_to_hit_s)
packet[8:11] = hit_pos
packet[11:14] = racket_velocity
packet[14] = float(command_id)
packet[15:18] = incoming_velocity
packet[18:20] = landing_xy
packet[PROTOCOL_VERSION_INDEX] = H2_PLANNER_PROTOCOL_VERSION
packet[ROBOT_SOURCE_MONOTONIC_INDEX] = float(robot_source_monotonic_s)
packet[BALL_SOURCE_MONOTONIC_INDEX] = float(ball_source_monotonic_s)
packet[ROBOT_BASE_ORIGIN_START_INDEX : ROBOT_BASE_ORIGIN_START_INDEX + 3] = origin
packet[FRAME_CONVENTION_INDEX] = FRAME_CONVENTION_WORLD_X
return packet
__all__ = [
"BALL_SOURCE_MONOTONIC_INDEX",
"FRAME_CONVENTION_INDEX",
"FRAME_CONVENTION_WORLD_X",
"H2_PLANNER_PACKET_LEN",
"H2_PLANNER_PROTOCOL_VERSION",
"LEGACY_FIELDS",
"PROTOCOL_VERSION_INDEX",
"ROBOT_SOURCE_MONOTONIC_INDEX",
"ROBOT_BASE_ORIGIN_START_INDEX",
"build_h2_planner_packet",
"source_age_s",
]