Skip to content

Commit 527f451

Browse files
author
hlink
committed
moonspace release v1
1 parent c616fc9 commit 527f451

2 files changed

Lines changed: 400 additions & 144 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
"""Run Panthera single-arm real-robot inference through ModelClient.
3+
4+
The TCP model server side should expose an RPC method such as ``infer(obs)``
5+
that returns either a single action [14] or an action chunk [T, 14]. For this
6+
single-arm robot, the first 7 dims are used as left_arm joint(6)+gripper(1),
7+
and the remaining right-arm dims are ignored.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import argparse
13+
from pathlib import Path
14+
import sys
15+
import time
16+
from typing import Any
17+
18+
import numpy as np
19+
20+
THIS_FILE = Path(__file__).resolve()
21+
DEPLOY_DIR = THIS_FILE.parent
22+
REPO_ROOT = DEPLOY_DIR.parent.parent
23+
24+
for path in (DEPLOY_DIR, REPO_ROOT, REPO_ROOT / "src"):
25+
path_str = str(path)
26+
if path_str not in sys.path:
27+
sys.path.insert(0, path_str)
28+
29+
from robot_client_server.model_client import ModelClient
30+
from my_robot.panthera_single import PantheraSingle
31+
32+
33+
ACT_STATE_DIM = 14
34+
LEFT_ACTION_DIM = 7
35+
36+
37+
def _color_to_chw(color: Any) -> np.ndarray:
38+
image = np.asarray(color)
39+
if image.ndim != 3 or image.shape[2] != 3:
40+
raise ValueError(f"expected HWC RGB/BGR image, got shape={image.shape}")
41+
return np.transpose(image.astype(np.uint8, copy=False), (2, 0, 1))
42+
43+
44+
def _build_state(controller_data: dict[str, Any]) -> np.ndarray:
45+
left = controller_data["left_arm"]
46+
joint = np.asarray(left["joint"], dtype=np.float32).reshape(-1)
47+
if joint.shape != (6,):
48+
raise ValueError(f"left_arm.joint must be shape (6,), got {joint.shape}")
49+
gripper = np.asarray(left["gripper"], dtype=np.float32).reshape(1)
50+
state = np.zeros((ACT_STATE_DIM,), dtype=np.float32)
51+
state[:6] = joint
52+
state[6] = gripper[0]
53+
return state
54+
55+
56+
def build_observation(raw_data: Any, prompt: str) -> dict[str, Any]:
57+
controller_data, sensor_data = raw_data
58+
state = _build_state(controller_data)
59+
cam_high = _color_to_chw(sensor_data["cam_head"]["color"])
60+
cam_wrist = _color_to_chw(sensor_data["cam_wrist"]["color"])
61+
62+
# Keep cam_wrist for single-arm checkpoints. Also provide the dual-arm keys as
63+
# aliases so older OpenPI/ALOHA wrappers that still look for wrist-left/right
64+
# can consume the same observation without changing this robot client.
65+
return {
66+
"state": state,
67+
"images": {
68+
"cam_high": cam_high,
69+
"cam_wrist": cam_wrist,
70+
"cam_right_wrist": cam_wrist,
71+
"cam_left_wrist": np.zeros_like(cam_wrist),
72+
},
73+
"prompt": prompt,
74+
}
75+
76+
77+
def _first_action(action_result: Any) -> np.ndarray:
78+
actions = np.asarray(action_result, dtype=np.float32)
79+
if actions.ndim == 0:
80+
raise ValueError("model returned scalar action")
81+
if actions.ndim == 1:
82+
action = actions
83+
else:
84+
action = actions[0]
85+
if action.shape[0] < LEFT_ACTION_DIM:
86+
raise ValueError(f"action must have at least {LEFT_ACTION_DIM} dims, got {action.shape}")
87+
return action
88+
89+
90+
def action_to_move_data(action: np.ndarray, *, gripper_min: float, gripper_max: float) -> dict[str, Any]:
91+
left = np.asarray(action[:LEFT_ACTION_DIM], dtype=np.float32).reshape(-1)
92+
joint = left[:6]
93+
gripper = float(np.clip(left[6], gripper_min, gripper_max))
94+
return {
95+
"arm": {
96+
"left_arm": {
97+
"joint": joint,
98+
"gripper": gripper,
99+
}
100+
}
101+
}
102+
103+
104+
def maybe_call(client: ModelClient, func_name: str) -> None:
105+
try:
106+
client.call(func_name=func_name)
107+
except Exception as exc:
108+
print(f"[panthera-client] ignore {func_name!r} RPC failure: {exc}")
109+
110+
111+
def run(args: argparse.Namespace) -> None:
112+
robot = PantheraSingle(enable_cameras=not args.no_cameras)
113+
client = ModelClient(host=args.host, port=args.port, timeout=args.timeout)
114+
period = 1.0 / args.hz
115+
116+
try:
117+
robot.set_up()
118+
if not args.no_reset:
119+
robot.reset()
120+
maybe_call(client, args.reset_rpc)
121+
122+
print("[panthera-client] ready. Press ENTER to start inference, Ctrl+C to stop.")
123+
input()
124+
print("[panthera-client] running...")
125+
126+
step = 0
127+
while args.max_steps <= 0 or step < args.max_steps:
128+
loop_start = time.monotonic()
129+
raw = robot.get()
130+
obs = build_observation(raw, args.prompt)
131+
action_result = client.call(func_name=args.infer_rpc, obs=obs)
132+
action = _first_action(action_result)
133+
move_data = action_to_move_data(
134+
action,
135+
gripper_min=args.gripper_min,
136+
gripper_max=args.gripper_max,
137+
)
138+
139+
if args.dry_run:
140+
print(f"[panthera-client] step={step} action={np.array2string(action[:7], precision=4)}")
141+
else:
142+
robot.move(move_data)
143+
144+
step += 1
145+
if args.log_every > 0 and step % args.log_every == 0:
146+
print(f"[panthera-client] step={step}")
147+
148+
elapsed = time.monotonic() - loop_start
149+
sleep_time = period - elapsed
150+
if sleep_time > 0:
151+
time.sleep(sleep_time)
152+
153+
except KeyboardInterrupt:
154+
print("\n[panthera-client] interrupted by user")
155+
finally:
156+
try:
157+
robot.shutdown(damped=args.brake_on_exit)
158+
finally:
159+
client.close()
160+
161+
162+
def parse_args() -> argparse.Namespace:
163+
parser = argparse.ArgumentParser(description="Panthera single-arm model-client deployment loop.")
164+
parser.add_argument("--host", default="localhost", help="model server host")
165+
parser.add_argument("--port", type=int, default=9999, help="model server TCP port")
166+
parser.add_argument("--timeout", type=float, default=30.0, help="socket timeout seconds")
167+
parser.add_argument("--infer-rpc", default="infer", help="model server inference method name")
168+
parser.add_argument("--reset-rpc", default="reset_episode", help="model server episode reset method name")
169+
parser.add_argument("--prompt", default="", help="language prompt sent with each observation")
170+
parser.add_argument("--hz", type=float, default=10.0, help="control loop frequency")
171+
parser.add_argument("--max-steps", type=int, default=0, help="0 means run until Ctrl+C")
172+
parser.add_argument("--log-every", type=int, default=10, help="print progress every N steps; <=0 disables")
173+
parser.add_argument("--gripper-min", type=float, default=0.0, help="minimum normalized gripper command")
174+
parser.add_argument("--gripper-max", type=float, default=1.0, help="maximum normalized gripper command")
175+
parser.add_argument("--dry-run", action="store_true", help="run inference but do not move the robot")
176+
parser.add_argument("--no-reset", action="store_true", help="do not reset robot before starting")
177+
parser.add_argument("--no-cameras", action="store_true", help="initialize robot without cameras")
178+
parser.add_argument("--brake-on-exit", action="store_true", help="brake instead of stop on exit")
179+
return parser.parse_args()
180+
181+
182+
if __name__ == "__main__":
183+
run(parse_args())

0 commit comments

Comments
 (0)