Add ASI LS50 Z-only focus stage (USE_ASI_Z_STAGE)#591
Open
Alpaca233 wants to merge 6 commits into
Open
Conversation
Contributor
There was a problem hiding this comment.
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.asiwith an MS-2000 serial transport, LS50 controller backend, simulator, andAbstractStageZ-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_snand 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) |
…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>
587260f to
98b2dbe
Compare
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>
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.
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
AbstractStageadapter composed with the configured XY stage via the (reused, unchanged)CombinedStage.What's in here
squid/stage/asi.py(new):MS2000Serial— the shared CR-terminated ASI command transport (locked write/read,:Nerror acks, test-injectable serial object). Deliberately placed at the same path asdragonfly-andor's XYZASIStage, so merging that branch raises an add/add conflict whose resolution is portingASIStageonto this core (no silently-duplicated ASI serial code).LS50Controller— the driver:M Z=/W Zin 0.1 µm units,/busy polling as the settle signal,SL/SUsoft limits (mm), fence clamping with pass-through while unfenced, relative moves resolved to absolute so jogs clamp, HALT tolerating its:N-21ack. Bring-up performs no motion and writes no parameters._SimulatedLS50+ASIZStage(adapter mirroringPIFocusStage: RLock, async-home_busyflag, close-race guard, XY warn-stubs, 0.1 µmz_mm_to_ustepsgrid) +connect_asi_z_stagefactory.ASI_Z_INVERTdefaults 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-inASI_Z_HOME_ON_STARTUP.uses_external_z_stage()in_def.py— the four external-Z policy sites (home_xyzZ-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.USE_PI_FOCUS_STAGE+USE_ASI_Z_STAGEtogether raises at config load AND atMicroscopeconstruction (prevents silently nesting oneCombinedStageinside the other).squid.stage.utils.resolve_serial_port_by_sn; the PI module delegates to it.Config
Tests
29 new tests in
tests/squid/test_stage.pymirroring 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_xyzretract, close chain.tests/squid: 132 passedtests/control/test_HighContentScreeningGui.py: 3 passedCombinedStagewrap → squid +0.5 = native −0.5 → retract to 0 → clean closeHardware 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 flipasi_z_invert), then considerasi_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