@@ -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