Skip to content

Add Labcyte Echo 525 support#5

Merged
alexjamesgodfrey merged 7 commits into
alexjamesgodfrey:labcyte-echo-plate-accessfrom
c-reiter:labcyte-echo-525
Jul 2, 2026
Merged

Add Labcyte Echo 525 support#5
alexjamesgodfrey merged 7 commits into
alexjamesgodfrey:labcyte-echo-plate-accessfrom
c-reiter:labcyte-echo-525

Conversation

@c-reiter

@c-reiter c-reiter commented Jun 24, 2026

Copy link
Copy Markdown

Add Labcyte Echo 525 support

Builds on top of PyLabRobot#1001 (Echo 650). This PR is stacked on labcyte-echo-plate-access, so the diff below is the 525-specific work only.

Background

The Echo 525 speaks the identical Medman SOAP-over-HTTP protocol as the Echo 650: POST /Medman, gzip-compressed SOAP 1.1 envelopes, the same RPC method set, the same Host: ip:slot_a:slot_b:epoch:pid token, and the same DoWellTransfer <Protocol><Layout><wp n dn v/></Layout></Protocol> XML.

This was verified end-to-end against a Wireshark capture of a physical Echo 525 (Model = Echo 525, serial E5XX-00000, software 2.7.3) running a HiFi PCR protocol — ~13.8k decoded SOAP messages, every method already implemented by the 650 driver.

What actually differs on the 525

The one functional difference is transfer-volume granularity: the device reported GetTransferVolIncrNl = GetTransferVolMinimumNl = 25 nL (the 650 uses 2.5 nL). Its HTTP headers also advertised Protocol: 2.6 / Client: 2.7.3.

Approach — reuse, don't fork

Rather than fork the 4.3k-line driver:

  • echo.py (650) — the volume increment is parameterized (transfer_volume_increment_nl on EchoDriver, threaded through build_echo_transfer_plan_validate_transfer_volume_nl); all defaults are unchanged so 650 behavior is identical. Echo gains a small driver_class / model_name factory hook so subclasses can inject a model-specific driver. A comment at ECHO_TRANSFER_VOLUME_INCREMENT_NL points to the 525.
  • echo525.py (new)Echo525 / Echo525Driver subclass the 650 classes and override only the 525 defaults (25 nL increment, observed 2.6/2.7.3 versions, Echo 525 model). Everything else (access control, plate survey, grippers, dry-plate, sessions, locking, DoWellTransfer) is inherited unchanged.

Mock server — EchoMockServer (hardware-free testing)

Following the MicroSpinMockServer pattern from PyLabRobot#1047, echo_mock.py adds an in-process asyncio server that speaks the Medman protocol (POST /Medman + gzip SOAP) and replays real responses captured from the physical 525, so Echo/Echo525 can be driven end-to-end with no instrument:

async with EchoMockServer() as srv:
    echo = Echo525(host=srv.host, rpc_port=srv.port)
    await echo.setup()
    info = await echo.get_instrument_info()        # -> Model "Echo 525", serial E5XX-00000
    await echo.driver.lock()
    await echo.driver.do_well_transfer(protocol_xml)
    await echo.driver.unlock()
  • Replays 33 captured device responses keyed by SOAP method; the bulky 384-well DoWellTransfer print-map is trimmed to 3 wells, keeping the embedded fixture ~3.5 KB gzipped.
  • Models the instrument lock, so motion/transfer RPCs issued without the lock get the Echo's real Caller does not own the lock fault — the locking workflow is exercised deterministically.

Validation

  • python -m unittest pylabrobot.labcyte.echo525_tests14 passed (incl. 4 end-to-end against the mock)
  • python -m unittest pylabrobot.labcyte.echo_tests (650) → 78 passed, no regression
  • Reproducing the captured 384-well transfer (150 nL from source A2 into every destination well) through Echo525Driver yields a <wp> set byte-identical (on n/dn/v) to what the instrument actually sent.

Notes

The Echo 525 speaks the identical Medman SOAP-over-HTTP protocol as the
Echo 650 (POST /Medman, gzip-compressed SOAP envelopes, same RPC method
set and DoWellTransfer <wp> layout), verified against a Wireshark capture
of a 525 (Model "Echo 525", software 2.7.3) running a HiFi PCR protocol.

The only functional difference is transfer volume granularity: the 525
dispenses in 25 nL increments (GetTransferVolIncrNl/MinimumNl == 25) vs
the 650's 2.5 nL. Rather than fork the 4.3k-line driver, this:

- parameterizes the volume increment in echo.py (backward-compatible
  defaults preserve all Echo 650 behavior) and adds a driver-factory hook
  to Echo so subclasses can inject model-specific drivers;
- adds echo525.py with Echo525 / Echo525Driver overriding only the 525
  defaults (25 nL increment, observed 2.6/2.7.3 protocol/client versions);
- adds echo525_tests.py (10 tests) pinned to the captured device traffic,
  including a byte-exact reproduction of the captured 384-well transfer.

Existing Echo 650 test suite (78 tests) still passes unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an in-process asyncio mock of the Echo Medman protocol (POST /Medman +
gzip SOAP), replaying real responses captured from the physical Echo 525, so
Echo/Echo525 can be driven end-to-end with no instrument attached. Mirrors the
MicroSpinMockServer pattern from PyLabRobot#1047.

- Replays 33 captured device responses keyed by SOAP method; the 384-well
  DoWellTransfer print-map is trimmed to 3 wells to keep the fixture ~3.5 KB.
- Models the instrument lock so motion/transfer RPCs issued without the lock
  get the Echo's real "Caller does not own the lock" fault.
- 4 new end-to-end tests: captured 525 identity (Model "Echo 525"), 25 nL
  increment read over the wire, full lock->transfer->unlock flow, lock gating.

14 Echo 525 tests pass; Echo 650 suite (78) still green.
c-reiter added 4 commits June 24, 2026 04:31
- new user-guide page docs/user_guide/labcyte/echo-525.md covering the 25 nL
  increment, the Echo525 frontend, the EchoMockServer hardware-free workflow,
  and live-validation override
- link it from the Labcyte index toctree and add a note on the Echo 650 page
- add Echo525 / Echo525Driver / EchoMockServer to the labcyte API reference

The documented mock-server example runs verbatim against EchoMockServer.
Document host/port/start/stop so the mock server's public surface is fully
covered (matches the response_for docstring already present).
…tured responses

Addresses the open "document live validation for the expanded survey/transfer
path" item from PyLabRobot#1001 in a hardware-free, reproducible way: drives
survey_source_plate (SetPlateMap -> PlateSurvey -> GetSurveyData -> DryPlate)
followed by DoWellTransfer against EchoMockServer, which replays responses
captured from a physical Echo 525 running that exact path.
…ator

Adds an open-source, vendor-SDK-free path to run an Echo picklist end-to-end:

- picklist.py: Transfer model, read_picklist() for the Echo cherry-pick CSV
  (a public data format), an EchoProtocolGenerator interface, and
  NaiveEchoProtocolGenerator (group by source plate type, emit <wp> in picklist
  order, pass through DestX/YOffset). Ordering is deliberately NOT optimised -
  the vendor's head-travel/serpentine path-finding is proprietary and is left to
  an out-of-tree generator that implements the same interface.
- Echo.run_picklist(): parse -> generate -> survey -> DoWellTransfer, no GUI.
  Plate loading/access is left to the caller (robotic arm) via PlateAccess.

The generator seam lets SDK owners swap in a vendor-exact generator without any
vendor code entering this repo. 5 new tests; full labcyte suite 98 green.
@c-reiter

c-reiter commented Jun 24, 2026

Copy link
Copy Markdown
Author

@rickwierenga (cc @alexjamesgodfrey) - a backend-architecture question I'd like your opinion on before I refine this, especially given the v1b1 Device/Driver + capabilities migration.

This 525 work has surfaced three distinct "implementations" behind one Echo:

  1. the Medman SOAP driver (real device, pure-Python - part of the PR already),
  2. a vendor-SDK-backed path (for sites that own the Labcyte SDK - kept out of this repo for licensing/legal reasons; it can reproduce the Echo Cherry Pick transfer-order/survey optimisation and the exotic ecp modes we deliberately don't clone), and
  3. a hardware-free path (the EchoMockServer in this PR, plus the PlateAccessChatterboxBackend).

Looking at how PLR usually does multiple backends per device (frontend + injected backend abstract base class + concrete subclasses + a chatterbox sibling, e.g. LiquidHandler(backend=STARBackend()/OpentronsBackend()/ChatterboxBackend())), two things in the current Echo design look slightly off-idiom and I'd value your opinion:

  • Echo525 is a subclass of Echo rather than the variant being a driver parameter. The only real difference between the 525 and 650 is the transfer increment (25 vs 2.5 nL) + version-string defaults - already a constructor arg in this PR. So the convention-aligned shape seems to be one Echo frontend with the model as a driver parameter / thin factory, not a frontend subclass. Is that your preference, or do you want per-model classes (à la STAR vs STARlet)?

  • Medman / vendor-SDK / chatterbox feel like they should be sibling EchoDriver implementations under one ABC, injected into a single Echo (Echo(driver=MedmanEchoDriver(...)) / VendorSdkEchoDriver(...) / EchoChatterboxDriver()), rather than today's single concrete EchoDriver + subclassed frontend. The picklist→protocol generator (EchoProtocolGenerator: naive open-source vs vendor-SDK) would stay a separate strategy injected into run_picklist, since it's orthogonal to transport.

Proposal: keep this PR as-is to avoid churning @alexjamesgodfrey's base PyLabRobot#1001, and do a follow-up that (a) makes EchoDriver an abstract base class with MedmanEchoDriver as the concrete impl, (b) collapses Echo525 into Echo(model=...)/factory with a deprecation shim, and (c) adds an EchoChatterboxDriver. Does that match where v1b1 is heading, or would you rather the Echo follow the older Machine/Backend shape until the capabilities migration lands? Happy to adjust either way.

@rickwierenga

Copy link
Copy Markdown

one Echo frontend with the model as a driver parameter / thin factory, not a frontend subclass

yes that sounds reasonable

EchoDriver implementations under one ABC, injected into a single Echo (Echo(driver=MedmanEchoDriver(...)) / VendorSdkEchoDriver(...) / EchoChatterboxDriver()), rather than today's single concrete EchoDriver + subclassed frontend.

yes

Does that match where v1b1 is heading, or would you rather the Echo follow the older Machine/Backend shape until the capabilities migration lands? Happy to adjust either way.

definitely targeting v1b1 with this since it's the future

will also be nice for us once we add the idot we recently bought :)

@c-reiter

c-reiter commented Jun 25, 2026

Copy link
Copy Markdown
Author

thanks @rickwierenga it's done, implemented as a follow-up so this PR / PyLabRobot#1001 stay clean: c-reiter#1 (stacked on this branch, so the diff there is the architecture change only).

  • one Echo frontend; the model is a parameter (Echo(host, model="Echo 525")) backed by an ECHO_MODELS registry -> no more Echo525 subclass (kept as a deprecation shim).
  • EchoDriver is now an ABC with MedmanEchoDriver (real SOAP) and EchoChatterboxDriver (logs, no I/O) as sibling implementations, injected via Echo(driver=...). The picklist→protocol generator stays a separate strategy.

102 labcyte tests pass. Kept it transport-abstract so the EchoDriver ABC holds the shared Medman logic and the chatterbox/vendor-SDK siblings only implement the transport seam, which should make the iDOT an easy add when you get to it. Happy to fold it into the main PR or keep it stacked, whichever you prefer for review/merge order.

- ruff format + import sorting across the labcyte files
- TYPE_CHECKING import so run_picklist's Transfer/EchoProtocolGenerator
  annotations resolve (F821)
- rename a loop var shadowing dataclasses.field (F402); drop a duplicate
  'fluids' annotation (mypy no-redef)
- echo_mock.py mypy: typed _load_captured_responses result, type _server as
  asyncio.Server, int() the port

ruff, mypy (labcyte), typos and the labcyte tests all pass.
@alexjamesgodfrey
alexjamesgodfrey merged commit 0b83658 into alexjamesgodfrey:labcyte-echo-plate-access Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants