@@ -24,6 +24,15 @@ def __init__(self, parent=None):
2424 self .currentDirection = np .zeros ((self .nMotors ))
2525 self .currentPosition = np .zeros ((self .nMotors ))
2626
27+ # Per-axis hardware-direction sign (+1 or -1), indexed as [A, X, Y, Z]
28+ # Applied internally at the firmware boundary: outbound commands are
29+ # multiplied by this sign before being sent, and inbound positions are
30+ # multiplied by this sign before being reported. This lets callers keep
31+ # a consistent physical/user coordinate frame while motor.py absorbs
32+ # any wiring polarity flips. Configured via setup_motor() (either via
33+ # an explicit `direction` argument or by passing a negative stepSize).
34+ self .direction = np .ones ((self .nMotors ), dtype = np .int8 )
35+
2736 self .minPosX = - np .inf
2837 self .minPosY = - np .inf
2938 self .minPosZ = - np .inf
@@ -83,8 +92,14 @@ def _callback_motor_status(self, data):
8392 stepSizes = np .array ((self .stepSizeA , self .stepSizeX , self .stepSizeY , self .stepSizeZ ))
8493 for iMotor in range (nSteppers ):
8594 stepperID = data ["steppers" ][iMotor ]["stepperid" ]
86- # Hardware returns steps, convert to physical units: (steps * stepSize)
87- self .currentPosition [stepperID ] = data ["steppers" ][iMotor ]["position" ] * stepSizes [stepperID ]
95+ # Hardware returns raw steps in firmware frame; convert to physical units
96+ # in user frame: phys = hw_steps * stepSize * direction. The direction sign
97+ # hides any wiring polarity flip from the caller.
98+ self .currentPosition [stepperID ] = (
99+ data ["steppers" ][iMotor ]["position" ]
100+ * stepSizes [stepperID ]
101+ * self .direction [stepperID ]
102+ )
88103 if callable (self ._callbackPerKey [0 ]):
89104 self ._callbackPerKey [0 ](self .currentPosition ) # we call the function with the value
90105 except Exception as e :
@@ -254,7 +269,42 @@ def stopFocusScanning(self):
254269 '''################################################################################################################################################
255270 HIGH-LEVEL Functions that rely on basic REST-API functions
256271 ################################################################################################################################################'''
257- def setup_motor (self , axis , minPos , maxPos , stepSize , backlash ):
272+ def setup_motor (self , axis , minPos , maxPos , stepSize , backlash , direction = None ):
273+ """Configure one motor axis.
274+
275+ The ``stepSize`` is stored internally as a positive magnitude (physical
276+ units per step). The wiring polarity is tracked separately in
277+ ``self.direction[axis]`` so the user/physical coordinate frame stays
278+ consistent in both directions of communication.
279+
280+ Parameters
281+ ----------
282+ axis : str
283+ One of "X", "Y", "Z", "A".
284+ minPos, maxPos : float
285+ Soft limits in physical (user-frame) units.
286+ stepSize : float
287+ Physical units per step. A negative value is interpreted as a
288+ request to flip the hardware direction for this axis (equivalent
289+ to passing ``direction=-1``) and is split into magnitude + sign.
290+ backlash : float
291+ Backlash in hardware steps (sign handled internally).
292+ direction : int or None, optional
293+ Explicit hardware-direction sign (+1 or -1). If ``None`` it is
294+ derived from the sign of ``stepSize`` (default +1).
295+ """
296+ # Split sign from magnitude. An explicit `direction` always wins;
297+ # otherwise the sign of stepSize is consumed and stepSize becomes
298+ # a positive scale factor.
299+ if direction is None :
300+ sign = - 1 if stepSize < 0 else 1
301+ else :
302+ sign = - 1 if direction < 0 else 1
303+ stepSize = abs (stepSize )
304+
305+ axisIdx = self .xyztTo1230 (axis )
306+ self .direction [axisIdx ] = sign
307+
258308 if axis == "X" :
259309 self .minPosX = minPos
260310 self .maxPosX = maxPos
@@ -271,7 +321,7 @@ def setup_motor(self, axis, minPos, maxPos, stepSize, backlash):
271321 self .minPosA = minPos
272322 self .maxPosA = maxPos
273323 self .stepSizeA = stepSize
274- self .backlash [self . xyztTo1230 ( axis ) ] = backlash
324+ self .backlash [axisIdx ] = backlash
275325
276326 def xyztTo1230 (self , axis ):
277327 axis = axis .upper ()
@@ -504,12 +554,16 @@ def move_stepper(self, steps=(0,0,0,0), speed=(1000,1000,1000,1000), is_absolute
504554
505555 # Store the target position in physical units BEFORE conversion to hardware steps
506556 targetPositionPhysical = steps .copy ()
507-
508- # convert from physical units to steps
509- steps [0 ] *= 1 / self .stepSizeA
510- steps [1 ] *= 1 / self .stepSizeX
511- steps [2 ] *= 1 / self .stepSizeY
512- steps [3 ] *= 1 / self .stepSizeZ
557+
558+ # Convert from physical (user-frame) units to firmware (hardware-frame) steps.
559+ # Apply the per-axis direction sign so wiring polarity is hidden from callers:
560+ # hw_steps = phys / |stepSize| * direction
561+ # (For relative moves this flips the requested delta; for absolute targets it
562+ # flips the absolute position the firmware should drive to.)
563+ steps [0 ] = steps [0 ] * self .direction [0 ] / self .stepSizeA
564+ steps [1 ] = steps [1 ] * self .direction [1 ] / self .stepSizeX
565+ steps [2 ] = steps [2 ] * self .direction [2 ] / self .stepSizeY
566+ steps [3 ] = steps [3 ] * self .direction [3 ] / self .stepSizeZ
513567
514568 # detect change in direction and compute distances in HARDWARE STEPS for travel time calculation
515569 absoluteDistances_steps = np .zeros ((4 )) # Distance in hardware steps
@@ -617,16 +671,19 @@ def move_stepper(self, steps=(0,0,0,0), speed=(1000,1000,1000,1000), is_absolute
617671 }
618672 }
619673
620- # Update currentPosition to track expected position in physical units
621- # steps are now in hardware units, so we need to convert back to physical
674+ # Update currentPosition to track expected position in physical (user-frame) units.
675+ # ``steps`` is in hardware/firmware frame at this point, so we convert back with
676+ # phys = hw_steps * stepSize * direction
622677 stepSizes = np .array ((self .stepSizeA , self .stepSizeX , self .stepSizeY , self .stepSizeZ ))
623678 for iMotor in range (self .nMotors ):
624679 if isAbsoluteArray [iMotor ]:
625- # For absolute: convert hardware steps back to physical units: (steps * stepSize)
626- self .currentPosition [iMotor ] = steps [iMotor ] * stepSizes [iMotor ]
680+ # For absolute: convert hardware steps back to physical (user) units.
681+ self .currentPosition [iMotor ] = steps [iMotor ] * stepSizes [iMotor ] * self . direction [ iMotor ]
627682 else :
628- # For relative: convert step delta to physical delta and add to current position
629- self .currentPosition [iMotor ] = self .currentPosition [iMotor ] + (steps [iMotor ] * stepSizes [iMotor ])
683+ # For relative: convert hw step delta to physical delta and accumulate.
684+ self .currentPosition [iMotor ] = self .currentPosition [iMotor ] + (
685+ steps [iMotor ] * stepSizes [iMotor ] * self .direction [iMotor ]
686+ )
630687
631688 # drive motor
632689 self .isRunning = True
@@ -757,7 +814,13 @@ def get_position(self, axis=None, timeout=1):
757814 if "motor" in r :
758815 for index , istepper in enumerate (r ["motor" ]["steppers" ]):
759816 if index > 3 : break # TODO: We would need to handle other values too soon
760- _position [istepper ["stepperid" ]]= istepper ["position" ]* _physicalStepSizes [self .motorAxisOrder [index ]]
817+ stepperID = istepper ["stepperid" ]
818+ # phys = hw_steps * stepSize * direction (apply wiring sign at boundary)
819+ _position [stepperID ] = (
820+ istepper ["position" ]
821+ * _physicalStepSizes [self .motorAxisOrder [index ]]
822+ * self .direction [stepperID ]
823+ )
761824
762825
763826 return _position
0 commit comments