Add ASI LS50 Z-only focus stage (USE_ASI_Z_STAGE)#591
Open
Alpaca233 wants to merge 16 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>
- MS2000Serial.open flushes stale input (cable-plug line noise earns an ':N-1' from the controller that otherwise shifts every reply by one) - initialize() failure now explains itself: baud (ASI RS-232 DIP default is 9600), port identity, power -- instead of a bare parse error - axis letter configurable (ASI_Z_AXIS_LETTER, default Z): single-axis MS-2000 builds may label their lone axis X - bring-up script: --scan-bauds sweeps the four ASI rates; --axis override Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed port A marginal RS-232 link or just-opened adapter can drop the first exchange without being broken -- initialize() now flushes, waits 200ms, and retries once before raising. connect_asi_z_stage logs the resolved port/baud/axis so a GUI-vs-script port discrepancy (e.g. a stale asi_z_serial_port in the ini overriding the SN) is visible in the log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…en closed GUI position/status pollers can fire after shutdown closes the serial port (observed on hardware: PortNotOpenError from widgets._update_position during cleanup). ASIZStage and PIFocusStage now cache the Squid-frame Z on every move/read and serve it once closed; get_state reports idle; moves warn and no-op. Same close-race philosophy as the existing home() guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware reality: the LS50 frame is power-on-relative, and the stage is not always powered on at the retracted end -- fixed squid-frame StageConfig limits then fence arbitrary positions (observed: the [0.5, 6.0] fence blocked reaching the true top). set_limits on the ASI Z is now a no-op unless ASI_Z_APPLY_SOFTWARE_LIMITS is set; the frame-independent ASI_Z_TRAVEL_MM fence still applies at connect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…zero at the away limit
Hardware bring-up showed native 0 is wherever the controller powered on (the
stage was observed mid-travel at Z=0), and stale controller-side SL/SU from
previous sessions silently clamp moves. Startup now: (1) ALWAYS clears the
controller soft limits (they would clamp the find-zero move itself), (2) with
ASI_Z_FIND_ZERO_ON_STARTUP (default on, skipped on in-place restart) drives
past full travel toward the away-from-sample limit switch — the safe
direction — and defines native 0 there ('H Z=0'), (3) applies the fresh
±travel fence in the new frame. Squid 0 is now always the true
farthest-from-sample position, matching the pre-serial convention; the
display negation (squid + toward sample) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs the startup frame routine observably (clear stale limits, drive to the away limit, zero there) with before/after positions and elapsed time, so the limit-switch stop behavior can be verified once before the GUI relies on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…SL Z-)
The MS-2000 manual states SL/SU limits persist across power cycles
automatically -- so any fence from any previous session sticks until
overwritten, which explains limits surviving with the software closed. The
clear now uses the documented restore-defaults dash form ('SL Z-'), avoiding
the direction-dependent sign semantics of numeric limits; pre-~2013 firmware
that error-acks the dash form falls back to a wide numeric window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ricating limits Per review feedback: the ±1000 fallback window and the 50.0 travel default were arbitrary. clear_travel_limits now uses only the documented restore-defaults dash form and raises actionably on firmware that lacks it (only the controller knows its own defaults); find-zero requires the configured physical travel (ASI_Z_TRAVEL_MM) for its overdrive; the bring-up --find-zero mode requires --travel-mm explicitly; the find-zero timeout is a named, documented constant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocity The controller is an MFC-2000 (not MS-2000): its datasheet max Z velocity is 0.6 mm/s, so a full 50 mm find-zero drive takes ~80 s -- the previous 60 s timeout would abort it mid-travel. The timeout now derives from the overdrive distance at the datasheet velocity with a 2x margin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ead flag, stale prose
From a 4-angle review of the branch:
- MS2000Serial owns the controller-global concerns its clients duplicated:
is_busy()/wait_idle() ('/' polling), command_with_settle_retry (first-
contact dead-air retry), reset_input_buffer(); parse_ms2000_number shared;
LS50Controller delegates instead of re-implementing.
- squid.stage.utils.resolve_port: the explicit-port/SN/raise ladder, folded
from both the ASI and PI factories.
- ASI_Z_HOME_ON_STARTUP removed: redundant (find-zero ends at the home
target; home_xyz retracts on every fresh boot) and its no-motion rationale
contradicted the find-zero default three lines below.
- find-zero + travel pairing now validated at machine-config load, next to
the other flag validators, instead of failing mid-bring-up.
- Stale prose from the pre-find-zero frame model corrected across asi.py
docstrings, _def.py flag comments, microscope.py, and the bring-up tool
("power on with the stage retracted" is no longer an operator instruction).
- _SimulatedLS50: unexercised fence-shift block dropped from zero_here.
- tests/tools.py gains shared FakeSerialConn/FakeZStage; test_stage.py uses
the shared fake.
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