Skip to content

V1b1 tecan evo#982

Draft
Robert-Keyser-Calico wants to merge 37 commits intoPyLabRobot:v1b1from
Robert-Keyser-Calico:v1b1-tecan-evo
Draft

V1b1 tecan evo#982
Robert-Keyser-Calico wants to merge 37 commits intoPyLabRobot:v1b1from
Robert-Keyser-Calico:v1b1-tecan-evo

Conversation

@Robert-Keyser-Calico
Copy link
Copy Markdown
Contributor

Plan: Migrate Tecan EVO Backend to v1b1 Architecture

Context

The pylabrobot v1b1 branch introduces a new Driver + Capability architecture replacing the legacy monolithic MachineBackend pattern. 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:

  1. Keep air-liha-backend branch as legacy (potential merge to main)
  2. Create v1b1-tecan-evo branch off origin/v1b1 on our fork
  3. Build a native v1b1 Tecan EVO implementation
  4. Support both syringe and Air LiHa variants

v1b1 Architecture Summary

Device (frontend)
  ├── _driver: Driver (USB I/O, connection lifecycle)
  └── _capabilities: [Capability, ...]
        └── backend: CapabilityBackend (protocol translation)
  • Driver → owns I/O, setup()/stop() open/close connection
  • CapabilityBackend → translates operations into driver commands, has _on_setup()/_on_stop()
  • Capability → user-facing API with tip tracking, validation
  • Device → orchestrates lifecycle: driver.setup() → cap._on_setup() in order
  • BackendParams dataclasses replace **kwargs
  • Legacy code lives in pylabrobot/legacy/, wrapped by adapters

File Structure

pylabrobot/tecan/                        # NEW vendor namespace
  __init__.py
  evo/
    __init__.py                          # exports TecanEVO, TecanEVODriver, etc.
    driver.py                            # TecanEVODriver(Driver)
    pip_backend.py                       # EVOPIPBackend(PIPBackend) — syringe LiHa
    air_pip_backend.py                   # AirEVOPIPBackend(EVOPIPBackend)
    roma_backend.py                      # EVORoMaBackend(GripperArmBackend)
    evo.py                               # TecanEVO(Resource, Device)
    params.py                            # BackendParams dataclasses
    errors.py                            # TecanError (moved from legacy)
    firmware/
      __init__.py
      arm_base.py                        # EVOArm base (collision cache)
      liha.py                            # LiHa firmware commands
      roma.py                            # RoMa firmware commands
    tests/
      driver_tests.py
      pip_backend_tests.py
      air_pip_backend_tests.py
      roma_backend_tests.py
      evo_tests.py

Unchanged files (no edits needed):

  • pylabrobot/resources/tecan/ — plates, tip racks, carriers, decks
  • pylabrobot/io/usb.py — USB I/O class
  • pylabrobot/device.py — Driver/Device base classes
  • pylabrobot/capabilities/ — PIPBackend, GripperArmBackend interfaces
  • pylabrobot/legacy/liquid_handling/backends/tecan/ — legacy backend stays

Liquid classes: Import TecanLiquidClass and get_liquid_class from pylabrobot.legacy.liquid_handling.liquid_classes.tecan. No duplication — this works until liquid classes are refactored upstream.

Class Hierarchy

TecanEVODriver (extracted from TecanLiquidHandler)

class TecanEVODriver(Driver):
    def __init__(self, packet_read_timeout=12, read_timeout=60, write_timeout=60)
    async def setup()          # opens USB
    async def stop()           # closes USB
    async def send_command(module, command, params=None, ...) -> dict
    def _assemble_command(module, command, params) -> str
    def parse_response(resp) -> dict

Source: extract from TecanLiquidHandler (lines 56-174 of EVO_backend.py)

EVOPIPBackend (syringe LiHa)

class EVOPIPBackend(PIPBackend):
    def __init__(self, driver: TecanEVODriver, deck: Resource, diti_count=0)
    STEPS_PER_UL = 3
    SPEED_FACTOR = 6

    async def _on_setup()      # PIA, init plungers, query ranges
    async def pick_up_tips(ops, use_channels, backend_params=None)
    async def drop_tips(ops, use_channels, backend_params=None)
    async def aspirate(ops, use_channels, backend_params=None)
    async def dispense(ops, use_channels, backend_params=None)
    def can_pick_up_tip(channel_idx, tip) -> bool

Source: port from EVOBackend methods, adapting SingleChannelAspirationAspiration etc.

AirEVOPIPBackend (Air LiHa / ZaapMotion)

class AirEVOPIPBackend(EVOPIPBackend):
    STEPS_PER_UL = 106.4
    SPEED_FACTOR = 213.0

    async def _on_setup()      # ZaapMotion config + safety + super()._on_setup()
    async def aspirate(...)     # wraps with force mode
    async def dispense(...)     # wraps with force mode

Source: port from AirEVOBackend

EVORoMaBackend (plate handling)

class EVORoMaBackend(GripperArmBackend):
    def __init__(self, driver: TecanEVODriver, deck: Resource)

    async def _on_setup()                    # PIA for RoMa, park
    async def pick_up_at_location(location, resource_width, backend_params=None)
    async def drop_at_location(location, resource_width, backend_params=None)
    async def move_to_location(location, backend_params=None)
    async def halt(backend_params=None)
    async def park(backend_params=None)
    async def open_gripper(gripper_width, backend_params=None)
    async def close_gripper(gripper_width, backend_params=None)
    async def is_gripper_closed(backend_params=None) -> bool
    async def get_gripper_location(backend_params=None) -> GripperLocation

Source: port from EVOBackend.pick_up_resource/drop_resource + RoMa class

TecanEVO (composite device)

class TecanEVO(Resource, Device):
    def __init__(self, name, deck, diti_count=0, air_liha=False, has_roma=True, ...)
    # Creates driver + capabilities based on config
    # self._capabilities = [arm, pip] (arm first for init ordering)

Key Design Decisions

Syringe vs Air LiHa → Subclass

AirEVOPIPBackend(EVOPIPBackend) with overridden constants and _on_setup. Same pattern as current AirEVOBackend(EVOBackend). Config flag on TecanEVO(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

LiHa and RoMa classes are extracted to firmware/ and accept a duck-typed interface (anything with send_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 of TecanEVO.

Operation Type Mapping

Legacy v1b1
SingleChannelAspiration Aspiration
SingleChannelDispense Dispense
Pickup Pickup (same name, different module)
Drop TipDrop

Fields 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

  • Updated CLAUDE.md with v1b1 architecture, branch strategy, Air LiHa facts
  • Created keyser-testing/v1b1_migration_plan.md (this file)
  • Created branch v1b1-tecan-evo off origin/v1b1 on fork
  • Verified v1b1 imports: Driver, Device, PIPBackend, GripperArmBackend
  • Verified legacy imports: EVOBackend, TecanLiquidClass
  • Verified Tecan resources unchanged between main and v1b1

Phase 1: Firmware Extraction ✅ COMPLETE

  • Created pylabrobot/tecan/evo/ directory structure
  • Extracted EVOArm, LiHa, RoMa into firmware/ with CommandInterface Protocol
  • Moved errors.py (TecanError, error_code_to_exception)
  • Renamed self.backendself.interface in firmware wrappers
  • Renamed LiHa._drop_disposable_tipdrop_disposable_tip (no leading _)
  • Verified imports, no circular deps

Phase 2: Driver ✅ COMPLETE

  • Created TecanEVODriver(Driver) from TecanLiquidHandler
  • USB connection, command assembly, response parsing, SET caching
  • Driver satisfies CommandInterface Protocol
  • 16 unit tests (command assembly, response parsing, caching, serialization)

Phase 3: Syringe PIP Backend ✅ COMPLETE

  • Created EVOPIPBackend(PIPBackend) with all operations
  • Ported _liha_positions, _aspirate_action, _dispense_action, _aspirate_airgap, _liquid_detection
  • Ported pick_up_tips, drop_tips, aspirate, dispense
  • Adapted from legacy types (SingleChannelAspiration → Aspiration, Drop → TipDrop)
  • Liquid class lookup imported from pylabrobot.legacy
  • Y-spacing fix: uses plate.item_dy (well pitch) not well.size_y

Phase 4: Air PIP Backend ✅ COMPLETE

  • Created AirEVOPIPBackend(EVOPIPBackend) with ZaapMotion support
  • ZaapMotion boot exit (T2xX) and motor config (33 commands × 8 tips)
  • Safety module (SPN/SPS3)
  • T23SDO11,1 before PIA
  • Init-skip via REE0 check (_is_initialized / _setup_quick)
  • Conversion factors: 106.4 steps/µL, 213 speed factor
  • Force mode wrapping (SFR/SFP/SDP) around all plunger ops
  • Direct z_start from tip rack for AGT
  • SDT + PPA0 before tip discard

Phase 5: RoMa Backend ✅ COMPLETE

  • Created EVORoMaBackend(GripperArmBackend)
  • Ported _roma_positions, vector coordinate table, gripper commands
  • pick_up_from_carrier / drop_at_carrier with carrier-aware coordinates
  • halt, park, move_to_location, get_gripper_location
  • open_gripper, close_gripper, is_gripper_closed

Phase 6: Device Composition ✅ COMPLETE

  • Created TecanEVO(Resource, Device) composing Driver + PIP + GripperArm
  • Constructor flags: air_liha=True, has_roma=True
  • Capability ordering: arm first (must park before LiHa X-init)
  • Deck assigned as child resource
  • Exports from pylabrobot.tecan.evo.__init__
  • Verified construction for all config combinations

Phase 6b: Tooling ✅ COMPLETE

  • Created keyser-testing/hardware_testing_checklist.md (8 test scenarios)
  • Created keyser-testing/test_v1b1_init.py (init test script)
  • Created keyser-testing/test_v1b1_pipette.py (pipetting test script)
  • Created keyser-testing/jog_and_teach.py (CLI jog/teach/labware editor)
  • Created keyser-testing/jog_ui.py (web-based jog UI with keyboard controls)
  • Ported keyser-testing/labware_library.py from air-liha-backend
  • Added ZaapDiTi 50µL liquid class entries to legacy liquid classes

Phase 6c: Firmware Feature Enhancements ✅ COMPLETE

  • Wrapped raw send_command calls (REE, PIA, PIB, BMX, BMA, PPA, SDT) in typed firmware methods
  • Added new firmware commands: RPP, RVZ, RTS, PAZ
  • Created firmware/zaapmotion.py — ZaapMotion class replacing ~50 raw T2{tip} string commands
  • Created params.pyTecanPIPParams (tip touch, LLD config) and TecanRoMaParams (speed profiles)
  • Implemented mixing (_perform_mix) and blow-out (_perform_blow_out) with Air LiHa force mode overrides
  • Implemented request_tip_presence() via RTS firmware command
  • Added tip touch (via TecanPIPParams) and LLD config override in aspirate
  • Added configurable RoMa speed profiles, post-grip plate verification, configurable park position
  • See completed-plans/v1b1_firmware_feature_plan.md for full plan

Phase 6d: System-Level Features — FUTURE

These items are deferred until after hardware validation (Phase 7). They build on the firmware wrappers from Phase 6c:

  • Error recovery: Use read_error_register() to detect axis errors, attempt re-init (PIA + BMX) for recoverable states (G=not initialized), surface unrecoverable errors to the user
  • Instrument status API: Aggregate read_error_register() + read_tip_status() + read_plunger_positions() into a status dict on TecanEVO, useful for dashboard/monitoring
  • Safety module monitoring: Periodic SPN/SPS checks during long operations (currently only sent during Air LiHa setup)
  • RoMa drop_at_carrier speed profiles: Wire TecanRoMaParams through drop_at_carrier() (currently only pick_up_from_carrier uses configurable speeds)
  • Legacy branch kwargs wiring: On air-liha-backend, tip touch and LLD config require **backend_kwargs to be plumbed through the LiquidHandlerBackend abstract interface — deferred as it touches shared base classes

Phase 7: Hardware Testing — TODO

  • Test init from cold boot (ZaapMotion config + PIA)
  • Test init-skip on warm reconnect
  • Calibrate Z positions using jog tool:
    • Tip rack z_start / z_max
    • Source plate z_start / z_dispense / z_max
    • Dest plate z_start / z_dispense / z_max
  • Test tip pickup (8 channels)
  • Test aspirate (25µL water)
  • Test dispense (25µL water)
  • Test tip drop
  • Test full cycle (pickup → aspirate → dispense → drop)
  • Test RoMa plate handling (if applicable)
  • Investigate and fix X offset (currently hardcoded +60 on air-liha-backend)

Phase 8: PR Cleanup — TODO

  • Remove debug print statements from backends
  • Add remaining unit tests (pip_backend_tests, air_pip_backend_tests, roma_backend_tests)
  • Add docstrings where missing
  • Run full lint/format/typecheck suite
  • Verify legacy backend still works unmodified
  • Create clean PR branches (strip keyser-testing/, claude.md, .claude/)
  • See keyser-testing/PR_cleanup_checklist.md for detailed steps
  • Submit PR to upstream v1b1 branch

BackendParams Dataclasses

@dataclass
class EVOAspirateParams(BackendParams):
    liquid: Liquid = Liquid.WATER
    lld_mode: Optional[int] = None

@dataclass
class EVODispenseParams(BackendParams):
    liquid: Liquid = Liquid.WATER

@dataclass
class EVOPickUpTipParams(BackendParams):
    z_start_override: Optional[int] = None
    z_search_override: Optional[int] = None

@dataclass
class EVODropTipParams(BackendParams):
    discard_height: int = 0

@dataclass
class EVORoMaParams(BackendParams):
    speed_x: int = 10000
    grip_speed: int = 100
    grip_pwm: int = 75

Testing Strategy

Unit Tests (mocked driver)

  • driver_tests.py — command assembly, response parsing, error codes
  • pip_backend_tests.py — verify firmware command sequences for each operation
  • air_pip_backend_tests.py — verify ZaapMotion config, force mode wrapping
  • roma_backend_tests.py — verify RoMa command sequences
  • evo_tests.py — lifecycle, capability ordering

Hardware 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

  • Verify legacy backend still works via pylabrobot/legacy/ imports
  • Run existing EVO_tests.py in legacy unchanged

CLAUDE.md Updates

Add to CLAUDE.md:

### v1b1 Architecture
- Branch: `v1b1-tecan-evo` (off origin/v1b1)
- Pattern: Driver + CapabilityBackend (see pylabrobot/device.py)
- Tecan EVO native: `pylabrobot/tecan/evo/`
- Legacy EVO: `pylabrobot/legacy/liquid_handling/backends/tecan/`
- Reference: Hamilton STAR at `pylabrobot/hamilton/liquid_handlers/star/`
- Key interfaces: PIPBackend, GripperArmBackend, BackendParams

Project Checklist

  • Phase 0: Update CLAUDE.md and create migration docs
  • Phase 0: Create branch v1b1-tecan-evo off origin/v1b1
  • Phase 0: Verify v1b1 branch builds
  • Phase 1: Firmware extraction (LiHa, RoMa, EVOArm)
  • Phase 2: TecanEVODriver + 16 unit tests
  • Phase 3: EVOPIPBackend (syringe)
  • Phase 4: AirEVOPIPBackend (Air LiHa)
  • Phase 5: EVORoMaBackend
  • Phase 6: TecanEVO device composition
  • Phase 6b: Test scripts, jog tools, labware library
  • Phase 7: Hardware testing (init, tips, aspirate, dispense)
  • Phase 8: PR cleanup (see PR_cleanup_checklist.md)

Encapsulation Verification

Both branches verified as fully encapsulated (2026-03-28):

air-liha-backend → PR to main:

  • Only modifies: backends/tecan/__init__.py (1 import), liquid_classes/tecan.py (8 entries appended)
  • All other files are NEW (no existing code modified)
  • No changes to: machines/, resources/, io/, base classes

v1b1-tecan-evo → PR to v1b1:

  • Only modifies: legacy/.../liquid_classes/tecan.py (8 entries appended), claude.md
  • All other files are NEW under pylabrobot/tecan/ (entirely new vendor namespace)
  • No changes to: device.py, capabilities/, resources/, io/, arms/, base classes

PR cleanup procedure documented in keyser-testing/PR_cleanup_checklist.md.

Estimated Effort

  • Phase 0: ~0.5 session ✅
  • Phases 1-2: ~1 session ✅
  • Phases 3-4: ~1 session ✅ (faster than estimated)
  • Phases 5-6: ~0.5 session ✅ (faster than estimated)
  • Phase 6b: ~0.5 session ✅ (tooling)
  • Phase 7: ~1 session (hardware testing + Z calibration) — NEXT
  • Phase 8: ~0.5 session (cleanup + PR)
  • Total: ~5 sessions, ~3.5 complete

Robert-Keyser-Calico and others added 30 commits March 28, 2026 10:48
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>
Robert-Keyser-Calico and others added 7 commits March 30, 2026 15:53
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>
@rickwierenga rickwierenga force-pushed the v1b1 branch 5 times, most recently from 475dbab to 588abbb Compare April 2, 2026 21:12
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.

1 participant