Skip to content

Commit e03e504

Browse files
authored
Merge pull request #121 from openUC2/mergemaster
Add soft limits and joystick direction to Motor API
2 parents b9d374c + 5d326c0 commit e03e504

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)