Skip to content
15 changes: 15 additions & 0 deletions docs/api/pylabrobot.azenta.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,18 @@ XPeel Peeler

.. autoclass:: pylabrobot.azenta.xpeel.XPeelPeelerBackend.PeelParams
:members:


FluidX IntelliXcap 96
---------------------

.. currentmodule:: pylabrobot.azenta.fluidx.intellixcap96

.. autosummary::
:toctree: _autosummary
:nosignatures:
:recursive:

FluidXIntelliXcap96
FluidXError
get_error_message
264 changes: 264 additions & 0 deletions docs/user_guide/azenta/fluidx/intellixcap96/hello-world.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# FluidX IntelliXcap 96\n",
"\n",
"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.\n",
"\n",
"## Resources\n",
"\n",
"- [Azenta IntelliXcap user manual](https://web.azenta.com/hubfs/azenta-files/resources/manuals-guides/319430-IXC-User-Manual.pdf)\n",
"- [Azenta IntelliXcap 96 product page](https://www.azenta.com/products/intellixcap-automated-screw-cap-decapper-recapper-96-format)\n",
"\n",
"| Property | Value |\n",
"| --- | --- |\n",
"| Communication | Serial, STX/ETX-framed ASCII |\n",
"| Serial settings | 9600 baud, 8 data bits, no parity, 1 stop bit, no handshake |\n",
"| Framing | each reply is wrapped in STX (`0x02`) .. ETX (`0x03`) |\n",
"| Reply pattern | ACK (`0x06`), then `<cmd>OK` echo, then a result frame |\n",
"\n",
"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`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Physical setup\n\n",
"\n\n",
"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`.\n\n",
"\n\n",
"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](https://web.azenta.com/hubfs/azenta-files/resources/manuals-guides/319430-IXC-User-Manual.pdf) for cartridge/profile setup and compatibility guidance.\n\n",
"\n\n",
"```{important}\n\n",
"If the instrument comes up reporting `StatusBUSY` and ignores commands, it is\n\n",
"locked out. The most common cause is an **engaged e-stop**; the safety guard/hood\n\n",
"and other interlocks do the same. There is no command to read the e-stop\n\n",
"directly, so `setup()` raises with this hint when it sees `StatusBUSY` at connect.\n\n",
"Clear the e-stop and retry.\n\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Connect\n\n",
"\n\n",
"`setup()` opens the serial port and reads the status. It raises if the device is not ready (e.g. e-stop)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pylabrobot.azenta.fluidx import FluidXIntelliXcap96\n\n",
"\n\n",
"decapper = FluidXIntelliXcap96(port=\"/dev/ttyUSB0\") # replace with your port\n\n",
"await decapper.setup()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Status\n\n",
"\n\n",
"`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), or `StatusMANUAL` (the instrument requires inspection and touchscreen recovery). Note the status word does **not** encode the tray position."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"status:\", await decapper.request_status())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Error codes\n",
"\n",
"The legacy serial protocol may report a generic operation failure such as `DecapERROR` without including the instrument's numeric error code. When a reply does include a known code, `FluidXError` decodes it automatically and exposes it as `error_code`. If the code is available separately from the instrument error log, look up its documented meaning with `get_error_message()` or construct the same exception with `FluidXError.from_error_code()`.\n",
"\n",
"```python\n",
"from pylabrobot.azenta.fluidx import FluidXError, get_error_message\n",
"\n",
"print(get_error_message(145))\n",
"# Light curtain calibration max retries exceeded.\n",
"\n",
"error = FluidXError.from_error_code(145)\n",
"print(error)\n",
"# IntelliXcap error 145: Light curtain calibration max retries exceeded.\n",
"```\n",
"\n",
"The complete local mapping comes from the error table in the [official Azenta IntelliXcap user manual](https://web.azenta.com/hubfs/azenta-files/resources/manuals-guides/319430-IXC-User-Manual.pdf)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Tray\n",
"\n",
"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`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await decapper.open_tray()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await decapper.close_tray()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Home\n\n",
"\n\n",
"Home all axes."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await decapper.home()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Decap, recap, waste\n",
"\n",
"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.\n",
"\n",
"A fault (e.g. decapping with no rack loaded) can latch the device in `StatusError`, and the failing call raises a `FluidXError` carrying the firmware's message. A latched error is cleared **only by homing** -- the reset command is ignored on this firmware. By default (`auto_recover=True`) the next operation you issue homes to clear `StatusError` 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.\n",
"\n",
"```{warning}\n",
"`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](https://web.azenta.com/hubfs/azenta-files/resources/manuals-guides/319430-IXC-User-Manual.pdf), which defines waste as dropping caps into a carrier.\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# After inspecting the rack/head and confirming motion is safe:\n",
"await decapper.reset_error()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await decapper.decap() # unscrew and hold all 96 caps"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Choose this path to retain the caps.\n",
"await decapper.recap() # screw and torque the held caps back on"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Alternative to recap: with caps held, remove the tube rack and position\n",
"# the correct cap carrier first. Do not run this after recap.\n",
"await decapper.waste() # irreversibly release held caps into the carrier"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Standby\n\n",
"\n\n",
"Put the decapper to sleep with `standby()` and wake it with `ready()`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await decapper.standby()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await decapper.ready()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Teardown\n\n",
"\n\n",
"`stop()` closes the serial connection."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await decapper.stop()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
1 change: 1 addition & 0 deletions docs/user_guide/azenta/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

a4s/hello-world
xpeel/hello-world
fluidx/intellixcap96/hello-world
```
1 change: 1 addition & 0 deletions pylabrobot/azenta/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .a4s import A4S, A4SDriver, A4SSealerBackend, A4SStatus, A4STemperatureBackend
from .fluidx import ERROR_CODE_MESSAGES, FluidXError, FluidXIntelliXcap96, get_error_message
from .xpeel import XPeel, XPeelDriver, XPeelPeelerBackend
6 changes: 6 additions & 0 deletions pylabrobot/azenta/fluidx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .intellixcap96 import (
ERROR_CODE_MESSAGES,
FluidXError,
FluidXIntelliXcap96,
get_error_message,
)
Loading
Loading