-- Glide into wind, LUA script for glide into wind functionality -- Background -- When flying a fixed-wing drone on ad-hoc BVLOS missions, it might not be -- suitable for the drone to return to home if the C2 link is lost, since that -- might mean flying without control for an extended time and distance. One -- option in ArduPlane is to set FS_Long to Glide, which makes the drone glide -- and land in the direction it happened to have when the command was invoked, -- without regard to the wind. This script offers a way to decrease the kinetic -- energy in this blind landing by means of steering the drone towards the wind -- as GLIDE is initiated, hence lowering the ground speed. The intention is to -- minimize impact energy at landing - foremost for any third party, but also to -- minimize damage to the drone. -- Functionality and setup -- 1. Set SCR_ENABLE = 1 -- 2. Put script in scripts folder, boot twice -- 3. A new parameter has appeared: -- - GLIDE_WIND_ENABL (0=disable, 1=enable) -- 4. Set GLIDE_WIND_ENABL = 1 -- 5. Read the docs on FS: -- https://ardupilot.org/plane/docs/apms-failsafe-function.html#failsafe-parameters-and-their-meanings -- 6. Set FS_LONG_ACTN = 2 -- 7. Set FS_LONG_TIMEOUT as appropriate -- 8. Set FS_GCS_ENABL = 1 -- 9. If in simulation, set SIM_WIND_SPD = 4 to get a reliable wind direction. -- 10. Test in simulation: Fly a mission, disable heartbeats by typing 'set -- heartbeat 0' into mavproxy/SITL, monitor what happens in the console. If -- QGC or similar GCS is used, make sure it does not send heartbeats. -- 11. Test in flight: Fly a mission, monitor estimated wind direction from GCS, -- then fail GCS link and see what happens. -- 12. Once heading is into wind script will stop steering and not steer again -- until state machine is reset and failsafe is triggered again. Steering in low -- airspeeds (thr=0) increases risks of stall and it is preferable touch -- ground in level roll attitude. If the script parameter hdg_ok_lim is set -- to tight or the wind estimate is not stable, the script will anyhow stop -- steering after override_time_lim and enter FBWA - otherwise the script -- would hinder the GLIDE fail safe. -- 13. Script will stop interfering as soon as a new goto-point is received or -- the flight mode is changed by the operator or the remote pilot. -- During the fail safe maneuver a warning tune is played. -- State machine -- CAN_TRIGGER -- - Do: Nothing -- - Change state: If the failsafe GLIDE is triggered: if FS_GCS_ENABL is set -- and FS_LONG_ACTN is 2, change to TRIGGERED else change to CANCELED -- -- TRIGGERED -- - Do: First use GUIDED mode to steer into wind, then switch to FBWA to -- Glide into wind. Play warning tune. -- - Change state: If flight mode is changed by operator/remote pilot or -- operator/remote pilot sends a new goto point, change state to CANCELED -- -- CANCELED -- - Do: Nothing -- - Change state: When new heart beat arrive, change state to CAN_TRIGGER -- Credits -- This script is developed by agising at UASolutions, commissioned by, and in -- cooperation with Remote.aero, with funding from Swedish AeroEDIH, in response -- to a need from the Swedish Sea Rescue Society (Sjöräddningssällskapet, SSRS). -- Disable diagnostics related to reading parameters to pass linter ---@diagnostic disable: need-check-nil ---@diagnostic disable: param-type-mismatch -- Tuning parameters local looptime = 250 -- Short looptime local long_looptime = 2000 -- Long looptime, GLIDE_WIND is not enabled local tune_repeat_t = 1000 -- How often to play tune in glide into wind, [ms] local hdg_ok_lim = 15 -- Acceptable heading error in deg (when to stop steering) local hdg_ok_t_lim = 5000 -- Stop steering towards wind after hdg_ok_t_lim ms with error less than hdg_ok_lim local override_time_lim = 15000 -- Max time in GUIDED during GLIDE, after limit set FBWA independent of hdg -- GCS text levels local _INFO = 6 local _WARNING = 4 -- Plane flight modes mapping local mode_FBWA = 5 local mode_GUIDED = 15 -- Tunes local _tune_glide_warn = "MFT240 L16 cdefgfgfgfg" -- The warning tone played during GLIDE_WIND --State variable local fs_state = nil -- Flags local override_enable = false -- Flag to allow RC channel loverride -- Variables local wind_dir_rad = nil -- param for wind dir in rad local wind_dir_180 = nil -- param for wind dir in deg local hdg_error = nil -- Heading error, hdg vs wind_dir_180 local gw_enable = nil -- glide into wind enable flag local hdg = nil -- vehicle heading local wind = Vector3f() -- wind 3Dvector local link_lost_for = nil -- link loss time counter local last_seen = nil -- timestamp last received heartbeat local tune_time_since = 0 -- Timer for last played tune local hdg_ok_t = 0 -- Timer local expected_flight_mode = nil -- Flight mode set by this script local location_here = nil -- Current location local location_upwind = nil -- Location to hold the target location local user_notified = false -- Flag to keep track user being notified or not local failed_location_counter = 0 -- Counter for failed location requests, possible GPS denied local upwind_distance = 500 -- Distance to the upwind location, minimum 4x turn radius local override_time = 0 -- Time since override started in ms -- Add param table local PARAM_TABLE_KEY = 74 local PARAM_TABLE_PREFIX = "GLIDE_WIND_" assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 30), 'could not add param table') ------- -- Init ------- function _init() -- Add and init paramters GLIDE_WIND_ENABL = bind_add_param('ENABL', 1, 0) -- Init parameters FS_GCS_ENABL = bind_param('FS_GCS_ENABL') -- Is set to 1 if GCS lol should trigger FS after FS_LONG_TIMEOUT FS_LONG_TIMEOUT = bind_param('FS_LONG_TIMEOUT') -- FS long timeout in seconds FS_LONG_ACTN = bind_param('FS_LONG_ACTN') -- Is set to 2 for Glide send_to_gcs(_INFO, 'LUA: FS_LONG_TIMEOUT timeout: ' .. FS_LONG_TIMEOUT:get() .. 's') -- Test paramter if GLIDE_WIND_ENABL:get() == nil then send_to_gcs(_INFO, 'LUA: Something went wrong, GLIDE_WIND_ENABL not created') return _init(), looptime else gw_enable = GLIDE_WIND_ENABL:get() send_to_gcs(_INFO, 'LUA: GLIDE_WIND_ENABL: ' .. gw_enable) end -- Init last_seen. last_seen = gcs:last_seen() -- Init link_lost_for [ms] to FS_LONG_TIMEOUT [s] to prevent link to recover without -- new heartbeat. This is to properly init the state machine. link_lost_for = FS_LONG_TIMEOUT:get() * 1000 -- Warn if GLIDE_WIND_ENABL is set and FS_LONG_ACTN is not GLIDE if gw_enable == 1 and FS_LONG_ACTN:get() ~= 2 then send_to_gcs(_WARNING, 'GLIDE_WIND_ENABL is set, but FS_LONG_ACTN is not GLIDE.') end -- Init fs_state machine to CANCELED. A heartbeat is required to set the state -- to CAN_TRIGGER from where Glide into wind can be triggered. fs_state = 'CANCELED' -- All set, go to update return update(), long_looptime end ------------ -- Main loop ------------ function update() -- Check if state of GLIDE_WIND_ENABL parameter changed, print every change if gw_enable ~= GLIDE_WIND_ENABL:get() then gw_enable = GLIDE_WIND_ENABL:get() send_to_gcs(_INFO, 'LUA: GLIDE_WIND_ENABL: ' .. gw_enable) -- If GLIDE_WIND_ENABL was enabled, warn if not FS_LONG_ACTN is set accordingly if gw_enable == 1 then if FS_LONG_ACTN:get() ~=2 then send_to_gcs(_WARNING, 'GLIDE_WIND_ENABL is set, but FS_LONG_ACTN is not GLIDE.') end end end -- -- If feature is not enabled, loop slowly if gw_enable == 0 then return update, long_looptime end -- GLIDE_WIND_ENABL is enabled, look for triggers -- Monitor time since last gcs heartbeat if last_seen == gcs:last_seen() then link_lost_for = link_lost_for + looptime else -- There has been a new heartbeat, update last_seen and reset link_lost_for last_seen = gcs:last_seen() link_lost_for = 0 end -- Run the state machine -- State CAN_TRIGGER if fs_state == 'CAN_TRIGGER' then if link_lost_for > FS_LONG_TIMEOUT:get() * 1000 then -- Double check that FS_GCS_ENABL is set if FS_GCS_ENABL:get() == 1 and FS_LONG_ACTN:get() == 2 then fs_state = "TRIGGERED" -- Reset some variables hdg_ok_t = 0 user_notified = false override_enable = true override_time = 0 failed_location_counter = 0 -- Set mode to GUIDED before entering TRIGGERED state set_flight_mode(mode_GUIDED, 'LUA: Glide into wind state TRIGGERED') else -- Do not trigger glide into wind, require new heart beats to get here again fs_state = "CANCELED" end end -- State TRIGGERED elseif fs_state == "TRIGGERED" then -- Check for flight mode changes from outside script if vehicle:get_mode() ~= expected_flight_mode then fs_state = "CANCELED" send_to_gcs(_INFO, 'LUA: Glide into wind state CANCELED: flight mode change') end -- In GUIDED, check for target location changes from outside script (operator) if vehicle:get_mode() == mode_GUIDED then if not locations_are_equal(vehicle:get_target_location(), location_upwind) then fs_state = "CANCELED" send_to_gcs(_INFO, 'LUA: Glide into wind state CANCELED: new goto-point') end end -- State CANCELED elseif fs_state == "CANCELED" then -- Await link is not lost if link_lost_for < FS_LONG_TIMEOUT:get() * 1000 then fs_state = "CAN_TRIGGER" send_to_gcs(_INFO, 'LUA: Glide into wind state CAN_TRIGGER') end end -- State TRIGGERED actions if fs_state == "TRIGGERED" then -- Get the heading angle hdg = math.floor(math.deg(ahrs:get_yaw())) -- Get wind direction. Function wind_estimate returns x and y for direction wind blows in, add pi to get true wind dir wind = ahrs:wind_estimate() wind_dir_rad = math.atan(wind:y(), wind:x())+math.pi wind_dir_180 = math.floor(wrap_180(math.deg(wind_dir_rad))) hdg_error = wrap_180(wind_dir_180 - hdg) -- Check if we are close to target heading if math.abs(hdg_error) < hdg_ok_lim then -- If we have been close to target heading for hdg_ok_t_lim, switch back to FBWA if hdg_ok_t > hdg_ok_t_lim then if override_enable then set_flight_mode(mode_FBWA,'LUA: Glide into wind steering complete, GLIDE in FBWA') end -- Do not override again until state machine has triggered again override_enable = false else hdg_ok_t = hdg_ok_t + looptime end -- Heading error is big, reset timer hdg_ok_t else hdg_ok_t = 0 end -- Play tune every tune_repeat_t [ms] if tune_time_since > tune_repeat_t then -- Play tune and reset timer send_to_gcs(_INFO, 'LUA: Play warning tune') play_tune(_tune_glide_warn) tune_time_since = 0 else tune_time_since = tune_time_since + looptime end -- If not steered into wind yet, update goto point into wind if override_enable then -- Check override time, if above limit, switch back to FBWA override_time = override_time + looptime if override_time > override_time_lim then set_flight_mode(mode_FBWA, "LUA: Glide into wind override time out, GLIDE in current heading") override_enable = false end -- Get current position and handle if not valid location_here = ahrs:get_location() if location_here == nil then -- In case we cannot get location for some time we must give up and continue with GLIDE failed_location_counter = failed_location_counter + 1 if failed_location_counter > 5 then set_flight_mode(mode_FBWA, "LUA: Glide failed to get location, GLIDE in current heading") override_enable = false return update, looptime end gcs:send_text(_WARNING, "LUA: Glide failed to get location") return update, looptime end -- Calc upwind position, copy and modify location_here location_upwind = location_here:copy() location_upwind:offset_bearing(wind_dir_180, upwind_distance) -- Set location_upwind as GUIDED target if vehicle:set_target_location(location_upwind) then if not user_notified then send_to_gcs(_INFO, "LUA: Guided target set " .. upwind_distance .. "m away at bearing " .. wind_dir_180) -- Just notify once user_notified = true end else -- Most likely we are not in GUIDED anymore (operator changed mode), state machine will handle this in next loop. gcs:send_text(_WARNING, "LUA: Glide failed to set upwind target") end end end return update, looptime end ------------------- -- Helper functions ------------------- -- Set mode and wait for mode change function set_flight_mode(mode, message) expected_flight_mode = mode vehicle:set_mode(expected_flight_mode) return wait_for_mode_change(mode, message, 0) end -- Wait for mode change function wait_for_mode_change(mode, message, attempt) -- If mode change does not go through after 10 attempts, give up if attempt > 10 then send_to_gcs(_WARNING, 'LUA: Glide into wind mode change failed.') return update, looptime -- If mode change does not go through, wait and try again elseif vehicle:get_mode() ~= mode then return wait_for_mode_change(mode, message, attempt + 1), 5 -- Mode change has gone through else send_to_gcs(_INFO, message) return update, looptime end end -- Function to compare two Location objects function locations_are_equal(loc1, loc2) -- If either location is nil, they are not equal if not loc1 or not loc2 then return false end -- Compare latitude and longitude, return bool return loc1:lat() == loc2:lat() and loc1:lng() == loc2:lng() end -- bind a parameter to a variable function bind_param(name) local p = Parameter() assert(p:init(name), string.format('could not find %s parameter', name)) return p end -- 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', name)) return bind_param(PARAM_TABLE_PREFIX .. name) end -- Print to GCS function send_to_gcs(level, mess) gcs:send_text(level, mess) end -- Play tune function play_tune(tune) notify:play_tune(tune) end -- Returns the angle in range 0-360 function wrap_360(angle) local res = math.fmod(angle, 360.0) if res < 0 then res = res + 360.0 end return res end -- Returns the angle in range -180-180 function wrap_180(angle) local res = wrap_360(angle) if res > 180 then res = res - 360 end return res end -- Start up the script return _init, 2000