@@ -757,6 +757,135 @@ def MAV_CMD_DO_CHANGE_SPEED(self):
757757 self .wait_groundspeed (speed - 0.2 , speed + 0.2 , minimum_duration = 2 , timeout = 60 )
758758 self .disarm_vehicle ()
759759
760+ def MAV_CMD_DO_SET_ACTUATOR (self ):
761+ '''Test MAV_CMD_DO_SET_ACTUATOR command'''
762+ # Map each supported actuator (1..6) onto a distinct SERVO output
763+ # with a distinct PWM range so any cross-actuator bleed-through
764+ # would change a PWM we did not intend to touch.
765+ # k_actuator1..k_actuator6 = 184..189.
766+ actuator_servos = {
767+ 1 : {"channel" : 9 , "min" : 1100 , "max" : 1900 , "trim" : 1500 },
768+ 2 : {"channel" : 10 , "min" : 1200 , "max" : 1800 , "trim" : 1500 },
769+ 3 : {"channel" : 11 , "min" : 1050 , "max" : 1950 , "trim" : 1500 },
770+ 4 : {"channel" : 12 , "min" : 1000 , "max" : 2000 , "trim" : 1500 },
771+ 5 : {"channel" : 13 , "min" : 1150 , "max" : 1850 , "trim" : 1500 },
772+ 6 : {"channel" : 14 , "min" : 1075 , "max" : 1925 , "trim" : 1500 },
773+ }
774+
775+ params = {}
776+ for actuator_num , cfg in actuator_servos .items ():
777+ chan = cfg ["channel" ]
778+ params [f"SERVO{ chan } _FUNCTION" ] = 183 + actuator_num # k_actuator1==184
779+ params [f"SERVO{ chan } _MIN" ] = cfg ["min" ]
780+ params [f"SERVO{ chan } _MAX" ] = cfg ["max" ]
781+ params [f"SERVO{ chan } _TRIM" ] = cfg ["trim" ]
782+ self .set_parameters (params )
783+ self .reboot_sitl ()
784+
785+ self .wait_ready_to_arm ()
786+ self .arm_vehicle ()
787+
788+ # Actuator value mapping (matches AP_Actuators::set_actuator):
789+ # internal_value = (input_value + 1) * 0.5 (maps -1..1 to 0..1)
790+ # pwm = servo_min + (servo_max - servo_min) * internal_value
791+
792+ def set_actuators (values ):
793+ '''Send MAV_CMD_DO_SET_ACTUATOR (COMMAND_LONG) with values
794+ for actuators 1..6.
795+
796+ values is a dict {actuator_num: value}; missing entries are
797+ sent as NaN ("ignore") per the MAVLink spec.
798+ '''
799+ params = [float ('nan' )] * 6
800+ for actuator_num , value in values .items ():
801+ params [actuator_num - 1 ] = value
802+ self .run_cmd (
803+ 187 , # MAV_CMD_DO_SET_ACTUATOR
804+ p1 = params [0 ],
805+ p2 = params [1 ],
806+ p3 = params [2 ],
807+ p4 = params [3 ],
808+ p5 = params [4 ],
809+ p6 = params [5 ],
810+ p7 = 0 , # actuator-set index 0 (actuators 1..6)
811+ )
812+
813+ def expected_pwm (actuator_num , value ):
814+ cfg = actuator_servos [actuator_num ]
815+ internal = (value + 1 ) * 0.5
816+ return int (cfg ["min" ] + (cfg ["max" ] - cfg ["min" ]) * internal )
817+
818+ # Exercise each supported actuator independently across the full
819+ # input range, asserting the right servo lands on the right PWM.
820+ for actuator_num in sorted (actuator_servos ):
821+ cfg = actuator_servos [actuator_num ]
822+ for value , label in [
823+ (- 1.0 , "minimum" ),
824+ (0.0 , "center" ),
825+ (1.0 , "maximum" ),
826+ (0.5 , "75%" ),
827+ (- 0.5 , "25%" ),
828+ ]:
829+ self .progress (
830+ f"Testing actuator { actuator_num } set to { value } ({ label } )"
831+ )
832+ set_actuators ({actuator_num : value })
833+ self .wait_servo_channel_value (
834+ cfg ["channel" ],
835+ expected_pwm (actuator_num , value ),
836+ timeout = 5 ,
837+ )
838+
839+ # A single command carrying values for every supported actuator
840+ # must apply all six in one shot. Pick distinct values so a
841+ # mis-ordered param->actuator mapping would be caught.
842+ self .progress ("Testing all six actuators set in one COMMAND_LONG" )
843+ combined = {1 : - 1.0 , 2 : - 0.6 , 3 : - 0.2 , 4 : 0.2 , 5 : 0.6 , 6 : 1.0 }
844+ set_actuators (combined )
845+ for actuator_num , value in combined .items ():
846+ cfg = actuator_servos [actuator_num ]
847+ self .wait_servo_channel_value (
848+ cfg ["channel" ],
849+ expected_pwm (actuator_num , value ),
850+ timeout = 5 ,
851+ )
852+
853+ # NaN ("ignore") slots must leave the previous value untouched.
854+ # Re-set actuators 1 and 5 to center (covering both the float
855+ # p1..p4 path and the scaled-int p5/p6 path), then send a
856+ # command that only moves actuator 4 and verify 1 and 5 stayed
857+ # at center.
858+ self .progress ("Testing NaN slots leave previous values untouched" )
859+ set_actuators ({1 : 0.0 , 5 : 0.0 })
860+ self .wait_servo_channel_value (
861+ actuator_servos [1 ]["channel" ], expected_pwm (1 , 0.0 ), timeout = 5 ,
862+ )
863+ self .wait_servo_channel_value (
864+ actuator_servos [5 ]["channel" ], expected_pwm (5 , 0.0 ), timeout = 5 ,
865+ )
866+ set_actuators ({4 : 0.0 })
867+ self .wait_servo_channel_value (
868+ actuator_servos [4 ]["channel" ], expected_pwm (4 , 0.0 ), timeout = 5 ,
869+ )
870+ self .wait_servo_channel_value (
871+ actuator_servos [1 ]["channel" ], expected_pwm (1 , 0.0 ), timeout = 2 ,
872+ )
873+ self .wait_servo_channel_value (
874+ actuator_servos [5 ]["channel" ], expected_pwm (5 , 0.0 ), timeout = 2 ,
875+ )
876+
877+ # An actuator-set index other than 0 is rejected because only
878+ # actuators 1..6 of the first set are supported.
879+ self .progress ("Testing actuator-set index != 0 is rejected" )
880+ self .run_cmd_int (
881+ 187 , # MAV_CMD_DO_SET_ACTUATOR
882+ p1 = 0 , p2 = 0 , p3 = 0 , p4 = 0 ,
883+ x = 0 , y = 0 , z = 1 ,
884+ want_result = mavutil .mavlink .MAV_RESULT_DENIED ,
885+ )
886+
887+ self .disarm_vehicle ()
888+
760889 def GPSForYaw (self ):
761890 '''Test consumption of heading from NMEA GPS and its propagation to ATTITUDE'''
762891
@@ -1352,6 +1481,7 @@ def tests(self):
13521481 self .MAV_CMD_DO_CHANGE_SPEED ,
13531482 self .MAV_CMD_CONDITION_YAW ,
13541483 self .MAV_CMD_DO_REPOSITION ,
1484+ self .MAV_CMD_DO_SET_ACTUATOR ,
13551485 self .TerrainMission ,
13561486 self .SetGlobalOrigin ,
13571487 self .BackupOrigin ,
0 commit comments