Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,17 @@ class MachineConfiguration:
MAX_ACCELERATION_Y_mm:float = 500.0
MAX_ACCELERATION_Z_mm:float = 100.0

# Soft (two-pass) homing for X and Y. The firmware's homing speed is
# HOMING_VELOCITY_{X,Y} * MAX_VELOCITY_{X,Y}_mm, so we temporarily lower
# MAX_VELOCITY_{X,Y}_mm to these values for the precise second pass.
# Run-of-the-mill mechanical limit switches have speed-dependent trip
# hysteresis; the slow second pass eliminates run-to-run jitter.
SOFT_HOMING_VELOCITY_X_mm:float = 4.0
SOFT_HOMING_VELOCITY_Y_mm:float = 4.0
SOFT_HOMING_ACCELERATION_X_mm:float = 100.0
SOFT_HOMING_ACCELERATION_Y_mm:float = 100.0
SOFT_HOMING_BACK_OFF_MM:float = 3.0

# end of actuator specific configurations

SCAN_STABILIZATION_TIME_MS_X:float = 160.0
Expand Down
79 changes: 70 additions & 9 deletions software/control/core/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ def __init__(self,
self.microcontroller.set_callback(self.update_pos)

self.is_in_loading_position:bool=False
# Record the microcontroller connection session_id at the time of the
# most recent successful homing. A subsequent reconnect bumps the MCU's
# session_id, so comparing the two tells us whether the firmware
# position frame is still the one we homed against. -1 means "never
# homed in this session".
self._homed_at_session_id:int=-1

@property
def has_been_homed(self)->bool:
"""True iff the stage was homed in this session AND the firmware
position frame is still valid (no microcontroller reconnect since)."""
return self._homed_at_session_id == self.microcontroller.connection_session_id and self._homed_at_session_id >= 0

@property
def plate_type(self)->WellplateFormatPhysical:
Expand Down Expand Up @@ -222,13 +234,50 @@ def update_pos(self,microcontroller:microcontroller.Microcontroller):
#def home_theta(self):
# self.microcontroller.home_theta()

def loading_position_enter(self,home_x:bool=True,home_y:bool=True,home_z:bool=True):
def loading_position_enter(self,home_x:bool=True,home_y:bool=True,home_z:bool=True,*,with_homing:bool=True):
"""Move the stage to the loading position.

with_homing=True (default): run the full homing sequence (Z retracted
first, then a two-pass soft home of Y and X). Use this on startup or
whenever the firmware position frame may be stale.

with_homing=False: skip homing and move via absolute coordinates.
Requires a prior successful home in the same microcontroller session
(raises RuntimeError otherwise).

home_x/home_y/home_z are legacy per-axis flags retained for the
existing startup-config call in core/__init__.py:587. They behave as
an all-or-nothing switch: the full XY homing only runs when all three
are True; home_z=False short-circuits the whole function. Prefer
with_homing for new callers.
"""
# if used through GUI, this should never be the case
# but the API must account for this function being called twice
if self.is_in_loading_position:
MAIN_LOG.log("tried to enter loading position when already in loading position")
return

if not with_homing:
# Move to the loading position using absolute moves, without re-homing.
# The firmware position counter MUST already be valid; otherwise
# move_z_to(0) can drive the objective into the plate. Refuse the
# call if we have no record of a valid prior home in this session.
if not self.has_been_homed:
raise RuntimeError(
"loading_position_enter(with_homing=False) requires a prior "
"soft_home in this microcontroller session — the firmware "
"position frame is not known to be valid."
)
# Z retracts first so the objective clears the sample, then Y and X
# move to the home corner.
self.move_z_to(0.0, wait_for_completion={'timeout_limit_s':10, 'time_step':0.005})
self.is_in_loading_position=True
MAIN_LOG.log('no-homing - objective retracted')
self.move_y_to(0.0, wait_for_completion={'timeout_limit_s':30, 'time_step':0.005})
self.move_x_to(0.0, wait_for_completion={'timeout_limit_s':30, 'time_step':0.005})
MAIN_LOG.log("no-homing - in loading position")
return

if home_z:
# retract the objective
self.microcontroller.home_z()
Expand All @@ -240,17 +289,29 @@ def loading_position_enter(self,home_x:bool=True,home_y:bool=True,home_z:bool=Tr
MAIN_LOG.log('homing - objective retracted')

if home_z and home_y and home_x:
# Snapshot the session_id before any XY motion. wait_till_operation_is_completed
# can autorecover from timeouts by calling attempt_connection() (which bumps
# session_id); if that happens mid-sequence we don't trust the resulting frame
# and force the next attempt to re-home.
session_id_before_xy_homing = self.microcontroller.connection_session_id

# for the new design, need to home y before home x; x also needs to be at > + 10 mm when homing y
self.move_x(12.0)
self.microcontroller.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='x moving timeout, the program will exit')

self.microcontroller.home_y()
self.microcontroller.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='y homing timeout, the program will exit')

self.microcontroller.home_x()
self.microcontroller.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='x homing timeout, the program will exit')

MAIN_LOG.log("homing - in loading position")

# Two-pass soft homing: coarse home at MAX_VELOCITY, back off, then a slow
# second pass for a repeatable trip point (limit-switch trip varies with
# approach speed, which was the source of plate-to-plate calibration drift).
self.microcontroller.soft_home_y()
self.microcontroller.soft_home_x()

if self.microcontroller.connection_session_id == session_id_before_xy_homing:
self._homed_at_session_id = session_id_before_xy_homing
MAIN_LOG.log("homing - in loading position")
else:
# _homed_at_session_id intentionally left unchanged: next GUI press will
# see has_been_homed=False and run the full homing path again.
MAIN_LOG.log("warning - microcontroller reconnected during XY homing; has_been_homed left invalid so next attempt re-homes")

def loading_position_leave(self,home_x:bool=True,home_y:bool=True,home_z:bool=True):
if not self.is_in_loading_position:
Expand Down
5 changes: 4 additions & 1 deletion software/control/gui_hcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,10 @@ def loading_position_toggle(self,loading_position_enter:bool):
"""
if loading_position_enter: # entering loading position
self.set_all_interactible_enabled(set_enabled=False,exceptions=[self.position_widget.btn_goToLoadingPosition]) # disable everything except the single button that can leave the loading position
self.core.navigation.loading_position_enter()
# First press of the session homes; subsequent presses move directly
# via absolute coordinates so the limit-switch trip point is only
# exercised once per session (avoids plate-to-plate calibration drift).
self.core.navigation.loading_position_enter(with_homing=not self.core.navigation.has_been_homed)

else: # leaving loading position
self.core.navigation.loading_position_leave()
Expand Down
101 changes: 100 additions & 1 deletion software/control/microcontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ def __init__(self,version:ControllerType=ControllerType.DUE,sn:Optional[str]=Non

self.has_been_initialized_at_least_once=False

# increments on every successful (re)connection; NavigationController
# uses it to invalidate has_been_homed when the firmware position frame
# is no longer valid (a microcontroller reboot resets its counter).
self.connection_session_id:int = 0

# Python-side mirror of the per-axis MAX_VELOCITY/ACCELERATION currently
# programmed into the firmware. Kept in sync by set_max_velocity_acceleration
# so soft_home_* can restore the *actually-active* values instead of the
# MACHINE_CONFIG defaults (which may have been overridden by other code).
self._max_velocity_x_mm:float = float(MACHINE_CONFIG.MAX_VELOCITY_X_mm)
self._max_velocity_y_mm:float = float(MACHINE_CONFIG.MAX_VELOCITY_Y_mm)
self._max_velocity_z_mm:float = float(MACHINE_CONFIG.MAX_VELOCITY_Z_mm)
self._max_acceleration_x_mm:float = float(MACHINE_CONFIG.MAX_ACCELERATION_X_mm)
self._max_acceleration_y_mm:float = float(MACHINE_CONFIG.MAX_ACCELERATION_Y_mm)
self._max_acceleration_z_mm:float = float(MACHINE_CONFIG.MAX_ACCELERATION_Z_mm)

self.attempt_connection()

self.new_packet_callback_external = None
Expand Down Expand Up @@ -141,7 +157,11 @@ def attempt_connection(self)->bool:
MAIN_LOG.log('controller reconnected')

self.has_been_initialized_at_least_once=True

# New connection means the firmware position counter restarted at 0
# (in whatever physical pose the stage happens to be in). Bump the
# session id so anything depending on a prior home invalidates itself.
self.connection_session_id += 1

return True

def close(self):
Expand Down Expand Up @@ -445,6 +465,74 @@ def home_xy(self):
cmd[4] = int((MACHINE_CONFIG.STAGE_MOVEMENT_SIGN_Y+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1
self.send_command(cmd)

# The firmware computes homing speed as HOMING_VELOCITY_{X,Y} * MAX_VELOCITY_{X,Y}_mm,
# and MAX_VELOCITY_{X,Y}_mm is mutable from the host. soft_home_* does a coarse home
# at the current MAX_VELOCITY, backs off, then re-homes at SOFT_HOMING_VELOCITY for a
# repeatable trip point that doesn't depend on approach speed.

def soft_home_x(self):
# snapshot the active firmware MAX_VELOCITY/ACCELERATION for X so the
# finally block restores whatever was actually programmed, not the
# MACHINE_CONFIG default (which may have been overridden elsewhere).
saved_v_x, saved_a_x = self._max_velocity_x_mm, self._max_acceleration_x_mm
self.home_x()
self.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='x soft-home (coarse) timeout, the program will exit')
# back off so the second pass actually traverses the switch trip point
self.move_x_usteps(self.mm_to_ustep_x(MACHINE_CONFIG.SOFT_HOMING_BACK_OFF_MM))
self.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='x soft-home (back-off) timeout, the program will exit')
# try covers everything that runs after MAX_VELOCITY is lowered, so a
# timeout on any intermediate wait still restores the original value.
try:
# lower MAX_VELOCITY so HOMING_VELOCITY_X * MAX_VELOCITY_X_mm becomes gentle
self.set_max_velocity_acceleration(AXIS.X, MACHINE_CONFIG.SOFT_HOMING_VELOCITY_X_mm, MACHINE_CONFIG.SOFT_HOMING_ACCELERATION_X_mm)
self.wait_till_operation_is_completed()
self.home_x()
self.wait_till_operation_is_completed(30, time_step=0.005, timeout_msg='x soft-home (fine) timeout, the program will exit')
finally:
self.set_max_velocity_acceleration(AXIS.X, saved_v_x, saved_a_x)
self.wait_till_operation_is_completed()

def soft_home_y(self):
saved_v_y, saved_a_y = self._max_velocity_y_mm, self._max_acceleration_y_mm
self.home_y()
self.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='y soft-home (coarse) timeout, the program will exit')
self.move_y_usteps(self.mm_to_ustep_y(MACHINE_CONFIG.SOFT_HOMING_BACK_OFF_MM))
self.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='y soft-home (back-off) timeout, the program will exit')
try:
self.set_max_velocity_acceleration(AXIS.Y, MACHINE_CONFIG.SOFT_HOMING_VELOCITY_Y_mm, MACHINE_CONFIG.SOFT_HOMING_ACCELERATION_Y_mm)
self.wait_till_operation_is_completed()
self.home_y()
self.wait_till_operation_is_completed(30, time_step=0.005, timeout_msg='y soft-home (fine) timeout, the program will exit')
finally:
self.set_max_velocity_acceleration(AXIS.Y, saved_v_y, saved_a_y)
self.wait_till_operation_is_completed()

def soft_home_xy(self):
saved_v_x, saved_a_x = self._max_velocity_x_mm, self._max_acceleration_x_mm
saved_v_y, saved_a_y = self._max_velocity_y_mm, self._max_acceleration_y_mm
# coarse simultaneous home
self.home_xy()
self.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='xy soft-home (coarse) timeout, the program will exit')
# back off both axes
self.move_x_usteps(self.mm_to_ustep_x(MACHINE_CONFIG.SOFT_HOMING_BACK_OFF_MM))
self.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='x soft-home (back-off) timeout, the program will exit')
self.move_y_usteps(self.mm_to_ustep_y(MACHINE_CONFIG.SOFT_HOMING_BACK_OFF_MM))
self.wait_till_operation_is_completed(10, time_step=0.005, timeout_msg='y soft-home (back-off) timeout, the program will exit')
try:
# lower both MAX_VELOCITY; try covers from the first lower onward so
# an X-only lower (with a subsequent Y-lower failure) still restores.
self.set_max_velocity_acceleration(AXIS.X, MACHINE_CONFIG.SOFT_HOMING_VELOCITY_X_mm, MACHINE_CONFIG.SOFT_HOMING_ACCELERATION_X_mm)
self.wait_till_operation_is_completed()
self.set_max_velocity_acceleration(AXIS.Y, MACHINE_CONFIG.SOFT_HOMING_VELOCITY_Y_mm, MACHINE_CONFIG.SOFT_HOMING_ACCELERATION_Y_mm)
self.wait_till_operation_is_completed()
self.home_xy()
self.wait_till_operation_is_completed(30, time_step=0.005, timeout_msg='xy soft-home (fine) timeout, the program will exit')
finally:
self.set_max_velocity_acceleration(AXIS.X, saved_v_x, saved_a_x)
self.wait_till_operation_is_completed()
self.set_max_velocity_acceleration(AXIS.Y, saved_v_y, saved_a_y)
self.wait_till_operation_is_completed()

def zero_x(self):
cmd = bytearray(self.tx_buffer_length)
cmd[1] = CMD_SET.HOME_OR_ZERO
Expand Down Expand Up @@ -535,6 +623,17 @@ def set_max_velocity_acceleration(self,axis:int,velocity:Union[int,float],accele
cmd[5] = int(acceleration*10) >> 8
cmd[6] = int(acceleration*10) & 0xff
self.send_command(cmd)
# mirror the value the firmware now has, so soft_home_* and any future
# save/restore-style code can use the *active* value as the baseline.
if axis == AXIS.X:
self._max_velocity_x_mm = float(velocity)
self._max_acceleration_x_mm = float(acceleration)
elif axis == AXIS.Y:
self._max_velocity_y_mm = float(velocity)
self._max_acceleration_y_mm = float(acceleration)
elif axis == AXIS.Z:
self._max_velocity_z_mm = float(velocity)
self._max_acceleration_z_mm = float(acceleration)

@TypecheckFunction
def set_leadscrew_pitch(self,axis:int,pitch_mm:Union[float,int]):
Expand Down