FluidX IntelliXcap 96#

The FluidX IntelliXcap 96 (an Azenta brand) is an automated screw-cap decapper. It unscrews, holds, and screws back on all 96 caps of a screw-cap tube rack in a single stroke. A plate mover loads the rack onto its one nest.

Resources#

Property

Value

Communication

Serial, STX/ETX-framed ASCII

Serial settings

9600 baud, 8 data bits, no parity, 1 stop bit, no handshake

Framing

each reply is wrapped in STX (0x02) .. ETX (0x03)

Reply pattern

ACK (0x06), then <cmd>OK echo, then a result frame

Every command is a single character written followed by ETX. A motion command is accepted with an ACK and a <cmd>OK echo; the status word then reads StatusBUSY while it moves. The terminal state depends on the operation: decap finishes at StatusRECAP while the head holds caps, whereas recap and waste finish at StatusOK. A refused or no-op command answers CommandIgnore.

Important

The instrument only answers this protocol in IntelliXcap mode. Set setpoint 86 to 2 on the instrument touchscreen; there is no serial command for it.

Physical setup#

Connect the decapper’s serial port to your computer through its USB-to-serial adapter and use the stable by-id path so the port survives re-enumeration, e.g. /dev/serial/by-id/usb-FTDI_USB_Serial_Converter_XXXXXXXX-if00-port0.

Tube volume is not a driver setting. The installed IntelliCartridge and its firmware profile define the supported tube height, cap geometry/thread, and motion settings. Use the cartridge specified for the exact tube family: two nominally 0.5 mL tube types may require different cartridges. Cartridge IDs 1–14 load their matching profiles automatically; extended cartridges require the corresponding stored profile to be selected on the instrument. The Python commands are otherwise identical for every supported tube type. See the official Azenta IntelliXcap user manual for cartridge/profile setup and compatibility guidance.

Important

If the instrument comes up reporting StatusBUSY and ignores commands, it is locked out. The usual cause is an engaged e-stop; the safety guard/hood and other interlocks do the same. setup() reads the e-stop bit from the extended status and says whether it is actually engaged, then raises. If the e-stop reads as released, check the guard/hood and interlocks.

request_extended_status().estop_active reports the same bit at any time.

Connect#

setup() opens the serial port and reads the status. It raises if the device is not ready (e.g. e-stop).

from pylabrobot.azenta.fluidx import FluidXIntelliXcap96



decapper = FluidXIntelliXcap96(port="/dev/ttyUSB0")  # replace with your port

await decapper.setup()

Status#

request_status() returns the current status word: StatusOK (idle with no caps held), StatusBUSY (moving), StatusRECAP (decapped; caps held and ready to recap or waste), StatusSLEEP (standby), StatusCAREJECT (cartridge ejected), or StatusMANUAL (halted; needs inspection, then reset_error() or one of the manual recovery commands). Note the status word does not encode the tray position.

print("status:", await decapper.request_status())

Error codes#

StatusERROR and StatusMANUAL only say that something went wrong. request_error_code() asks the instrument for the numeric code behind it, and returns None when nothing is latched.

The driver does this for you: whenever an operation fails, it reads the code back and raises a FluidXError carrying it in error_code, with the documented meaning in the message. get_error_message() looks a code up by hand, and is_recoverable_error() (also on the exception, as .recoverable) says whether homing will clear it:

  • Recoverable codes latch StatusERROR, which homing clears: 113/114 (decap), 117/118 (recap), 142 (cartridge eject), 143/144 (cartridge load).

  • Every other code halts the instrument in StatusMANUAL, which needs an operator to inspect it.

The code table comes from the error list in the official Azenta IntelliXcap user manual.

from pylabrobot.azenta.fluidx import get_error_message, is_recoverable_error

code = await decapper.request_error_code()
if code is None:
    print("no error latched")
else:
    print(code, get_error_message(code), "recoverable:", is_recoverable_error(code))

What the instrument can tell you about itself#

Four queries describe the machine and its consumable:

  • request_firmware_versions() -> unit, touchscreen and light curtain firmware. Two features depend on it: dry-run mode needs touchscreen V14 or above, and waste() is broken in V44 (the command list does not say which firmware that version refers to).

  • request_cartridge_info() -> the installed cartridge’s profile, cycle count and serial. The serial always reads "00000000"; the firmware does not implement it.

  • request_profile() -> the active profile number and its decap/recap retry limits.

  • request_extended_status() -> the flags the status word leaves out, including whether caps are held on the pins, whether a cartridge is installed and whether the e-stop is engaged. caps_on_pins() is a shortcut for the first of those.

Note

The command list names twelve extended-status flags but shows an eleven-character answer. request_extended_status() rejects any width other than twelve rather than guess which end is missing, because the most significant bit is CAPS_ON_PINS. If it raises on your instrument, the raw string is in the message.

print("firmware:", await decapper.request_firmware_versions())
print("cartridge:", await decapper.request_cartridge_info())
print("profile:", await decapper.request_profile())
print("extended status:", await decapper.request_extended_status())
print("caps held:", await decapper.caps_on_pins())

Tray#

Open and close the loading tray. Asking for a position the tray is already in is a harmless no-op. Tray motion does not change whether caps are held, so after decapping the terminal state remains StatusRECAP rather than returning to StatusOK.

await decapper.open_tray()
await decapper.close_tray()

Presenting the tray further out#

Past the load position the tray can travel out to an extended position, which is useful for handing decapped tubes to an operator or another instrument. The two ends are setpoints 3 (load) and 127 (extended), and the step size is setpoint 88; all three are configured on the instrument.

  • extend_tray() / retract_tray() move between the two ends. Extending requires the tray to be at the load position, so open the tray first.

  • step_tray_out() / step_tray_in() move by one step. A step that would leave the S3..S127 range fails rather than clipping.

# t fails unless the tray is already at the load position, so open it first.
await decapper.open_tray()
await decapper.extend_tray()    # S3 -> S127, all the way out
await decapper.step_tray_in()   # one S88 step back in
await decapper.retract_tray()   # all the way back to S3

Home#

Home all axes.

await decapper.home()

Decap, recap, waste#

With a rack of screw-cap tubes loaded on the nest, decap() unscrews and holds all 96 caps. After decapping, choose one terminal operation: recap() screws those caps back onto the tubes, while waste() releases them into a separately positioned cap carrier. decap() refuses if caps are already held (recap or waste first); recap() refuses if none are held.

A fault (e.g. decapping with no rack loaded) can latch the device in StatusError, and the failing call raises a FluidXError carrying the instrument’s error code and its documented meaning. Homing clears a latched StatusError. By default (auto_recover=True) the next operation you issue homes to clear it and then proceeds; pass auto_recover=False to have it raise instead. StatusMANUAL requires an explicit safety decision: inspect the rack and cap head, confirm that axis motion is safe, then call reset_error(). Hardware testing confirmed that it homes the machine from StatusMANUAL through StatusBUSY to StatusOK. Normal motion commands remain blocked until recovery completes.

The command list says the initialize sequence “will not drop caps”, but it also documents a separate command for initializing without dropping them. Neither has been checked on hardware. With caps held, use initialize_keeping_caps_on_pins() (see Manual recovery below), which is the command documented for that case.

Warning

waste() is irreversible and does not detect whether a cap carrier is present. Before calling it, open the tray, remove the sample-tube rack, and position the correct cap carrier or collection vessel according to your instrument’s procedure. On hardware, leaving the tube rack beneath the head caused the released caps to fall back onto the tube openings. They looked recapped but were not guaranteed to be threaded or torqued. See the official Azenta IntelliXcap user manual, which defines waste as dropping caps into a carrier.

Firmware V44 does not implement waste() at all — the command list does not say which of the three firmwares it means. On that version, load a store rack and use decap()/recap(), which select the store sequence themselves. request_firmware_versions() reports all three.

# After inspecting the rack/head and confirming motion is safe:
await decapper.reset_error()
await decapper.decap()   # unscrew and hold all 96 caps
# Choose this path to retain the caps.
await decapper.recap()   # screw and torque the held caps back on
# Alternative to recap: with caps held, remove the tube rack and position
# the correct cap carrier first. Do not run this after recap.
await decapper.waste()   # irreversibly release held caps into the carrier

Forcing another decap attempt#

You only need this if you have turned light curtain error detection off. With detection on, the instrument spots the tubes that failed to decap and retries the stroke by itself inside decap(), up to the profile’s decap retry limit (request_profile().decap_max_retry). There is nothing left for you to force.

With detection off the instrument cannot see those tubes, so decap() returns successfully and leaves them capped. retry_decap() forces another stroke; decap() itself refuses, because caps are held. Call it as many times as you need.

Note

The command list documents only that this command is used “after a successful Decap” and “requires lightcurtain OFF”. That those are the same condition is inferred from the automatic-retry and error-detection commands rather than stated by the vendor.

await decapper.set_error_detection_enabled(False)
await decapper.retry_decap()

Changing the cartridge#

The IntelliCartridge defines which tubes the instrument can handle, so switching tube families means swapping it. eject_cartridge() puts the installed cartridge on the tray and load_cartridge() picks up the one resting there, returning the profile number the instrument loaded. Both read the extended status first, so calling them when the instrument is already in that state is a no-op.

Physically remove the ejected cartridge from the tray, and place the new one, between the two calls.

# Empty the tray and make sure no caps are held first.
await decapper.eject_cartridge()
# Place the replacement cartridge on the tray, then pick it up.
print("loaded profile:", await decapper.load_cartridge())
# Zero the cycle counter, e.g. after fitting a rebuilt cartridge.
await decapper.reset_cartridge_counter()

Settings#

Three flags change how the instrument behaves during a run.

  • set_error_detection_enabled(False) stops the light curtain from failing a decap/recap on error 135/136, so the stroke always finishes. It is also the prerequisite for retry_decap(). Detection is on at power-up.

  • set_dry_run_enabled(True) makes the instrument pause and wait for the operator on light curtain errors 114, 118, 135 and 136 instead of failing the operation. Needs touchscreen firmware V14 or above.

  • set_safety_door_enabled(False) disables the safety door, which leaves it open. Call it with True to re-enable; the door is also enabled at power-up.

Warning

Each of these weakens a safety or error check.

await decapper.set_error_detection_enabled(False)
# Requires touchscreen firmware V14 or above.
await decapper.set_dry_run_enabled(True)
# Opens the door and keeps it open; call with True to re-enable.
await decapper.set_safety_door_enabled(False)

Manual recovery#

These commands are only accepted while the instrument is halted in StatusMANUAL, and it stays there afterwards. They exist to get a stuck instrument back to a state you can inspect and clear.

Warning

All of these move axes or open the safety door. Look inside the instrument and confirm that motion is safe before running any of them.

await decapper.open_safety_door()
# Home the Z axis, keeping any held caps on the pins.
await decapper.head_up()
# Drops any caps still on the cap drivers, wherever the head is standing.
# Clear the deck first; use waste() when you want to collect them.
await decapper.eject_caps()
# Clears the error state and homes without dropping caps held on the pins.
# Follow it with recap() to put the caps back on the tubes.
await decapper.initialize_keeping_caps_on_pins()

Standby#

Put the decapper to sleep with standby() and wake it with ready().

await decapper.standby()
await decapper.ready()

Teardown#

stop() closes the serial connection.

await decapper.stop()