Skip to content

Commit cbefde8

Browse files
authored
Expose STARBackend.request_iswap_relative_wrist_orientation() (#667)
1 parent 856fbf4 commit cbefde8

File tree

2 files changed

+196
-2
lines changed

2 files changed

+196
-2
lines changed

Untitled.ipynb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 2,
6+
"id": "6174d785-e532-4df1-ba61-235f0fe9f000",
7+
"metadata": {},
8+
"outputs": [
9+
{
10+
"name": "stdout",
11+
"output_type": "stream",
12+
"text": [
13+
"SCARA Arm Position -> X: 134.69, Y: 163.40, Z: 35.00 | Joint Angles: [30°, 45°, -20°], Z Offset: -15\n"
14+
]
15+
}
16+
],
17+
"source": [
18+
"import math\n",
19+
"\n",
20+
"class SCARARobotArm:\n",
21+
" def __init__(self, link_lengths, base_height=0.0):\n",
22+
" \"\"\"\n",
23+
" Initializes the SCARA robot arm with a dynamic number of revolute joints.\n",
24+
"\n",
25+
" :param link_lengths: List of link lengths [L1, L2, ...]\n",
26+
" :param base_height: Base height for the prismatic joint (Z-axis)\n",
27+
" \"\"\"\n",
28+
" self.link_lengths = link_lengths\n",
29+
" self.n_joints = len(link_lengths)\n",
30+
" self.joint_angles = [0.0] * self.n_joints # In degrees\n",
31+
" self.base_height = base_height\n",
32+
" self.z_offset = 0.0 # Vertical prismatic joint (Z-axis)\n",
33+
"\n",
34+
" def set_joint_angles(self, angles):\n",
35+
" \"\"\"\n",
36+
" Sets the revolute joint angles (in degrees).\n",
37+
"\n",
38+
" :param angles: List of joint angles, same length as link_lengths\n",
39+
" \"\"\"\n",
40+
" if len(angles) != self.n_joints:\n",
41+
" raise ValueError(\"Number of angles must match number of links.\")\n",
42+
" self.joint_angles = angles\n",
43+
"\n",
44+
" def set_z_offset(self, z):\n",
45+
" \"\"\"Set vertical offset for the prismatic joint (Z-axis).\"\"\"\n",
46+
" self.z_offset = z\n",
47+
"\n",
48+
" def forward_kinematics(self):\n",
49+
" \"\"\"\n",
50+
" Calculates the end-effector position (x, y, z) based on current joint angles.\n",
51+
" Returns:\n",
52+
" (x, y, z): End-effector Cartesian coordinates\n",
53+
" \"\"\"\n",
54+
" x, y = 0.0, 0.0\n",
55+
" angle_sum = 0.0\n",
56+
"\n",
57+
" for i in range(self.n_joints):\n",
58+
" angle_sum += math.radians(self.joint_angles[i])\n",
59+
" x += self.link_lengths[i] * math.cos(angle_sum)\n",
60+
" y += self.link_lengths[i] * math.sin(angle_sum)\n",
61+
"\n",
62+
" z = self.base_height + self.z_offset\n",
63+
" return (x, y, z)\n",
64+
"\n",
65+
" def __str__(self):\n",
66+
" x, y, z = self.forward_kinematics()\n",
67+
" angles_str = \", \".join([f\"{a}°\" for a in self.joint_angles])\n",
68+
" return (f\"SCARA Arm Position -> X: {x:.2f}, Y: {y:.2f}, Z: {z:.2f} | \"\n",
69+
" f\"Joint Angles: [{angles_str}], Z Offset: {self.z_offset}\")\n",
70+
"\n",
71+
"# Example usage:\n",
72+
"if __name__ == \"__main__\":\n",
73+
" # Create a 3-joint SCARA-like robot (not common, but for flexibility/testing)\n",
74+
" arm = SCARARobotArm(link_lengths=[100, 75, 50], base_height=50)\n",
75+
" arm.set_joint_angles([30, 45, -20])\n",
76+
" arm.set_z_offset(-15)\n",
77+
" print(arm)\n"
78+
]
79+
},
80+
{
81+
"cell_type": "code",
82+
"execution_count": null,
83+
"id": "259ff2bb-7891-4f33-af24-6ec565cd0cc7",
84+
"metadata": {},
85+
"outputs": [],
86+
"source": []
87+
}
88+
],
89+
"metadata": {
90+
"kernelspec": {
91+
"display_name": "Python 3 (ipykernel)",
92+
"language": "python",
93+
"name": "python3"
94+
},
95+
"language_info": {
96+
"codemirror_mode": {
97+
"name": "ipython",
98+
"version": 3
99+
},
100+
"file_extension": ".py",
101+
"mimetype": "text/x-python",
102+
"name": "python",
103+
"nbconvert_exporter": "python",
104+
"pygments_lexer": "ipython3",
105+
"version": "3.12.11"
106+
}
107+
},
108+
"nbformat": 4,
109+
"nbformat_minor": 5
110+
}

pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6763,6 +6763,90 @@ async def iswap_put_plate(
67636763
self._iswap_parked = False
67646764
return command_output
67656765

6766+
async def request_iswap_rotation_drive_position_increments(self) -> int:
6767+
"""Query the iSWAP rotation drive position (units: increments) from the firmware."""
6768+
response = await self.send_command(module="R0", command="RS", fmt="rs######")
6769+
return cast(int, response["rs"])
6770+
6771+
async def request_iswap_rotation_drive_orientation(self) -> "RotationDriveOrientation":
6772+
"""
6773+
Request the iSWAP rotation drive orientation.
6774+
This is the orientation of the iSWAP rotation drive (relative to the machine).
6775+
6776+
Uses empirically determined increment values:
6777+
FRONT: -25 ± 50
6778+
RIGHT: +29068 ± 50
6779+
LEFT: -29116 ± 50
6780+
6781+
Returns:
6782+
RotationDriveOrientation: The interpreted rotation orientation (LEFT, FRONT, RIGHT).
6783+
"""
6784+
# Map motor increments to rotation orientations (constant lookup table).
6785+
rotation_orientation_to_motor_increment_dict = {
6786+
STARBackend.RotationDriveOrientation.FRONT: range(-75, 26),
6787+
STARBackend.RotationDriveOrientation.RIGHT: range(29018, 29119),
6788+
STARBackend.RotationDriveOrientation.LEFT: range(-29166, -29065),
6789+
}
6790+
6791+
motor_position_increments = await self.request_iswap_rotation_drive_position_increments()
6792+
6793+
for orientation, increment_range in rotation_orientation_to_motor_increment_dict.items():
6794+
if motor_position_increments in increment_range:
6795+
return orientation
6796+
6797+
raise ValueError(
6798+
f"Unknown rotation orientation: {motor_position_increments}. "
6799+
f"Expected one of {list(rotation_orientation_to_motor_increment_dict)}."
6800+
)
6801+
6802+
async def request_iswap_wrist_drive_position_increments(self) -> int:
6803+
"""Query the iSWAP wrist drive position (units: increments) from the firmware."""
6804+
response = await self.send_command(module="R0", command="RT", fmt="rt######")
6805+
return cast(int, response["rt"])
6806+
6807+
async def request_iswap_wrist_drive_orientation(self) -> "WristDriveOrientation":
6808+
"""
6809+
Request the iSWAP wrist drive orientation.
6810+
This is the orientation of the iSWAP wrist drive (always in relation to the
6811+
iSWAP arm/rotation drive).
6812+
6813+
e.g.:
6814+
1) iSWAP RotationDriveOrientation.FRONT (i.e. pointing to the front of the machine) +
6815+
iSWAP WristDriveOrientation.STRAIGHT (i.e. wrist is also pointing to the front)
6816+
6817+
2) iSWAP RotationDriveOrientation.LEFT (i.e. pointing to the left of the machine) +
6818+
iSWAP WristDriveOrientation.STRAIGHT (i.e. wrist is also pointing to the left)
6819+
6820+
3) iSWAP RotationDriveOrientation.FRONT (i.e. pointing to the front of the machine) +
6821+
iSWAP WristDriveOrientation.RIGHT (i.e. wrist is pointing to the left !)
6822+
6823+
The relative wrist orientation is reported as a motor position increment by the STAR
6824+
firmware. This value is mapped to a `WristDriveOrientation` enum member.
6825+
6826+
Returns:
6827+
WristDriveOrientation: The interpreted wrist orientation
6828+
(e.g., RIGHT, STRAIGHT, LEFT, REVERSE).
6829+
"""
6830+
6831+
# Map motor increments to wrist orientations (constant lookup table).
6832+
wrist_orientation_to_motor_increment_dict = {
6833+
STARBackend.WristDriveOrientation.RIGHT: range(-26_627, -26_527),
6834+
STARBackend.WristDriveOrientation.STRAIGHT: range(-8_804, -8_704),
6835+
STARBackend.WristDriveOrientation.LEFT: range(9_051, 9_151),
6836+
STARBackend.WristDriveOrientation.REVERSE: range(26_802, 26_902),
6837+
}
6838+
6839+
motor_position_increments = await self.request_iswap_wrist_drive_position_increments()
6840+
6841+
for orientation, increment_range in wrist_orientation_to_motor_increment_dict.items():
6842+
if motor_position_increments in increment_range:
6843+
return orientation
6844+
6845+
raise ValueError(
6846+
f"Unknown wrist orientation: {motor_position_increments}. "
6847+
f"Expected one of {list(wrist_orientation_to_motor_increment_dict)}."
6848+
)
6849+
67666850
async def iswap_rotate(
67676851
self,
67686852
rotation_drive: "RotationDriveOrientation",
@@ -7491,13 +7575,13 @@ async def rotate_iswap_rotation_drive(self, orientation: RotationDriveOrientatio
74917575
wp=orientation.value,
74927576
)
74937577

7494-
class WristOrientation(enum.Enum):
7578+
class WristDriveOrientation(enum.Enum):
74957579
RIGHT = 1
74967580
STRAIGHT = 2
74977581
LEFT = 3
74987582
REVERSE = 4
74997583

7500-
async def rotate_iswap_wrist(self, orientation: WristOrientation):
7584+
async def rotate_iswap_wrist(self, orientation: WristDriveOrientation):
75017585
return await self.send_command(
75027586
module="R0",
75037587
command="TP",

0 commit comments

Comments
 (0)