|
1 | | -"""Abstract robot client: policy predict + platform-specific hardware hooks.""" |
| 1 | +"""Base control layer for robot platforms (hardware or sim adapters).""" |
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -from abc import ABC, abstractmethod |
| 5 | +import importlib |
| 6 | +import os |
| 7 | +from types import ModuleType |
6 | 8 | from typing import Any |
7 | 9 |
|
8 | | -from model_client import ModelClient, ModelResponse, image_to_rgb_hwc_u8_bytes, state_to_list |
| 10 | +import numpy as np |
9 | 11 |
|
10 | 12 |
|
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 | +} |
13 | 17 |
|
14 | | - def __init__(self, policy: ModelClient): |
15 | | - self._policy = policy |
16 | 18 |
|
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 | + |
46 | 22 | def connect(self) -> None: |
47 | | - """Connect robot hardware and capture platform-specific state.""" |
| 23 | + """Open platform resources (serial, cameras, sim env, etc.).""" |
48 | 24 |
|
49 | | - @abstractmethod |
50 | 25 | def disconnect(self) -> None: |
51 | | - """Disconnect robot hardware.""" |
| 26 | + """Release platform resources.""" |
52 | 27 |
|
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 |
56 | 31 |
|
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()") |
60 | 51 |
|
61 | | - @abstractmethod |
62 | 52 | 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.""" |
64 | 58 |
|
65 | 59 | @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 |
69 | 62 |
|
70 | 63 | @property |
71 | 64 | 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" |
74 | 67 |
|
75 | 68 | @property |
76 | | - @abstractmethod |
77 | 69 | 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) |
0 commit comments