Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f71bfb4
Add Echo plate access support on v1b1
alexjamesgodfrey Apr 3, 2026
281307c
Expand Echo Medman coverage and event support
alexjamesgodfrey Apr 15, 2026
0ff0856
docs: document Echo transfer execution
alexjamesgodfrey Apr 20, 2026
fc577f0
Expand Echo low-level workflow support
alexjamesgodfrey Apr 21, 2026
a850e7f
Harden Echo gzip survey handling
alexjamesgodfrey Apr 22, 2026
258e7c1
Read Echo gzip responses without content length
alexjamesgodfrey Apr 22, 2026
a19d269
Fix Echo transfer print option SOAP encoding
alexjamesgodfrey Apr 23, 2026
d8d5b03
Fix Echo transfer SOAP envelope and dump bad responses
alexjamesgodfrey Apr 23, 2026
a8e0f91
Expose Echo event stream and harden Labcyte framing
alexjamesgodfrey Apr 28, 2026
90d10b2
Add Echo 650 live validation
alexjamesgodfrey May 1, 2026
d630a93
Add high-level Echo transfer API
alexjamesgodfrey May 1, 2026
eedd372
Model Echo plate positions as PLR resources
alexjamesgodfrey May 1, 2026
05b1ff6
Add Echo workcell examples and parity tests
alexjamesgodfrey May 1, 2026
6996982
Preserve Echo plate access barcodes
alexjamesgodfrey May 1, 2026
6d895e7
Reconcile Echo live service fixes
alexjamesgodfrey May 1, 2026
a40f93c
Declare Echo SOAP envelope namespaces
alexjamesgodfrey May 5, 2026
5e7fa00
Prepare Echo branch after v1b1 rebase
alexjamesgodfrey May 6, 2026
2523635
Add Echo focus calibration APIs
alexjamesgodfrey May 8, 2026
03c1668
feat(labcyte): add Labcyte Echo 525 support
Jun 24, 2026
68c1f18
test(labcyte): add EchoMockServer for hardware-free Echo 525 testing
Jun 24, 2026
34ef74a
docs(labcyte): document the Echo 525
Jun 24, 2026
b8f1f3b
docs(labcyte): docstrings for EchoMockServer public API
Jun 24, 2026
c6f7f97
test(labcyte): validate the full survey/dry/transfer path against cap…
Jun 24, 2026
6518fb9
feat(labcyte): SDK-free picklist execution + pluggable protocol gener…
Jun 24, 2026
0b83658
style(labcyte): satisfy CI (ruff format/lint, mypy, imports)
Jul 2, 2026
34bd1ae
refactor(labcyte): one Echo frontend with model= + driver injection
Jun 25, 2026
a0ae35a
refactor(labcyte): make EchoDriver an ABC with Medman + Chatterbox si…
Jun 25, 2026
3988442
refactor(labcyte): address review — store model spec, drop shims
Jul 1, 2026
11131b8
style(labcyte): satisfy CI (ruff format/lint, mypy, imports)
Jul 2, 2026
caa72be
style(labcyte): sort typing imports after rebase
Jul 2, 2026
1b7e537
labcyte/echo: optional per-transfer destination XY offset (dx/dy) + tag
alexjamesgodfrey Jul 2, 2026
5268c50
labcyte/echo: rename driver methods to vendor verbs (v1b1 P-01: drive…
alexjamesgodfrey Jul 2, 2026
049d547
labcyte/echo: route run_picklist plate-access ops through the capabil…
alexjamesgodfrey Jul 2, 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
3 changes: 3 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ UE = "UE"
commutated = "commutated"
commutating = "commutating"
DOUT = "DOUT"
LSI = "LSI"
LSO = "LSO"
SPP = "SPP"
inconsistence = "inconsistence"
mis = "mis"
RHE = "RHE"
Expand Down
15 changes: 15 additions & 0 deletions docs/api/pylabrobot.capabilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,21 @@ Barcode Scanning
BarcodeScannerBackend


Plate Access
------------

.. currentmodule:: pylabrobot.capabilities.plate_access

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

PlateAccess
PlateAccessBackend
PlateAccessState


Microscopy
----------

Expand Down
43 changes: 43 additions & 0 deletions docs/api/pylabrobot.labcyte.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.. currentmodule:: pylabrobot.labcyte

pylabrobot.labcyte package
==========================

Echo
----

.. currentmodule:: pylabrobot.labcyte.echo

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

Echo
EchoDriver
MedmanEchoDriver
EchoChatterboxDriver
EchoPlateMap
EchoInstrumentInfo
EchoSurveyParams
EchoSurveyWell
EchoSurveyData
EchoSurveyRunResult
EchoDryPlateMode
EchoDryPlateParams
EchoPlateAccessBackend
EchoError
EchoProtocolError
EchoCommandError

Testing
-------

.. currentmodule:: pylabrobot.labcyte.echo_mock

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

EchoMockServer
1 change: 1 addition & 0 deletions docs/api/pylabrobot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Manufacturers
pylabrobot.byonoy
pylabrobot.hamilton
pylabrobot.inheco
pylabrobot.labcyte
pylabrobot.liconic
pylabrobot.mettler_toledo
pylabrobot.molecular_devices
Expand Down
1 change: 1 addition & 0 deletions docs/user_guide/capabilities/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ loading-tray
pumping
weighing
barcode-scanning
plate-access
microscopy
automated-retrieval
absorbance
Expand Down
69 changes: 69 additions & 0 deletions docs/user_guide/capabilities/plate-access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Plate Access

The plate access capability standardizes a narrow but common class of machine interactions:
locking an instrument, presenting an access path for the source or destination side, polling
access state, and closing the door afterwards.

This is useful for devices where the user-facing control surface is about getting hardware into
an accessible state rather than immediately running a transfer or assay.

## API

```python
from pylabrobot.capabilities.plate_access import PlateAccessState
```

Capability methods:

- `lock(app=None, owner=None)`
- `unlock()`
- `get_access_state()`
- `open_source_plate(timeout=30.0, poll_interval=0.1) -> PlateAccessState`
- `close_source_plate(plate_type=None, barcode_location=None, barcode="", timeout=30.0, poll_interval=0.1) -> PlateAccessState`
- `open_destination_plate(timeout=30.0, poll_interval=0.1) -> PlateAccessState`
- `close_destination_plate(plate_type=None, barcode_location=None, barcode="", timeout=30.0, poll_interval=0.1) -> PlateAccessState`
- `close_door(timeout=30.0, poll_interval=0.1) -> PlateAccessState`

`get_access_state()` returns a `PlateAccessState` with normalized fields for:

- source access open/closed
- destination access open/closed when the backend can infer them
- door open/closed
- source and destination plate position values when available
- a `raw` dictionary with the backend's native state payload

## Echo Example

```python
import asyncio

from pylabrobot.labcyte import Echo


async def main():
async with Echo(host="192.168.0.25") as echo:
info = await echo.get_instrument_info()
print(info.model, info.serial_number)

await echo.lock()
try:
baseline = await echo.get_access_state()
print("baseline:", baseline)

opened = await echo.open_source_plate(timeout=2.0)
print("opened:", opened)

retracted = await echo.close_source_plate(timeout=2.0)
print("retracted:", retracted)

closed = await echo.close_door(timeout=2.0)
print("closed:", closed)
finally:
await echo.unlock()


asyncio.run(main())
```

For the Echo integration, motion commands require an active lock. Read-only polling and
instrument info queries do not.
1 change: 1 addition & 0 deletions docs/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ brooks/index
byonoy/index
hamilton/index
inheco/index
labcyte/index
liconic/index
mettler_toledo/index
molecular_devices/index
Expand Down
106 changes: 106 additions & 0 deletions docs/user_guide/labcyte/echo-525.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Echo 525

The Labcyte **Echo 525** speaks the exact same Medman protocol as the [Echo 650](echo): the same
`POST /Medman` transport, gzip-compressed SOAP bodies, RPC method set, lock/session model, plate
survey, and `DoWellTransfer` `<wp>` layout. Selecting `model="Echo 525"` therefore reuses the entire
Echo 650 implementation and overrides only the handful of defaults that differ on the 525.

Everything on the [Echo](echo) page — access cycles, surveys, transfers, loading/ejecting,
calibration, plate definitions — applies unchanged. This page covers only the 525-specific parts.

## What differs from the Echo 650

The one behavioural difference is **transfer volume granularity**: the Echo 525 dispenses in
**25 nL** increments, where the Echo 650 uses 2.5 nL. The instrument confirms this over the wire —
`GetTransferVolIncrNl` and `GetTransferVolMinimumNl` both return `25`. Requested volumes must be a
whole multiple of 25 nL; anything else is rejected during transfer planning:

```python
from pylabrobot.labcyte import Echo

echo = Echo("192.168.0.25", model="Echo 525")
# 150 nL is fine (6 x 25 nL); 10 nL would raise ValueError on a 525.
```

The defaults below were reverse-engineered from a Wireshark capture of a physical Echo 525
(`Model` = `Echo 525`, software `2.7.3`) running a HiFi PCR reformat, so they reflect real device
traffic rather than assumptions.

| Default | Echo 525 | Echo 650 |
|---------|----------|----------|
| `transfer_volume_increment_nl` | `25.0` | `2.5` |
| `protocol_version` | `2.6` | `3.1` |
| `client_version` | `2.7.3` | `3.1.0` |
| reported `Model` | `Echo 525` | `Echo 650` |

These are defined per model in `ECHO_MODELS` in `echo.py`; add or adjust an entry there to support
another Echo variant or newer firmware.

## Running a transfer

`Echo(model="Echo 525")` is a drop-in replacement for the 650; the transfer API is identical:

```python
import asyncio

from pylabrobot.labcyte import Echo

async def main(source_plate, destination_plate):
async with Echo("192.168.0.25", model="Echo 525") as echo:
await echo.lock()
try:
result = await echo.transfer(
[(source_plate.get_well("A1"), destination_plate.get_well("B1"), 150)], # nL, multiple of 25
source_plate_type="6RES_AQ_BP2",
destination_plate_type="384PP_AQ_BP2",
)
print(len(result.transfers), len(result.skipped))
finally:
await echo.unlock()
```

## Hardware-free testing with the mock server

`EchoMockServer` is an in-process `asyncio` server that emulates the Echo Medman protocol and
replays real responses captured from a physical Echo 525. It lets you exercise the full
`Echo` stack — setup, lock, survey, `DoWellTransfer`, unlock — with no instrument
attached:

```python
import asyncio

from pylabrobot.labcyte import Echo, EchoMockServer

async def main():
async with EchoMockServer() as srv:
echo = Echo(srv.host, model="Echo 525", rpc_port=srv.port)
await echo.setup()

info = await echo.get_instrument_info()
assert info.model == "Echo 525"

await echo.driver.lock()
report = await echo.driver.do_well_transfer(
'<?xml version="1.0"?><Protocol Name="demo"><Name/>'
'<Layout><wp n="A2" dn="A1" v="150"/></Layout></Protocol>'
)
await echo.driver.unlock()
print(report.succeeded, len(report.transfers))

asyncio.run(main())
```

The mock dispatches by SOAP method name, replays the captured response for that method, and models
the instrument lock — motion and transfer RPCs issued without holding the lock get the Echo's real
`Caller does not own the lock` fault, so the locking workflow can be tested deterministically.

## Live validation

The opt-in live tests from the [Echo](echo) page work against a 525 as well; set the expected model
so the identity check passes:

```bash
PYLABROBOT_ECHO_HOST=192.168.0.25 \
PYLABROBOT_ECHO_EXPECTED_MODEL="Echo 525" \
uv run --extra dev pytest pylabrobot/labcyte/echo_live_tests.py
```
Loading