A REST/JSON API for the PerkinElmer Wallac Victor2 (1420) multimode microplate reader — start assays, stream live status, and pull results over HTTP, driven from Linux.
The original instrument software exposes only a 2005-vintage Windows GUI with no automatable interface. This project runs the unmodified OEM software inside a Windows 7 VM and wraps its COM automation server behind a clean HTTP API, so a used Victor2 / 1420 can drop straight into a Linux/robotics screening pipeline (TR-fluorescence, prompt fluorescence, absorbance/ photometry, luminescence, fluorescence polarization).
A second stack — the bridge — runs on the Linux host and integrates the instrument with eLabFTW (electronic lab notebook): a Run Builder UI for protocol authoring, a direct-submit job API, result write-back, and live status streaming.
Scope: this repository contains the instrument API microservice (
vm-agent/), the eLabFTW bridge (bridge/), and documentation. It does not contain or redistribute any PerkinElmer / Wallac software, firmware, or installation media — you must supply a legitimately licensed OEM installation in the VM.
Deployment scope: the bridge and designer services are designed for deployment on a trusted local area network or Tailscale network and must not be exposed to the public internet without additional hardening. See
SECURITY.mdfor the threat model and the steps required to deploy beyond a trusted LAN.
The HTTP API driving the Victor2 / 1420 from Linux, with the run-builder UI assembling a protocol and streaming live results back over SSE.
Two stacks glue together over the libvirt NAT: the vm-agent (Python 3.8 +
comtypes) runs inside the Windows 7 VM and drives the OEM COM automation
server (Wallac1420.Server) behind a REST/JSON + SSE interface; the bridge
(FastAPI, on the Linux host) sits between the Run Builder UI and the vm-agent,
integrating with eLabFTW as the durable archive while submitting and
executing jobs directly over HTTP.
Run Builder ──► designer :8422 ──► eLabFTW (drafts, signed specs)
bridge :8423 ──► eLabFTW (experiment + results)
vm-agent :8420 ──► OEM COM ──► Victor2 / 1420
For the full architecture (component breakdown, threading model, key design
decisions) see docs/architecture.md; for the
direct-submit design rationale see
docs/architecture-direct-submit.md.
| Method & path | Purpose |
|---|---|
POST /measure |
one call: run a protocol by name, wait, return the OD table |
GET /docs |
self-describing catalog of every endpoint |
GET /health |
liveness + ready flag + instrument connection state |
GET /status |
latest cached instrument snapshot (from the monitor) |
GET /monitor |
SSE real-time state stream (~1 Hz) |
GET /instrument |
model, serial, technologies, temperature |
GET /protocols |
assay protocols (?q= to search, ?refresh=1 to reload) |
GET /protocols/{name|id} |
resolve one protocol by name or id |
POST /runs |
start an assay ({"protocol": "<name|id>"}; "dry_run": true) |
GET /runs / GET /runs/{id} |
run list / single run state |
GET /runs/{id}/results |
results — live while running, persisted once measured |
GET /runs/{id}/export |
results as CSV (?shape=grid, ?value=od|raw) |
POST /runs/{id}/abort |
cancel a run (guarded: only ≥60 s in) |
DELETE /runs/{id} |
drop a finished/failed/stuck run record (?force=1) |
GET /jobs / GET /jobs/{id} |
completed assays |
GET /jobs/{id}/results |
per-well results (JSON, deduped; ?dedup=0 for raw rows) |
GET /jobs/{id}/export |
CSV — ?format=long|grid, ?value=raw|od |
POST /admin/reconnect |
re-establish the COM link |
The friendly entrypoint is POST /measure: give it a protocol name, it
resolves the id, starts the run, waits for the plate read, and returns the
deduped per-well OD table (optionally an 8×12 grid). Errors are actionable —
e.g. 409 instrument_not_ready with a hint telling you to close the lid —
rather than raw COM tracebacks.
The bridge (job submit / execute / abort) and designer (Run Builder, draft
finalize / clone) expose their own REST endpoints. Tables and the shared
auth model live in docs/bridge-api.md.
Auth (summary): all three services use optional
Authorization: Bearer <token>. The vm-agent reads its token from a file on
the VM (TOKEN_FILE in agent.py); the bridge and designer read from env
vars (WALLAC_BRIDGE_TOKEN, WALLAC_DESIGNER_TOKEN). The vm-agent logs a
warning when its token file is absent; bridge/designer disable auth with no
warning when their env token is unset. Tokens must not be committed to
this repository; tracked templates contain placeholder values.
See docs/auth-secrets-policy.md.
Full request/response details: docs/api-reference.md
and docs/bridge-api.md.
Prerequisites
- A Linux/QEMU host running a Windows 7 SP1 guest on the libvirt NAT.
- A licensed Wallac 1420 / Victor2 OEM installation inside the guest, with
the reader connected and recognized by the OEM stack (i.e.
MlrMgrshows the instrument online). - In the guest: Python 3.8 (32-bit) +
comtypes(the OEM COM server and DAO are 32-bit). - On the host: Python 3.11+ +
uv(for tooling) and the bridge dependencies (fastapi,uvicorn,pydantic).
Deploy the vm-agent (in the VM)
- Copy
vm-agent/into the guest (e.g.C:\install\). - (Optional auth) write a secret token to the path named by
TOKEN_FILEinagent.py(defaultC:\Users\Public\agent_token.txt). - Enable autologon for the desktop user and put
start-stack.batin that user's Startup folder. On boot it launches the OEM GUI, the lid watcher, and the agent.
On boot the agent listens on the guest's libvirt NAT address, port 8420.
Deploy the bridge (on the Linux host)
-
Copy
deploy/bridge.env.exampleto/etc/wallac-bridge/bridge.envand fill in the eLabFTW URL + API key, vm-agent URL + token, and optional designer/bridge tokens. For browser/Run Builder deployments, setWALLAC_CORS_ORIGINSto the exact Run Builder origin (defaulthttp://localhost:8422) — the designer at:8422makes cross-origin requests to the bridge at:8423and the browser enforces CORS. -
Install the systemd services:
sudo cp deploy/wallac-bridge.service /etc/systemd/system/ sudo cp deploy/wallac-designer.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now wallac-bridge wallac-designer
The bridge listens on :8423, the designer/Run Builder on :8422.
Use — from the Linux host (replace the IP with your guest's NAT address;
H is the optional bearer header):
H='Authorization: Bearer <token>'
VM=192.168.122.203:8420 # your Windows 7 guest on the libvirt NAT
curl -H "$H" "http://$VM/health"
curl -H "$H" "http://$VM/protocols?q=absorb" # search protocols by name
# the easy path — run a protocol by NAME, wait, get the OD table back:
curl -H "$H" -H 'Content-Type: application/json' \
-d '{"protocol":"Absorbance @ 600"}' "http://$VM/measure"
# ...or drive it yourself: start without waiting, poll, then fetch results:
curl -H "$H" -H 'Content-Type: application/json' \
-d '{"protocol":"Absorbance @ 600","wait":false}' "http://$VM/measure"
curl -H "$H" "http://$VM/runs/<run_id>"
curl -H "$H" "http://$VM/runs/<run_id>/results?shape=grid&value=od"
curl -N -H "$H" "http://$VM/monitor" # live state (SSE)
# any past run's results as an 8x12 grid of computed absorbance:
curl -H "$H" "http://$VM/jobs/<id>/export?format=grid&value=od"The operational runbook (start / verify / restart the win7-wallac VM, the
ARCnet / VFIO passthrough setup) and environment-specific deployment gotchas —
including the nested-Wallac\ OEM install path — live in
docs/deployment-notes.md. The bench-level
runbook is in the internal sister repo wallac-victor2-linux
(host-config/VM-OPERATIONS.md).
Quality gates apply to both maintained stacks — the vm-agent
(vm-agent/{agent,lid_watcher,launch_as_user}.py) and the bridge
(bridge/*.py). Tools run via uv, so no global installs are needed:
make setup # create/update the locked project environment
make validate # ruff + pyright + format-check + pytest (bridge cov >=80%) + complexipy (max 15/func)
make format # auto-fix lint + format
make test # fast suite — unit tests only
make coverage # pytest with the bridge coverage gate (>=80%)
make complexity # cognitive complexity check (max 15 per function)
make setup_dev # create environment + install pre-commit, commit-msg, and pre-push hooksThe unit tests cover pure data-shaping helpers in both stacks: vm-agent
parsing, OD computation, plate-grid CSV; bridge analysis, canonical schemas,
designer CRUD, executor, job manager, and config. Hypothesis drives
property-based tests for canonicalization, well-name normalization,
dedup-key stability, and WellSpec round-trip invariants. The bridge coverage
metric intentionally excludes bridge/elabftw.py and
bridge/vm_agent_client.py — those volatile HTTP adapters are exercised by
hardware/integration testing, not Linux unit tests. The COM/HTTP paths in the
vm-agent require Windows, comtypes, and a live instrument.
| Authority | What it covers |
|---|---|
| README | Entry point — overview, quick-start, API summary, this index |
| Architecture | System design, vm-agent/bridge components, threading model |
| Direct-submit architecture | Direct-submit decision, role changes vs the legacy polling model |
| Bridge & designer API | :8423 / :8422 endpoints and auth model |
| vm-agent API reference | Full :8420 request/response reference |
| Abort & recovery | Abort state machine, race guarantees, incident/rollback |
| eLabFTW object model | Signed object types, canonical JSON, signing model |
| Auth & secrets policy | Service identity, token handling, strict-auth mode |
| Security | Threat model, hardening steps, vulnerability reporting |
| Deployment notes | OEM install path gotchas, VM-operations pointer |
| Protocol authoring plan | Generated protocol design + spec contract |
| Stage-7 hardware e2e plan | Manual integration test plan for the instrument |
| Agent learnings | Dev-only gotchas and patterns (not user-facing) |
Licensed under the Apache License, Version 2.0 — see LICENSE
and NOTICE. This project controls, but does not include or
redistribute, any PerkinElmer / Wallac instrument software, firmware, or
installation media; supply your own legitimately licensed OEM installation.
