Draft
Conversation
Add v1b1 architecture documentation, branch strategy, Air LiHa key facts, and file structure plan for the native v1b1 EVO backend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract EVOArm, LiHa, RoMa firmware command wrappers from the legacy EVO_backend.py into a standalone firmware module under the new vendor namespace pylabrobot/tecan/evo/firmware/. Key changes from legacy: - Accept duck-typed CommandInterface (Protocol) instead of concrete EVOBackend reference — allows firmware wrappers to work with both legacy backend and new TecanEVODriver - self.backend renamed to self.interface for clarity - LiHa._drop_disposable_tip renamed to drop_disposable_tip (no leading _) - TecanError and error_code_to_exception moved to pylabrobot/tecan/evo/errors.py File structure: pylabrobot/tecan/evo/errors.py - error types pylabrobot/tecan/evo/firmware/arm_base.py - EVOArm base + CommandInterface pylabrobot/tecan/evo/firmware/liha.py - LiHa commands pylabrobot/tecan/evo/firmware/roma.py - RoMa commands Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TecanEVODriver(Driver) owns the USB connection and implements the Tecan
firmware command protocol. Extracted from legacy TecanLiquidHandler.
- USB connection lifecycle (setup/stop)
- Command assembly (\x02{module}{cmd},{params}\x00)
- Response parsing with error code handling
- SET command caching (skip redundant sends)
- Satisfies CommandInterface protocol for firmware wrappers
- 16 unit tests (command assembly, response parsing, caching, serialization)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EVOPIPBackend(PIPBackend) implements the v1b1 PIP interface for Tecan EVO syringe-based liquid handling. Ported from legacy EVOBackend with: - pick_up_tips, drop_tips, aspirate, dispense via LiHa firmware wrapper - Position calculation (_liha_positions) with X/Y/Z coordinate transforms - Liquid class lookup from legacy module (import, not duplicated) - Airgap, liquid detection, tracking movement helpers - Y-spacing fix: uses plate.item_dy (well pitch) not well size - Conversion factors as class attributes (STEPS_PER_UL=3, SPEED_FACTOR=6) - Accepts TecanEVODriver via constructor, uses LiHa firmware wrapper Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AirEVOPIPBackend(EVOPIPBackend) overrides for air displacement pipetting: - ZaapMotion boot exit (T2xX) and motor config (33 commands per tip) - Safety module power-on (O1 SPN/SPS3) - Init-skip when already initialized (REE0 check) - Conversion factors: 106.4 steps/uL, 213 speed factor - Force mode wrapping (SFR/SFP/SDP) around all plunger operations - Direct z_start from tip rack for AGT (bypass coordinate transform) - SDT/PPA before tip discard Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 5 — EVORoMaBackend(GripperArmBackend): - RoMa plate handling via vector coordinate trajectories - Gripper control (open, close, grip_plate) - Park, halt, move_to_location, get_gripper_location - Carrier-based coordinate computation (_roma_positions) - pick_up_from_carrier / drop_at_carrier for carrier-aware operations Phase 6 — TecanEVO(Resource, Device): - Composite device with Driver + PIP + GripperArm capabilities - Constructor flags: air_liha=True selects AirEVOPIPBackend, has_roma=True adds arm - Capability ordering: arm first (must park before LiHa X-init) - Deck assigned as child resource for coordinate calculations - Exports from pylabrobot.tecan.evo.__init__ Usage: evo = TecanEVO(deck=EVO150Deck(), diti_count=8, air_liha=True) await evo.setup() await evo.pip.pick_up_tips(...) await evo.pip.aspirate(...) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- hardware_testing_checklist.md: 8 test scenarios with pass/fail tables, Z-calibration procedure, failure actions - test_v1b1_init.py: v1b1 TecanEVO initialization test (cold + warm boot) - test_v1b1_pipette.py: v1b1 full pipetting cycle test - jog_and_teach.py: Interactive jog/teach/labware editor with: - X/Y/Z jogging with configurable step sizes - Position recording and goto - Deck layout visualization (carriers, sites, contents) - Labware detail viewer (z_start, z_max, area, well pitch) - Live Z-value teaching (teach z_start <name> from current position) - Labware edits saved to labware_edits.json - REE axis status, tip status checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flask-based single-page app at http://localhost:5050 with: - Real-time LiHa and RoMa position display with bar graphs - Numpad keyboard controls for LiHa (4/6=X, 8/2=Y, +/-=Z, 7/9=step) - Arrow key controls for RoMa (arrows=X/Y, PgUp/PgDn=Z, Home/End=R) - Adjustable step sizes (0.1mm to 50mm) - Teach positions: record current position with label - Teach labware: set z_start/z_dispense/z_max from current Z - Quick actions: home LiHa, park RoMa, tip status, axis errors - Saved positions display - Activity log - Dark theme UI with position bar graphs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom labware definitions: Eppendorf plate (skirted TecanPlate variant) and DiTi 50uL SBS tip rack with corrected tip length and AIRDITI tip type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds 8 DiTi 50uL-specific liquid class entries for AIRDITI tip type (Water and DMSO, free dispense and wet contact, various volume ranges from 0.5-50uL). These were originally derived from EVOware's DefaultLCs.XML and validated against USB captures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents which files to include/exclude when creating clean PRs, pre-PR checks (lint, format, type, tests), and branch creation steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Mark Phases 0-6b as COMPLETE with detailed sub-task checkboxes - Add Phase 7 (hardware testing) and Phase 8 (PR cleanup) TODO items - Add encapsulation verification section confirming both branches are clean - Reference PR_cleanup_checklist.md for detailed PR preparation steps - Update effort estimates (3.5 of ~5 sessions complete) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
58 total tests across 4 test files (all passing): pip_backend_tests.py (14 tests): - Conversion factors (3/6 for syringe) - Utility methods (bin_use_channels, first_valid, get_ys) - Tip pickup command sequence and syringe conversion - Drop tips sends AST - Aspirate sends tracking commands (MTR, MAZ) - Dispense sends tracking commands (MTR, SPP) - num_channels property and pre-setup error air_pip_backend_tests.py (18 tests): - Air conversion factors (106.4/213) - Force mode on/off sends 16 commands each (8 SFR + 8 SFP/SDP) - Init-skip: all-@ = initialized, A/G = not, Y = initialized, timeout = not - ZaapMotion config: 33 commands, skip on RCS OK, boot exit sends X - Safety module sends SPN/SPS3 - Air tip pickup uses Air conversion factors and force mode wrapping - AGT uses tip rack z_start directly roma_backend_tests.py (10 tests): - Park sends SAA+AAC, halt sends BMA - Open/close gripper, is_gripper_closed - get_gripper_location returns correct coordinates - pick_up_from_carrier sends trajectory (SFX, SAA, AAC, SGG, AGR) - drop_at_carrier sends trajectory (STW, SAA, AAC, PAG) driver_tests.py (16 tests): unchanged from Phase 2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- RoMa backend checks REE before PIA — if all axes @ (OK), skip the 56-second PIA+park sequence on warm reconnect - Driver uses 1s packet timeout for USB buffer drain (was 30s) - Test script shows per-step timing and pre-init axis status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Busy lock prevents sending commands while previous move is executing (fixes 'Command overflow of TeCU' error 8) - All jog commands logged to UI: "> LIHA x+ 5.0mm" - Firmware command shown in log on success: "C5 PRX50" - Status bar shows 'Moving...' during commands - Actions and teach operations also logged with busy lock - Keyboard input ignored while busy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Position polling and commands were interleaving on the USB bus, causing garbled responses that showed as wild position jumps. - Server-side threading.Lock prevents concurrent USB access - Position poll skips (returns empty) if a command holds the lock - Client-side poll skips while busy flag is set - All routes (jog, record, teach, action) hold the lock Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Labware tab buttons for each item on the deck - Detail panel shows: type, model, dimensions, location, z_start, z_dispense, z_max, area, well pitch, well count, tip length, tip type - Edited values highlighted with EDITED tag in yellow - Teach updates refresh labware display immediately - Teach dropdown auto-populated from deck labware - /labware API endpoint discovers all labware on carrier sites Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- UI starts immediately without hardware connection - Deck and labware visible before connecting - Connect button initiates EVO setup (ZaapMotion + PIA) - Disconnect button cleanly closes USB - Jog/actions disabled when disconnected with user message - Position polling skips when disconnected - Button style changes: blue=Connect, red=Disconnect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Lamp Green: O1,SSL1,1 (safety level 1 on) - Lamp Off: O1,SSL1,0 - Lamp Test: cycles through SSL1,SSL2,SPS1,SPS2,SPS3 with 1s delays to identify which commands control which lamp states - Motor Power: O1,SPN + O1,SPS3 - Power Off: O1,SPS0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The carrier.sites returns integers, not resources. Labware is found by recursing through carrier.children → PlateHolder.children → plates. Also collapse lamp/power buttons into a <details> expander. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tecan Z coords: 0 = deck, z_range = top. Positive PRZ = move up. Numpad - should decrease Z (move down toward deck). Also fix RoMa PgUp/PgDn Z direction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Plate z_start/z_dispense/z_max updated from jog tool taught values (absolute Tecan Z: 0=deck, taught tops at ~260-300) - Aspirate uses plate.z_start directly instead of broken _liha_positions - Dispense uses plate.z_dispense directly - Tip rack z_start fine-tuned to 820 (taught top at 780) - Retract after aspirate uses plate z_start not _liha_positions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Taught Z positions were measured with bare channels (no tip). When tips are mounted, the effective reach extends by (tip_length - nesting_depth). Z target moves UP by this amount. tip_ext = total_tip_length * 10 - 50 (58.1mm tip, ~5mm nesting = 531 units) z_target = plate.z_start + tip_ext Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Taught positions showed different X/Y offsets for different labware: - Tips: +62 X (+6.2mm), +18 Y (+1.8mm) - Plates: +103 X (+10.3mm), -6 Y (-0.6mm) Added x_offset/y_offset attributes to labware definitions, applied via _apply_calibration_offsets() in all Air PIP operations (pick_up_tips, drop_tips, aspirate, dispense). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
z_start=850 (above tip top at 780), z_max=550 (30mm search range). Wider range ensures tips fully seat for force feedback confirmation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mix/blow-out/tip-touch
- Wrap raw send_command calls (REE, PIA, PIB, BMX, BMA, PPA, SDT) in typed firmware methods
- Add new firmware commands: RPP, RVZ, RTS, PAZ for plunger/Z/tip queries
- Create ZaapMotion firmware class replacing ~50 raw T2{tip} string commands
- Implement mixing (_perform_mix) and blow-out (_perform_blow_out) with Air LiHa force mode overrides
- Add TecanPIPParams (tip_touch, LLD config) and TecanRoMaParams (speed profiles)
- Implement request_tip_presence() via RTS firmware command
- Add configurable RoMa speed profiles, post-grip plate verification, configurable park position
- Update tests for new firmware wrapper call patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test 1 (cold boot init): PASSED — all axes initialized - Test 2 (warm reconnect): PASSED — 3.4s total (was 60s) - Test 3 (tip pickup): IN PROGRESS — Z/X calibration done, AGT error 26 needs further tuning - Tests 4-8: NOT STARTED, blocked by Test 3 - Documented all taught positions, calibration offsets, Z coordinate notes, and available tools Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…re dev notes - Updated v1b1_migration_plan.md with Phase 6c (firmware enhancements) and Phase 6d (system-level future work) - Marked v1b1 and legacy firmware plans with completion status and deferred items - Moved both plans to keyser-testing/completed-plans/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused Union import from arm_base.py - Exclude OCR'd manual text and USB capture logs from typo checks - Whitelist Tecan firmware command abbreviations (ALO, SOM, SHS, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
475dbab to
588abbb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan: Migrate Tecan EVO Backend to v1b1 Architecture
Context
The pylabrobot v1b1 branch introduces a new Driver + Capability architecture replacing the legacy monolithic
MachineBackendpattern. Hamilton backends have already been migrated. The Tecan EVO backend (both syringe LiHa and Air LiHa) currently exists only in legacy form. We need to:air-liha-backendbranch as legacy (potential merge to main)v1b1-tecan-evobranch offorigin/v1b1on our forkv1b1 Architecture Summary
Driver→ owns I/O,setup()/stop()open/close connectionCapabilityBackend→ translates operations into driver commands, has_on_setup()/_on_stop()Capability→ user-facing API with tip tracking, validationDevice→ orchestrates lifecycle: driver.setup() → cap._on_setup() in orderBackendParamsdataclasses replace**kwargspylabrobot/legacy/, wrapped by adaptersFile Structure
Unchanged files (no edits needed):
pylabrobot/resources/tecan/— plates, tip racks, carriers, deckspylabrobot/io/usb.py— USB I/O classpylabrobot/device.py— Driver/Device base classespylabrobot/capabilities/— PIPBackend, GripperArmBackend interfacespylabrobot/legacy/liquid_handling/backends/tecan/— legacy backend staysLiquid classes: Import
TecanLiquidClassandget_liquid_classfrompylabrobot.legacy.liquid_handling.liquid_classes.tecan. No duplication — this works until liquid classes are refactored upstream.Class Hierarchy
TecanEVODriver (extracted from TecanLiquidHandler)
Source: extract from
TecanLiquidHandler(lines 56-174 of EVO_backend.py)EVOPIPBackend (syringe LiHa)
Source: port from
EVOBackendmethods, adaptingSingleChannelAspiration→Aspirationetc.AirEVOPIPBackend (Air LiHa / ZaapMotion)
Source: port from
AirEVOBackendEVORoMaBackend (plate handling)
Source: port from
EVOBackend.pick_up_resource/drop_resource+RoMaclassTecanEVO (composite device)
Key Design Decisions
Syringe vs Air LiHa → Subclass
AirEVOPIPBackend(EVOPIPBackend)with overridden constants and_on_setup. Same pattern as currentAirEVOBackend(EVOBackend). Config flag onTecanEVO(air_liha=True)selects the backend class.Arm Init Ordering
RoMa must park before LiHa initializes (clears X-axis path). Set
self._capabilities = [arm, pip]so arm's_on_setup()runs first.Firmware Wrappers → Shared via Driver
LiHaandRoMaclasses are extracted tofirmware/and accept a duck-typed interface (anything withsend_command). Both PIP backend and RoMa backend instantiate their own firmware wrapper with a reference to the shared driver. The collision cache (EVOArm._pos_cache) remains a class variable.Deck Reference
PIP and RoMa backends need the deck for coordinate calculations. Passed via constructor, stored as
self._deck. The deck is also a child resource ofTecanEVO.Operation Type Mapping
SingleChannelAspirationAspirationSingleChannelDispenseDispensePickupPickup(same name, different module)DropTipDropFields are nearly identical. The v1b1 types add
liquid_height,blow_out_air_volume,mix— ignored by Tecan backend (uses liquid class values instead).Implementation Phases
Phase 0: Documentation & Project Setup ✅ COMPLETE
keyser-testing/v1b1_migration_plan.md(this file)v1b1-tecan-evoofforigin/v1b1on forkPhase 1: Firmware Extraction ✅ COMPLETE
pylabrobot/tecan/evo/directory structureEVOArm,LiHa,RoMaintofirmware/withCommandInterfaceProtocolerrors.py(TecanError, error_code_to_exception)self.backend→self.interfacein firmware wrappersLiHa._drop_disposable_tip→drop_disposable_tip(no leading _)Phase 2: Driver ✅ COMPLETE
TecanEVODriver(Driver)fromTecanLiquidHandlerCommandInterfaceProtocolPhase 3: Syringe PIP Backend ✅ COMPLETE
EVOPIPBackend(PIPBackend)with all operations_liha_positions,_aspirate_action,_dispense_action,_aspirate_airgap,_liquid_detectionpick_up_tips,drop_tips,aspirate,dispensepylabrobot.legacyplate.item_dy(well pitch) notwell.size_yPhase 4: Air PIP Backend ✅ COMPLETE
AirEVOPIPBackend(EVOPIPBackend)with ZaapMotion supportT2xX) and motor config (33 commands × 8 tips)_is_initialized/_setup_quick)Phase 5: RoMa Backend ✅ COMPLETE
EVORoMaBackend(GripperArmBackend)_roma_positions, vector coordinate table, gripper commandspick_up_from_carrier/drop_at_carrierwith carrier-aware coordinateshalt,park,move_to_location,get_gripper_locationopen_gripper,close_gripper,is_gripper_closedPhase 6: Device Composition ✅ COMPLETE
TecanEVO(Resource, Device)composing Driver + PIP + GripperArmair_liha=True,has_roma=Truepylabrobot.tecan.evo.__init__Phase 6b: Tooling ✅ COMPLETE
keyser-testing/hardware_testing_checklist.md(8 test scenarios)keyser-testing/test_v1b1_init.py(init test script)keyser-testing/test_v1b1_pipette.py(pipetting test script)keyser-testing/jog_and_teach.py(CLI jog/teach/labware editor)keyser-testing/jog_ui.py(web-based jog UI with keyboard controls)keyser-testing/labware_library.pyfrom air-liha-backendPhase 6c: Firmware Feature Enhancements ✅ COMPLETE
send_commandcalls (REE, PIA, PIB, BMX, BMA, PPA, SDT) in typed firmware methodsfirmware/zaapmotion.py— ZaapMotion class replacing ~50 raw T2{tip} string commandsparams.py—TecanPIPParams(tip touch, LLD config) andTecanRoMaParams(speed profiles)_perform_mix) and blow-out (_perform_blow_out) with Air LiHa force mode overridesrequest_tip_presence()via RTS firmware commandTecanPIPParams) and LLD config override in aspiratecompleted-plans/v1b1_firmware_feature_plan.mdfor full planPhase 6d: System-Level Features — FUTURE
These items are deferred until after hardware validation (Phase 7). They build on the firmware wrappers from Phase 6c:
read_error_register()to detect axis errors, attempt re-init (PIA + BMX) for recoverable states (G=not initialized), surface unrecoverable errors to the userread_error_register()+read_tip_status()+read_plunger_positions()into a status dict onTecanEVO, useful for dashboard/monitoringSPN/SPSchecks during long operations (currently only sent during Air LiHa setup)TecanRoMaParamsthroughdrop_at_carrier()(currently onlypick_up_from_carrieruses configurable speeds)air-liha-backend, tip touch and LLD config require**backend_kwargsto be plumbed through theLiquidHandlerBackendabstract interface — deferred as it touches shared base classesPhase 7: Hardware Testing — TODO
Phase 8: PR Cleanup — TODO
keyser-testing/PR_cleanup_checklist.mdfor detailed stepsBackendParams Dataclasses
Testing Strategy
Unit Tests (mocked driver)
driver_tests.py— command assembly, response parsing, error codespip_backend_tests.py— verify firmware command sequences for each operationair_pip_backend_tests.py— verify ZaapMotion config, force mode wrappingroma_backend_tests.py— verify RoMa command sequencesevo_tests.py— lifecycle, capability orderingHardware Tests
test_air_evo_init.py— ZaapMotion boot exit + PIA (already exists)test_air_evo_pipette.py— full pipetting cycle (already exists, needs v1b1 adaptation)test_evo_roma.py— plate pickup and placement (new)Regression
pylabrobot/legacy/importsEVO_tests.pyin legacy unchangedCLAUDE.md Updates
Add to CLAUDE.md:
Project Checklist
v1b1-tecan-evoofforigin/v1b1PR_cleanup_checklist.md)Encapsulation Verification
Both branches verified as fully encapsulated (2026-03-28):
air-liha-backend→ PR tomain:backends/tecan/__init__.py(1 import),liquid_classes/tecan.py(8 entries appended)v1b1-tecan-evo→ PR tov1b1:legacy/.../liquid_classes/tecan.py(8 entries appended),claude.mdpylabrobot/tecan/(entirely new vendor namespace)PR cleanup procedure documented in
keyser-testing/PR_cleanup_checklist.md.Estimated Effort