Skip to content

Add ASI 6-position objective turret on the shared MFC-2000 (USE_ASI_OBJECTIVE_TURRET)#594

Open
Alpaca233 wants to merge 15 commits into
feat/ls50-asi-z-stagefrom
feat/asi-objective-turret
Open

Add ASI 6-position objective turret on the shared MFC-2000 (USE_ASI_OBJECTIVE_TURRET)#594
Alpaca233 wants to merge 15 commits into
feat/ls50-asi-z-stagefrom
feat/asi-objective-turret

Conversation

@Alpaca233

Copy link
Copy Markdown
Collaborator

Adds the 6-position objective turret driven by the same MFC-2000 controller as the LS50 Z stage, as a Squid objective changer: the GUI objective dropdown rotates the turret with the standard Z retract→rotate→restore safety dance.

Stacked on #591 (feat/ls50-asi-z-stage) — shares its MS2000Serial transport. Merge #591 first; this PR's diff is the turret-side work only.

What's in here

  • control/asi_objective_turret.py (new): ASIObjectiveTurret + simulation implementing ObjectiveChangerProtocol. Rotation via M F=<slot> (axis F, hardware-confirmed; raw slot index 1–6, never scaled — a test proves it). Position via best-effort W F (probed at connect, read back after each move as a cross-check) degrading to last-commanded-slot tracking. No homing exists: home() only refreshes the tracked slot; the startup move_to_objective(DEFAULT_OBJECTIVE) lands the turret on a known slot at every boot.
  • Shared serial with ownership semantics: when the LS50 Z is enabled, the turret reuses its lock-protected MS2000Serial via find_shared_ms2000(stage) (new public accessors) and never closes it; standalone, it opens its own connection (SN/port/baud defaulting to the ASI_Z_* values) and owns the close. Consumes the transport-level helpers from Add ASI LS50 Z-only focus stage (USE_ASI_Z_STAGE) #591 (wait_idle, command_with_settle_retry, parse_ms2000_number) rather than re-implementing them.
  • CombinedStage relocated to vendor-neutral squid/stage/composite.py (it composes both PI and ASI Z stages now); pi.py keeps no stale re-export.
  • Shared Z-safety helpers: the retract/restore dance is hoisted to module level in objective_turret_controller.py; all four changer classes (NiMotion real+sim, ASI real+sim) delegate to one implementation.
  • Protocol-complete cleanup: Xeryon changers gained the close() the protocol always promised, so GUI shutdown closes any changer unconditionally — no vendor-flag OR chain.
  • Flags: USE_ASI_OBJECTIVE_TURRET + ASI_OBJECTIVE_TURRET_* (three-way mutual exclusion with Xeryon/NiMotion validated at config load); positions dict keyed by objectives.csv names.
  • tools/asi_z_bringup.py --turret / --turret-slot N: read-only W F + busy probes and a timestamped rotation test for the remaining hardware unknowns.

Config

[GENERAL]
use_asi_objective_turret = True
asi_objective_turret_positions = {"4x": 1, "10x": 2, "20x": 3, "40x": 4}   ; your layout; JSON, double-quoted keys

No SN/port/baud needed when the LS50 Z stage is enabled — the turret rides its connection.

Tests

31 turret tests (simulation + real-over-scripted-serial: raw-slot framing, busy polling, timeout-still-restores-Z, probe seeding/degradation, close-ownership shared vs owned, KeyError dialog path) + simulated end-to-end Microscope builds incl. startup objective selection and PI+ASI mutual-exclusion. Affected suites: 225 passed.

Parked hardware questions (answered by the bring-up probe before enabling)

  1. Does W F report the slot (and in what format)? Driver degrades to software tracking either way.
  2. Does the / busy byte cover turret rotation? If not, the timestamped probe shows it and a settle-wait follow-up is trivial.

🤖 Generated with Claude Code

Alpaca233 and others added 15 commits July 10, 2026 14:18
CombinedStage.z_stage, LS50Controller.serial, ASIZStage.ms2000_serial, and a
module-level find_shared_ms2000(stage) so a same-controller addon (the ASI
objective turret) can reuse the Z stage's lock-protected transport without
reaching through private attrs. Simulation-aware (None for _SimulatedLS50);
ownership stays with the Z stage.

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

control/asi_objective_turret.py: ASIObjectiveTurret + simulation implementing
ObjectiveChangerProtocol. Rotates with 'M T=<slot>' (RAW slot index 1..6, not
scaled like linear-axis units); reads position via best-effort 'W T' (probed
at connect, verified after each move) degrading to last-commanded-slot
tracking; NO homing exists -- home() only refreshes the tracked slot;
Z retract/restore dance on every switch (template semantics); shared-serial
ownership: never closes a transport the Z stage owns, closes its own.

Wiring: USE_ASI_OBJECTIVE_TURRET + ASI_OBJECTIVE_TURRET_* flags (SN/port/baud
default to the ASI_Z_* values, resolved at construction time), 3-way
mutual-exclusion validator, MicroscopeAddons elif branch using
find_shared_ms2000(stage), GUI cleanup condition widened.

31 new turret tests + 6 config tests, incl. simulated end-to-end Microscope
builds and the raw-slot-framing proof.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--turret: read-only raw 'W <axis>' + '/' replies (answers the W-T-semantics
question). --turret-slot N (behind --allow-motion + confirmation): rotate,
log '/' busy transitions with timestamps (measures whether the global busy
byte covers turret rotation), read back, repeat same slot. read_only_checks
switched to the public backend.serial accessor.

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

The composite went multi-vendor (PI V-308, ASI LS50) but lived in the
vendor-named pi.py, forcing the ASI turret's find_shared_ms2000 to duck-type
around an import cycle and microscope.py's ASI branch to import through the
PI module. CombinedStage now lives in squid/stage/composite.py; pi.py keeps a
compatibility re-export; find_shared_ms2000 uses a real isinstance; canonical
import sites (microscope.py, tests) migrated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	software/tests/squid/test_stage.py
# Conflicts:
#	software/tools/asi_z_bringup.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocol close, lazy import

- retract_z_if_possible/restore_z_if_captured promoted to module level in
  objective_turret_controller.py; all four changer classes (NiMotion real+sim,
  ASI real+sim) delegate — the objective-crash-avoidance policy now has one
  home instead of four copies.
- ASI turret consumes the MS2000Serial transport helpers (wait_idle,
  command_with_settle_retry, parse_ms2000_number) and utils.resolve_port
  instead of re-implementing them; unused threading import dropped.
- Xeryon changers implement the close() that ObjectiveChangerProtocol always
  promised (no-op), so the GUI cleanup closes any changer unconditionally —
  no more vendor-flag OR chain.
- ASI turret classes imported inside their construction branch instead of a
  module-level conditional with None sentinels; the e2e tests drop the
  class-name monkeypatch boilerplate that pattern forced.
- pi.py: dead CombinedStage re-export removed (zero consumers after the
  composite migration); docstring grammar fixed.
- Turret tests use the shared tests.tools fakes (FakeSerialConn, FakeZStage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware-confirmed: the objective turret rides the F axis, not T. The
configurable ASI_OBJECTIVE_TURRET_AXIS_LETTER now defaults to F, along with
the driver/tool defaults, tests, and prose.

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

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 a 6-position ASI objective turret that shares the MS-2000 controller/transport with the ASI LS50 Z stage, including Microscope integration, a vendor-neutral CombinedStage relocation, shared Z-safety helpers, and extensive tests/bring-up tooling.

Changes:

  • Introduces ASIObjectiveTurret (+ simulation) using MS2000Serial commands (M <axis>=<slot>, W <axis>, / busy) with shared-serial ownership semantics.
  • Moves CombinedStage into squid.stage.composite and updates call sites/tests accordingly; adds find_shared_ms2000() accessor plumbing.
  • Consolidates Z retract/restore “safety dance” helpers and ensures GUI shutdown can call close() on any objective changer implementation.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
software/tools/asi_z_bringup.py Adds turret probe/move bring-up options for MS-2000 turret axis.
software/tests/squid/test_stage.py Updates CombinedStage import path and tests new shared-MS2000 accessor behavior.
software/tests/control/test_objective_turret_controller.py Refactors tests to reuse shared FakeZStage helper.
software/tests/control/test_objective_changer_config.py Extends objective-changer mutual exclusion coverage for the new ASI turret flag.
software/tests/control/test_asi_objective_turret.py Adds comprehensive unit tests for ASI turret behavior and ownership semantics.
software/squid/stage/pi.py Removes CombinedStage from PI module and updates docs to point at squid.stage.composite.
software/squid/stage/composite.py New vendor-neutral CombinedStage implementation for composing XY + external Z-only stages.
software/squid/stage/asi.py Exposes MS2000 serial accessors and adds find_shared_ms2000() for turret sharing.
software/control/objective_turret_controller.py Hoists shared Z retract/restore helpers for all objective changers to use.
software/control/objective_changer_2_pos_controller.py Adds close() implementations for Xeryon changer(s) to match protocol.
software/control/microscope.py Wires ASI turret creation into Microscope build and clarifies homing semantics.
software/control/gui_hcs.py Simplifies shutdown by calling close() on any configured objective changer.
software/control/asi_objective_turret.py New ASI turret driver + simulation implementing the objective changer protocol.
software/control/_def.py Adds ASI turret flags/config and extends objective-changer mutual exclusion validation.

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

Comment on lines +39 to +42
def close(self):
# ObjectiveChangerProtocol promises close(); the Xeryon library holds no OS handle
# that needs explicit release (cleanup zeroes the axis via moveToZero instead).
pass
Comment on lines +195 to +198
parser.add_argument("--turret", action="store_true", help="Probe the objective turret axis (read-only)")
parser.add_argument("--turret-axis", default="F", help="Turret axis letter (default F, per the MFC-2000)")
parser.add_argument("--turret-slot", type=int, help="Rotate to this slot (1..6); requires --allow-motion")

Comment on lines +13 to +17
import squid.logging
from squid.abc import AbstractStage, Pos, StageStage
from squid.config import StageConfig

_log = squid.logging.get_logger(__name__)
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