3131READY = "k"
3232CARTRIDGE_EJECT = "c"
3333CARTRIDGE_LOAD = "C"
34- TRAY_EXTEND = "t"
35- TRAY_RETRACT = "T"
34+ # Newer-firmware tray opcodes, intentionally not sent until verified there:
35+ # TRAY_EXTEND = "t"
36+ # TRAY_RETRACT = "T"
3637TRAY_STEP_OUT = "s"
3738TRAY_STEP_IN = "S"
3839CARTRIDGE_COUNTER_RESET = "X"
@@ -457,22 +458,35 @@ class FluidXIntelliXcap96:
457458 to select a compatible cartridge.
458459
459460 Single-character commands, each written followed by ETX:
460- a request status e extended status bitmask
461- h start decapping E cartridge profile settings
462- i start recapping V firmware versions
463- b release held caps into a carrier N cartridge profile/cycles/serial
464- f open the tray 8 latched error code
465- g close the tray c eject the cartridge onto the tray
466- Z home all axes C load the cartridge from the tray
467- j enter standby X reset the cartridge cycle counter
468- k leave standby (ready) t extend the tray to S127
469- l light curtain detection off T retract the tray to S3
470- L light curtain detection on s step the tray out by S88
471- d dry-run mode on S step the tray in by S88
472- D dry-run mode off Q force a decap retry
473- + safety door on 5 eject held caps (manual mode)
474- - safety door off 6 home the cap head (manual mode)
475- z initialize keeping caps on pins 7 open the safety door (manual mode)
461+ a request status
462+ b release held caps into a carrier
463+ c eject the cartridge onto the tray
464+ d dry-run mode on
465+ D dry-run mode off
466+ e extended status bitmask
467+ E cartridge profile settings
468+ f open the tray
469+ g close the tray
470+ h start decapping
471+ i start recapping
472+ j enter standby
473+ k leave standby (ready)
474+ l light curtain detection off
475+ L light curtain detection on
476+ N cartridge profile/cycles/serial
477+ Q force a decap retry
478+ s step the tray out by S88
479+ S step the tray in by S88
480+ V firmware versions
481+ X reset the cartridge cycle counter
482+ Z home all axes
483+ z initialize keeping caps on pins
484+ + safety door on
485+ - safety door off
486+ 5 eject held caps (manual mode)
487+ 6 home the cap head (manual mode)
488+ 7 open the safety door (manual mode)
489+ 8 latched error code
476490
477491 A command is answered with an ACK frame (0x06), a ``<cmd>OK`` echo frame, and a
478492 result frame. The status word, in the priority the firmware reports it, is
@@ -495,11 +509,17 @@ class FluidXIntelliXcap96:
495509 an operation issued while the device is latched in error homes to recover and
496510 then proceeds.
497511
498- Verified against hardware: connection, status, tray open/close, home,
499- standby/ready, the decap error/recovery path, and decap/recap with a loaded
500- 0.5 mL rack, including release of held caps with ``waste``. The remaining
501- commands are implemented from the RS232 command list (Azenta part 386063 Rev.
502- A) and have not been exercised on an instrument.
512+ Newer firmware documents ``t``/``T`` commands for absolute tray travel
513+ between S3 and S127. Unit firmware V49 ignored both commands completely, so
514+ :meth:`extend_tray` and :meth:`retract_tray` raise
515+ :class:`NotImplementedError` without writing to the instrument.
516+
517+ Verified against hardware: connection, queries, tray open/close, home,
518+ standby/ready, settings, cartridge eject/load, the decap error/recovery path,
519+ forced decap retry, and decap/recap with a loaded 0.5 mL rack, including
520+ release of held caps with ``waste``. Unit firmware V49 did not acknowledge
521+ the four tray travel commands. The cartridge counter reset and manual
522+ recovery commands have not been exercised on an instrument.
503523
504524 See the Azenta IntelliXcap user manual for the required carrier and physical
505525 setup:
@@ -547,10 +567,10 @@ def __init__(
547567 async def setup (self ) -> None :
548568 await self .io .setup ()
549569 logger .warning (
550- "[IntelliXcap96 %s] most of this driver's commands are implemented from the RS232 command "
551- "list and have not been run on an instrument. The queries, tray open/close, home, "
552- "standby/ready and decap/recap/waste are hardware-verified; the tray travel, cartridge, "
553- "settings and manual recovery commands are not. Please report back so this can be updated ." ,
570+ "[IntelliXcap96 %s] connection, queries, tray open/close, home, standby/ready, settings, "
571+ "cartridge eject/load, forced decap retry and decap/recap/waste are hardware-verified. "
572+ "Unit firmware V49 does not acknowledge the tray travel commands. The cartridge counter "
573+ "reset and manual recovery commands are not hardware-verified ." ,
554574 self .io .port ,
555575 )
556576 status = await self .request_status ()
@@ -920,6 +940,8 @@ async def _tray_move(self, command: str, name: str, timeout: float) -> None:
920940
921941 These acknowledge and answer with the same ``<cmd>OK`` frame, so a completed
922942 move is a second echo and a failed one is ``<cmd>ERROR``.
943+
944+ Unit firmware V49 does not acknowledge these commands.
923945 """
924946 await self ._ensure_ready ()
925947 echo = f"{ command } OK"
@@ -934,14 +956,26 @@ async def _tray_move(self, command: str, name: str, timeout: float) -> None:
934956 async def extend_tray (self , timeout : float = 15.0 ) -> None :
935957 """Move the tray from the load position (S3) out to the extended position (S127).
936958
937- Presents decapped tubes further out of the instrument. Fails unless the tray
938- is at the load position, so open the tray first.
959+ This command is documented for newer firmware but is not implemented here:
960+ unit firmware V49 ignored it completely. The method raises without writing
961+ to the instrument.
939962 """
940- await self ._tray_move (TRAY_EXTEND , "Extending the tray" , timeout )
963+ raise NotImplementedError (
964+ "extend_tray() uses the newer-firmware 't' command, which is not supported by "
965+ "IntelliXcap unit firmware V49"
966+ )
941967
942968 async def retract_tray (self , timeout : float = 15.0 ) -> None :
943- """Move the tray from the extended position (S127) back to the load position (S3)."""
944- await self ._tray_move (TRAY_RETRACT , "Retracting the tray" , timeout )
969+ """Move the tray from the extended position (S127) back to the load position (S3).
970+
971+ This command is documented for newer firmware but is not implemented here:
972+ unit firmware V49 ignored it completely. The method raises without writing
973+ to the instrument.
974+ """
975+ raise NotImplementedError (
976+ "retract_tray() uses the newer-firmware 'T' command, which is not supported by "
977+ "IntelliXcap unit firmware V49"
978+ )
945979
946980 async def step_tray_out (self , timeout : float = 15.0 ) -> None :
947981 """Move the tray further out by the step distance in setpoint 88.
@@ -1100,7 +1134,9 @@ async def retry_decap(self, timeout: float = 60.0) -> None:
11001134 The command list documents only that this command is used "after a
11011135 successful Decap" and "requires lightcurtain OFF". That those are the same
11021136 condition is inferred from the automatic-retry and error-detection commands
1103- rather than stated by the vendor.
1137+ rather than stated by the vendor. The instrument finishes in
1138+ ``StatusRECAP`` because it is still holding the caps for a subsequent
1139+ :meth:`recap`.
11041140 """
11051141 await self ._ensure_ready ()
11061142 frames = await self .send_command (RETRY_DECAP )
@@ -1110,7 +1146,7 @@ async def retry_decap(self, timeout: float = 60.0) -> None:
11101146 timeout ,
11111147 "Retrying the decap" ,
11121148 "RetryDecapWasNotSuccesful" ,
1113- ("StatusOK " ,),
1149+ ("StatusRECAP " ,),
11141150 done_frames = ("DecapDONE" ,),
11151151 )
11161152 logger .info ("[IntelliXcap96 %s] decap retry complete" , self .io .port )
@@ -1262,8 +1298,8 @@ async def reset_cartridge_counter(self) -> None:
12621298 """Reset the installed cartridge's cycle counter to zero."""
12631299 name = "Resetting the cartridge counter"
12641300 frames = await self .send_command (CARTRIDGE_COUNTER_RESET )
1265- self ._require_accepted (CARTRIDGE_COUNTER_RESET , frames , name , idempotent = True )
1266- self ._require_answer (frames , "CarResetDONE" , name )
1301+ if self ._require_accepted (CARTRIDGE_COUNTER_RESET , frames , name , idempotent = True ):
1302+ self ._require_answer (frames , "CarResetDONE" , name )
12671303 logger .info ("[IntelliXcap96 %s] cartridge counter reset" , self .io .port )
12681304
12691305 # === Settings ===
@@ -1278,8 +1314,8 @@ async def set_error_detection_enabled(self, enabled: bool) -> None:
12781314 command = ERROR_DETECTION_ON if enabled else ERROR_DETECTION_OFF
12791315 name = "Enabling error detection" if enabled else "Disabling error detection"
12801316 frames = await self .send_command (command )
1281- self ._require_accepted (command , frames , name , idempotent = True )
1282- self ._require_answer (frames , "ErrorDetectON" if enabled else "ErrorDetectOFF" , name )
1317+ if self ._require_accepted (command , frames , name , idempotent = True ):
1318+ self ._require_answer (frames , "ErrorDetectON" if enabled else "ErrorDetectOFF" , name )
12831319 logger .info ("[IntelliXcap96 %s] error detection %s" , self .io .port , "on" if enabled else "off" )
12841320
12851321 async def set_dry_run_enabled (self , enabled : bool ) -> None :
@@ -1293,8 +1329,8 @@ async def set_dry_run_enabled(self, enabled: bool) -> None:
12931329 command = DRY_RUN_ON if enabled else DRY_RUN_OFF
12941330 name = "Enabling dry-run mode" if enabled else "Disabling dry-run mode"
12951331 frames = await self .send_command (command )
1296- self ._require_accepted (command , frames , name , idempotent = True )
1297- self ._require_answer (frames , "DryRunON" if enabled else "DryRunOFF" , name )
1332+ if self ._require_accepted (command , frames , name , idempotent = True ):
1333+ self ._require_answer (frames , "DryRunON" if enabled else "DryRunOFF" , name )
12981334 logger .info ("[IntelliXcap96 %s] dry-run mode %s" , self .io .port , "on" if enabled else "off" )
12991335
13001336 async def set_safety_door_enabled (self , enabled : bool ) -> None :
@@ -1306,8 +1342,8 @@ async def set_safety_door_enabled(self, enabled: bool) -> None:
13061342 command = SAFETY_DOOR_ENABLE if enabled else SAFETY_DOOR_DISABLE
13071343 name = "Enabling the safety door" if enabled else "Disabling the safety door"
13081344 frames = await self .send_command (command )
1309- self ._require_accepted (command , frames , name , idempotent = True )
1310- self ._require_answer (frames , "DoorONDONE" if enabled else "DoorOFFDONE" , name )
1345+ if self ._require_accepted (command , frames , name , idempotent = True ):
1346+ self ._require_answer (frames , "DoorONDONE" if enabled else "DoorOFFDONE" , name )
13111347 logger .info ("[IntelliXcap96 %s] safety door %s" , self .io .port , "on" if enabled else "off" )
13121348
13131349 # === Manual recovery ===
0 commit comments