Skip to content

Commit 6e8d51c

Browse files
committed
Apply per-axis direction to motor/objective
Introduce Motor.signed_step_size() to combine step size and wiring-polarity direction and use it for conversions between firmware hardware steps and user-frame physical units. Apply axis direction sign to movement speeds so positive user-frame speeds move user-positive even on inverted axes. Update set_position() to convert user positions to rounded hardware steps (handle zero step scale safely) and add docs/warning for startStageScanning about raw hardware-step parameters. Use signed_step_size in Objective to preserve previous inversion behaviour when reading and writing x0/x1/z0/z1.
1 parent 367780e commit 6e8d51c

2 files changed

Lines changed: 73 additions & 12 deletions

File tree

uc2rest/motor.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ def setTrigger(self, axis="X", pin=1, offset=0, period=1):
208208

209209
# {"task": "/motor_act", "stagescan": {"nStepsLine": 50, "dStepsLine": 1, "nTriggerLine": 1, "nStepsPixel": 50, "dStepsPixel": 1, "nTriggerPixel": 1, "delayTimeStep": 10, "stopped": 0, "nFrames": 50}}"}}
210210
def startStageScanning(self, nStepsLine=100, dStepsLine=1, nTriggerLine=1, nStepsPixel=100, dStepsPixel=1, nTriggerPixel=1, delayTimeStep=10, nFrames=5, isBlocking = False):
211+
"""Start a firmware-driven XY stage scan.
212+
213+
.. warning:: **Raw hardware-step values.**
214+
All step parameters (``dStepsLine``, ``dStepsPixel``) are forwarded
215+
to the firmware without applying the per-axis direction sign, because
216+
the scan API does not carry explicit axis labels and the firmware
217+
assigns axes internally. Callers that have configured an inverted
218+
axis via :meth:`setup_motor` must negate the relevant ``dSteps*``
219+
argument themselves before calling this method.
220+
"""
211221
path = "/motor_act"
212222
payload = {
213223
"task": path,
@@ -335,6 +345,30 @@ def xyztTo1230(self, axis):
335345
axis = 0
336346
return axis
337347

348+
def signed_step_size(self, axis):
349+
"""Return stepSize * direction for an axis.
350+
351+
This combines the magnitude scale (µm/step) with the wiring-polarity
352+
sign into a single signed scalar, preserving the behaviour that existed
353+
before the direction field was introduced (when callers passed a
354+
negative stepSize to setup_motor() to flip polarity).
355+
356+
Parameters
357+
----------
358+
axis : str or int
359+
Axis name (\"X\"/\"Y\"/\"Z\"/\"A\") or hardware index (0-3).
360+
361+
Returns
362+
-------
363+
float
364+
Positive if the axis is not inverted, negative if it is.
365+
``phys = hw_steps * signed_step_size(axis)``
366+
``hw_steps = phys / signed_step_size(axis)``
367+
"""
368+
idx = self.xyztTo1230(axis) if isinstance(axis, str) else int(axis)
369+
scales = (self.stepSizeA, self.stepSizeX, self.stepSizeY, self.stepSizeZ)
370+
return scales[idx] * int(self.direction[idx])
371+
338372
def cartesian2corexy(self, x, y):
339373
# convert cartesian coordinates to coreXY coordinates
340374
# https://www.corexy.com/theory.html
@@ -432,6 +466,9 @@ def move_forever(self, speed=(0,0,0,0), is_stop=False, is_blocking=False):
432466
speed=(speed, speed, speed, speed)
433467
if len(speed)==3:
434468
speed = (*speed,0)
469+
# Apply per-axis direction sign so that a user-frame positive speed
470+
# moves the stage in the user-positive direction even on inverted axes.
471+
speed = tuple(int(speed[i]) * int(self.direction[i]) for i in range(4))
435472
'''
436473
{"task":"/motor_act",
437474
"motor":
@@ -826,21 +863,39 @@ def get_position(self, axis=None, timeout=1):
826863
return _position
827864

828865
def set_position(self, axis=1, position=0, timeout=1):
829-
830866
'''
831-
{"task":"/motor_act", "setpos": {"steppers": [{"stepperid":1, "posval": 0}]}}
867+
Tell the firmware what the current position register should be for an
868+
axis. ``position`` is in **user-frame physical units** (same frame as
869+
``get_position()``). The direction sign and step size are applied
870+
internally so the firmware stores the corresponding hardware-step value:
871+
872+
hw_steps = round(position / signed_step_size(axis))
873+
874+
Special cases:
875+
- ``position=0`` always maps to hw 0 regardless of direction (used
876+
after homing to reset the step counter to zero).
877+
- ``set_motor()`` / ``set_motor_currentPosition()`` are lower-level
878+
helpers that accept raw hardware step values directly and do *not*
879+
apply the direction conversion.
880+
881+
Firmware payload: {"task":"/motor_act", "setpos": {"steppers": [{"stepperid":1, "posval": 0}]}}
832882
'''
833883
path = "/motor_act"
834-
if type(axis) !=int:
884+
if not isinstance(axis, int):
835885
axis = self.xyztTo1230(axis)
836886

887+
# Convert user-frame physical position to firmware hardware steps.
888+
ss = self.signed_step_size(axis)
889+
# Avoid division by zero for degenerate configurations.
890+
hw_position = int(round(position / ss)) if ss != 0 else 0
891+
837892
payload = {
838893
"task": path,
839894
"setpos":{
840895
"steppers": [
841896
{
842897
"stepperid": axis,
843-
"posval": int(position)
898+
"posval": hw_position
844899
}]
845900
}}
846901
r = self._parent.post_json(path, payload, timeout=timeout)

uc2rest/objective.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,14 @@ def getstatus(self):
8484
try:
8585
status = r[-1]["objective"]
8686
status["state"] = int(status["state"])
87-
status["x0"] = np.round(status["x0"]*self._parent.motor.stepSizeA)
88-
status["z0"] = np.round(status["z0"]*self._parent.motor.stepSizeZ)
89-
status["x1"] = np.round(status["x1"]*self._parent.motor.stepSizeA)
90-
status["z1"] = np.round(status["z1"]*self._parent.motor.stepSizeZ)
87+
# Use signed_step_size so that a direction=-1 axis (wiring polarity
88+
# flip) still inverts the returned physical position, matching the
89+
# behaviour that existed when a negative stepSize was passed to
90+
# setup_motor().
91+
status["x0"] = np.round(status["x0"] * self._parent.motor.signed_step_size("A"))
92+
status["z0"] = np.round(status["z0"] * self._parent.motor.signed_step_size("Z"))
93+
status["x1"] = np.round(status["x1"] * self._parent.motor.signed_step_size("A"))
94+
status["z1"] = np.round(status["z1"] * self._parent.motor.signed_step_size("Z"))
9195
except Exception as e:
9296
print(f"Could not get objective status: {e}")
9397
status = {"x0":0,
@@ -213,14 +217,16 @@ def setPositions(self, x0=None, x1=None, z0=None, z1=None, isBlocking=False):
213217

214218
# Only include x0 or x1 in JSON if they were explicitly provided
215219
# If x0 == -1, that instructs the MCU to take current motor position
220+
# Divide by signed_step_size to convert user-frame physical units to
221+
# firmware hardware steps, including the direction/polarity sign.
216222
if x0 is not None:
217-
payload["x0"] = x0/self._parent.motor.stepSizeA
223+
payload["x0"] = x0 / self._parent.motor.signed_step_size("A")
218224
if x1 is not None:
219-
payload["x1"] = x1/self._parent.motor.stepSizeA
225+
payload["x1"] = x1 / self._parent.motor.signed_step_size("A")
220226
if z0 is not None:
221-
payload["z0"] = z0/self._parent.motor.stepSizeZ
227+
payload["z0"] = z0 / self._parent.motor.signed_step_size("Z")
222228
if z1 is not None:
223-
payload["z1"] = z1/self._parent.motor.stepSizeZ
229+
payload["z1"] = z1 / self._parent.motor.signed_step_size("Z")
224230

225231
nResponses = 1 if isBlocking else 0
226232
r = self._parent.post_json(

0 commit comments

Comments
 (0)