From 9aeb6b80ecdeee79c7ba25c9b9d1c1dac256be82 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:24:16 +0000 Subject: [PATCH] Carry the seventh shape wire element, and stop faking a TCP offset waldoctl 0.13 adds `physics` to shapes, so `ShapeBase.to_wire()` returns SEVEN elements. `ShapeWire` declared six, and `set_shapes` builds one by positional unpack -- `ShapeWire(*s.to_wire())` -- so every call raised `TypeError: Extra positional arguments provided` the moment the waldoctl pin moved. The two read paths dropped `physics` on the floor for the same reason. `physics` is now a seventh field with a default, which keeps the struct readable both ways across that boundary: a peer still sending six decodes with `physics` absent, one sending seven decodes fully. Verified against waldoctl 0.13.1 -- 7-element encode, physics round-trips intact through `shape_from_wire`, a six-element peer still decodes, and msgspec agrees both directions. `tcp_offset()` also answered `[0, 0, 0]` when nothing replied. Zero is a legitimate offset -- a tool deliberately cleared -- so a caller handed it as a not-answered sentinel cannot tell "the offset is zero" from "there is no controller", and a host that adopts the readback quietly erases the offset the user just set. waldoctl's contract forbids exactly this. It raises `ConnectionError` now; `_request` already retries, so reaching that line means unreachable rather than one lost datagram. The waldoctl pin moves to v0.13.1 with them, since the shape fix is only meaningful there. Suite: 280 passed, 8 skipped. The one failure, `test_msg_mounts_on_the_flange_not_inside_the_wrist`, fails identically with these changes stashed -- it is pre-existing, not from this. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012tyAH93sLjc6W8Kp9ygFS8 --- parol6/client/async_client.py | 30 +++++++++++++++++++++++++++--- parol6/protocol/wire.py | 10 +++++++++- pyproject.toml | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/parol6/client/async_client.py b/parol6/client/async_client.py index ac41cd1..e48a825 100644 --- a/parol6/client/async_client.py +++ b/parol6/client/async_client.py @@ -1003,11 +1003,27 @@ async def shapes(self) -> ShapeWorld | None: return None return ShapeWorld( installation=tuple( - shape_from_wire(w.kind, w.params, w.pose, w.collision, w.margin, w.name) + shape_from_wire( + w.kind, + w.params, + w.pose, + w.collision, + w.margin, + w.name, + w.physics, + ) for w in resp.installation ), program=tuple( - shape_from_wire(w.kind, w.params, w.pose, w.collision, w.margin, w.name) + shape_from_wire( + w.kind, + w.params, + w.pose, + w.collision, + w.margin, + w.name, + w.physics, + ) for w in resp.program ), ) @@ -1015,6 +1031,14 @@ async def shapes(self) -> ShapeWorld | None: async def tcp_offset(self) -> list[float]: """Query current TCP offset in mm [x, y, z]. + Raises ``ConnectionError`` when the controller does not answer. + ``[0, 0, 0]`` is a legitimate offset -- a tool deliberately cleared + -- so returning it as a not-answered sentinel leaves the caller + unable to tell "the offset is zero" from "there is no controller", + and a host that adopts the readback quietly erases the offset the + user just set. ``_request`` already retries, so reaching the end + here means unreachable, not one lost datagram. + Category: Configuration Example: @@ -1023,7 +1047,7 @@ async def tcp_offset(self) -> list[float]: resp = await self._request(TcpOffsetCmd()) if isinstance(resp, TcpOffsetResultStruct): return [resp.x, resp.y, resp.z] - return [0.0, 0.0, 0.0] + raise ConnectionError("the controller did not answer tcp_offset()") async def select_profile(self, profile: str) -> int: """Set the motion profile (e.g. ``"TOPPRA"``). diff --git a/parol6/protocol/wire.py b/parol6/protocol/wire.py index 8a821f7..11bb709 100644 --- a/parol6/protocol/wire.py +++ b/parol6/protocol/wire.py @@ -638,7 +638,14 @@ class SetTcpOffsetCmd( class ShapeWire(msgspec.Struct, array_like=True, frozen=True, gc=False): - """One workspace shape — mirrors waldoctl ``Shape.to_wire()``.""" + """One workspace shape — mirrors waldoctl ``Shape.to_wire()``. + + ``physics`` is seventh and defaulted, which is what keeps this readable + both ways across the waldoctl 0.13 boundary: a peer still sending the + six-element form decodes with ``physics`` absent, and one sending seven + decodes fully. Without the field at all, every ``set_shapes`` against + 0.13 raised ``TypeError: Extra positional arguments provided``. + """ kind: str params: list[float] @@ -646,6 +653,7 @@ class ShapeWire(msgspec.Struct, array_like=True, frozen=True, gc=False): collision: bool margin: float | None name: str + physics: list | None = None class SetShapesCmd( diff --git a/pyproject.toml b/pyproject.toml index 692601c..a2c61a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "psutil>=5.9", "msgspec>=0.18", "ormsgpack>=1.4.0", - "waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.12.0", + "waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.13.1", ] [tool.setuptools.packages.find]