| """Server for serving a policy over websockets. |
| |
| Adapted from https://github.com/robo-arena/roboarena/ |
| |
| """ |
|
|
|
|
| import asyncio |
| import dataclasses |
| import logging |
| import traceback |
|
|
| from openpi_client.base_policy import BasePolicy |
| from openpi_client import msgpack_numpy |
| import websockets.asyncio.server |
| import websockets.frames |
|
|
|
|
| @dataclasses.dataclass |
| class PolicyServerConfig: |
| |
| |
| image_resolution: tuple[int, int] | None = (224, 224) |
| |
| needs_wrist_camera: bool = True |
| |
| n_external_cameras: int = 1 |
| |
| needs_stereo_camera: bool = False |
| |
| needs_session_id: bool = False |
| |
| action_space: str = "joint_position" |
|
|
|
|
| class WebsocketPolicyServer: |
| """ |
| Serves a policy using the websocket protocol. |
| |
| Interface: |
| Observation: |
| - observation/wrist_image_left: (H, W, 3) if needs_wrist_camera is True |
| - observation/wrist_image_right: (H, W, 3) if needs_wrist_camera is True and needs_stereo_camera is True |
| - observation/exterior_image_{i}_left: (H, W, 3) if n_external_cameras >= 1 |
| - observation/exterior_image_{i}_right: (H, W, 3) if needs_stereo_camera is True |
| - session_id: (1,) if needs_session_id is True |
| - observation/joint_position: (7,) |
| - observation/cartesian_position: (6,) |
| - observation/gripper_position: (1,) |
| - prompt: str, the natural language task instruction for the policy |
| |
| Action: |
| - action: (N, 8,) or (N, 7,): either 7 movement actions (for joint action spaces) or 6 (for cartesian) plus one dimension for gripper position |
| --> all N actions will get executed on the robot before the server is queried again |
| |
| """ |
|
|
| def __init__( |
| self, |
| policy: BasePolicy, |
| server_config: PolicyServerConfig, |
| host: str = "0.0.0.0", |
| port: int = 8000, |
| ) -> None: |
| self._policy = policy |
| self._server_config = server_config |
| self._host = host |
| self._port = port |
| logging.getLogger("websockets.server").setLevel(logging.INFO) |
|
|
| def serve_forever(self) -> None: |
| asyncio.run(self.run()) |
|
|
| async def run(self): |
| async with websockets.asyncio.server.serve( |
| self._handler, |
| self._host, |
| self._port, |
| compression=None, |
| max_size=None, |
| ) as server: |
| await server.serve_forever() |
|
|
| async def _handler(self, websocket: websockets.asyncio.server.ServerConnection): |
| logging.info(f"Connection from {websocket.remote_address} opened") |
| packer = msgpack_numpy.Packer() |
|
|
| |
| await websocket.send(packer.pack(dataclasses.asdict(self._server_config))) |
|
|
| while True: |
| try: |
| obs = msgpack_numpy.unpackb(await websocket.recv()) |
| |
| endpoint = obs["endpoint"] |
| del obs["endpoint"] |
| if endpoint == "reset": |
| self._policy.reset(obs) |
| to_return = "reset successful" |
| else: |
| action = self._policy.infer(obs) |
| to_return = packer.pack(action) |
| await websocket.send(to_return) |
| except websockets.ConnectionClosed: |
| logging.info(f"Connection from {websocket.remote_address} closed") |
| break |
| except Exception: |
| await websocket.send(traceback.format_exc()) |
| await websocket.close( |
| code=websockets.frames.CloseCode.INTERNAL_ERROR, |
| reason="Internal server error. Traceback included in previous frame.", |
| ) |
| raise |
|
|
|
|
| if __name__ == "__main__": |
| import numpy as np |
|
|
| class DummyPolicy(BasePolicy): |
| def infer(self, obs): |
| return np.zeros((1, 8), dtype=np.float32) |
| |
| def reset(self, reset_info): |
| pass |
| |
| logging.basicConfig(level=logging.INFO) |
| policy = DummyPolicy() |
| server = WebsocketPolicyServer(policy, PolicyServerConfig()) |
| server.serve_forever() |
| |