AP_Scripting: copter-slung-payload suppresses oscillation

Co-authored-by: Leonard Hall <leonardthall@gmail.com>
This commit is contained in:
Randy Mackay 2024-07-26 21:12:02 +09:00
parent bff288d498
commit 73dfeac547
2 changed files with 426 additions and 0 deletions

View File

@ -0,0 +1,396 @@
-- Move a Copter so as to reduce a slung payload's oscillation. Requires the payload be capable of sending its position and velocity to the main vehicle
--
-- How To Use
-- 1. copy this script to the autopilot's "scripts" directory
-- 2. within the "scripts" directory create a "modules" directory
-- 3. copy the MAVLink/mavlink_msgs_xxx files to the "scripts" directory
-- 4. add an autopilot and GPS to the payload and configure it to send GLOBAL_POSITION_INT messages at 10hz to the vehicle
-- 5. create a mission with a SCRIPT_TIME or PAYLOAD_PLACE command included
-- 6. fly the mission and the vehicle should move so as to reduce the payload's oscillations while executing the SCRIPT_TIME or PAYLOAD_PLACE commands
-- 7. optionally set SLUP_SYSID to the system id of the payload autopilot
-- 8. optionally set WP_YAW_BEHAVIOR to 0 to prevent the vehicle from yawing while moving to the payload
-- load mavlink message definitions from modules/MAVLink directory
local mavlink_msgs = require("MAVLink/mavlink_msgs")
-- global definitions
local MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7}
local UPDATE_INTERVAL_MS = 10 -- update at about 100hz
local COPTER_MODE_AUTO = 3
local MAV_CMD_NAV_PAYLOAD_PLACE = 94
local MAV_CMD_NAV_SCRIPT_TIME = 42702
local PAYLOAD_OFFSET_COMP_VEL_MAX = 1 -- payload offset compensation will be active when the payload's horizontal velocity is no more than this speed in m/s
local PAYLOAD_UPDATE_TIMEOUT_MS = 1000 -- payload update timeout, used to warn user on loss of connection
local CONTROL_TIMEOUT_MS = 3000 -- control timeout, used to reset offsets if they have not been set for more than 3 seconds
local TEXT_PREFIX_STR = "copter-slung-payload:" -- prefix for all text messages
-- setup script specific parameters
local PARAM_TABLE_KEY = 82
local PARAM_TABLE_PREFIX = "SLUP_"
assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 7), 'could not add param table')
-- add a parameter and bind it to a variable
function bind_add_param(name, idx, default_value)
assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', PARAM_TABLE_PREFIX .. name))
return Parameter(PARAM_TABLE_PREFIX .. name)
end
--[[
// @Param: SLUP_ENABLE
// @DisplayName: Slung Payload enable
// @Description: Slung Payload enable
// @Values: 0:Disabled,1:Enabled
// @User: Standard
--]]
local SLUP_ENABLE = bind_add_param("ENABLE", 1, 1)
--[[
// @Param: SLUP_VEL_P
// @DisplayName: Slung Payload Velocity P gain
// @Description: Slung Payload Velocity P gain, higher values will result in faster movements in sync with payload
// @Range: 0 0.8
// @User: Standard
--]]
local SLUP_VEL_P = bind_add_param("VEL_P", 2, 0.5)
--[[
// @Param: SLUP_DIST_MAX
// @DisplayName: Slung Payload horizontal distance max
// @Description: Oscillation is suppressed when vehicle and payload are no more than this distance horizontally. Set to 0 to always suppress
// @Range: 0 30
// @User: Standard
--]]
local SLUP_DIST_MAX = bind_add_param("DIST_MAX", 3, 15)
--[[
// @Param: SLUP_SYSID
// @DisplayName: Slung Payload mavlink system id
// @Description: Slung Payload mavlink system id. 0 to use any/all system ids
// @Range: 0 255
// @User: Standard
--]]
local SLUP_SYSID = bind_add_param("SYSID", 4, 0)
--[[
// @Param: SLUP_WP_POS_P
// @DisplayName: Slung Payload return to WP position P gain
// @Description: WP position P gain. higher values will result in vehicle moving more quickly back to the original waypoint
// @Range: 0 1
// @User: Standard
--]]
local SLUP_WP_POS_P = bind_add_param("WP_POS_P", 5, 0.05)
--[[
// @Param: SLUP_RESTOFS_TC
// @DisplayName: Slung Payload resting offset estimate filter time constant
// @Description: payload's position estimator's time constant used to compensate for GPS errors and wind. Higher values result in smoother estimate but slower response
// @Range: 1 20
// @User: Standard
--]]
local SLUP_RESTOFS_TC = bind_add_param("RESTOFS_TC", 6, 10)
--[[
// @Param: SLUP_DEBUG
// @DisplayName: Slung Payload debug output
// @Description: Slung payload debug output, set to 1 to enable debug
// @Values: 0:Disabled,1:Enabled
// @User: Standard
--]]
local SLUP_DEBUG = bind_add_param("DEBUG", 7, 0)
-- mavlink definitions
local GLOBAL_POSITION_INT_ID = 33
local msg_map = {}
msg_map[GLOBAL_POSITION_INT_ID] = "GLOBAL_POSITION_INT"
-- initialize MAVLink rx with number of messages, and buffer depth
mavlink:init(2, 5)
-- register message id to receive
mavlink:register_rx_msgid(GLOBAL_POSITION_INT_ID)
-- variables
local payload_sysid = nil -- holds sysid of payload once a matching global position int message has been received
local payload_loc = Location() -- payload location
local payload_vel = Vector3f() -- payload velocity
local payload_acc = Vector3f() -- payload acceleration
local payload_update_timeout_prev = true -- payload update timeout state from previous iteration, used to detect loss/recovery of payload updates
local payload_loc_update_ms = uint32_t(0) -- system time that payload_loc was last updated
local payload_dist_NED = Vector3f() -- distance between vehicle and payload in NED frame
local global_pos_int_timebootms_prev = 0 -- global position int message's time_boot_ms field from previous iteration (used to calc dt)
local resting_offset_NED = Vector3f() -- estimated position offset between payload and vehicle
local resting_vel_NED = Vector3f() -- estimated velocity offset. should be near zero when hovering
local resting_offset_valid = false -- true if resting_offset_NED and resting_vel_NED can be used
local resting_offset_update_ms = uint32_t(0) -- system time that resting_offset_NED was last updated
local resting_offset_notify_ms = uint32_t(0) -- system time that the user was sent the resting_offset_NED
local send_velocity_offsets = false -- true if we should send vehicle velocity offset commands to reduce payload oscillation
local sent_velocity_offsets_ms = uint32_t(0) -- system time that the last velocity offset was sent to the vehicle
local control_timeout_ms = uint32_t(0) -- system time that the control timeout occurred
local payload_vel_prev = Vector3f() -- previous iterations payload velocity used to calculate acceleration
local print_warning_ms = uint32_t(0) -- system time that the last warning was printed. used to prevent spamming the user with warnings
-- display a text warning to the user. only displays a warning every second
-- prefix is automatically added
function print_warning(text_warning)
local now_ms = millis()
if (now_ms - print_warning_ms > 1000) then
gcs:send_text(MAV_SEVERITY.WARNING, string.format("%s %s", TEXT_PREFIX_STR, text_warning))
print_warning_ms = now_ms
end
end
-- calculate sign of a number. 1 if positive, -1 if negative, 0 if exactly zero
function get_sign(value)
if value > 0 then
return 1
elseif value < 0 then
return -1
end
return 0
end
-- calculate an alpha for a first order low pass filter
function calc_lowpass_alpha(dt, time_constant)
local rc = time_constant/(math.pi*2)
return dt/(dt+rc)
end
-- handle global position int message
-- returns true if the message was from the payload and updates payload_loc, payload_vel, payload_acc and payload_loc_update_ms
function handle_global_position_int(msg)
-- check if message is from the correct system id
if (SLUP_SYSID:get() > 0 and msg.sysid ~= SLUP_SYSID:get()) then
return false
end
-- lock onto the first matching system id
if payload_sysid == nil then
payload_sysid = msg.sysid
gcs:send_text(MAV_SEVERITY.INFO, string.format("%s found sysid:%d", TEXT_PREFIX_STR, msg.sysid))
elseif payload_sysid ~= msg.sysid then
return false
end
-- check for duplicate messages and calculate dt
local time_boot_ms = msg.time_boot_ms
local dt = (time_boot_ms - global_pos_int_timebootms_prev) * 0.001
global_pos_int_timebootms_prev = time_boot_ms
if dt <= 0 or dt > 1 then
return false
end
-- update payload location
payload_loc:lat(msg.lat)
payload_loc:lng(msg.lon)
payload_loc:alt(msg.alt * 0.1)
-- update payload velocity
payload_vel:x(msg.vx * 0.01)
payload_vel:y(msg.vy * 0.01)
payload_vel:z(msg.vz * 0.01)
-- calc payload acceleration
payload_acc:x((payload_vel:x() - payload_vel_prev:x()) / dt)
payload_acc:y((payload_vel:y() - payload_vel_prev:y()) / dt)
payload_acc:z((payload_vel:z() - payload_vel_prev:z()) / dt)
payload_vel_prev = payload_vel:copy()
-- record time of update
payload_loc_update_ms = millis()
return true
end
-- estimate the payload's resting position offset based on its current offset and velocity
-- relies on payload_dist_NED and payload_vel being updated
function update_payload_resting_offset()
-- calculate dt since last update
local now_ms = millis()
local dt = (now_ms - resting_offset_update_ms):tofloat() * 0.001
resting_offset_update_ms = now_ms
-- sanity check dt
if (dt <= 0) then
resting_offset_valid = false
do return end
end
-- if not updated for more than 1 second, reset resting offset to current offset
if (dt > 1) then
resting_offset_NED = payload_dist_NED
resting_vel_NED = payload_vel
resting_offset_valid = false
do return end
end
-- use a low-pass filter to move the resting offset NED towards the pos_offset_NED
local alpha = calc_lowpass_alpha(dt, SLUP_RESTOFS_TC:get())
resting_offset_NED:x(resting_offset_NED:x() + (payload_dist_NED:x() - resting_offset_NED:x()) * alpha)
resting_offset_NED:y(resting_offset_NED:y() + (payload_dist_NED:y() - resting_offset_NED:y()) * alpha)
resting_offset_NED:z(resting_offset_NED:z() + (payload_dist_NED:z() - resting_offset_NED:z()) * alpha)
resting_vel_NED:x(resting_vel_NED:x() + (payload_vel:x() - resting_vel_NED:x()) * alpha)
resting_vel_NED:y(resting_vel_NED:y() + (payload_vel:y() - resting_vel_NED:y()) * alpha)
resting_vel_NED:z(resting_vel_NED:z() + (payload_vel:z() - resting_vel_NED:z()) * alpha)
-- debug output every 3 seconds
local print_debug = false
if (SLUP_DEBUG:get() > 0) and (now_ms - resting_offset_notify_ms > 3000) then
print_debug = true
resting_offset_notify_ms = now_ms
end
-- validate that resting offsets are valid
-- resting velocity should be low to ensure the resting position estimate is accurate
if (resting_vel_NED:xy():length() > PAYLOAD_OFFSET_COMP_VEL_MAX) then
resting_offset_valid = false
if print_debug then
print_warning("resting velocity too high")
end
return
end
-- resting position should be within SLUP_DIST_MAX of the vehicle
if resting_offset_NED:xy():length() > SLUP_DIST_MAX:get() then
resting_offset_valid = false
if print_debug then
print_warning("payload resting pos too far, ignoring");
end
return
end
-- update user
if (print_debug) then
gcs:send_text(MAV_SEVERITY.INFO, string.format("resting a:%f px:%4.1f py:%4.1f pz:%4.1f vx:%4.1f vy:%4.1f vz:%4.1f", alpha, resting_offset_NED:x(), resting_offset_NED:y(), resting_offset_NED:z(), resting_vel_NED:x(), resting_vel_NED:y(), resting_vel_NED:z()))
end
-- if set got this far the resting offsets must be valid
resting_offset_valid = true
end
-- move vehicle to reduce payload oscillation
-- relies on payload_dist_NED, payload_vel, payload_acc, resting_offset_NED, resting_vel_NED being updated
function move_vehicle()
-- check horizontal distance is less than SLUP_DIST_MAX
if SLUP_DIST_MAX:get() > 0 then
local dist_xy = payload_dist_NED:xy():length()
if (dist_xy > SLUP_DIST_MAX:get()) then
print_warning(string.format("payload too far %4.1fm", dist_xy));
do return end
end
end
-- get long-term payload offset used to compensate for GPS errors and wind
local payload_offset_NED = Vector3f()
if resting_offset_valid then
payload_offset_NED = resting_offset_NED
end
-- get position offset (cumulative effect of velocity offsets) and use to slowly move back to waypoint
local pos_offset_NED, _, _ = poscontrol:get_posvelaccel_offset()
if pos_offset_NED == nil then
print_warning("unable to get dist to waypoint")
pos_offset_NED = Vector3f()
end
-- calculate send velocity offsets in m/s in NED frame
local vel_offset_NED = Vector3f()
vel_offset_NED:x(-payload_acc:x() * SLUP_VEL_P:get() + (-pos_offset_NED:x() - payload_offset_NED:x()) * SLUP_WP_POS_P:get())
vel_offset_NED:y(-payload_acc:y() * SLUP_VEL_P:get() + (-pos_offset_NED:y() - payload_offset_NED:y()) * SLUP_WP_POS_P:get())
if poscontrol:set_posvelaccel_offset(pos_offset_NED, vel_offset_NED, Vector3f()) then
sent_velocity_offsets_ms = millis()
end
end
-- display welcome message
gcs:send_text(MAV_SEVERITY.INFO, "copter-slung-payload script loaded")
-- update function to receive location from payload and move vehicle to reduce payload's oscillation
function update()
-- exit immediately if not enabled
if (SLUP_ENABLE:get() <= 0) then
return update, 1000
end
-- get vehicle location
local curr_loc = ahrs:get_location()
if curr_loc == nil then
return update, UPDATE_INTERVAL_MS
end
-- consume all available mavlink messages
local payload_update_received = false
local msg
repeat
msg, _ = mavlink:receive_chan()
if (msg ~= nil) then
local parsed_msg = mavlink_msgs.decode(msg, msg_map)
if (parsed_msg ~= nil) then
if parsed_msg.msgid == GLOBAL_POSITION_INT_ID then
if handle_global_position_int(parsed_msg) then
payload_update_received = true
end
end
end
end
until msg == nil
-- warn user on loss of recovery of telemetry from payload
local payload_timeout = millis() - payload_loc_update_ms > PAYLOAD_UPDATE_TIMEOUT_MS
if payload_timeout ~= payload_update_timeout_prev then
if payload_timeout then
gcs:send_text(MAV_SEVERITY.WARNING, string.format("%s payload updates lost", TEXT_PREFIX_STR))
else
gcs:send_text(MAV_SEVERITY.INFO, string.format("%s payload updates received", TEXT_PREFIX_STR))
end
end
payload_update_timeout_prev = payload_timeout
if payload_update_received then
-- calculate position difference vs vehicle
payload_dist_NED = curr_loc:get_distance_NED(payload_loc)
-- estimate the payload's resting position offset based on its current offset and velocity
-- relies on payload_dist_NED and payload_vel being updated
update_payload_resting_offset()
end
-- check if we can control the vehicle
-- vehicle must be in Auto mode executing a SCRIPT_TIME or PAYLOAD_PLACE command
local send_velocity_offsets_prev = send_velocity_offsets
local armed_and_flying = arming:is_armed() and vehicle:get_likely_flying()
local takingoff_or_landing = vehicle:is_landing() or vehicle:is_taking_off()
local auto_mode = (vehicle:get_mode() == COPTER_MODE_AUTO)
local scripting_or_payloadplace = (mission:get_current_nav_id() == MAV_CMD_NAV_SCRIPT_TIME) or (mission:get_current_nav_id() == MAV_CMD_NAV_PAYLOAD_PLACE)
send_velocity_offsets = armed_and_flying and not takingoff_or_landing and auto_mode and scripting_or_payloadplace and not payload_timeout
-- alert user if we start or stop sending velocity offsets
if (send_velocity_offsets and not send_velocity_offsets_prev) then
gcs:send_text(MAV_SEVERITY.INFO, string.format("%s activated", TEXT_PREFIX_STR))
end
if (not send_velocity_offsets and send_velocity_offsets_prev) then
gcs:send_text(MAV_SEVERITY.INFO, string.format("%s deactivated", TEXT_PREFIX_STR))
poscontrol:set_posvelaccel_offset(Vector3f(), Vector3f(), Vector3f())
end
-- move vehicle to reduce payload oscillation
if send_velocity_offsets then
move_vehicle()
-- check for unexpected control timeout
-- reset vehicle offsets if not sent within last 3 seconds
local now_ms = millis()
local time_since_vel_offset_sent = now_ms - sent_velocity_offsets_ms
local time_since_last_control_timeout = now_ms - control_timeout_ms
if (time_since_vel_offset_sent > CONTROL_TIMEOUT_MS) and (time_since_last_control_timeout > CONTROL_TIMEOUT_MS) then
poscontrol:set_posvelaccel_offset(Vector3f(), Vector3f(), Vector3f())
control_timeout_ms = now_ms
print_warning("control timeout, clearing offsets")
end
end
return update, UPDATE_INTERVAL_MS
end
return update()

View File

@ -0,0 +1,30 @@
# Slung Payload
This script moves a Copter so as to reduce a slung payload's oscillation. Requires the payload be capable of sending its position and velocity to the main vehicle
# Parameters
SLUP_ENABLE : Set to 1 to enable this script
SLUP_VEL_P : Oscillation controller velocity P gain. Higher values will result in the vehicle moving more quickly in sync with the payload
SLUP_DIST_MAX : maximum acceptable distance between vehicle and payload. Within this distance oscillation suppression will operate
SLUP_SYSID : System id of payload's autopilot. If zero any system id is accepted
SLUP_WP_POS_P : Return to waypoint position P gain. Higher values result in the vehicle returning more quickly to the latest waypoint
SLUP_RESTOFS_TC : Slung Payload resting offset estimate filter time constant. Higher values result in smoother estimate but slower response
SLUP_DEBUG : Slung payload debug output, set to 1 to enable debug
# How To Use
1. mount an autopilot on the payload connected to the main vehicle using telemetry
2. ensure the vehicle and payload autopilots have unique system ids
3. copy this script to the vehicle autopilot's "scripts" directory
4. within the "scripts" directory create a "modules" directory
5. copy the MAVLink/mavlink_msgs_xxx files to the "scripts" directory
# How It Works
The script's algorithm is implemented as follows
1. Consume GLOBAL_POSITION_INT messages from the payload
2. Calculate the payload's position vs the vehicle position
3. Use a P controller to move the vehicle towards the payload to reduce oscillation
4. Simultaneously the vehicle moves back towards the original location. The speed depends upon the SLUP_WP_POS_P parameter