Skip to content

Add ASI LS50 Z-only focus stage (USE_ASI_Z_STAGE)#591

Open
Alpaca233 wants to merge 6 commits into
masterfrom
feat/ls50-asi-z-stage
Open

Add ASI LS50 Z-only focus stage (USE_ASI_Z_STAGE)#591
Alpaca233 wants to merge 6 commits into
masterfrom
feat/ls50-asi-z-stage

Conversation

@Alpaca233

Copy link
Copy Markdown
Collaborator

Adds the ASI LS50 Z-only linear stage (own MS-2000-family controller, own USB serial port) as the microscope's Z axis, following the PR #569 pattern: a Z-only AbstractStage adapter composed with the configured XY stage via the (reused, unchanged) CombinedStage.

Stacked on #569 (feat/v308-pi-focus-stage) — needs CombinedStage from that branch. Merge #569 first; this PR's diff is the last 4 commits.

What's in here

  • squid/stage/asi.py (new):
    • MS2000Serial — the shared CR-terminated ASI command transport (locked write/read, :N error acks, test-injectable serial object). Deliberately placed at the same path as dragonfly-andor's XYZ ASIStage, so merging that branch raises an add/add conflict whose resolution is porting ASIStage onto this core (no silently-duplicated ASI serial code).
    • LS50Controller — the driver: M Z=/W Z in 0.1 µm units, / busy polling as the settle signal, SL/SU soft limits (mm), fence clamping with pass-through while unfenced, relative moves resolved to absolute so jogs clamp, HALT tolerating its :N-21 ack. Bring-up performs no motion and writes no parameters.
    • _SimulatedLS50 + ASIZStage (adapter mirroring PIFocusStage: RLock, async-home _busy flag, close-race guard, XY warn-stubs, 0.1 µm z_mm_to_usteps grid) + connect_asi_z_stage factory.
  • Frame semantics (hardware-verified convention): native + is away from the sample, native 0 = the retracted position, no referencing routine exists or is needed. ASI_Z_INVERT defaults True: squid shows −native, so squid 0 = retracted and squid Z+ is toward the sample (Cephla convention). home() = retract to native 0. The only possible bring-up motion is the doubly-opt-in ASI_Z_HOME_ON_STARTUP.
  • uses_external_z_stage() in _def.py — the four external-Z policy sites (home_xyz Z-homing skip + retract-before-XY, cached-Z restore, move_z_axis_to_safety_position) now ask this one predicate instead of OR-ing vendor flags. It is a function, not a derived constant, because the machine-config loader overrides flag globals after definition.
  • Mutual exclusion: USE_PI_FOCUS_STAGE + USE_ASI_Z_STAGE together raises at config load AND at Microscope construction (prevents silently nesting one CombinedStage inside the other).
  • Shared port-by-SN resolver extracted to squid.stage.utils.resolve_serial_port_by_sn; the PI module delegates to it.

Config

[GENERAL]
use_asi_z_stage = True
asi_z_stage_sn = <controller USB serial>   ; or asi_z_serial_port = /dev/ttyUSB0
asi_z_travel_mm = 50                        ; coarse ± sanity fence around power-on zero

Tests

29 new tests in tests/squid/test_stage.py mirroring the PI suite (all written red-first): sim clamp/unfenced-passthrough, sign-flip mapping, protocol framing/parsing against a scripted serial, / wait-idle + timeout, builder no-motion guarantees, predicate call-time reads, Microscope wrap/mutual-exclusion/home_xyz retract, close chain.

  • tests/squid: 132 passed
  • tests/control/test_HighContentScreeningGui.py: 3 passed
  • full-tree collection (1487 tests) clean — all imports work against the new flags
  • simulated end-to-end: CombinedStage wrap → squid +0.5 = native −0.5 → retract to 0 → clean close

Hardware bring-up

Power the controller on with the stage retracted (native 0 = retracted), set asi_z_stage_sn + asi_z_travel_mm = 50, jog to confirm squid + moves toward the sample (else flip asi_z_invert), then consider asi_z_home_on_startup. Known caveat (documented in the flag comments): the frame survives power cycles only if the stage is powered off retracted; LS50Controller.zero_here() (H Z=0) exists unwired for a future re-zeroing flow.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for using an ASI LS50 Z-only focus stage (MS-2000-family controller over USB serial) as the microscope’s Z axis, composed with the configured XY stage via the existing CombinedStage adapter (from the PI focus stage implementation).

Changes:

  • Introduces squid.stage.asi with an MS-2000 serial transport, LS50 controller backend, simulator, and AbstractStage Z-only adapter + connection factory.
  • Centralizes “external Z stage” policy via control._def.uses_external_z_stage() and applies it across homing / cached-position restore / safety-Z behavior.
  • Extracts shared “resolve serial port by USB serial number” helper into squid.stage.utils.resolve_serial_port_by_sn and reuses it from the PI module; adds comprehensive tests for the ASI stage path.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
software/tests/squid/test_stage.py Adds a full test suite for ASI LS50 Z-stage behavior, transport framing, controller units/status polling, builder behavior, and integration with CombinedStage/Microscope.
software/squid/stage/utils.py Switches safety-Z logic to the shared external-Z predicate; adds shared serial-port-by-SN resolver.
software/squid/stage/pi.py Refactors PI port resolution to call the shared resolver in squid.stage.utils.
software/squid/stage/asi.py New ASI LS50 implementation: serial transport, controller backend, simulator, Z-only AbstractStage adapter, and connection factory.
software/control/microscope.py Adds ASI Z-stage wiring (mutual exclusion with PI), and updates homing semantics to use the shared external-Z predicate.
software/control/gui_hcs.py Imports ASI stage when enabled and updates cached-Z restore logic to use the shared external-Z predicate.
software/control/_def.py Adds ASI configuration flags, the uses_external_z_stage() predicate, and mutual-exclusion validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +161 to 165
if _def.uses_external_z_stage():
# External Z focus stages have no Z_HOME_SAFETY_POINT concept; near-retracted is the safe Z.
stage.move_z_to(_def.OBJECTIVE_RETRACTED_POS_MM)
else:
stage.move_z_to(int(_def.Z_HOME_SAFETY_POINT) / 1000.0)
@Alpaca233 Alpaca233 changed the base branch from feat/v308-pi-focus-stage to master July 8, 2026 20:10
Alpaca233 and others added 5 commits July 8, 2026 13:12
…kend

MS2000Serial is the shared CR-terminated ASI command transport (locked
write/read, ':N' error acks, test-injectable serial object) that the
dragonfly-andor XYZ ASIStage should adopt on merge. _SimulatedLS50 mirrors
the LS50 contract: power-on zero, unfenced-passthrough clamping, zero_here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ome)

Mirrors PIFocusStage: RLock over the backend, _busy flag for async home,
close-race guard, XY warn-stubs, 0.1 um z_mm_to_usteps grid. Inversion is a
pure sign flip (squid_z = -native_z) since the LS50 has no absolute travel
to offset against; home() retracts to the configured squid-frame target
(native 0 by convention) and is a warn no-op when unset. Reuses
pi.CombinedStage unchanged for the XY pairing (covered by tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…red port-by-SN resolver

LS50Controller mirrors C414FocusStage's shape over MS2000Serial: W Z position
parse, '/' busy polling as the only settle signal, fence clamp with warning
(pass-through while unfenced -- limits are unknowable at power-on), relative
moves resolved to absolute so jogs clamp, HALT tolerating its ':N-21' ack.
Bring-up performs no motion and writes no parameters; home_on_startup is the
one doubly-opt-in exception. The generic port-by-SN resolver moves to
squid.stage.utils.resolve_serial_port_by_sn; pi._resolve_port_by_sn delegates,
keeping its FTDI-bind-rule hint and existing tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e; convert policy sites

- _def.py: ASI_Z_* flag family (ASI_Z_INVERT defaults True: native + is away
  from the sample, squid shows -native; ASI_Z_HOME_MM defaults 0.0 = the
  retracted end). uses_external_z_stage() is a function, not a derived
  constant, because the machine-config loader overrides flag globals after
  definition. _validate_external_z_stage_flags rejects PI+ASI together at
  both config-load sites.
- microscope.py: elif construction block wrapping the XY stage in
  CombinedStage with the LS50, plus a runtime mutual-exclusion guard so
  programmatic/test configs can't silently double-wrap.
- The four external-Z policy sites (home_xyz Z-homing skip + retract-first,
  gui_hcs cached-Z restore, move_z_axis_to_safety_position) now ask
  uses_external_z_stage() instead of OR-ing vendor flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ior change)

Lint: black reformat of asi.py and test_stage.py (CI failure).

Simplification/dedup from a 4-angle review:
- drop dead PI-referencing vestiges: backend is_referenced() methods (the
  adapter answers True directly), unused _SimulatedLS50.stop/_halt_count
- unify the factory's simulated/real tail (one initialize/fence/wrap path)
- one unit constant: z_mm_to_usteps uses STEPS_PER_MM; _Z_RESOLUTION_MM
  (same 0.1 um fact) removed
- store the home target in the squid frame and flip at use (no pre-derived
  native state)
- microscope.py: mutual-exclusion delegates to the shared _def validator;
  _config_sn_to_str helper replaces the copy-pasted SN normalization; single
  CombinedStage wrap after the vendor if/elif; fixed a comment that
  contradicted the shared retract flows
- pi.py: CombinedStage docstring/comments no longer claim it is V-308-only
- gui_hcs.py: drop the dead ASI import (the import block has no consumers)
- _def.py: note that the power-on-retracted convention is load-bearing for
  the shared retract flows, not just home()
- tests: _sim_combined_stage takes a z_stage, _ls50_ctrl helper, drop a
  redundant assertion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Alpaca233 Alpaca233 force-pushed the feat/ls50-asi-z-stage branch from 587260f to 98b2dbe Compare July 8, 2026 20:13
Read-only by default (port discovery, W Z / status / build-info sanity);
--allow-motion gates a small away-from-sample jog with return-to-start
verification and an optional soft-limit fence clamp test. --simulate dry-runs
against the simulated backend. Reports native and squid-frame positions to
verify the asi_z_invert convention on hardware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants