Add PI V-308 / C-414 voice-coil focus stage (USE_PI_FOCUS_STAGE)#569
Open
hongquanli wants to merge 17 commits into
Open
Add PI V-308 / C-414 voice-coil focus stage (USE_PI_FOCUS_STAGE)#569hongquanli wants to merge 17 commits into
hongquanli wants to merge 17 commits into
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilder Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add config flags in _def.py, import branch in gui_hcs.py, and a CombinedStage wrap in microscope.build_from_global_config so the V-308 becomes the main Z on top of the configured XY stage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…safety) - close(): PIFocusStage/CombinedStage define close(); Microscope.close() releases the stage; connect_pi_focus_stage closes the GCSDevice on any connect/init failure. - CombinedStage delegates x/y/z_mm_to_usteps to the XY stage so NavigationWidget no longer crashes at startup. - Guard moves against an unreferenced axis (sim + real); add a serial RLock so a non-blocking home() cannot race GUI position polling on the shared GCS handle. - _resolve_port_by_sn compares serials as strings (numeric-SN coercion footgun); set_limits warns on a one-sided Z bound. - PI_FOCUS_VELOCITY_MM_S config (optional); setup_22.04.sh installs the C-414 udev bind + latency rules and reloads. - Tests for close, usteps delegation, the referenced guard, numeric SN, no-port error. Deferred (needs design): coordinate-frame mismatch -- V-308 range/limits/step grid still reuse the Cephla Z config (SOFTWARE_POS_LIMIT.Z, Z_AXIS, GUI scroll guard). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…per (#6) CombinedStage.z_mm_to_usteps delegates to the V-308 (continuous, ~1 nm) instead of the Cephla stepper grid, so GUI Z deltas / Z-stack slice spacing are not snapped to a coarse microstep grid. The only consumer is the GUI step-grid (widgets.py set_deltaZ). No Z offset: the C-414 reports its frame as 0..7 mm (TMN?=0, TMX?=7, queried live), which already matches Squid's SOFTWARE_POS_LIMIT.Z, so pure pass-through is correct and the 'half the range fenced off' review finding (premised on a +/-3.5 frame) does not apply. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
home_xyz() now drives Z to PI_FOCUS_SAFE_Z_MM (default 0.0 = retracted) before the XY homing sweep when USE_PI_FOCUS_STAGE, so the un-self-locking voice coil cannot drag the objective through the sample. Gated on is_referenced() (FRF?/qFRF, exposed via PIFocusStage/CombinedStage) -- skips with a warning rather than commanding a move on an unreferenced axis. Tests cover both the retract and the unreferenced-skip paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion - PIFocusStage.home(z=True) references only if not already referenced (qFRF -- no redundant FRF sweep), then moves Z to the objective-clear home position (home_mm, wired from PI_FOCUS_SAFE_Z_MM). The GUI Home button always drives Z home, without re-sweeping a referenced axis. - home_xyz() calls home(z) before the XY sweep, so Z is referenced and retracted to the objective-clear position first -- the un-self-locking coil cannot drag the objective. - Sim initialize() made idempotent; sim tracks a reference count so tests can assert that an already-referenced home() does not re-sweep. - Tests: home moves-home-without-resweep when referenced; references-then-moves when not; home_xyz references + retracts even when starting unreferenced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tness) - set_travel_limits: clamp the requested range to the controller travel (qTMN/qTMX) and wrap CCL(1)/SPA/CCL(0) in try/finally so the C-414 never stays in advanced command level if an SPA write raises. - PIFocusStage: guard the non-blocking home() against a use-after-close race (skip if closed); a _busy flag lets get_state() report busy without blocking on the lock during an async home. - CombinedStage: initialise _scanning_position_z_mm (read by the loading/scanning flow). - microscope: gate the HOMING_ENABLED_Z home with 'not USE_PI_FOCUS_STAGE' so home_xyz homes the V-308 once; normalise PI_FOCUS_STAGE_SN so a numeric serial 0 is not collapsed to None. - tests: home-after-close no-op, CombinedStage _scanning attr, and scope.close() on the three microscope-level tests (exercises the V-308 close path). Deferred to the frame/safe-Z design: V-308-correct CombinedStage.get_config() Z grid (autofocus /multipoint step snapping) and routing safety/retract/loading Z through PI_FOCUS_SAFE_Z_MM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…home Drop the PI-specific PI_FOCUS_SAFE_Z_MM and reuse Squid's existing OBJECTIVE_RETRACTED_POS_MM (already the loading-position retract value) as the single source of truth for the V-308's retracted/home Z. home(z) and the pre-XY-homing retract now move there, consistent with move_to_loading_position. One configurable value, no duplicate 'safe Z'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…complete) CombinedStage.get_config() now returns a Z_AXIS at the Z stage's continuous ~10 nm resolution, so the AutoFocus + multipoint Z-step widgets (which snap via get_config().Z_AXIS, not z_mm_to_usteps) reflect the V-308 grid instead of the coarse Cephla stepper grid -- closing the path the earlier #6 fix missed. z_mm_to_usteps rounds to an integer ustep for consistency. Note: Z_HOME_SAFETY_POINT (the GUI 'Home Z'/startup safe-Z height) stays config-driven -- a V-308 scope's .ini should set it (and OBJECTIVE_RETRACTED_POS_MM) for the V-308's frame. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d state - The V-308 has no Z_HOME_SAFETY_POINT concept (its retracted position is the safe Z, and the safety-floor direction doesn't map to the voice coil). move_z_axis_to_safety_position and the startup cached-Z restore now use OBJECTIVE_RETRACTED_POS_MM / restore the cached Z directly when USE_PI_FOCUS_STAGE, instead of the 1.2 mm floor. - C414FocusStage caches the qFRF referenced state (refreshed by is_referenced()/reference()), so move_to/move_relative no longer issue a qFRF round-trip per move -- matters for fast Z-stacking. ([14]) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ perms Addresses findings from V-308 (C-414) bring-up testing on Linux: - connect_serial/connect_tcpip now build the GCSDevice on pipython's pure-Python PISerial/PISocket gateway instead of gcs.ConnectRS232(), which on Linux routes through the proprietary libpi_pi_gcs2.so that `pip install pipython` does not provide. Defer GCSDevice creation to connect time so __init__ no longer makes a DLL-backed device (whose stale connection callback crashed against the missing DLL). connect_usb keeps the DLL path (USB requires it). Guard close()/stop() for the not-yet-connected case. Verified on hardware (C-414.13030): connect + moves. - setup_22.04.sh: `udevadm trigger --action=add` so the ACTION=="add" bind rule applies to an already-connected C-414 (plain trigger defaults to change and no-ops). - 99-pi-ftdi-latency.rules: add a tty-subsystem MODE="0666" rule so the /dev/ttyUSB* node is usable without dialout membership / re-login (the latency ATTR sits on the usb-serial device, not the tty node). - _SimulatedC414 travel -3.5..3.5 -> 0.0..7.0 mm to match the V-308's reported qTMN/qTMX. Existing simulated unit-test assertions still hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
Hardware test evidence — V-308 / C-414 Z control verified ✅Tested on a Toupcam + Cephla-XY microscope with the target hardware attached: PI C-414.13030 (S/N Z control works end-to-endExercised the PR's own driver + adapter against the real controller (referenced with the objective clear):
Hardware reports Findings addressed in
|
…pright) For an upright system the V-308's native 0 is DOWN (toward the sample); the PR's pass-through Z and retract-to-~0 suit an inverted scope but drive the objective into the sample on an upright one. Add configurable inversion + upright homing: - PIFocusStage(invert_z): squid_z = (native positive limit) - native, so Squid Z 0 is fully retracted and Z increases toward the sample (Z+ = focus down). Applied consistently to get_pos/move_z/move_z_to/set_limits. - PIFocusStage(home_to_positive_limit): home()/retract drives Z to the native positive travel limit (furthest from the sample), narrowed to the fenced upper end by set_limits, instead of OBJECTIVE_RETRACTED_POS_MM. - reset_range_limit(): on the C-414 qTMN/qTMX ARE the Position Range Limit params, so set_travel_limits() shrinks them irreversibly and the inversion offset/fencing would drift smaller each software restart. connect_pi_focus_stage(z_travel_mm=...) restores [0, travel] at connect so the offset is stable. - close(): release the PISerial/PISocket gateway via its own close(); GCSDevice. CloseConnection() raises AttributeError on a gateway (previously suppressed, so the port was not released cleanly). - New config: PI_FOCUS_INVERT_Z, PI_FOCUS_HOME_TO_POSITIVE_LIMIT, PI_FOCUS_Z_TRAVEL_MM (all default off/0 -> existing behaviour unchanged). Wired through microscope.py. - Tests for inverted mapping, upright home, range-limit reset, offset stability. Verified on hardware (C-414.13030): offset restored to 7.0, home retracts UP to the positive limit, Z+ moves toward the sample, gateway close releases the port. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… GCSError Jogging Z past the fenced range in the GUI raised an uncaught `GCSError: Position out of limits` (the C-414 rejects an out-of-range MOV/MVR), logged as an "Uncaught Exception". Clamp instead: - C414FocusStage caches the Position Range Limit ([lo, hi], refreshed at connect and on set_travel_limits/reset_range_limit) and clamps every move target to it via a module-logger warning. move_relative now resolves to an absolute target and MOVs the clamped value (rather than MVR) so a relative jog past the limit also stops cleanly. - No extra qTMN/qTMX query per move (uses the cache). Verified on hardware: move_z_to past the top limit warns and clamps (no exception); in-range moves unchanged. Adds a unit test for the clamp helper. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Summary
Adds support for the PI V-308 voice-coil PIFOC focus drive (on a C-414 PIMag
controller) as a selectable main-Z stage, alongside the existing Cephla and Prior stages.
The V-308 gives fast, high-resolution closed-loop Z for focus / Z-stacking.
Enable with
USE_PI_FOCUS_STAGE = True. The V-308 becomes the Z axis, wrapping whatever XYstage is configured (Cephla µC or Prior) — so it composes with the existing stage, it doesn't
replace XY.
What's included
software/squid/stage/pi.py(new, self-contained likeprior.py/cephla.py):C414FocusStage— GCS-2.0 driver over serial,pipythonimported lazily (module importsfine without it; CI and simulated builds never need it).
_SimulatedC414— pure-Python backend for hardware-free / CI use.PIFocusStage(AbstractStage)— Z-only adapter, pure pass-through mm (no sign flip, neverreads
Z_AXIS.MOVEMENT_SIGN).CombinedStage(AbstractStage)— routes X/Y/θ to the existing stage, Z to the V-308; presents aV-308-correct Z axis (continuous ~10 nm grid) so GUI Z-step snapping isn't coarsened.
control/_def.py):USE_PI_FOCUS_STAGE,SIMULATE_PI_FOCUS_STAGE,PI_FOCUS_STAGE_SN,PI_FOCUS_SERIAL_PORT,PI_FOCUS_BAUDRATE,PI_FOCUS_AXIS,PI_FOCUS_REFERENCE_ON_STARTUP,PI_FOCUS_VELOCITY_MM_S.gui_hcs.py;CombinedStageconstruction inmicroscope.build_from_global_config; stageclose()on shutdown.drivers and libraries/pi/udev/{98-pi-c414-bind,99-pi-ftdi-latency}.rulesand anoptional
pip install pipythoninsetup_22.04.sh.tests/squid/test_stage.py(26 V-308 tests), simulated, no hardware.Hardware / connection (verified on a real unit)
The C-414 is a custom-VID FTDI (
1a72:102b, FT-X), serial1UETR6I!, controller frame0..7 mm(TMN?=0,TMX?=7) — not the spec sheet's mechanical "±3.5". Because the VID iscustom, the OS doesn't auto-create a COM port:
98-pi-c414-bind.rulesbinds it toftdi_sio(new_id) so/dev/ttyUSB*appears;99-pi-ftdi-latency.rulesdrops the FTDI latency timer to 1 ms.setup_22.04.shinstalls both.PI_FOCUS_STAGE_SN(the FTDI serial) resolves to the port.A V-308 scope's machine config should set
PI_FOCUS_STAGE_SN(orPI_FOCUS_SERIAL_PORT).OBJECTIVE_RETRACTED_POS_MM— the V-308's retracted / objective-clear Z;home(z)and thepre-XY-homing retract drive here. (Reused as the single safe-Z source;
Z_HOME_SAFETY_POINTisbypassed for the V-308.)
SOFTWARE_POS_LIMIT.Z— the reachable Z range (the controller travel registers are clamped tothe hardware travel via
qTMN/qTMX).Behavior notes
0..7, matchesSOFTWARE_POS_LIMIT.Z); no offset.home(z)references only if not already referenced (no redundantFRFsweep), then moves tothe retracted position.
home_xyz()retracts Z before the XY sweep (the voice coil has noself-locking).
zero(z)is an inert warning (V-308 uses an absolute optical reference).Safety
The voice coil has no self-locking: referencing and bring-up move the stage, and servo-off
lets a vertical stage drift. Run bring-up with the objective clear; fence Z via
SOFTWARE_POS_LIMIT.Z.autozero()(weight compensation) is implemented but not auto-wired.Testing
tests/squid/— 96 passed (26 new V-308 tests), simulated;blackclean.Status / follow-ups
port); end-to-end serial verification happens on the Linux scope. GCS protocol confirmed over
libusb (
*IDN?→C-414.13030, fw1.003).if reversed, fix at the controller/mounting level, not in
pi.py.PI_FOCUS_REFERENCE_ON_STARTUP=Falseto avoid the bring-up referencemove; analog / wave-generator V-308 modes; the V-308 as a separate fast-fine-Z on top of a coarse
Z (this PR makes it the main Z).
🤖 Generated with Claude Code