From bb3263e8cd3f8602daed2f7b9d5900d2d95d2ecb Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Mon, 22 Jun 2026 11:24:02 +0100 Subject: [PATCH 1/2] v1: `PreciseFlex`: move the arm family into a `brooks.precise_flex` package Restructure only - no behaviour change. Moves the entire PreciseFlex arm family into a `brooks/precise_flex/` package, matching how pylabrobot organises other manufacturers as manufacturer/ packages (thermo_fisher/multidrop_combi, ufactory/xarm6, inheco/scila); the flat `brooks/` was the outlier. Every PreciseFlex arm (PF400, PF3400, and the c-series / direct-drive / rail variants) runs the same Guidance/TCS controller and shares the bulk of this driver, so the family lives in one package: separate device classes (`PreciseFlex400`, `PreciseFlex3400`, ...) over one `PreciseFlexArmBackend`, differing only in kinematics and gripper. Everything PreciseFlex moves in - the device classes/driver (`precise_flex`), `kinematics`, `confirmed_firmware_versions`, the controller modules (`tcs_modules`, `error_codes`, `data_ids`), the tests, and the notebook - because the PreciseFlex line is the only user of the Guidance/TCS controller. `brooks/` is left as the manufacturer namespace holding the one package. The package `__init__` re-exports the public classes, so `from pylabrobot.brooks.precise_flex import PreciseFlex400` and the `pylabrobot.brooks` exports are unchanged. Done as `git mv`, so history is preserved (the diff is renames + import-path updates + the new `__init__`). Co-Authored-By: Claude Opus 4.8 (1M context) --- pylabrobot/brooks/precise_flex/__init__.py | 55 +++++++++++++++++++ .../confirmed_firmware_versions.py | 0 .../brooks/{ => precise_flex}/data_ids.py | 0 .../brooks/{ => precise_flex}/error_codes.py | 0 .../brooks/{ => precise_flex}/kinematics.py | 4 +- .../{ => precise_flex}/pf400_test.ipynb | 0 .../brooks/{ => precise_flex}/precise_flex.py | 22 ++++---- .../{ => precise_flex}/precise_flex_tests.py | 3 +- .../brooks/{ => precise_flex}/tcs_modules.py | 0 9 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 pylabrobot/brooks/precise_flex/__init__.py rename pylabrobot/brooks/{ => precise_flex}/confirmed_firmware_versions.py (100%) rename pylabrobot/brooks/{ => precise_flex}/data_ids.py (100%) rename pylabrobot/brooks/{ => precise_flex}/error_codes.py (100%) rename pylabrobot/brooks/{ => precise_flex}/kinematics.py (97%) rename pylabrobot/brooks/{ => precise_flex}/pf400_test.ipynb (100%) rename pylabrobot/brooks/{ => precise_flex}/precise_flex.py (99%) rename pylabrobot/brooks/{ => precise_flex}/precise_flex_tests.py (99%) rename pylabrobot/brooks/{ => precise_flex}/tcs_modules.py (100%) diff --git a/pylabrobot/brooks/precise_flex/__init__.py b/pylabrobot/brooks/precise_flex/__init__.py new file mode 100644 index 00000000000..1ae738031a7 --- /dev/null +++ b/pylabrobot/brooks/precise_flex/__init__.py @@ -0,0 +1,55 @@ +"""Brooks PreciseFlex robots. + +Why one package for the family - every PreciseFlex arm runs the same Guidance/TCS controller and +speaks the same GPL command protocol, DataIDs, and error codes, so they share the bulk of this +driver. They differ only in kinematics (per geometry, e.g. the c10's R-P-R-R joint order, the c8A's +six axes) and gripper, which are handled by per-model device classes and per-geometry kinematics +modules within the package. Grouping by the shared controller keeps that common driver in one place +rather than duplicated per arm model. + +Scope - the PreciseFlex robot line. Implemented: + +- PreciseFlex 400 (PF400) +- PreciseFlex 3400 (PF3400) + +To be added here: + +- PreciseFlex 100 / 1400 (PF100 / PF1400) +- c-series: c3, c5, c8A, c10 +- direct-drive: DD4, DD6 +- linear rail + +Everything here is PreciseFlex-specific, including the TCS controller protocol (``tcs_modules``), +``error_codes``, and the controller DataIDs (``data_ids``) - the PreciseFlex line is the only user of +the Guidance/TCS controller, so they live with it. A future, genuinely different Brooks device family +would get its own sibling package under ``brooks/``, and anything shared would be lifted up then. + +Re-exports the public classes so ``from pylabrobot.brooks.precise_flex import PreciseFlex400`` keeps +working. +""" + +from pylabrobot.brooks.precise_flex.precise_flex import ( + Axis, + PreciseFlex400, + PreciseFlex400Backend, + PreciseFlex3400Backend, + PreciseFlexArmBackend, + PreciseFlexCartesianPose, + PreciseFlexConfiguration, + PreciseFlexDriver, + PreciseFlexError, + WorkingVolume, +) + +__all__ = [ + "Axis", + "PreciseFlex400", + "PreciseFlex400Backend", + "PreciseFlex3400Backend", + "PreciseFlexArmBackend", + "PreciseFlexCartesianPose", + "PreciseFlexConfiguration", + "PreciseFlexDriver", + "PreciseFlexError", + "WorkingVolume", +] diff --git a/pylabrobot/brooks/confirmed_firmware_versions.py b/pylabrobot/brooks/precise_flex/confirmed_firmware_versions.py similarity index 100% rename from pylabrobot/brooks/confirmed_firmware_versions.py rename to pylabrobot/brooks/precise_flex/confirmed_firmware_versions.py diff --git a/pylabrobot/brooks/data_ids.py b/pylabrobot/brooks/precise_flex/data_ids.py similarity index 100% rename from pylabrobot/brooks/data_ids.py rename to pylabrobot/brooks/precise_flex/data_ids.py diff --git a/pylabrobot/brooks/error_codes.py b/pylabrobot/brooks/precise_flex/error_codes.py similarity index 100% rename from pylabrobot/brooks/error_codes.py rename to pylabrobot/brooks/precise_flex/error_codes.py diff --git a/pylabrobot/brooks/kinematics.py b/pylabrobot/brooks/precise_flex/kinematics.py similarity index 97% rename from pylabrobot/brooks/kinematics.py rename to pylabrobot/brooks/precise_flex/kinematics.py index 77590ef71ab..5df2fd612dd 100644 --- a/pylabrobot/brooks/kinematics.py +++ b/pylabrobot/brooks/precise_flex/kinematics.py @@ -22,7 +22,7 @@ from pylabrobot.capabilities.arms.standard import JointPose if TYPE_CHECKING: - from pylabrobot.brooks.precise_flex import PreciseFlexCartesianPose + from pylabrobot.brooks.precise_flex.precise_flex import PreciseFlexCartesianPose # Known PF400 link-length configs (l1 = shoulder->elbow, l2 = elbow->wrist), in mm, per the 615287 @@ -78,7 +78,7 @@ def fk(joints: JointPose, p: PF400Params) -> "PreciseFlexCartesianPose": orientation/wrist derived from the joint configuration (J3 sign and wrapped J4 sign, respectively). """ - from pylabrobot.brooks.precise_flex import PreciseFlexCartesianPose + from pylabrobot.brooks.precise_flex.precise_flex import PreciseFlexCartesianPose from pylabrobot.resources import Coordinate, Rotation j1 = joints[1] diff --git a/pylabrobot/brooks/pf400_test.ipynb b/pylabrobot/brooks/precise_flex/pf400_test.ipynb similarity index 100% rename from pylabrobot/brooks/pf400_test.ipynb rename to pylabrobot/brooks/precise_flex/pf400_test.ipynb diff --git a/pylabrobot/brooks/precise_flex.py b/pylabrobot/brooks/precise_flex/precise_flex.py similarity index 99% rename from pylabrobot/brooks/precise_flex.py rename to pylabrobot/brooks/precise_flex/precise_flex.py index b534f5d6c0b..b6d14b6a9a1 100644 --- a/pylabrobot/brooks/precise_flex.py +++ b/pylabrobot/brooks/precise_flex/precise_flex.py @@ -7,16 +7,6 @@ from enum import IntEnum from typing import Dict, List, Literal, Optional -from pylabrobot.brooks import kinematics -from pylabrobot.brooks.confirmed_firmware_versions import ( - SUPPORTED_ROBOT_TYPES, - is_confirmed, - is_supported_model, - suggest_entry, -) -from pylabrobot.brooks.data_ids import DataID -from pylabrobot.brooks.error_codes import ERROR_CODES -from pylabrobot.brooks.tcs_modules import missing_required_modules from pylabrobot.capabilities.arms.backend import ( CanFreedrive, HasJoints, @@ -30,6 +20,18 @@ from pylabrobot.resources import Coordinate, Rotation from pylabrobot.resources.resource import Resource +# PreciseFlex-specific siblings - relative imports. +from . import kinematics +from .confirmed_firmware_versions import ( + SUPPORTED_ROBOT_TYPES, + is_confirmed, + is_supported_model, + suggest_entry, +) +from .data_ids import DataID +from .error_codes import ERROR_CODES +from .tcs_modules import missing_required_modules + logger = logging.getLogger(__name__) diff --git a/pylabrobot/brooks/precise_flex_tests.py b/pylabrobot/brooks/precise_flex/precise_flex_tests.py similarity index 99% rename from pylabrobot/brooks/precise_flex_tests.py rename to pylabrobot/brooks/precise_flex/precise_flex_tests.py index 5c8c80c3207..9c7934e1628 100644 --- a/pylabrobot/brooks/precise_flex_tests.py +++ b/pylabrobot/brooks/precise_flex/precise_flex_tests.py @@ -2,8 +2,7 @@ from typing import Tuple from unittest.mock import AsyncMock, MagicMock -from pylabrobot.brooks import kinematics -from pylabrobot.brooks.precise_flex import Axis, PreciseFlex400Backend +from pylabrobot.brooks.precise_flex import Axis, PreciseFlex400Backend, kinematics def _make_backend( diff --git a/pylabrobot/brooks/tcs_modules.py b/pylabrobot/brooks/precise_flex/tcs_modules.py similarity index 100% rename from pylabrobot/brooks/tcs_modules.py rename to pylabrobot/brooks/precise_flex/tcs_modules.py From 51ea4180a213d43618df19d5420da9d08a1f11b5 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Mon, 22 Jun 2026 12:16:19 +0100 Subject: [PATCH 2/2] v1: `PreciseFlex`: name the error module `errors.py` and co-locate `PreciseFlexError` `errors.py` is the codebase convention for a backend's error module - the Hamilton STAR `errors.py` holds both the error-code tables and the exception classes. This renames the PreciseFlex code-table module from `error_codes.py` to `errors.py` and moves `PreciseFlexError` (which reads `ERROR_CODES`) into it, so the table and the exception that consumes it live together. No behaviour change. The public import paths are unchanged: `PreciseFlexError` is still re-exported from the package `__init__` and remains importable from `precise_flex`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../{error_codes.py => errors.py} | 12 ++++++++++++ .../brooks/precise_flex/precise_flex.py | 19 +------------------ 2 files changed, 13 insertions(+), 18 deletions(-) rename pylabrobot/brooks/precise_flex/{error_codes.py => errors.py} (99%) diff --git a/pylabrobot/brooks/precise_flex/error_codes.py b/pylabrobot/brooks/precise_flex/errors.py similarity index 99% rename from pylabrobot/brooks/precise_flex/error_codes.py rename to pylabrobot/brooks/precise_flex/errors.py index c254e5418c9..f9d246f140c 100644 --- a/pylabrobot/brooks/precise_flex/error_codes.py +++ b/pylabrobot/brooks/precise_flex/errors.py @@ -1919,3 +1919,15 @@ "description": "A remote request to load a new vision project has failed because the current project has not been saved. Save the current project before attempting to load a new one.", }, } + + +class PreciseFlexError(Exception): + def __init__(self, replycode: int, message: str): + self.replycode = replycode + self.message = message + if replycode in ERROR_CODES: + text = ERROR_CODES[replycode]["text"] + description = ERROR_CODES[replycode]["description"] + super().__init__(f"PreciseFlexError {replycode}: {text}. {description} - {message}") + else: + super().__init__(f"PreciseFlexError {replycode}: {message}") diff --git a/pylabrobot/brooks/precise_flex/precise_flex.py b/pylabrobot/brooks/precise_flex/precise_flex.py index b6d14b6a9a1..54bf57c1704 100644 --- a/pylabrobot/brooks/precise_flex/precise_flex.py +++ b/pylabrobot/brooks/precise_flex/precise_flex.py @@ -29,7 +29,7 @@ suggest_entry, ) from .data_ids import DataID -from .error_codes import ERROR_CODES +from .errors import PreciseFlexError from .tcs_modules import missing_required_modules logger = logging.getLogger(__name__) @@ -159,23 +159,6 @@ def working_volume(self) -> WorkingVolume: return WorkingVolume(inner=inner, outer=outer, zmin=zmin, zmax=zmax) -# --------------------------------------------------------------------------- -# Exceptions -# --------------------------------------------------------------------------- - - -class PreciseFlexError(Exception): - def __init__(self, replycode: int, message: str): - self.replycode = replycode - self.message = message - if replycode in ERROR_CODES: - text = ERROR_CODES[replycode]["text"] - description = ERROR_CODES[replycode]["description"] - super().__init__(f"PreciseFlexError {replycode}: {text}. {description} - {message}") - else: - super().__init__(f"PreciseFlexError {replycode}: {message}") - - # --------------------------------------------------------------------------- # Driver — owns Socket I/O and device lifecycle # ---------------------------------------------------------------------------