44import time
55from typing import List , Literal , Optional
66
7+ from pylabrobot .resources .plate import Plate
8+
79try :
810 from pylibftdi import Device
911
@@ -126,6 +128,7 @@ def __init__(self, timeout: float = 20, camera_serial_number: Optional[float] =
126128 self .camera_serial_number = camera_serial_number
127129 self .max_image_read_attempts = 8
128130
131+ self ._plate : Optional [Plate ] = None
129132 self ._exposure : Optional [Exposure ] = None
130133 self ._focal_height : Optional [FocalPosition ] = None
131134 self ._gain : Optional [Gain ] = None
@@ -137,10 +140,16 @@ async def setup(self, use_cam: bool = False) -> None:
137140 logger .info ("[cytation5] setting up" )
138141
139142 self .dev .open ()
140- # self.dev.baudrate = 9600 # worked in the past
141- self .dev .baudrate = 38400
143+ self .dev .ftdi_fn .ftdi_usb_reset ()
144+ self .dev .ftdi_fn .ftdi_set_latency_timer (16 ) # 0x10
145+
146+ self .dev .baudrate = 9600 # 0x38 0x41
147+ # self.dev.baudrate = 38400
142148 self .dev .ftdi_fn .ftdi_set_line_property (8 , 2 , 0 ) # 8 bits, 2 stop bits, no parity
143149 SIO_RTS_CTS_HS = 0x1 << 8
150+ self .dev .ftdi_fn .ftdi_setdtr (1 )
151+ self .dev .ftdi_fn .ftdi_setrts (1 )
152+
144153 self .dev .ftdi_fn .ftdi_setflowctrl (SIO_RTS_CTS_HS )
145154 self .dev .ftdi_fn .ftdi_setrts (1 )
146155
@@ -265,31 +274,36 @@ async def _read_until(self, char: bytes, timeout: Optional[float] = None) -> byt
265274 return res
266275
267276 async def send_command (
268- self , command : str , parameter : Optional [str ] = None , wait_for_response = True
277+ self ,
278+ command : str ,
279+ parameter : Optional [str ] = None ,
280+ wait_for_response = True ,
281+ timeout : Optional [float ] = None ,
269282 ) -> Optional [bytes ]:
270283 await self ._purge_buffers ()
271284 self .dev .write (command .encode ())
272285 logger .debug ("[cytation5] sent %s" , command )
273286 response : Optional [bytes ] = None
274287 if wait_for_response or parameter is not None :
275- # print("reading until", b"\x06" if parameter is not None else b"\x03")
276- response = await self ._read_until (b"\x06 " if parameter is not None else b"\x03 " )
288+ response = await self ._read_until (
289+ b"\x06 " if parameter is not None else b"\x03 " , timeout = timeout
290+ )
277291
278292 if parameter is not None :
279293 self .dev .write (parameter .encode ())
280294 logger .debug ("[cytation5] sent %s" , parameter )
281295 if wait_for_response :
282- response = await self ._read_until (b"\x03 " )
296+ response = await self ._read_until (b"\x03 " , timeout = timeout )
283297
284298 return response
285299
286300 async def get_serial_number (self ) -> str :
287- resp = await self .send_command ("C" )
301+ resp = await self .send_command ("C" , timeout = 1 )
288302 assert resp is not None
289303 return resp [1 :].split (b" " )[0 ].decode ()
290304
291305 async def get_firmware_version (self ) -> str :
292- resp = await self .send_command ("e" )
306+ resp = await self .send_command ("e" , timeout = 1 )
293307 assert resp is not None
294308 return " " .join (resp [1 :- 1 ].decode ().split (" " )[0 :4 ])
295309
@@ -323,11 +337,62 @@ def _parse_body(self, body: bytes) -> List[List[float]]:
323337 parsed_data [row_idx ].append (value )
324338 return parsed_data
325339
326- async def read_absorbance (self , wavelength : int ) -> List [List [float ]]:
340+ async def set_plate (self , plate : Plate ):
341+ """
342+ 08120112207434014351135308559127881422
343+ ^^^^ plate size z
344+ ^^^^^ plate size x
345+ ^^^^^ plate size y
346+ ^^^^^ bottom right x
347+ ^^^^^ top left x
348+ ^^^^^ bottom right y
349+ ^^^^^ top left y
350+ ^^ columns
351+ ^^ rows
352+ """
353+
354+ if plate is self ._plate :
355+ return
356+
357+ rows = plate .num_items_y
358+ columns = plate .num_items_x
359+
360+ bottom_right_well = plate .get_item (plate .num_items - 1 )
361+ assert bottom_right_well .location is not None
362+ bottom_right_well_center = bottom_right_well .location + bottom_right_well .get_anchor (
363+ x = "c" , y = "c"
364+ )
365+ top_left_well = plate .get_item (0 )
366+ assert top_left_well .location is not None
367+ top_left_well_center = top_left_well .location + top_left_well .get_anchor (x = "c" , y = "c" )
368+
369+ plate_size_y = plate .get_size_y ()
370+ plate_size_x = plate .get_size_x ()
371+ plate_size_z = plate .get_size_z ()
372+
373+ top_left_well_center_y = plate .get_size_y () - top_left_well_center .y # invert y axis
374+ bottom_right_well_center_y = plate .get_size_y () - bottom_right_well_center .y # invert y axis
375+
376+ cmd = (
377+ f"{ rows :02} "
378+ f"{ columns :02} "
379+ f"{ int (top_left_well_center_y * 100 ):05} "
380+ f"{ int (bottom_right_well_center_y * 100 ):05} "
381+ f"{ int (top_left_well_center .x * 100 ):05} "
382+ f"{ int (bottom_right_well_center .x * 100 ):05} "
383+ f"{ int (plate_size_y * 100 ):05} "
384+ f"{ int (plate_size_x * 100 ):05} "
385+ f"{ int (plate_size_z * 100 ):04} "
386+ "\x03 "
387+ )
388+
389+ return await self .send_command ("y" , cmd )
390+
391+ async def read_absorbance (self , plate : Plate , wavelength : int ) -> List [List [float ]]:
327392 if not 230 <= wavelength <= 999 :
328393 raise ValueError ("Wavelength must be between 230 and 999" )
329394
330- await self .send_command ( "y" , "08120112207434014351135308559127881772 \x03 " )
395+ await self .set_plate ( plate )
331396
332397 wavelength_str = str (wavelength ).zfill (4 )
333398 cmd = f"00470101010812000120010000110010000010600008{ wavelength_str } 1"
@@ -343,14 +408,14 @@ async def read_absorbance(self, wavelength: int) -> List[List[float]]:
343408 assert resp is not None
344409 return self ._parse_body (body )
345410
346- async def read_luminescence (self , focal_height : float ) -> List [List [float ]]:
411+ async def read_luminescence (self , plate : Plate , focal_height : float ) -> List [List [float ]]:
347412 if not 4.5 <= focal_height <= 13.88 :
348413 raise ValueError ("Focal height must be between 4.5 and 13.88" )
349414
350415 cmd = f"3{ 14220 + int (1000 * focal_height )} \x03 "
351416 await self .send_command ("t" , cmd )
352417
353- await self .send_command ( "y" , "08120112207434014351135308559127881772 \x03 " )
418+ await self .set_plate ( plate )
354419
355420 cmd = "008401010108120001200100001100100000123000500200200-001000-00300000000000000000001351092"
356421 await self .send_command ("D" , cmd )
@@ -364,6 +429,7 @@ async def read_luminescence(self, focal_height: float) -> List[List[float]]:
364429
365430 async def read_fluorescence (
366431 self ,
432+ plate : Plate ,
367433 excitation_wavelength : int ,
368434 emission_wavelength : int ,
369435 focal_height : float ,
@@ -378,7 +444,7 @@ async def read_fluorescence(
378444 cmd = f"{ 614220 + int (1000 * focal_height )} \x03 "
379445 await self .send_command ("t" , cmd )
380446
381- await self .send_command ( "y" , "08120112207434014351135308559127881772 \x03 " )
447+ await self .set_plate ( plate )
382448
383449 excitation_wavelength_str = str (excitation_wavelength ).zfill (4 )
384450 emission_wavelength_str = str (emission_wavelength ).zfill (4 )
@@ -512,14 +578,14 @@ async def led_off(self):
512578 async def set_focus (self , focal_position : FocalPosition ):
513579 """focus position in mm"""
514580
515- if focal_position == self ._focal_height :
516- logger .debug ("Focus position is already set to %s" , focal_position )
517- return
518-
519581 if focal_position == "auto" :
520582 await self .auto_focus ()
521583 return
522584
585+ if focal_position == self ._focal_height :
586+ logger .debug ("Focus position is already set to %s" , focal_position )
587+ return
588+
523589 # There is a difference between the number in the program and the number sent to the machine,
524590 # which is modelled using the following linear relation. R^2=0.999999999
525591 # convert from mm to um
@@ -534,6 +600,9 @@ async def set_focus(self, focal_position: FocalPosition):
534600 self ._focal_height = focal_position
535601
536602 async def auto_focus (self , timeout : float = 30 ):
603+ plate = self ._plate
604+ if plate is None :
605+ raise RuntimeError ("Plate not set. Run set_plate() first." )
537606 imaging_mode = self ._imaging_mode
538607 if imaging_mode is None :
539608 raise RuntimeError ("Imaging mode not set. Run set_imaging_mode() first." )
@@ -557,6 +626,7 @@ async def auto_focus(self, timeout: float = 30):
557626 # objective function: variance of laplacian
558627 async def evaluate_focus (focus_value ):
559628 image = await self .capture (
629+ plate = plate ,
560630 row = row ,
561631 column = column ,
562632 mode = imaging_mode ,
@@ -769,6 +839,7 @@ async def capture(
769839 exposure_time : Exposure ,
770840 focal_height : FocalPosition ,
771841 gain : Gain ,
842+ plate : Plate ,
772843 color_processing_algorithm : int = SPINNAKER_COLOR_PROCESSING_ALGORITHM_HQ_LINEAR ,
773844 pixel_format : int = PixelFormat_Mono8 ,
774845 ) -> List [List [float ]]:
@@ -788,6 +859,8 @@ async def capture(
788859 if self .cam is None :
789860 raise ValueError ("Camera not initialized. Run setup(use_cam=True) first." )
790861
862+ await self .set_plate (plate )
863+
791864 await self .select (row , column )
792865 await self .set_imaging_mode (mode )
793866 await self .set_exposure (exposure_time )
0 commit comments