Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/api/pylabrobot.kbiosystems.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ pylabrobot.kbiosystems package
KBiosystemsUltrasealEPRO
UltrasealEPROStatus

.. currentmodule:: pylabrobot.kbiosystems.ultraseal_pro

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

KBiosystemsUltrasealPRO
UltrasealPROStatus

.. currentmodule:: pylabrobot.kbiosystems.ultraseal_xt_pro

.. autosummary::
Expand Down
1 change: 1 addition & 0 deletions docs/user_guide/kbiosystems/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
:maxdepth: 1

ultraseal-epro/hello-world
ultraseal-pro/hello-world
ultraseal-xt-pro/hello-world
```
98 changes: 98 additions & 0 deletions docs/user_guide/kbiosystems/ultraseal-pro/hello-world.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ultraseal-pro-intro",
"source": "# KBiosystems Ultraseal PRO (formerly WASP)\n\nThe Ultraseal PRO - formerly sold as the WASP - is an inline, pneumatically driven heat sealer for microplates from KBiosystems, controlled over an RS-232 serial interface. A plate is placed on a shuttle that extends from the front of the unit; the seal command draws the shuttle in, seals, and returns it automatically. The Ultraseal PRO advances and cuts its own film internally and has no foil-length, force, or distance parameters. It requires a compressed air supply.\n\n| Property | Value |\n|---|---|\n| [OEM link](https://kbiosystems.com/product/ultraseal-pro) (formerly the WASP) | |\n| Communication | Serial / RS-232, ASCII command protocol |\n| Serial settings | 9600 baud, 8 data bits, no parity, 1 stop bit, no handshake |\n| Terminator | CR (`\\r`) |\n| Temperature | 25-200 °C |\n| Sealing time | 0.5-9.9 s |",
"metadata": {}
},
{
"cell_type": "markdown",
"id": "ultraseal-pro-setup-md",
"source": "## Physical setup\n\nConnect the sealer's RS-232 port to your computer, typically through a USB-to-serial adapter, and connect the compressed air supply. Make sure the sealer's serial parameters match the driver defaults (9600 baud, 8 data bits, no parity, 1 stop bit, no handshake).",
"metadata": {}
},
{
"cell_type": "markdown",
"id": "ultraseal-pro-connect-md",
"source": "## Connect\n\n`setup()` opens the serial port, waits for the device to be ready, and pre-heats to the preheating temperature.",
"metadata": {}
},
{
"cell_type": "code",
"id": "ultraseal-pro-connect-code",
"source": "from pylabrobot.kbiosystems import KBiosystemsUltrasealPRO\n\nsealer = KBiosystemsUltrasealPRO(port=\"/dev/ttyUSB0\") # replace with your port\nawait sealer.setup()",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "ultraseal-pro-seal-md",
"source": "## Seal a plate\n\nPlace a plate on the extended shuttle. `seal()` applies the time and temperature, waits for the heater to reach the setpoint, then draws the shuttle in, seals the plate, and returns the shuttle to the start position automatically before returning to the idle temperature. Sealing time is 0.5-9.9 s; temperature is 25-200 °C.",
"metadata": {}
},
{
"cell_type": "code",
"id": "ultraseal-pro-seal-code",
"source": "await sealer.seal(temperature=170, duration=2.5)",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "ultraseal-pro-temp-md",
"source": "## Temperature\n\nSet the sealing temperature setpoint and read the current heater temperature.",
"metadata": {}
},
{
"cell_type": "code",
"id": "ultraseal-pro-temp-code",
"source": "await sealer.set_temperature(160)\nprint(\"Current temperature:\", await sealer.request_temperature(), \"C\")",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "ultraseal-pro-status-md",
"source": "## Status\n\nRead the status byte as a set of flags. On the Ultraseal PRO this includes `LowAir`, which indicates the compressed air supply is insufficient, and `ParkMode`, which indicates the shuttle is parked inside.",
"metadata": {}
},
{
"cell_type": "code",
"id": "ultraseal-pro-status-code",
"source": "print(\"Status:\", await sealer.request_status())",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "ultraseal-pro-teardown-md",
"source": "## Teardown",
"metadata": {}
},
{
"cell_type": "code",
"id": "ultraseal-pro-teardown-code",
"source": "await sealer.stop()",
"metadata": {},
"execution_count": null,
"outputs": []
}
],
"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 pylabrobot/kbiosystems/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .sealer import KBiosystemsError, KBiosystemsSealer
from .ultraseal_epro import KBiosystemsUltrasealEPRO, UltrasealEPROStatus
from .ultraseal_pro import KBiosystemsUltrasealPRO, UltrasealPROStatus
from .ultraseal_xt_pro import KBiosystemsUltrasealXTPro, UltrasealXTProStatus
11 changes: 1 addition & 10 deletions pylabrobot/kbiosystems/sealer.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,7 @@ async def seal(self, temperature: int, duration: float) -> None:
"""Seal a plate at the given sealing temperature (C) and duration (s)."""

async def _open(self) -> None:
"""Open the port, wait out the post-open settling, and clear the buffer.

Emits the not-tested warning: no KBiosystems sealer driver has been verified
against hardware in PyLabRobot yet.
"""
logger.warning(
"%s has NOT been tested against hardware in PyLabRobot. Please make a PR "
"to remove this message if you have verified it on your hardware.",
type(self).__name__,
)
"""Open the port, wait out the post-open settling, and clear the buffer."""
await self.io.setup()
# The device drops characters for a few seconds after the port opens.
await asyncio.sleep(self.settle_time)
Expand Down
51 changes: 51 additions & 0 deletions pylabrobot/kbiosystems/sealer_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
KBiosystemsError,
KBiosystemsSealer,
KBiosystemsUltrasealEPRO,
KBiosystemsUltrasealPRO,
KBiosystemsUltrasealXTPro,
UltrasealEPROStatus,
UltrasealPROStatus,
UltrasealXTProStatus,
)

Expand Down Expand Up @@ -171,10 +173,56 @@ async def test_error_status_raises_with_code(self):
self.assertEqual(ctx.exception.message, "Low air pressure.")


class TestUltrasealPRO(KBiosystemsSealerTestBase):
def _make(self, responses: Dict[str, str]) -> KBiosystemsUltrasealPRO:
sealer = KBiosystemsUltrasealPRO(port="FAKE")
sealer.io = FakeSealerSerial(responses) # type: ignore[assignment]
return sealer

async def test_setup_sequence(self):
sealer = self._make({"?": "00", "A100": "ok"})
await sealer.setup()
self.assertEqual(sealer.io.written, ["?", "A100"]) # type: ignore[attr-defined]

async def test_seal_wire_bytes(self):
sealer = self._make({"?": "00", "B30": "ok", "A180": "ok", "A100": "ok", "S": "ok"})
await sealer.seal(temperature=180, duration=3.0)
written = sealer.io.written # type: ignore[attr-defined]
for expected in ["B30", "A180", "S", "A100"]:
self.assertIn(expected, written)
# The Ultraseal PRO has no foil/force/distance/eco or shuttle commands.
self.assertFalse(
any(
w.startswith(("L=", "DO=", "PS=", "FS=", "ECO_")) or w in ("P", "U", "R") for w in written
)
)

async def test_park_mode_is_bit_7(self):
# The Ultraseal PRO reports Park Mode at 0x80 (bit 6 is spare) - unlike the
# XT Pro, where Park Mode is 0x40. Decoding either byte must not raise.
sealer = self._make({"?": "80"})
self.assertEqual(await sealer.request_status(), UltrasealPROStatus.ParkMode)
sealer = self._make({"?": "40"})
self.assertEqual(await sealer.request_status(), UltrasealPROStatus.Spare)

async def test_status_decode_lowair(self):
sealer = self._make({"?": "24"})
status = await sealer.request_status()
self.assertEqual(status, UltrasealPROStatus.LowAir | UltrasealPROStatus.Busy)

async def test_error_status_raises_with_code(self):
sealer = self._make({"?": "02", "E": "09"})
with self.assertRaises(KBiosystemsError) as ctx:
await sealer.wait_for_idle()
self.assertEqual(ctx.exception.error_code, 9)
self.assertEqual(ctx.exception.message, "The sealer is overheating.")


class TestSharedBase(unittest.TestCase):
def test_both_drivers_subclass_base(self):
self.assertTrue(issubclass(KBiosystemsUltrasealEPRO, KBiosystemsSealer))
self.assertTrue(issubclass(KBiosystemsUltrasealXTPro, KBiosystemsSealer))
self.assertTrue(issubclass(KBiosystemsUltrasealPRO, KBiosystemsSealer))

def test_base_is_abstract(self):
with self.assertRaises(TypeError):
Expand All @@ -186,6 +234,9 @@ def test_shared_low_status_bits(self):
self.assertEqual(
int(getattr(UltrasealEPROStatus, bit)), int(getattr(UltrasealXTProStatus, bit))
)
self.assertEqual(
int(getattr(UltrasealEPROStatus, bit)), int(getattr(UltrasealPROStatus, bit))
)


if __name__ == "__main__":
Expand Down
4 changes: 4 additions & 0 deletions pylabrobot/kbiosystems/ultraseal_epro.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ def __init__(
self.firmware_version: Optional[str] = None

async def setup(self) -> None:
logger.warning(
"KBiosystemsUltrasealEPRO has NOT been tested against hardware in PyLabRobot. "
"Please make a PR to remove this message if you have verified it on your hardware."
)
await self._open()
# Initialize/home the sealer (``I``).
if self._check(await self.send_command("I"), {"ok", "err"}, "I") != "ok":
Expand Down
149 changes: 149 additions & 0 deletions pylabrobot/kbiosystems/ultraseal_pro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import enum
import logging
from typing import Dict

from pylabrobot.kbiosystems.sealer import KBiosystemsError, KBiosystemsSealer

logger = logging.getLogger(__name__)

__all__ = ["KBiosystemsUltrasealPRO", "UltrasealPROStatus", "KBiosystemsError"]


class UltrasealPROStatus(enum.IntFlag):
"""Status byte returned by the ``?`` command (two hex digits).

``Ready`` (0x00) is the all-clear value; any other bit is a condition that
must be cleared, or masked when ignorable, before an operation runs. The low
bits (Ready/Error/Busy/NotAtSealTemperature) are the shared set
:class:`KBiosystemsSealer` acts on.

The Ultraseal PRO differs from the Ultraseal XT Pro in the top of the byte:
``ParkMode`` is bit 7 (0x80) with bit 6 (0x40) a documented spare, whereas the
XT Pro reports ``ParkMode`` at 0x40. ``Spare`` is kept as a member so decoding
a byte with that bit set does not raise.
"""

Ready = 0x00
NoFoil = 0x01
Error = 0x02
Busy = 0x04
NotAtSealTemperature = 0x08
PlateNotPresent = 0x10
LowAir = 0x20
Spare = 0x40
ParkMode = 0x80


# Text for each status bit, used to describe a not-ready condition.
STATUS_MESSAGES: Dict[enum.IntFlag, str] = {
UltrasealPROStatus.NoFoil: "No foil detected.",
UltrasealPROStatus.Error: "Error.",
UltrasealPROStatus.Busy: "Device busy.",
UltrasealPROStatus.NotAtSealTemperature: "Waiting for seal temperature.",
UltrasealPROStatus.PlateNotPresent: "No plate detected.",
UltrasealPROStatus.LowAir: "Low air pressure - check the compressed air supply.",
UltrasealPROStatus.ParkMode: "Shuttle parked (inside).",
}

# Error codes returned by the ``E`` command (two decimal digits).
DEVICE_ERRORS = {
1: "Vertical shuttle down.",
2: "Heater up.",
3: "Shuttle in.",
4: "Shuttle out.",
7: "Thermocouple error - ambient temperature may be too low.",
9: "The sealer is overheating.",
10: "No foil detected.",
}

MIN_SEALING_TEMPERATURE = 25
MAX_SEALING_TEMPERATURE = 200
MIN_SEALING_DURATION = 0.5
MAX_SEALING_DURATION = 9.9


class KBiosystemsUltrasealPRO(KBiosystemsSealer):
"""KBiosystems Ultraseal PRO heat sealer (formerly the WASP).

An inline, pneumatically driven heat sealer that advances and cuts its own
film internally. A plate is placed on the shuttle while it is extended; the
seal command draws the shuttle in, seals, and returns it automatically, so -
unlike the Ultraseal XT Pro - the Ultraseal PRO exposes no shuttle
park/unpark/reset commands over serial. The system initializes on power up
(reporting ``Busy`` while it does), so there is no initialize command either.

Commands (in addition to the shared ``?``/``E``/``S``/``A``/``B``/``C``/
``D``/``F``, see :class:`KBiosystemsSealer`): none. The Ultraseal PRO host
protocol is exactly the shared command set.

Requires a compressed air supply (see the ``LowAir`` status bit).

Verified against hardware.
"""

_HUMAN_READABLE_NAME = "KBiosystems Ultraseal PRO Heat Sealer"

STATUS = UltrasealPROStatus
STATUS_MESSAGES = STATUS_MESSAGES
ERRORS = DEVICE_ERRORS
MIN_SEALING_TEMPERATURE = MIN_SEALING_TEMPERATURE
MAX_SEALING_TEMPERATURE = MAX_SEALING_TEMPERATURE

def __init__(
self,
port: str,
timeout: float = 5.0,
settle_time: float = 5.0,
preheating_temperature: int = 100,
offline_temperature: int = 25,
) -> None:
super().__init__(
port,
timeout=timeout,
settle_time=settle_time,
preheating_temperature=preheating_temperature,
offline_temperature=offline_temperature,
)

async def setup(self) -> None:
await self._open()
await self.wait_for_idle(
UltrasealPROStatus.NoFoil
| UltrasealPROStatus.NotAtSealTemperature
| UltrasealPROStatus.ParkMode
)
await self.set_temperature(self.preheating_temperature)
logger.info("[Ultraseal PRO %s] connected", self.io.port)

# === Operations ===

async def seal(
self,
temperature: int,
duration: float,
idle_temperature: int = 100,
) -> None:
"""Seal the plate currently on the shuttle.

Waits for the device to be ready, applies the time and temperature, waits
for the heater to reach the setpoint, seals, then returns to
``idle_temperature``. The Ultraseal PRO draws the shuttle in, seals, and
returns it automatically; film advance and cutting are handled by the device.

Args:
temperature: sealing temperature in degrees C (25..200).
duration: sealing time in seconds (0.5..9.9).
idle_temperature: temperature to hold after sealing (25..200).
"""
logger.info("[Ultraseal PRO %s] sealing at %d C for %.1fs", self.io.port, temperature, duration)
ignore = UltrasealPROStatus.NotAtSealTemperature | UltrasealPROStatus.ParkMode

await self.wait_for_idle(ignore)
await self.set_sealing_time(duration)
await self.set_temperature(temperature)
await self.wait_for_sealing_temperature()
if self._check(await self.send_command("S"), {"ok", "err"}, "S") != "ok":
raise KBiosystemsError(title="Seal command returned 'err'")
await self.wait_for_idle(ignore)

await self.set_temperature(idle_temperature)
4 changes: 4 additions & 0 deletions pylabrobot/kbiosystems/ultraseal_xt_pro.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ def __init__(
)

async def setup(self) -> None:
logger.warning(
"KBiosystemsUltrasealXTPro has NOT been tested against hardware in PyLabRobot. "
"Please make a PR to remove this message if you have verified it on your hardware."
)
await self._open()
await self.wait_for_idle(
UltrasealXTProStatus.NoFoil
Expand Down
Loading