Skip to content

Commit 4d83eaa

Browse files
authored
Merge pull request #37 from PCrnjak/feat/thin-python-binding
RobotError is waldoctl's; type the sync tools
2 parents d6a68e8 + b05aed8 commit 4d83eaa

6 files changed

Lines changed: 27 additions & 48 deletions

File tree

‎parol6/client/sync_client.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from collections.abc import Callable, Coroutine
1212
from typing import Any, TypeVar, overload
1313

14-
from waldoctl.tools import ToolSpec
14+
from waldoctl.sync_tools import SyncTool
1515

1616
from waldoctl import PingResult, ToolStatus
1717
from waldoctl.status import ActivityResult, LoopStatsResult, ToolResult
@@ -131,7 +131,7 @@ def __init__(
131131
# `from parol6 import RobotClient; rbt = RobotClient(...)` works without
132132
# going through Robot.create_sync_client(). The Robot factory rebinds
133133
# these afterwards from the same registry.
134-
self._bound_tools: dict[str, ToolSpec] = {}
134+
self._bound_tools: dict[str, SyncTool] = {}
135135
self._bind_default_tools()
136136

137137
def _bind_default_tools(self) -> None:
@@ -146,7 +146,7 @@ def _bind_default_tools(self) -> None:
146146
# ---------- tool access ----------
147147

148148
@property
149-
def tool(self) -> ToolSpec:
149+
def tool(self) -> SyncTool:
150150
"""Active bound tool. Raises if no tool has been set."""
151151
key = (self._inner._active_tool_key or "").upper()
152152
if not key:

‎parol6/robot.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@ def create_sync_client(self, **kwargs: Any) -> SyncRobotClient:
967967
import copy
968968

969969
from parol6.client.sync_client import _run
970-
from waldoctl.sync_tools import make_sync_tool
970+
from waldoctl.sync_tools import SyncTool, make_sync_tool
971971

972972
host: str = kwargs.get("host", self._host)
973973
port: int = kwargs.get("port", self._port)
@@ -980,7 +980,7 @@ def create_sync_client(self, **kwargs: Any) -> SyncRobotClient:
980980
bound_spec._get_status = client._inner._tool_status # type: ignore[attr-defined, ty:unresolved-attribute]
981981
async_bound[spec.key] = bound_spec
982982
client._inner._bound_tools = async_bound
983-
bound: dict[str, ToolSpec] = {}
983+
bound: dict[str, SyncTool] = {}
984984
for key, async_tool in async_bound.items():
985985
bound[key] = make_sync_tool(async_tool, _run)
986986
client._bound_tools = bound

‎parol6/server/controller.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import sys
1111
import threading
1212
import time
13-
from dataclasses import dataclass, replace
13+
from dataclasses import dataclass
1414
from typing import Any
1515

1616

@@ -422,7 +422,12 @@ def _tick_tool_cmd(self, state: ControllerState) -> None:
422422
raw_error = self._tool_cmd.robot_error or make_error(
423423
ErrorCode.MOTN_TICK_FAILED, detail=type(self._tool_cmd).__name__
424424
)
425-
state.error = replace(raw_error, command_index=self._tool_cmd_index)
425+
# Rebuilt from the wire, not `replace`d: the refusal is an
426+
# exception now, and a dataclass replace on one does not
427+
# survive the copy the state makes of it.
428+
attributed = raw_error.to_wire()
429+
attributed[0] = self._tool_cmd_index
430+
state.error = RobotError.from_wire(attributed)
426431
state.action_state = ActionState.ERROR
427432
state.completed_command_index = max(
428433
state.completed_command_index, self._tool_cmd_index

‎parol6/utils/error_catalog.py‎

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Structured robot error types and error catalog.
1+
"""The error catalog: waldoctl's RobotError plus this backend's templates.
22
33
RobotError carries KUKA-style structured fields: code, title, cause, effect, remedy.
44
The catalog maps ErrorCode → template; make_error() instantiates with runtime params.
@@ -8,45 +8,16 @@
88

99
from dataclasses import dataclass
1010

11-
from .error_codes import ErrorCode
12-
11+
from waldoctl.errors import RobotError as _RobotError
1312

14-
@dataclass(frozen=True)
15-
class RobotError:
16-
"""Structured error with code, title, cause, effect, and remedy."""
13+
from .error_codes import ErrorCode
1714

18-
command_index: int
19-
code: int
20-
title: str
21-
cause: str
22-
effect: str
23-
remedy: str
2415

25-
def to_wire(self) -> list:
26-
"""Serialize to a list for ormsgpack packing."""
27-
return [
28-
self.command_index,
29-
self.code,
30-
self.title,
31-
self.cause,
32-
self.effect,
33-
self.remedy,
34-
]
35-
36-
@staticmethod
37-
def from_wire(data: list) -> RobotError:
38-
"""Reconstruct from a wire-format list."""
39-
return RobotError(
40-
command_index=data[0],
41-
code=data[1],
42-
title=data[2],
43-
cause=data[3],
44-
effect=data[4],
45-
remedy=data[5],
46-
)
47-
48-
def __str__(self) -> str:
49-
return f"[{self.code}] {self.title}: {self.cause}"
16+
# The refusal type is the contract's, not this backend's: a frontend
17+
# represents a refused command the same way whichever arm raised it, and
18+
# the six fields and the wire list are identical in both directions. An
19+
# exception rather than a dataclass, so a client can raise it as-is.
20+
RobotError = _RobotError
5021

5122

5223
@dataclass(frozen=True)

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ dependencies = [
4646
"psutil>=5.9",
4747
"msgspec>=0.18",
4848
"ormsgpack>=1.4.0",
49-
"waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.10.0",
49+
"waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.12.0",
5050
]
5151

5252
[tool.setuptools.packages.find]

‎tests/integration/test_shapes_e2e.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import pytest
1818

1919
from parol6 import MotionError, RobotClient
20+
21+
from tests.conftest import free_udp_port
2022
from waldoctl import Box
2123

2224
pytestmark = pytest.mark.integration
@@ -75,9 +77,10 @@ def test_set_shapes_ack_readback_rejection_and_timeout(
7577
assert tuple(s.name for s in world.program) == ("table",)
7678

7779
# Unreachable controller → unconfirmed (0), never a fake success.
78-
dead = RobotClient(
79-
host=ports.server_ip, port=ports.server_port + 91, timeout=0.3
80-
)
80+
# A port the kernel just handed out and nothing bound: arithmetic on
81+
# the live port runs past 65535 whenever the ephemeral range hands
82+
# out a high one, which is a connect() overflow, not a dead server.
83+
dead = RobotClient(host=ports.server_ip, port=free_udp_port(), timeout=0.3)
8184
assert dead.set_shapes([box]) == 0
8285
assert dead.shapes() is None
8386
finally:

0 commit comments

Comments
 (0)