Skip to content

Commit 936ec03

Browse files
committed
rewrite base policy/platform
1 parent 4cecaf1 commit 936ec03

11 files changed

Lines changed: 428 additions & 417 deletions

File tree

eval/base_platform.py

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,86 @@
1-
"""Abstract robot client: policy predict + platform-specific hardware hooks."""
1+
"""Base control layer for robot platforms (hardware or sim adapters)."""
22

33
from __future__ import annotations
44

5-
from abc import ABC, abstractmethod
5+
import importlib
6+
import os
7+
from types import ModuleType
68
from typing import Any
79

8-
from model_client import ModelClient, ModelResponse, image_to_rgb_hwc_u8_bytes, state_to_list
10+
import numpy as np
911

1012

11-
class BasePolicy(ABC):
12-
"""Base class for robot policy clients."""
13+
PLATFORM_MODULES = {
14+
"so101": "eval.lerobot_so101.so101_client",
15+
"lerobot_so101": "eval.lerobot_so101.so101_client",
16+
}
1317

14-
def __init__(self, policy: ModelClient):
15-
self._policy = policy
1618

17-
@property
18-
def policy(self) -> ModelClient:
19-
return self._policy
20-
21-
def predict(self, image: Any, state: Any, prompt: str, *, image_name: str = "camera1") -> ModelResponse:
22-
"""Synchronous policy inference."""
23-
rgb, width, height, stride = image_to_rgb_hwc_u8_bytes(image)
24-
observation = {
25-
"images": [
26-
{
27-
"name": image_name,
28-
"rgb_hwc_u8": rgb,
29-
"width": width,
30-
"height": height,
31-
"stride_bytes": stride,
32-
}
33-
],
34-
"state": state_to_list(state),
35-
"prompt": prompt,
36-
}
37-
return self._policy.predict(observation)
38-
39-
def health(self) -> str:
40-
return self._policy.health()
41-
42-
def reset_policy(self) -> str:
43-
return self._policy.reset()
44-
45-
@abstractmethod
19+
class BasePlatform:
20+
"""Platform-side observe / act hooks. Override only what the deployment uses."""
21+
4622
def connect(self) -> None:
47-
"""Connect robot hardware and capture platform-specific state."""
23+
"""Open platform resources (serial, cameras, sim env, etc.)."""
4824

49-
@abstractmethod
5025
def disconnect(self) -> None:
51-
"""Disconnect robot hardware."""
26+
"""Release platform resources."""
5227

53-
@abstractmethod
54-
def get_observation(self) -> dict[str, Any]:
55-
"""Return the latest robot observation dict."""
28+
def __enter__(self) -> BasePlatform:
29+
self.connect()
30+
return self
5631

57-
@abstractmethod
58-
def send_action(self, action: dict[str, float]) -> None:
59-
"""Send one control step to the robot."""
32+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
33+
self.disconnect()
34+
35+
def get_observation(self) -> dict[str, Any]:
36+
raise NotImplementedError(f"{type(self).__name__} must implement get_observation()")
37+
38+
def send_action(self, action: Any) -> None:
39+
if isinstance(action, dict):
40+
payload = action
41+
else:
42+
keys = self.action_keys
43+
if not keys:
44+
raise RuntimeError("platform.action_keys is empty; connect the robot first")
45+
values = np.asarray(action, dtype=np.float32).reshape(-1)
46+
payload = {key: float(values[index]) for index, key in enumerate(keys)}
47+
self._send_action(payload)
48+
49+
def _send_action(self, action: dict[str, float]) -> None:
50+
raise NotImplementedError(f"{type(self).__name__} must implement _send_action()")
6051

61-
@abstractmethod
6252
def reset_home(self) -> None:
63-
"""Move robot back to the platform home pose and reset policy state."""
53+
"""Optional platform homing motion."""
54+
self.on_reset_home()
55+
56+
def on_reset_home(self) -> None:
57+
"""Override for platform-specific homing."""
6458

6559
@property
66-
@abstractmethod
67-
def camera_key(self) -> str:
68-
"""Primary camera key inside ``get_observation()``."""
60+
def camera_key(self) -> str | None:
61+
return None
6962

7063
@property
7164
def model_image_name(self) -> str:
72-
"""Image key sent to the model server (defaults to ``camera_key``)."""
73-
return self.camera_key
65+
key = self.camera_key
66+
return key if key is not None else "image"
7467

7568
@property
76-
@abstractmethod
7769
def action_keys(self) -> list[str]:
78-
"""Ordered action feature keys for this robot."""
70+
return []
71+
72+
73+
def load_platform_module(name: str | None = None) -> ModuleType:
74+
platform_name = name or os.environ.get("ROBOT_PLATFORM") or os.environ.get("PLATFORM") or "so101"
75+
module_name = PLATFORM_MODULES.get(platform_name)
76+
if module_name is None:
77+
supported = ", ".join(sorted(PLATFORM_MODULES))
78+
raise SystemExit(f"Unknown platform {platform_name!r}. Supported: {supported}")
79+
return importlib.import_module(module_name)
80+
81+
82+
def create_platform(cfg: Any = None) -> BasePlatform:
83+
module = load_platform_module()
84+
if cfg is None:
85+
cfg = module.config_from_env()
86+
return module.create_platform(cfg)

eval/lerobot_so101/run_sync.py

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,24 @@
33

44
from __future__ import annotations
55

6-
import importlib
76
import logging
87
import os
98
import sys
109
from pathlib import Path
11-
from types import ModuleType
1210

13-
_BASE_POLICY_DIR = Path(__file__).resolve().parent
14-
_ROBOT_CLIENT_DIR = _BASE_POLICY_DIR.parent
15-
_VLA_CPP_ROOT = _ROBOT_CLIENT_DIR.parent
16-
_MODEL_CLIENT_PYTHON = _ROBOT_CLIENT_DIR / "python"
17-
_LEROBOT_SO101 = _VLA_CPP_ROOT / "eval" / "lerobot_so101"
11+
_ROBOT_CPP_ROOT = Path(__file__).resolve().parents[2]
12+
_LEROBOT_SO101 = Path(__file__).resolve().parent
13+
_MODEL_CLIENT_PYTHON = _ROBOT_CPP_ROOT / "robot_client" / "python"
1814

19-
if str(_ROBOT_CLIENT_DIR) not in sys.path:
20-
sys.path.insert(0, str(_ROBOT_CLIENT_DIR))
21-
if str(_MODEL_CLIENT_PYTHON) not in sys.path:
22-
sys.path.insert(0, str(_MODEL_CLIENT_PYTHON))
23-
if str(_LEROBOT_SO101) not in sys.path:
24-
sys.path.insert(0, str(_LEROBOT_SO101))
15+
for path in (_ROBOT_CPP_ROOT, _LEROBOT_SO101, _MODEL_CLIENT_PYTHON):
16+
text = str(path)
17+
if text not in sys.path:
18+
sys.path.insert(0, text)
2519

2620
from model_client import ModelClient
27-
from base_policy.sync_loop import SyncControlLoop, SyncLoopConfig
28-
29-
DEFAULT_PLATFORM = "lerobot_so101"
30-
PLATFORM_MODULES = {
31-
"lerobot_so101": "so101_client",
32-
}
21+
from eval.base_platform import create_platform
22+
from robot_client.policy.base_policy import RobotPolicy
23+
from robot_client.policy.sync_loop import SyncControlLoop, SyncLoopConfig
3324

3425

3526
def _parse_host_port(target: str, default_port: int = 5555) -> tuple[str, int]:
@@ -47,25 +38,29 @@ def server_from_env(default_host: str = "127.0.0.1", default_port: int = 5555) -
4738
return host, port, server_timeout
4839

4940

50-
def load_platform_module(name: str | None = None) -> ModuleType:
51-
platform = name or os.environ.get("ROBOT_PLATFORM", DEFAULT_PLATFORM)
52-
module_name = PLATFORM_MODULES.get(platform)
53-
if module_name is None:
54-
supported = ", ".join(sorted(PLATFORM_MODULES))
55-
raise SystemExit(f"Unknown ROBOT_PLATFORM={platform!r}. Supported: {supported}")
56-
return importlib.import_module(module_name)
57-
58-
5941
def main() -> int:
6042
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
6143

62-
platform = load_platform_module()
6344
host, port, timeout = server_from_env()
64-
policy = ModelClient(host=host, port=port, timeout=timeout)
65-
cfg = platform.config_from_env()
66-
robot = platform.create_robot_client(policy, cfg)
45+
client = ModelClient(host=host, port=port, timeout=timeout)
46+
policy = RobotPolicy(client)
47+
48+
from eval.lerobot_so101.so101_client import config_from_env
49+
50+
cfg = config_from_env()
51+
platform = create_platform(cfg)
52+
53+
logging.info(
54+
"platform=SO101 server=%s:%s camera_key=%s model_image_name=%s",
55+
host,
56+
port,
57+
platform.camera_key,
58+
platform.model_image_name,
59+
)
60+
6761
SyncControlLoop(
68-
robot,
62+
platform,
63+
policy,
6964
SyncLoopConfig(task=cfg.task, fps=cfg.fps, loops=cfg.loops),
7065
).run()
7166
return 0

eval/lerobot_so101/shell/run_robot_client.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ echo "[robot_sync] platform=${ROBOT_PLATFORM} server=${SERVER}"
77
echo "[robot_sync] robot_port=${ROBOT_PORT} camera_key=${CAMERA_KEY} fps=${FPS}"
88
echo "[robot_sync] Start server first: bash robot_server/shell/launch_robot_server_mac_cpu.sh"
99

10-
run_python -m base_policy
10+
run_python "${ROBOT_CPP_ROOT}/eval/lerobot_so101/run_sync.py"

eval/lerobot_so101/shell/so101_env.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
# Edit the values below for your machine (serial ports, camera index, dataset hub id, etc.).
44

55
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6-
VLA_CPP_ROOT="$(cd "${ROOT}/../.." && pwd)"
6+
ROBOT_CPP_ROOT="$(cd "${ROOT}/../.." && pwd)"
77

88
export CONDA_ENV="${CONDA_ENV:-lerobot-demo}"
99

10-
export PYTHONPATH="${ROOT}:${ROOT}/lerobot_camera_opencv_crop:${VLA_CPP_ROOT}/robot_client/python:${VLA_CPP_ROOT}/robot_client"
10+
export PYTHONPATH="${ROOT}:${ROOT}/lerobot_camera_opencv_crop:${ROBOT_CPP_ROOT}/robot_client/python:${ROBOT_CPP_ROOT}/robot_client"
1111

1212
# --- Robot serial ports ---
1313
export ROBOT_PORT="/dev/tty.usbmodem5B3E1195731"

eval/lerobot_so101/so101_client.py

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""LeRobot SO101 implementation of ``BasePolicy``."""
1+
"""LeRobot SO101 platform adapter."""
22

33
from __future__ import annotations
44

@@ -10,8 +10,7 @@
1010
from lerobot.robots.so_follower.config_so_follower import SOFollowerRobotConfig
1111
from lerobot.robots.so_follower.so_follower import SOFollower
1212

13-
from model_client import ModelClient
14-
from base_policy.base import BasePolicy
13+
from eval.base_platform import BasePlatform
1514
from utils.robot import build_camera_config, extract_home_action
1615

1716
DEFAULT_FPS = 25
@@ -55,24 +54,14 @@ def config_from_env() -> SO101ClientConfig:
5554
)
5655

5756

58-
class SO101RobotClient(BasePolicy):
59-
60-
def __init__(self, policy: ModelClient, cfg: SO101ClientConfig | None = None):
61-
super().__init__(policy)
57+
class SO101Platform(BasePlatform):
58+
def __init__(self, cfg: SO101ClientConfig | None = None):
6259
self.cfg = cfg or config_from_env()
6360
self._robot: SOFollower | None = None
6461
self._home_action: dict[str, float] = {}
6562
self._dt = 1.0 / max(1, self.cfg.fps)
6663

6764
def connect(self) -> None:
68-
health = self._policy.health()
69-
logging.info(
70-
"Connected to robot server at %s:%s (%s)",
71-
self._policy.host,
72-
self._policy.port,
73-
health,
74-
)
75-
7665
cams = build_camera_config(self.cfg.robot_cameras)
7766
robot_cfg = SOFollowerRobotConfig(
7867
port=self.cfg.robot_port,
@@ -101,17 +90,16 @@ def get_observation(self) -> dict:
10190
raise RuntimeError("SO101 robot is not connected")
10291
return self._robot.get_observation()
10392

104-
def send_action(self, action: dict[str, float]) -> None:
93+
def _send_action(self, action: dict[str, float]) -> None:
10594
if self._robot is None:
10695
raise RuntimeError("SO101 robot is not connected")
10796
self._robot.send_action(action)
10897

109-
def reset_home(self) -> None:
98+
def on_reset_home(self) -> None:
11099
if self._robot is None:
111100
raise RuntimeError("SO101 robot is not connected")
112-
self.reset_policy()
113101
for _ in range(max(6, int(self.cfg.fps * 0.8))):
114-
self._robot.send_action(dict(self._home_action))
102+
self._send_action(dict(self._home_action))
115103
time.sleep(max(0.01, self._dt))
116104

117105
@property
@@ -128,5 +116,6 @@ def action_keys(self) -> list[str]:
128116
return []
129117
return list(self._robot.action_features.keys())
130118

131-
def create_robot_client(policy: ModelClient, cfg: SO101ClientConfig | None = None) -> SO101RobotClient:
132-
return SO101RobotClient(policy, cfg)
119+
120+
def create_platform(cfg: SO101ClientConfig | None = None) -> SO101Platform:
121+
return SO101Platform(cfg or config_from_env())

0 commit comments

Comments
 (0)