Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
9a50606
added first version of backend
sam-adaptyv Jan 13, 2026
8d2500b
init file
sam-adaptyv Jan 13, 2026
d5c06b4
init updates and backend bug fixes
sam-adaptyv Jan 13, 2026
19a6cef
Adding Barcode scanning
sam-adaptyv Jan 14, 2026
93602d2
fixed import for Keyence
sam-adaptyv Jan 14, 2026
ad05107
Updated Liconic Backend to use KeyenceBarcodeScannerBackend
sam-adaptyv Jan 14, 2026
ba80382
Merge branch 'liconic-backend' of https://github.com/sam-adaptyv/pyla…
sam-adaptyv Jan 14, 2026
1d6250d
Combined barcode backend with liconic backend (optional)
sam-adaptyv Jan 16, 2026
3bc0dfb
Added scan barcode to incubator front end
sam-adaptyv Jan 16, 2026
1f5ae26
Added Liconic commands for front end and backend
sam-adaptyv Jan 19, 2026
b164596
Merge branch 'liconic-backend' of https://github.com/sam-adaptyv/pyla…
sam-adaptyv Jan 19, 2026
9335d50
More backend functions
sam-adaptyv Jan 21, 2026
f66dbad
add plate storage functionality
sam-adaptyv Jan 22, 2026
3098843
test plate fetch
sam-adaptyv Jan 22, 2026
1ad06c2
Motor step retrieval completed
sam-adaptyv Jan 22, 2026
259f1c5
Merge branch 'liconic-backend' of https://github.com/sam-adaptyv/pyla…
sam-adaptyv Jan 22, 2026
67cabb2
Move position to position
sam-adaptyv Jan 22, 2026
f4dce80
fixed buys with move position to position successful test
sam-adaptyv Jan 22, 2026
7814fcb
removed merge artifact in liconic backend take_in_plates
sam-adaptyv Jan 22, 2026
1efe1f9
All the error handling and scan cassette function
sam-adaptyv Jan 23, 2026
57fe9c7
bug fixes
sam-adaptyv Jan 23, 2026
42462df
continuous streaming on BCR
sam-adaptyv Jan 26, 2026
ac62347
generator function
sam-adaptyv Jan 26, 2026
6058913
still not working
sam-adaptyv Jan 26, 2026
a74051d
How to guide rough
sam-adaptyv Jan 27, 2026
74683f5
cleanup of imports
sam-adaptyv Jan 27, 2026
23c1daf
Merge branch 'liconic-backend' of https://github.com/sam-adaptyv/pyla…
sam-adaptyv Jan 27, 2026
a4bb914
More edits and update to doc
sam-adaptyv Jan 27, 2026
8e085da
bug fixes
sam-adaptyv Jan 27, 2026
0c9b52b
fix for barcode scan
sam-adaptyv Jan 28, 2026
56898b4
barcode fix
sam-adaptyv Jan 28, 2026
1252454
Merge branch 'liconic-backend' of https://github.com/sam-adaptyv/pyla…
sam-adaptyv Jan 28, 2026
2c31103
Final fixes
sam-adaptyv Jan 28, 2026
85b21a5
Updated documentation
sam-adaptyv Jan 28, 2026
6c18d8f
format
rickwierenga Jan 31, 2026
420d067
Add BarcodeScanner frontend class
rickwierenga Jan 31, 2026
1579d78
Change barcode scanner return type from str to Barcode
rickwierenga Jan 31, 2026
b50f06f
Fix type annotation in fetch_plate_to_loading_tray
rickwierenga Jan 31, 2026
19b454b
Refactor barcode handling to use dependency injection
rickwierenga Jan 31, 2026
b316aa2
Fix bugs in LiconicBackend
rickwierenga Jan 31, 2026
a6a109c
Merge branch 'main' into liconic-backend
rickwierenga Feb 1, 2026
01a69c0
logger in KeyenceBarcodeScannerBackend
rickwierenga Feb 6, 2026
24226ea
get_set_ -> get_target_
rickwierenga Feb 6, 2026
99c4196
more hardware agnostic
rickwierenga Feb 6, 2026
f723399
use fractions instead of percentages for humidity, CO2, and N2 levels
rickwierenga Feb 6, 2026
028b57b
io_plc -> io, _send_command_plc -> _send_command
rickwierenga Feb 6, 2026
4122ed0
move UNTESTED and doc comments into docstrings in LiconicBackend
rickwierenga Feb 6, 2026
d8ee95d
fix copy-paste bugs in Liconic constants and backend
rickwierenga Feb 6, 2026
8b656aa
raise on unknown site height instead of silently returning None
rickwierenga Feb 6, 2026
64d65df
fix serialize/deserialize to include model parameter
rickwierenga Feb 6, 2026
787735a
fix shaking frequency conversion and missing _wait_ready in set_n2
rickwierenga Feb 6, 2026
088755e
remove dead code from KeyenceBarcodeScannerBackend
rickwierenga Feb 6, 2026
0553e03
simplify rack definitions: remove unused params, add docstrings
rickwierenga Feb 6, 2026
df0d682
fix trailing spaces in error messages
rickwierenga Feb 6, 2026
1897f17
Merge branch 'main' into liconic-backend
rickwierenga Mar 4, 2026
3df9680
no changes to frontend / backend
rickwierenga Mar 4, 2026
f4c6abb
format
rickwierenga Mar 4, 2026
b786c5d
add to docs index
rickwierenga Mar 4, 2026
7aa666c
Fix review bugs and add tests for untested backend methods
rickwierenga Mar 4, 2026
df75f0f
tiny doc format etc.
rickwierenga Mar 4, 2026
9b20675
Use 0-indexed site names with Cytomat format in Liconic racks
rickwierenga Mar 9, 2026
623c14c
Merge branch 'main' into liconic-backend
rickwierenga Mar 9, 2026
8b704a8
Fix CI: add human_readable_device_name to Serial calls, fix override …
rickwierenga Mar 9, 2026
ba6b975
ExperimentalLiconicBackend
rickwierenga Mar 9, 2026
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
260 changes: 260 additions & 0 deletions docs/user_guide/01_material-handling/storage/liconic.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b63b4656",
"metadata": {},
"source": [
"# Liconic STX Series\n",
"\n",
"The Liconic STX line of automated incubators come in a variety of sizes including STX 1000, STX 500, STX 280, STX 220, STX 110, STX 44. Which corresponds to the number of plates each size can store using the standard 22 plate capacity cassettes/cartridges (plate height 17mm, 505mm total height.) There are other cassette size for plates height ranging from 5 to 104mm in height (higher plates = less number of plates storage capacity.)\n",
"\n",
"The Liconic STX line comes in a variety of climate control options including Ultra High Temp. (HTT), Incubator (IC), Dry Storage (DC2), Humid Cooler (HC), Humid Wide Range (HR), Dry Wide Range (DR2), Humidity Controlled (AR), Deep Freezer (DF) and Ultra Deep Freezer (UDF). Each have different ranges of temperatures and humidity control ability.\n",
"\n",
"Other accessories that can be included with the STX and can be utilized with this driver include N2 gassing, CO2 gassing, a Turn Station (rotation of plates 90 degrees on the transfer station), internal barcode scanners, a swap station (two transfer plate positions that can be rotated 180 degrees) and internal shaking. \n",
"\n",
"This tutorial shows how to\n",
" - Connect the Liconic incubator\n",
" - Configure racks\n",
" - Move plates in and out\n",
" - Set and monitor temperature and humidity values"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fcd75e15",
"metadata": {},
"outputs": [],
"source": [
"from pylabrobot.barcode_scanners import BarcodeScanner, KeyenceBarcodeScannerBackend\n",
"from pylabrobot.resources.coordinate import Coordinate\n",
"from pylabrobot.storage import ExperimentalLiconicBackend\n",
"from pylabrobot.storage.incubator import Incubator\n",
"from pylabrobot.storage.liconic.racks import liconic_rack_17mm_22, liconic_rack_44mm_10\n",
"\n",
"\n",
"barcode_scanner_backend = KeyenceBarcodeScannerBackend(port=\"COM4\")\n",
"barcode_scanner = BarcodeScanner(backend=barcode_scanner_backend)\n",
"\n",
"liconic_backend = ExperimentalLiconicBackend(port=\"COM3\", model=\"STX220_HC\", barcode_scanner=barcode_scanner)\n",
"\n",
"rack = [\n",
" liconic_rack_44mm_10(\"cassette_0\"),\n",
" liconic_rack_44mm_10(\"cassette_1\"),\n",
" liconic_rack_44mm_10(\"cassette_2\"),\n",
" liconic_rack_17mm_22(\"cassette_3\"),\n",
" liconic_rack_17mm_22(\"cassette_4\"),\n",
" liconic_rack_17mm_22(\"cassette_5\"),\n",
" liconic_rack_17mm_22(\"cassette_6\"),\n",
" liconic_rack_17mm_22(\"cassette_7\"),\n",
" liconic_rack_17mm_22(\"cassette_8\"),\n",
" liconic_rack_17mm_22(\"cassette_9\")\n",
"]\n",
"\n",
"incubator = Incubator(\n",
" backend=liconic_backend,\n",
" name=\"My Incubator\",\n",
" size_x=100, size_y=100, size_z=100, # stubs for now...\n",
" racks=rack,\n",
" loading_tray_location=Coordinate(x=0, y=0, z=0),\n",
")\n",
"\n",
"await incubator.setup()"
]
},
{
"cell_type": "markdown",
"id": "19b3a6cc",
"metadata": {
"vscode": {
"languageId": "plaintext"
}
},
"source": [
"## Setup\n",
"\n",
"To setup the incubator and start sending commands first the backed needs to be declared. For the Liconic the LiconcBackend class is used with the COM port used for connection (in this case COM3) and the model needs to specified (in this case the STX 220 Humid Cooler, STX220_HC). If an internal barcode is installed the barcode_installed parameter is set to True and its COM port is also specified. These two parameters are optional so can be omitted for Liconics without an internal barcode scanner. \n",
"\n",
"Given a STX 220 (220 plate position / 22 plates per rack = 10 racks) can hold 10 racks the list of racks is built and includes mixing and matching different plate height racks. The differences in racks are handled prior to plate retrieval and storage. \n",
"\n",
"Once the these are built the base Incubator class is created and the connection to the incubator is initialized using:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9d7a4f49",
"metadata": {},
"outputs": [],
"source": [
"await incubator.setup()"
]
},
{
"cell_type": "markdown",
"id": "52f79811",
"metadata": {},
"source": [
"## Usage\n",
"\n",
"To store a plate first a plate resource is initialized and then assigned to the loading tray. The method take_in_plate is then called on the incubator object."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d26e039d",
"metadata": {},
"outputs": [],
"source": [
"from pylabrobot.resources import Azenta4titudeFrameStar_96_wellplate_200ul_Vb\n",
"\n",
"new_plate = Azenta4titudeFrameStar_96_wellplate_200ul_Vb(name=\"TEST\")\n",
"incubator.loading_tray.assign_child_resource(new_plate)\n",
"await incubator.take_in_plate(\"smallest\") # choose the smallest free site\n",
"\n",
"# other options:\n",
"# await incubator.take_in_plate(\"random\") # random free site\n",
"# await incubator.take_in_plate(rack[3]) # store at rack position 3"
]
},
{
"cell_type": "markdown",
"id": "85dcddb7",
"metadata": {},
"source": [
"To retrieve a plate the plate name can used"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "00308838",
"metadata": {},
"outputs": [],
"source": [
"await incubator.fetch_plate_to_loading_tray(plate_name=\"TEST\")\n",
"retrieved = incubator.loading_tray.resource"
]
},
{
"cell_type": "markdown",
"id": "0045e703",
"metadata": {},
"source": [
"You can also print a barcode from this call (if barcode is installed per the backend insatiation). Returning of the barcode as a return object still needs to be implemented. Currently the barcode is just printed to the terminal.\n",
"\n",
"Barcode can returned by setting the read_barcode to True for \n",
"- take_in_plate\n",
"- fetch_plate_to_loading_tray\n",
"- move_position_to_position"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c8730560",
"metadata": {},
"outputs": [],
"source": [
"position = rack[9][0] # rack number 9 position 1\n",
"\n",
"await incubator.fetch_plate_to_loading_tray(plate_name=\"TEST\", read_barcode=True)\n",
"\n",
"await incubator.take_in_plate(position, read_barcode=True)\n"
]
},
{
"cell_type": "markdown",
"id": "d137d333",
"metadata": {},
"source": [
"move plate from one internal position to another internal position"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1aa68983",
"metadata": {},
"outputs": [],
"source": [
"await liconic_backend.move_position_to_position(plate=new_plate, dest_site=position, read_barcode=True)\n",
"# will set new_plate.barcode to the barcode read from the plate at position and move it to position."
]
},
{
"cell_type": "markdown",
"id": "14efdf69",
"metadata": {},
"source": [
"The humdity, temperature, N2 gas and CO2 gas levels can all be controlled and queried. For temperature for example:\n",
"\n",
"- To get the current temperature"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "73e38f2a",
"metadata": {},
"outputs": [],
"source": [
"temperature = await liconic_backend.get_temperature() # returns temperature as float in Celsius to the 10th place\n",
"print(str(temperature))"
]
},
{
"cell_type": "markdown",
"id": "c7383277",
"metadata": {},
"source": [
"- To set the temperature of the Liconic"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2c51c385",
"metadata": {},
"outputs": [],
"source": [
"await incubator.set_temperature(8.0) # set the temperature to 8 degrees Celsius"
]
},
{
"cell_type": "markdown",
"id": "4f07f349",
"metadata": {},
"source": [
"- You can also retrieve the set value (the value sent for set_temperature) using:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "288dea91",
"metadata": {},
"outputs": [],
"source": [
"set_temperature = await liconic_backend.get_target_temperature() # will return a float for the set temperature in degrees Celsius"
]
},
{
"cell_type": "markdown",
"id": "3a1d9ef3",
"metadata": {},
"source": [
"This pattern is the same for CO2, N2 and Humidity control of the Liconic. "
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
39 changes: 20 additions & 19 deletions docs/user_guide/01_material-handling/storage/storage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ A storage machine is defined as a **machine whose primary feature is**

Examples of this simplest form of a storage machine include:

- `Agilent Labware MiniHub <https://www.agilent.com/en/product/automated-liquid-handling/automated-microplate-management/labware-minihub>`_ open storage of labware with rotation feature
- `Lab Services PlateCarousel <https://www.lab-services.nl/en/products/platebutler/platecarousel>`_ open storage of labware with rotation feature
- `Agilent Labware MiniHub <https://www.agilent.com/en/product/automated-liquid-handling/automated-microplate-management/labware-minihub>`_ - open storage of labware with rotation feature
- `Lab Services PlateCarousel <https://www.lab-services.nl/en/products/platebutler/platecarousel>`_ - open storage of labware with rotation feature


However, this purposefully broad definition means most storage machines also include other features such as:
Expand Down Expand Up @@ -48,7 +48,7 @@ However, this purposefully broad definition means most storage machines also inc
</p>
<p>
The only time the term <code>plate hotel</code> or <code>hotel</code> is used in PyLabRobot is when
referring to the name of a specific machine, such as the
referring to the name of a specific machine, such as the
<a href="https://www.thermofisher.com/order/catalog/product/50078485" target="_blank" rel="noopener">
TFS Cytomat™ 2 Hotel Automated Storage</a>.
In this case, it is used as a noun to refer to a specific product.
Expand All @@ -73,21 +73,21 @@ Retrieval Pattern: Stacking (Sequential) vs. Random Access

* - **Stacking Access (Sequential)**
- **Random Access**
* - Materials stored in a fixed order (e.g. vertical stack, rotating carousel).
* - Materials stored in a fixed order (e.g. vertical stack, rotating carousel).
Only the top/front-most item is accessible without mechanical movement.
- Materials stored in individually addressable slots or shelves.
- Materials stored in individually addressable slots or shelves.
Any item can be accessed directly.
* - Slower access time for deeper items.
- Faster access to any item.
* - Simpler mechanics, smaller footprint.
- More flexible but mechanically complex.
* - **Examples:**
* - **Examples:**

- Agilent Labware MiniHub
- Agilent Labware MiniHub
- Lab Services PlateCarousel
- **Examples:**
- Thermo Cytomat 2 C450
- **Examples:**

- Thermo Cytomat 2 C450
- LiCONiC STX Series

Accessibility: Open vs. Closed Storage
Expand All @@ -99,19 +99,19 @@ Accessibility: Open vs. Closed Storage

* - **Open Storage**
- **Closed Storage**
* - Materials are exposed without obstruction.
* - Materials are exposed without obstruction.
No barrier between the robot and the stored material.
- Materials enclosed in a chamber.
- Materials enclosed in a chamber.
Access requires opening a door, drawer, or robotic port.
* - Simplifies integration and visual inspection.
- Enables environmental control (temperature, humidity, sterility).
* - No protection from contamination or temperature drift.
- Ideal for incubators, cold storage, and sterile handling.
* - **Examples:**
- Agilent Labware MiniHub
* - **Examples:**
- Agilent Labware MiniHub
- Manual stackers
- **Examples:**
- Thermo Cytomat 2
- **Examples:**
- Thermo Cytomat 2
- LiCONiC STX incubators

Combined Retrieval & Access Summary
Expand All @@ -120,16 +120,16 @@ Combined Retrieval & Access Summary
.. list-table::
:header-rows: 1

* -
* -
- **Open Storage**
- **Closed Storage**
* - **Stacking Access (Sequential)**
- Agilent Labware MiniHub
- Agilent Labware MiniHub
Lab Services PlateCarousel
- STX incubators with drawer-based shelves
* - **Random Access**
- Rare in open format (e.g. manual racks)
- Thermo Cytomat 2 C450
- Thermo Cytomat 2 C450
LiCONiC STX Series


Expand All @@ -142,3 +142,4 @@ Combined Retrieval & Access Summary
cytomat
inheco/incubator_shaker
inheco/scila
liconic
3 changes: 3 additions & 0 deletions pylabrobot/barcode_scanners/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .backend import BarcodeScannerBackend, BarcodeScannerError
from .barcode_scanner import BarcodeScanner
from .keyence import KeyenceBarcodeScannerBackend
17 changes: 17 additions & 0 deletions pylabrobot/barcode_scanners/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from abc import ABCMeta, abstractmethod

from pylabrobot.machines.backend import MachineBackend
from pylabrobot.resources.barcode import Barcode


class BarcodeScannerError(Exception):
"""Error raised by a barcode scanner backend."""


class BarcodeScannerBackend(MachineBackend, metaclass=ABCMeta):
def __init__(self):
super().__init__()

@abstractmethod
async def scan_barcode(self) -> Barcode:
"""Scan a barcode and return its value."""
15 changes: 15 additions & 0 deletions pylabrobot/barcode_scanners/barcode_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pylabrobot.barcode_scanners.backend import BarcodeScannerBackend
from pylabrobot.machines.machine import Machine
from pylabrobot.resources.barcode import Barcode


class BarcodeScanner(Machine):
"""Frontend for barcode scanners."""

def __init__(self, backend: BarcodeScannerBackend):
super().__init__(backend=backend)
self.backend: BarcodeScannerBackend = backend

async def scan(self) -> Barcode:
"""Scan a barcode and return its value."""
return await self.backend.scan_barcode()
Loading