Add Labcyte Echo 525 support#5
Conversation
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>
a8c8f0c to
03c1668
Compare
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.
4ecd6b9 to
68c1f18
Compare
- 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.
|
@rickwierenga (cc @alexjamesgodfrey) - a backend-architecture question I'd like your opinion on before I refine this, especially given the v1b1 This 525 work has surfaced three distinct "implementations" behind one Echo:
Looking at how PLR usually does multiple backends per device (frontend + injected backend abstract base class + concrete subclasses + a chatterbox sibling, e.g.
Proposal: keep this PR as-is to avoid churning @alexjamesgodfrey's base PyLabRobot#1001, and do a follow-up that (a) makes |
yes that sounds reasonable
yes
definitely targeting v1b1 with this since it's the future will also be nice for us once we add the idot we recently bought :) |
|
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).
102 labcyte tests pass. Kept it transport-abstract so the |
- 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.
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 sameHost: ip:slot_a:slot_b:epoch:pidtoken, and the sameDoWellTransfer<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, serialE5XX-00000, software2.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 advertisedProtocol: 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_nlonEchoDriver, threaded throughbuild_echo_transfer_plan→_validate_transfer_volume_nl); all defaults are unchanged so 650 behavior is identical.Echogains a smalldriver_class/model_namefactory hook so subclasses can inject a model-specific driver. A comment atECHO_TRANSFER_VOLUME_INCREMENT_NLpoints to the 525.echo525.py(new) —Echo525/Echo525Driversubclass the 650 classes and override only the 525 defaults (25 nL increment, observed2.6/2.7.3versions,Echo 525model). Everything else (access control, plate survey, grippers, dry-plate, sessions, locking,DoWellTransfer) is inherited unchanged.Mock server —
EchoMockServer(hardware-free testing)Following the
MicroSpinMockServerpattern from PyLabRobot#1047,echo_mock.pyadds an in-processasyncioserver that speaks the Medman protocol (POST /Medman+ gzip SOAP) and replays real responses captured from the physical 525, soEcho/Echo525can be driven end-to-end with no instrument:DoWellTransferprint-map is trimmed to 3 wells, keeping the embedded fixture ~3.5 KB gzipped.Caller does not own the lockfault — the locking workflow is exercised deterministically.Validation
python -m unittest pylabrobot.labcyte.echo525_tests→ 14 passed (incl. 4 end-to-end against the mock)python -m unittest pylabrobot.labcyte.echo_tests(650) → 78 passed, no regressionA2into every destination well) throughEcho525Driveryields a<wp>set byte-identical (onn/dn/v) to what the instrument actually sent.Notes
plr.iotransport: I deliberately kept this PR free of transport changes. The HTTP/socket layer lives in the base Add Labcyte Echo Support PyLabRobot/pylabrobot#1001 PR and uses one TCP connection per RPC; migrating it toplr.io(for logging + validation) is best done in Add Labcyte Echo Support PyLabRobot/pylabrobot#1001 so both the 650 and 525 benefit, rather than diverging the transport in a stacked PR. Happy to do that migration in Add Labcyte Echo Support PyLabRobot/pylabrobot#1001 (or a follow-up) if you'd like.Protocol/Clientdefault to the captured2.6/2.7.3and are constructor-overridable for newer firmware.