Skip to content

Commit 5cebe3f

Browse files
authored
Merge pull request #28 from wilsonh1/main
Update Tecan config and plate robot
2 parents 861d2e9 + 6c259c8 commit 5cebe3f

File tree

4 files changed

+413
-80
lines changed

4 files changed

+413
-80
lines changed

pylabrobot/liquid_handling/backends/tecan/EVO.py

Lines changed: 119 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
Resource,
2626
Coordinate,
2727
Liquid,
28+
TecanPlateCarrier,
2829
TecanTipRack,
2930
TecanPlate,
3031
TecanTip,
@@ -41,9 +42,9 @@ class TecanLiquidHandler(USBBackend, metaclass=ABCMeta):
4142
@abstractmethod
4243
def __init__(
4344
self,
44-
packet_read_timeout: int = 3,
45-
read_timeout: int = 30,
46-
write_timeout: int = 30,
45+
packet_read_timeout: int = 120,
46+
read_timeout: int = 300,
47+
write_timeout: int = 300,
4748
):
4849
"""
4950
@@ -155,11 +156,11 @@ class EVO(TecanLiquidHandler):
155156
def __init__(
156157
self,
157158
diti_count: int = 0,
158-
packet_read_timeout: int = 10,
159-
read_timeout: int = 30,
160-
write_timeout: int = 30,
159+
packet_read_timeout: int = 120,
160+
read_timeout: int = 300,
161+
write_timeout: int = 300,
161162
):
162-
""" Create a new STAR interface.
163+
""" Create a new EVO interface.
163164
164165
Args:
165166
packet_read_timeout: timeout in seconds for reading a single packet.
@@ -243,14 +244,17 @@ async def setup(self):
243244
if self.roma_connected: # position_initialization_x in reverse order from setup_arm
244245
self.roma = RoMa(self, EVO.ROMA)
245246
await self.roma.position_initialization_x()
247+
# move to home position (TBD) after initialization
248+
await self.roma.set_vector_coordinate_position(1, 9000, 2000, 2464, 1800, None, 1, 0)
249+
await self.roma.action_move_vector_coordinate_position()
246250
if self.liha_connected:
247251
self.liha = LiHa(self, EVO.LIHA)
248252
await self.liha.position_initialization_x()
249253

250254
self._num_channels = await self.liha.report_number_tips()
251255
self._x_range = await self.liha.report_x_param(5)
252256
self._y_range = (await self.liha.report_y_param(5))[0]
253-
self._z_range = (await self.liha.report_z_param(5))[0] # TODO: assert all are same?
257+
self._z_range = (await self.liha.report_z_param(5))[0]
254258

255259
# Initialize plungers. Assumes wash station assigned at rail 1.
256260
await self.liha.set_z_travel_height([self._z_range] * self.num_channels)
@@ -263,8 +267,6 @@ async def setup(self):
263267
await self.liha.move_plunger_relative([-100] * self.num_channels)
264268
await self.liha.position_absolute_all_axis(45, 1031, 90, [self._z_range] * self.num_channels)
265269

266-
# TODO: cache arm positions to prevent collisions
267-
268270
async def setup_arm(self, module):
269271
try:
270272
await self.send_command(module, command="PIA")
@@ -276,6 +278,10 @@ async def setup_arm(self, module):
276278
await self.send_command(module, command="BMX", params=[2])
277279
return True
278280

281+
async def _park_liha(self):
282+
await self.liha.set_z_travel_height([self._z_range] * self.num_channels)
283+
await self.liha.position_absolute_all_axis(45, 1031, 90, [self._z_range] * self.num_channels)
284+
279285
# ============== LiquidHandlerBackend methods ==============
280286

281287
async def aspirate(
@@ -495,28 +501,29 @@ async def move_resource(self, move: Move):
495501
""" Pick up a resource and move it to a new location. """
496502

497503
# TODO: implement PnP for moving tubes
504+
assert self.roma_connected
498505

499-
x, y, z = self._roma_positions(move.resource, move.resource.get_absolute_location() )
506+
z_range = await self.roma.report_z_param(5)
507+
x, y, z = self._roma_positions(move.resource, move.resource.get_absolute_location(), z_range)
500508
h = int(move.resource.get_size_y() * 10)
501-
xt, yt, zt = self._roma_positions(move.resource, move.to)
509+
xt, yt, zt = self._roma_positions(move.resource, move.to, z_range)
502510

503511
# move to resource
504-
# TODO: check collision with other arms
505512
await self.roma.set_smooth_move_x(1)
506513
await self.roma.set_fast_speed_x(10000)
507514
await self.roma.set_fast_speed_y(5000, 1500)
508-
await self.roma.set_fast_speed_z(1000)
515+
await self.roma.set_fast_speed_z(1300)
509516
await self.roma.set_fast_speed_r(5000, 1500)
510-
await self.roma.set_vector_coordinate_position(1, x, y, 1608, 900, None, 1, 0)
517+
await self.roma.set_vector_coordinate_position(1, x, y, z["safe"], 900, None, 1, 0)
511518
await self.roma.action_move_vector_coordinate_position()
512519
await self.roma.set_smooth_move_x(0)
513520

514521
# pick up resource
515522
await self.roma.position_absolute_g(900) # TODO: verify
516-
await self.roma.set_target_window_class(1, 0, 0, 55, 135, 0)
517-
await self.roma.set_vector_coordinate_position(1, x, y, 1241, 900, None, 1, 1)
523+
await self.roma.set_target_window_class(1, 0, 0, 0, 135, 0)
524+
await self.roma.set_vector_coordinate_position(1, x, y, z["travel"], 900, None, 1, 1)
518525
# TODO verify z param
519-
await self.roma.set_vector_coordinate_position(1, x, y, z, 900, None, 1, 0)
526+
await self.roma.set_vector_coordinate_position(1, x, y, z["end"], 900, None, 1, 0)
520527
await self.roma.action_move_vector_coordinate_position()
521528
await self.roma.set_fast_speed_y(3500, 1000)
522529
await self.roma.set_fast_speed_r(2000, 600)
@@ -528,21 +535,21 @@ async def move_resource(self, move: Move):
528535
await self.roma.set_target_window_class(2, 0, 0, 0, 53, 0)
529536
await self.roma.set_target_window_class(3, 0, 0, 0, 55, 0)
530537
await self.roma.set_target_window_class(4, 45, 0, 0, 0, 0)
531-
await self.roma.set_vector_coordinate_position(1, x, y, z, 900, None, 1, 1)
532-
await self.roma.set_vector_coordinate_position(2, x, y, 1241, 900, None, 1, 2)
533-
await self.roma.set_vector_coordinate_position(3, x, y, 1608, 900, None, 1, 3)
534-
await self.roma.set_vector_coordinate_position(4, xt, yt, 1608, 900, None, 1, 4)
535-
await self.roma.set_vector_coordinate_position(5, xt, yt, 1241, 900, None, 1, 3)
536-
await self.roma.set_vector_coordinate_position(6, xt, yt, zt, 900, None, 1, 0)
538+
await self.roma.set_vector_coordinate_position(1, x, y, z["end"], 900, None, 1, 1)
539+
await self.roma.set_vector_coordinate_position(2, x, y, z["travel"], 900, None, 1, 2)
540+
await self.roma.set_vector_coordinate_position(3, x, y, z["safe"], 900, None, 1, 3)
541+
await self.roma.set_vector_coordinate_position(4, xt, yt, zt["safe"], 900, None, 1, 4)
542+
await self.roma.set_vector_coordinate_position(5, xt, yt, zt["travel"], 900, None, 1, 3)
543+
await self.roma.set_vector_coordinate_position(6, xt, yt, zt["end"], 900, None, 1, 0)
537544
await self.roma.action_move_vector_coordinate_position()
538545

539546
# release resource
540547
await self.roma.position_absolute_g(900)
541548
await self.roma.set_fast_speed_y(5000, 1500)
542549
await self.roma.set_fast_speed_r(5000, 1500)
543-
await self.roma.set_vector_coordinate_position(1, xt, yt, zt, 900, None, 1, 1)
544-
await self.roma.set_vector_coordinate_position(2, xt, yt, 1241, 900, None, 1, 2)
545-
await self.roma.set_vector_coordinate_position(3, xt, yt, 1608, 900, None, 1, 0)
550+
await self.roma.set_vector_coordinate_position(1, xt, yt, zt["end"], 900, None, 1, 1)
551+
await self.roma.set_vector_coordinate_position(2, xt, yt, zt["travel"], 900, None, 1, 2)
552+
await self.roma.set_vector_coordinate_position(3, xt, yt, zt["safe"], 900, None, 1, 0)
546553
await self.roma.action_move_vector_coordinate_position()
547554
await self.roma.set_fast_speed_y(3500, 1000)
548555
await self.roma.set_fast_speed_r(2000, 600)
@@ -590,7 +597,7 @@ def get_z_position(z, z_off, tip_length):
590597
for i, channel in enumerate(use_channels):
591598
location = ops[i].resource.get_absolute_location() + ops[i].resource.center()
592599
x_positions[channel] = int((location.x - 100) * 10)
593-
y_positions[channel] = int((345 - location.y) * 10) # TODO: verify
600+
y_positions[channel] = int((346.5 - location.y) * 10) # TODO: verify
594601

595602
par = ops[i].resource.parent
596603
if not isinstance(par, (TecanPlate, TecanTipRack)):
@@ -634,11 +641,11 @@ def _aspirate_airgap(
634641
assert tlc is not None
635642
pvl[channel] = 0
636643
if airgap == "lag":
637-
sep[channel] = int(tlc.aspirate_lag_speed * 6) # 12? TODO: verify step unit
638-
ppr[channel] = int(tlc.aspirate_lag_volume * 3) # 6?
644+
sep[channel] = int(tlc.aspirate_lag_speed * 12) # 6? TODO: verify step unit
645+
ppr[channel] = int(tlc.aspirate_lag_volume * 6) # 3?
639646
elif airgap == "tag":
640-
sep[channel] = int(tlc.aspirate_tag_speed * 6) # 12?
641-
ppr[channel] = int(tlc.aspirate_tag_volume * 3) # 6?
647+
sep[channel] = int(tlc.aspirate_tag_speed * 12) # 6?
648+
ppr[channel] = int(tlc.aspirate_tag_volume * 6) # 3?
642649

643650
return pvl, sep, ppr
644651

@@ -703,9 +710,9 @@ def _aspirate_action(
703710
tlc = tecan_liquid_classes[i]
704711
z = zadd[channel]
705712
assert tlc is not None and z is not None
706-
sep[channel] = int(tlc.aspirate_speed * 6) # 12?
713+
sep[channel] = int(tlc.aspirate_speed * 12) # 6?
707714
ssz[channel] = round(z * tlc.aspirate_speed / ops[i].volume)
708-
mtr[channel] = round(ops[i].volume * 3) # 6?
715+
mtr[channel] = round(ops[i].volume * 6) # 3?
709716
ssz_r[channel] = int(tlc.aspirate_retract_speed * 10)
710717

711718
return ssz, sep, stz, mtr, ssz_r
@@ -733,29 +740,49 @@ def _dispense_action(
733740
for i, channel in enumerate(use_channels):
734741
tlc = tecan_liquid_classes[i]
735742
assert tlc is not None
736-
sep[channel] = int(tlc.dispense_speed * 6) # 12?
737-
spp[channel] = int(tlc.dispense_breakoff * 6) # 12?
743+
sep[channel] = int(tlc.dispense_speed * 12) # 6?
744+
spp[channel] = int(tlc.dispense_breakoff * 12) # 6?
738745
stz[channel] = 0
739-
mtr[channel] = -round(ops[i].volume * 3) # 6?
746+
mtr[channel] = -round(ops[i].volume * 6) # 3?
740747

741748
return sep, spp, stz, mtr
742749

743750
def _roma_positions(
744751
self,
745752
resource: Resource,
746-
offset: Coordinate
747-
) -> Tuple[int, int, int]:
753+
offset: Coordinate,
754+
z_range: int
755+
) -> Tuple[int, int, Dict[str, int]]:
748756
""" Creates x, y, and z positions used by RoMa ops. """
749757

750-
center = offset + resource.center()
751-
return int((center.x - 100)* 10) + 1240, int((344.5 - center.y) * 10), int(center.z * 10) + 17
758+
par = resource.parent
759+
if par is None:
760+
raise ValueError(f"Operation is not supported by resource {resource}.")
761+
par = par.parent
762+
if not isinstance(par, TecanPlateCarrier):
763+
raise ValueError(f"Operation is not supported by resource {par}.")
764+
765+
if par.roma_x is None or par.roma_y is None or par.roma_z_safe is None \
766+
or par.roma_z_travel is None or par.roma_z_end is None:
767+
raise ValueError(f"Operation is not supported by resource {par}.")
768+
x_position = int((offset.x - 100)* 10 + par.roma_x)
769+
y_position = int((347.1 - (offset.y + resource.get_size_y())) * 10 + par.roma_y)
770+
z_positions = {
771+
"safe": z_range - int(par.roma_z_safe),
772+
"travel": z_range - int(par.roma_z_travel - offset.z * 10),
773+
"end": z_range - int(par.roma_z_end - offset.z * 10)
774+
}
775+
776+
return x_position, y_position, z_positions
752777

753778

754779
class EVOArm:
755780
"""
756-
Provides firmware commands for EVO arms
781+
Provides firmware commands for EVO arms. Caches arm positions.
757782
"""
758783

784+
_pos_cache: Dict[str, int] = {}
785+
759786
def __init__(
760787
self,
761788
backend: EVO,
@@ -791,17 +818,6 @@ async def report_y_param(self, param: int) -> List[int]:
791818
command="RPY", params=[param]))["data"]
792819
return resp
793820

794-
async def report_z_param(self, param: int) -> List[int]:
795-
""" Report current parameters for z-axis.
796-
797-
Args:
798-
param: 0 - current position, 5 - actual machine range
799-
"""
800-
801-
resp: List[int] = (await self.backend.send_command(module=self.module,
802-
command="RPZ", params=[param]))["data"]
803-
return resp
804-
805821

806822
class LiHa(EVOArm):
807823
"""
@@ -816,6 +832,17 @@ async def initialize_plunger(self, tips):
816832
"""
817833
await self.backend.send_command(module=self.module, command="PID", params=[tips])
818834

835+
async def report_z_param(self, param: int) -> List[int]:
836+
""" Report current parameters for z-axis.
837+
838+
Args:
839+
param: 0 - current position, 5 - actual machine range
840+
"""
841+
842+
resp: List[int] = (await self.backend.send_command(module=self.module,
843+
command="RPZ", params=[param]))["data"]
844+
return resp
845+
819846
async def report_number_tips(self) -> int:
820847
""" Report number of tips on arm. """
821848

@@ -832,10 +859,22 @@ async def position_absolute_all_axis(self, x: int, y: int, ys: int, z: List[int]
832859
ys: absolute y spacing in 1/10 mm, must be between 90 and 380
833860
z: absolute z position in 1/10 mm for each channel, must be in
834861
allowed machine range
862+
863+
Raises:
864+
TecanError: if moving to the target position causes a collision
835865
"""
836866

867+
cur_x = EVOArm._pos_cache.setdefault(self.module, await self.report_x_param(0))
868+
for module, pos in EVOArm._pos_cache.items():
869+
if module == self.module:
870+
continue
871+
if cur_x < pos < x or x < pos < cur_x or abs(pos - x) < 1500:
872+
raise TecanError("Invalid command (collision)", self.module, 2)
873+
837874
await self.backend.send_command(module=self.module, command="PAA", params=list([x, y, ys] + z))
838875

876+
EVOArm._pos_cache[self.module] = x
877+
839878
async def position_valve_logical(self, param: List[Optional[int]]):
840879
""" Position valve logical for each channel.
841880
@@ -1021,6 +1060,17 @@ class RoMa(EVOArm):
10211060
Provides firmware commands for the RoMa plate robot
10221061
"""
10231062

1063+
async def report_z_param(self, param: int) -> int:
1064+
""" Report current parameter for z-axis.
1065+
1066+
Args:
1067+
param: 0 - current position, 5 - actual machine range
1068+
"""
1069+
1070+
resp: List[int] = (await self.backend.send_command(module=self.module,
1071+
command="RPZ", params=[param]))["data"]
1072+
return resp[0]
1073+
10241074
async def report_r_param(self, param: int) -> int:
10251075
""" Report current parameter for r-axis.
10261076
@@ -1095,10 +1145,10 @@ async def set_fast_speed_r(self, speed: Optional[int], accel: Optional[int] = No
10951145
async def set_vector_coordinate_position(
10961146
self,
10971147
v: int,
1098-
x: Optional[int],
1099-
y: Optional[int],
1100-
z: Optional[int],
1101-
r: Optional[int],
1148+
x: int,
1149+
y: int,
1150+
z: int,
1151+
r: int,
11021152
g: Optional[int],
11031153
speed: int,
11041154
tw: int = 0
@@ -1114,8 +1164,18 @@ async def set_vector_coordinate_position(
11141164
g: aboslute g position in 1/10 mm
11151165
speed: speed select, 0 - slow, 1 - fast
11161166
tw: target window class, set with STW
1167+
1168+
Raises:
1169+
TecanError: if moving to the target position causes a collision
11171170
"""
11181171

1172+
cur_x = EVOArm._pos_cache.setdefault(self.module, await self.report_x_param(0))
1173+
for module, pos in EVOArm._pos_cache.items():
1174+
if module == self.module:
1175+
continue
1176+
if cur_x < pos < x or x < pos < cur_x or abs(pos - x) < 1500:
1177+
raise TecanError("Invalid command (collision)", self.module, 2)
1178+
11191179
await self.backend.send_command(module=self.module, command="SAA",
11201180
params=[v, x, y, z, r, g, speed, 0, tw])
11211181

@@ -1124,6 +1184,8 @@ async def action_move_vector_coordinate_position(self):
11241184

11251185
await self.backend.send_command(module=self.module, command="AAC")
11261186

1187+
EVOArm._pos_cache[self.module] = await self.report_x_param(0)
1188+
11271189
async def position_absolute_g(self, g: int):
11281190
""" Position absolute for G-axis
11291191

0 commit comments

Comments
 (0)