Skip to content

Commit 5d326c0

Browse files
committed
Add soft limits and joystick direction to Motor API
Introduces methods to set, get, enable, and disable soft limits for each motor axis, as well as to configure and query joystick direction inversion. Also improves travel time calculation by converting all relevant units to hardware steps for accuracy. Additionally, the serial command processor now logs all serial input to a file for debugging purposes.
1 parent 2da99ef commit 5d326c0

3 files changed

Lines changed: 374 additions & 114 deletions

File tree

.DS_Store

2 KB
Binary file not shown.

uc2rest/motor.py

Lines changed: 263 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -448,31 +448,62 @@ def move_stepper(self, steps=(0,0,0,0), speed=(1000,1000,1000,1000), is_absolute
448448
steps[2] *= 1/self.stepSizeY
449449
steps[3] *= 1/self.stepSizeZ
450450

451-
# detect change in direction
452-
absoluteDistances = np.zeros((4))
451+
# detect change in direction and compute distances in HARDWARE STEPS for travel time calculation
452+
absoluteDistances_steps = np.zeros((4)) # Distance in hardware steps
453+
stepSizes = np.array((self.stepSizeA, self.stepSizeX, self.stepSizeY, self.stepSizeZ))
454+
453455
for iMotor in range(4):
454456
# for absolute motion:
455457
if isAbsoluteArray[iMotor]:
456458
# Compare current position (physical) with target (physical, already includes offset)
457459
self.currentDirection[iMotor] = 1 if (self.currentPosition[iMotor] > targetPositionPhysical[iMotor]) else -1
460+
# Calculate distance to travel in HARDWARE STEPS:
461+
# Current position (physical) -> convert to steps, then subtract target (already in steps)
462+
currentPosition_steps = self.currentPosition[iMotor] / stepSizes[iMotor]
463+
absoluteDistances_steps[iMotor] = abs(currentPosition_steps - steps[iMotor])
458464
else:
459465
self.currentDirection[iMotor] = np.sign(steps[iMotor])
466+
# For relative motion, steps[iMotor] is already the distance in hardware steps
467+
absoluteDistances_steps[iMotor] = abs(steps[iMotor])
468+
460469
if self.lastDirection[iMotor] != self.currentDirection[iMotor]:
461-
# we want to overshoot a bit (backlash is in steps, so apply before division)
470+
# we want to overshoot a bit (backlash is in steps, so apply AFTER conversion to steps)
462471
steps[iMotor] = steps[iMotor] + self.currentDirection[iMotor]*self.backlash[iMotor]
463-
472+
# Update distance calculation if backlash was applied
473+
if not isAbsoluteArray[iMotor]:
474+
absoluteDistances_steps[iMotor] = abs(steps[iMotor])
464475

465-
if isAbsoluteArray[iMotor]:
466-
absoluteDistances[iMotor] = abs(self.currentPosition[iMotor] - steps[iMotor])
476+
# Convert speed and acceleration from physical units to steps/second
477+
speed_steps = np.zeros(4)
478+
acceleration_steps = np.zeros(4)
479+
for iMotor in range(4):
480+
if speed[iMotor] != 0:
481+
# Speed: µm/s -> steps/s => divide by stepSize (µm/step)
482+
speed_steps[iMotor] = abs(speed[iMotor]) / stepSizes[iMotor]
483+
if acceleration[iMotor] is not None and acceleration[iMotor] != 0:
484+
# Acceleration: µm/s² -> steps/s² => divide by stepSize
485+
acceleration_steps[iMotor] = abs(acceleration[iMotor]) / stepSizes[iMotor]
467486
else:
468-
absoluteDistances[iMotor] = abs(steps[iMotor])
469-
# experimental: Let's make the timeout adaptive:
470-
# the speed of the motors is given in steps/second, so we can calculate the time it takes to move the given steps
471-
# we will add a bit of time to the timeout to make sure we get a return
472-
# 1.5 accounts for accel/decceleration
473-
traveltime = self.compute_travel_time(np.max(np.abs(absoluteDistances)), np.max(np.abs(speed)), np.max(np.where(acceleration == None, 20000, acceleration)))
474-
timeout = np.uint8(abs(timeout)>0)*(traveltime + 1) # add 1 second to the timeout to make sure we get a return
475-
487+
# Default acceleration in steps/s²
488+
acceleration_steps[iMotor] = 20000 # This should also be converted, but we use a safe default
489+
490+
# Calculate travel time using HARDWARE STEPS and converted speed/acceleration
491+
# Find the axis that will take the longest (limits overall movement time)
492+
max_travel_time = 0
493+
for iMotor in range(4):
494+
if absoluteDistances_steps[iMotor] > 0 and speed_steps[iMotor] > 0:
495+
axis_time = self.compute_travel_time(
496+
absoluteDistances_steps[iMotor],
497+
speed_steps[iMotor],
498+
acceleration_steps[iMotor]
499+
)
500+
max_travel_time = max(max_travel_time, axis_time)
501+
502+
# Set timeout based on calculated travel time (add 2 seconds safety margin)
503+
if max_travel_time > 0:
504+
timeout = np.uint8(abs(timeout) > 0) * (max_travel_time + 2)
505+
else:
506+
timeout = np.uint8(abs(timeout) > 0) * 3 # Minimum 3 seconds if no movement detected
476507
# get current position
477508
#_positions = self.get_position() # x,y,z,t = 1,2,3,0
478509
#pos_3, pos_0, pos_1, pos_2 = _positions[0],_positions[1],_positions[2],_positions[3]
@@ -696,6 +727,224 @@ def set_position(self, axis=1, position=0, timeout=1):
696727

697728
return r
698729

730+
def set_soft_limits(self, axis=1, min_pos=None, max_pos=None, is_enabled=None, timeout=1):
731+
'''
732+
Set soft limits for a motor axis. Soft limits prevent motor movement beyond specified boundaries.
733+
734+
Parameters:
735+
-----------
736+
axis : int or str
737+
Motor axis (0/"A", 1/"X", 2/"Y", 3/"Z")
738+
min_pos : int, optional
739+
Minimum position limit in steps
740+
max_pos : int, optional
741+
Maximum position limit in steps
742+
is_enabled : bool or int, optional
743+
Enable (1/True) or disable (0/False) soft limits for this axis
744+
timeout : int
745+
Command timeout in seconds
746+
747+
Returns:
748+
--------
749+
Response from ESP32
750+
751+
Example:
752+
--------
753+
# Set limits for X-axis
754+
motor.set_soft_limits(axis="X", min_pos=-10000, max_pos=10000, is_enabled=True)
755+
# Disable limits for Z-axis
756+
motor.set_soft_limits(axis="Z", is_enabled=False)
757+
758+
Note:
759+
-----
760+
Soft limits are automatically ignored during homing operations.
761+
'''
762+
if type(axis) != int:
763+
axis = self.xyztTo1230(axis)
764+
765+
path = "/motor_act"
766+
payload = {
767+
"task": path,
768+
"softlimits": {
769+
"steppers": [{
770+
"stepperid": axis
771+
}]
772+
}
773+
}
774+
775+
if min_pos is not None:
776+
payload["softlimits"]["steppers"][0]["min"] = int(min_pos)
777+
if max_pos is not None:
778+
payload["softlimits"]["steppers"][0]["max"] = int(max_pos)
779+
if is_enabled is not None:
780+
payload["softlimits"]["steppers"][0]["isen"] = int(is_enabled)
781+
782+
r = self._parent.post_json(path, payload, timeout=timeout)
783+
return r
784+
785+
def get_soft_limits(self, axis=None, timeout=1):
786+
'''
787+
Get current soft limits configuration for all axes or a specific axis.
788+
789+
Parameters:
790+
-----------
791+
axis : int or str, optional
792+
Motor axis (0/"A", 1/"X", 2/"Y", 3/"Z"). If None, returns all axes.
793+
timeout : int
794+
Command timeout in seconds
795+
796+
Returns:
797+
--------
798+
dict : Soft limits configuration
799+
Contains min, max, and isen (enabled) for each axis
800+
801+
Example:
802+
--------
803+
# Get limits for all axes
804+
limits = motor.get_soft_limits()
805+
# Get limits for X-axis only
806+
x_limits = motor.get_soft_limits(axis="X")
807+
'''
808+
motors = self.get_motors(timeout=timeout)
809+
810+
if motors and "steppers" in motors:
811+
if axis is not None:
812+
if type(axis) != int:
813+
axis = self.xyztTo1230(axis)
814+
# Find the specific axis
815+
for stepper in motors["steppers"]:
816+
if stepper.get("stepperid") == axis:
817+
return {
818+
"axis": axis,
819+
"min": stepper.get("min", 0),
820+
"max": stepper.get("max", 0),
821+
"enabled": stepper.get("isen", 0)
822+
}
823+
else:
824+
# Return all axes
825+
result = []
826+
for stepper in motors["steppers"]:
827+
result.append({
828+
"axis": stepper.get("stepperid"),
829+
"min": stepper.get("min", 0),
830+
"max": stepper.get("max", 0),
831+
"enabled": stepper.get("isen", 0)
832+
})
833+
return result
834+
return None
835+
836+
def enable_soft_limits(self, axis, timeout=1):
837+
'''
838+
Enable soft limits for a specific axis.
839+
840+
Parameters:
841+
-----------
842+
axis : int or str
843+
Motor axis (0/"A", 1/"X", 2/"Y", 3/"Z")
844+
timeout : int
845+
Command timeout in seconds
846+
'''
847+
return self.set_soft_limits(axis=axis, is_enabled=True, timeout=timeout)
848+
849+
def disable_soft_limits(self, axis, timeout=1):
850+
'''
851+
Disable soft limits for a specific axis.
852+
853+
Parameters:
854+
-----------
855+
axis : int or str
856+
Motor axis (0/"A", 1/"X", 2/"Y", 3/"Z")
857+
timeout : int
858+
Command timeout in seconds
859+
'''
860+
return self.set_soft_limits(axis=axis, is_enabled=False, timeout=timeout)
861+
862+
def set_joystick_direction(self, axis, inverted=False, timeout=1):
863+
'''
864+
Set joystick direction inversion for a specific motor axis.
865+
When inverted is True, joystick movements for this axis will be reversed.
866+
867+
Parameters:
868+
-----------
869+
axis : int or str
870+
Motor axis (0/"A", 1/"X", 2/"Y", 3/"Z")
871+
inverted : bool or int
872+
True/1 to invert joystick direction, False/0 for normal direction
873+
timeout : int
874+
Command timeout in seconds
875+
876+
Returns:
877+
--------
878+
Response from ESP32
879+
880+
Example:
881+
--------
882+
# Invert joystick direction for X-axis
883+
motor.set_joystick_direction(axis="X", inverted=True)
884+
# Set normal direction for Y-axis
885+
motor.set_joystick_direction(axis="Y", inverted=False)
886+
'''
887+
if type(axis) != int:
888+
axis = self.xyztTo1230(axis)
889+
890+
path = "/motor_act"
891+
payload = {
892+
"task": path,
893+
"joystickdir": {
894+
"steppers": [{
895+
"stepperid": axis,
896+
"inverted": int(inverted)
897+
}]
898+
}
899+
}
900+
901+
r = self._parent.post_json(path, payload, timeout=timeout)
902+
return r
903+
904+
def get_joystick_direction(self, axis=None, timeout=1):
905+
'''
906+
Get joystick direction configuration for axes.
907+
908+
Parameters:
909+
-----------
910+
axis : int or str, optional
911+
Motor axis (0/"A", 1/"X", 2/"Y", 3/"Z"). If None, returns all axes.
912+
timeout : int
913+
Command timeout in seconds
914+
915+
Returns:
916+
--------
917+
dict or bool : Joystick direction configuration
918+
Returns inverted status for the specified axis or all axes
919+
920+
Example:
921+
--------
922+
# Get direction for X-axis
923+
x_inverted = motor.get_joystick_direction(axis="X")
924+
# Get direction for all axes
925+
all_directions = motor.get_joystick_direction()
926+
'''
927+
motors = self.get_motors(timeout=timeout)
928+
929+
if motors and "steppers" in motors:
930+
if axis is not None:
931+
if type(axis) != int:
932+
axis = self.xyztTo1230(axis)
933+
# Find the specific axis
934+
for stepper in motors["steppers"]:
935+
if stepper.get("stepperid") == axis:
936+
return stepper.get("joystickDirectionInverted", False)
937+
else:
938+
# Return all axes
939+
result = []
940+
for stepper in motors["steppers"]:
941+
result.append({
942+
"axis": stepper.get("stepperid"),
943+
"inverted": stepper.get("joystickDirectionInverted", False)
944+
})
945+
return result
946+
return None
947+
699948
def set_offset(self, axis=1, offset=0):
700949
'''
701950
This sets the offset relative to the homed position, helpful if you have a reference point and you need to compute the offset

0 commit comments

Comments
 (0)