Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions pylabrobot/hamilton/liquid_handlers/star/iswap.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def __init__(self, driver: STARDriver, traversal_height: float = 280.0):
self.traversal_height = traversal_height
self._version: Optional[str] = None
self._parked: Optional[bool] = None
self._rotation_drive_x_offset: Optional[float] = None # cached in _on_setup

@property
def version(self) -> str:
Expand Down Expand Up @@ -96,6 +97,9 @@ async def _on_setup(self, backend_params: Optional[BackendParams] = None) -> Non
if self._version is None:
self._version = await self._request_version()

if self._rotation_drive_x_offset is None:
self._rotation_drive_x_offset = await self._rotation_drive_request_x_offset()

async def _request_version(self) -> str:
"""Request the iSWAP firmware version from the device."""
return cast(str, (await self.driver.send_command("R0", "RF", fmt="rf" + "&" * 15))["rf"])
Expand Down Expand Up @@ -239,12 +243,12 @@ async def move_z(self, z: float) -> None:

# -- rotation / wrist drive ------------------------------------------------

async def request_rotation_drive_position_increments(self) -> int:
"""Query the iSWAP rotation drive position in increments (R0 RW)."""
async def rotation_drive_request_angle_increments(self) -> int:
"""Query the iSWAP rotation drive angle in increments (R0 RW)."""
response = await self.driver.send_command(module="R0", command="RW", fmt="rw######")
return cast(int, response["rw"])

async def request_rotation_drive_orientation(self) -> "iSWAPBackend.RotationDriveOrientation":
async def rotation_drive_request_orientation(self) -> "iSWAPBackend.RotationDriveOrientation":
"""Request the iSWAP rotation drive orientation.

Uses empirically determined increment values:
Expand All @@ -258,19 +262,19 @@ async def request_rotation_drive_orientation(self) -> "iSWAPBackend.RotationDriv
RDO.PARKED_RIGHT: range(29450, 29550),
}

motor_position_increments = await self.request_rotation_drive_position_increments()
motor_angle_increments = await self.rotation_drive_request_angle_increments()

for orientation, increment_range in rotation_orientation_to_motor_increment_dict.items():
if motor_position_increments in increment_range:
if motor_angle_increments in increment_range:
return orientation

raise ValueError(
f"Unknown rotation orientation: {motor_position_increments}. "
f"Unknown rotation orientation: {motor_angle_increments}. "
f"Expected one of {list(rotation_orientation_to_motor_increment_dict.values())}."
)

async def request_wrist_drive_position_increments(self) -> int:
"""Query the iSWAP wrist drive position in increments (R0 RT)."""
async def request_wrist_drive_angle_increments(self) -> int:
"""Query the iSWAP wrist drive angle in increments (R0 RT)."""
response = await self.driver.send_command(module="R0", command="RT", fmt="rt######")
return cast(int, response["rt"])

Expand All @@ -287,17 +291,50 @@ async def request_wrist_drive_orientation(self) -> "iSWAPBackend.WristDriveOrien
WDO.REVERSE: range(26_802, 26_902),
}

motor_position_increments = await self.request_wrist_drive_position_increments()
motor_angle_increments = await self.request_wrist_drive_angle_increments()

for orientation, increment_range in wrist_orientation_to_motor_increment_dict.items():
if motor_position_increments in increment_range:
if motor_angle_increments in increment_range:
return orientation

raise ValueError(
f"Unknown wrist orientation: {motor_position_increments}. "
f"Unknown wrist orientation: {motor_angle_increments}. "
f"Expected one of {list(wrist_orientation_to_motor_increment_dict)}."
)

async def _rotation_drive_request_x_offset(self) -> float:
"""Read the X-offset i.e. X-axis center <-> iSWAP rotation drive, in mm.

Stored in the master EEPROM as parameter ``kg`` (set via ``C0:AG`` —
see ``driver.set_x_offset_x_axis_iswap``).
Previously measured to be 32.8 mm by contributor;
per-machine calibrated during service. Required for deriving the
iSWAP rotation drive's deck X coordinate from the X-arm carriage center.
Cached on the backend as ``_rotation_drive_x_offset`` during setup.
"""
resp = await self.driver.send_command(module="C0", command="RA", ra="kg", fmt="kg###")
return cast(int, resp["kg"]) / 10.0

# Vertical drop from the iSWAP rotation drive plane to the gripper
# finger plane, per VENUS Programmer Guide §15.1.1.
rotation_drive_z_offset_above_finger = 13.0

async def rotation_drive_request_position(self) -> Coordinate:
"""Position of the iSWAP rotation drive (joint 0) in deck coordinates, mm."""
if not self.driver.extended_conf.left_x_drive.iswap_installed: # type: ignore[union-attr]
raise RuntimeError("iSWAP is not installed")
assert self._rotation_drive_x_offset is not None, "Call setup() first"

x_arm_center = await self.driver.left_x_arm.request_position() # type: ignore[union-attr]
rotation_drive_y = await self.rotation_drive_request_y()
finger_loc = (await self.request_gripper_location()).location

return Coordinate(
x=x_arm_center - self._rotation_drive_x_offset,
y=rotation_drive_y,
z=finger_loc.z + self.rotation_drive_z_offset_above_finger,
)
Comment on lines +310 to +336
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is worth considering making rotation_drive_request_position private, just loading it in _on_setup and then storing it locally so we don't have to call this firmware command every time.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what you mean:

Every time the STAR's X-arm moves this position is different, same for when the iSWAP performs any action, or channels require the iSWAP to move out of the way?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the offset should be constant right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhh you don't mean making rotation_drive_request_position private,
but instead you mean making rotation_drive_request_x_offset private and cached during setup?

yes, that makes sense - will implement

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes sorry


async def rotate(
self,
rotation_drive: "iSWAPBackend.RotationDriveOrientation",
Expand Down Expand Up @@ -357,7 +394,7 @@ async def rotate(
tw=wrist_protection,
)

async def rotate_rotation_drive(
async def rotation_drive_rotate(
self, orientation: "iSWAPBackend.RotationDriveOrientation"
) -> None:
"""Rotate the rotation drive to the given orientation (R0 WP)."""
Expand Down
15 changes: 6 additions & 9 deletions pylabrobot/hamilton/liquid_handlers/star/pip_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,20 +334,17 @@ async def move_tool_z(self, z: float):
zj=f"{round(z * 10):04}",
)

# -- C0:RA request X position ------------------------------------------------

# -- delegate to left_x_arm (C0 RX) — channels share the X carriage ----------
# TODO: we assume `C0RX` references the center of the x-arm, figure out what it
# references for half-arms (see issue 822 and new Fluid Motion STAR)

async def request_x_pos(self) -> float:
"""Request current X-position of this channel (mm).

All PIP channels share the same X arm, so this returns the arm position.
"""
resp = await self.driver.send_command(
module="C0",
command="RA",
fmt="ra#####",
pn=f"{self.index + 1:02}",
)
return float(resp["ra"] / 10)
assert self.driver.left_x_arm is not None, "left_x_arm not set; call driver.setup() first"
return await self.driver.left_x_arm.request_position()

# -- C0:RB request Y position ------------------------------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7171,16 +7171,16 @@ async def iswap_put_plate(

async def request_iswap_rotation_drive_position_increments(self) -> int:
"""Deprecated: use ``star.iswap.request_rotation_drive_position_increments()``."""
return await self._iswap.request_rotation_drive_position_increments()
return await self._iswap.rotation_drive_request_angle_increments()

async def request_iswap_rotation_drive_orientation(self) -> "RotationDriveOrientation":
"""Deprecated: use ``star.iswap.request_rotation_drive_orientation()``."""
new_orient = await self._iswap.request_rotation_drive_orientation()
new_orient = await self._iswap.rotation_drive_request_orientation()
return STARBackend.RotationDriveOrientation(new_orient.value)

async def request_iswap_wrist_drive_position_increments(self) -> int:
"""Deprecated: use ``star.iswap.request_wrist_drive_position_increments()``."""
return await self._iswap.request_wrist_drive_position_increments()
return await self._iswap.request_wrist_drive_angle_increments()

async def request_iswap_wrist_drive_orientation(self) -> "WristDriveOrientation":
"""Deprecated: use ``star.iswap.request_wrist_drive_orientation()``."""
Expand Down Expand Up @@ -7751,7 +7751,7 @@ class RotationDriveOrientation(enum.Enum):

async def rotate_iswap_rotation_drive(self, orientation: RotationDriveOrientation):
"""Deprecated: use ``star.iswap.rotate_rotation_drive()``."""
return await self._iswap.rotate_rotation_drive(orientation) # type: ignore[arg-type]
return await self._iswap.rotation_drive_rotate(orientation) # type: ignore[arg-type]

class WristDriveOrientation(enum.Enum):
RIGHT = 1
Expand Down
Loading