ardupilot/libraries/AP_Scripting/applets/Aerobatics/FixedWing/plane_aerobatics.lua

3278 lines
104 KiB
Lua

--[[
trajectory tracking aerobatic control
See README.md for usage
Written by Matthew Hampsey, Andy Palmer and Andrew Tridgell, with controller
assistance from Paul Riseborough, testing by Henry Wurzburg
]]--
-- luacheck: ignore 212 (Unused argument)
---@diagnostic disable: param-type-mismatch
---@diagnostic disable: undefined-field
---@diagnostic disable: missing-parameter
---@diagnostic disable: cast-local-type
---@diagnostic disable: need-check-nil
---@diagnostic disable: undefined-global
---@diagnostic disable: inject-field
-- setup param block for aerobatics, reserving 35 params beginning with AERO_
local PARAM_TABLE_KEY = 70
local PARAM_TABLE_PREFIX = 'AEROM_'
assert(param:add_table(PARAM_TABLE_KEY, "AEROM_", 40), 'could not add param table')
local MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7}
-- 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 Parameter(PARAM_TABLE_PREFIX .. name)
end
--[[
// @Param: AEROM_ANG_ACCEL
// @DisplayName: Angular acceleration limit
// @Description: Maximum angular acceleration in maneuvers
// @Units: deg/s/s
--]]
AEROM_ANG_ACCEL = bind_add_param('ANG_ACCEL', 1, 6000)
--[[
// @Param: AEROM_ANG_TC
// @DisplayName: Roll control filtertime constant
// @Description: This is the time over which we filter the desired roll to smooth it
// @Units: s
--]]
AEROM_ANG_TC = bind_add_param('ANG_TC', 2, 0.1)
-- 3 was AEROM_KE_ANG
--[[
// @Param: AEROM_THR_PIT_FF
// @DisplayName: Throttle feed forward from pitch
// @Description: This controls how much extra throttle to add based on pitch ange. The value is for 90 degrees and is applied in proportion to pitch
// @Units: %
--]]
THR_PIT_FF = bind_add_param('THR_PIT_FF', 4, 80)
--[[
// @Param: AEROM_SPD_P
// @DisplayName: P gain for speed controller
// @Description: This controls how rapidly the throttle is raised to compensate for a speed error
// @Units: %
--]]
SPD_P = bind_add_param('SPD_P', 5, 5)
--[[
// @Param: AEROM_SPD_I
// @DisplayName: I gain for speed controller
// @Description: This controls how rapidly the throttle is raised to compensate for a speed error
// @Units: %
--]]
SPD_I = bind_add_param('SPD_I', 6, 25)
--[[
// @Param: AEROM_ROL_COR_TC
// @DisplayName: Roll control time constant
// @Description: This is the time constant for correcting roll errors. A smaller value leads to faster roll corrections
// @Units: s
--]]
ROLL_CORR_TC = bind_add_param('ROL_COR_TC', 8, 0.25)
-- removed 9 and 10
--[[
// @Param: AEROM_TIME_COR_P
// @DisplayName: Time constant for correction of our distance along the path
// @Description: This is the time constant for correcting path position errors
// @Units: s
--]]
TIME_CORR_P = bind_add_param('TIME_COR_P', 11, 1.0)
--[[
// @Param: AEROM_ERR_COR_P
// @DisplayName: P gain for path error corrections
// @Description: This controls how rapidly we correct back onto the desired path
--]]
ERR_CORR_P = bind_add_param('ERR_COR_P', 12, 2.0)
--[[
// @Param: AEROM_ERR_COR_D
// @DisplayName: D gain for path error corrections
// @Description: This controls how rapidly we correct back onto the desired path
--]]
ERR_CORR_D = bind_add_param('ERR_COR_D', 13, 2.8)
--[[
// @Param: AEROM_ENTRY_RATE
// @DisplayName: The roll rate to use when entering a roll maneuver
// @Description: This controls how rapidly we roll into a new orientation
// @Units: deg/s
--]]
AEROM_ENTRY_RATE = bind_add_param('ENTRY_RATE', 14, 60)
--[[
// @Param: AEROM_THR_LKAHD
// @DisplayName: The lookahead for throttle control
// @Description: This controls how far ahead we look in time along the path for the target throttle
// @Units: s
--]]
AEROM_THR_LKAHD = bind_add_param('THR_LKAHD', 15, 1)
--[[
// @Param: AEROM_DEBUG
// @DisplayName: Debug control
// @Description: This controls the printing of extra debug information on paths
--]]
AEROM_DEBUG = bind_add_param('DEBUG', 16, 0)
--[[
// @Param: AEROM_THR_MIN
// @DisplayName: Minimum Throttle
// @Description: Lowest throttle used during maneuvers
// @Units: %
--]]
AEROM_THR_MIN = bind_add_param('THR_MIN', 17, 0)
--[[
// @Param: AEROM_THR_BOOST
// @DisplayName: Throttle boost
// @Description: This is the extra throttle added in schedule elements marked as needing a throttle boost
// @Units: %
--]]
AEROM_THR_BOOST = bind_add_param('THR_BOOST', 18, 50)
--[[
// @Param: AEROM_YAW_ACCEL
// @DisplayName: Yaw acceleration
// @Description: This is maximum yaw acceleration to use
// @Units: deg/s/s
--]]
AEROM_YAW_ACCEL = bind_add_param('YAW_ACCEL', 19, 1500)
--[[
// @Param: AEROM_LKAHD
// @DisplayName: Lookahead
// @Description: This is how much time to look ahead in the path for calculating path rates
// @Units: s
--]]
AEROM_LKAHD = bind_add_param('LKAHD', 20, 0.5)
--[[
// @Param: AEROM_PATH_SCALE
// @DisplayName: Path Scale
// @Description: Scale factor for Path/Box size. 0.5 would half the distances in maneuvers. Radii are unaffected.
// @Range: 0.1 100
--]]
AEROM_PATH_SCALE = bind_add_param('PATH_SCALE', 21, 1.0)
--[[
// @Param: AEROM_BOX_WIDTH
// @DisplayName: Box Width
// @Description: Length of aerobatic "box"
// @Units: m
--]]
AEROM_BOX_WIDTH = bind_add_param('BOX_WIDTH', 22, 400)
--[[
// @Param: AEROM_STALL_THR
// @DisplayName: Stall turn throttle
// @Description: Amount of throttle to reduce to for a stall turn
// @Units: %
--]]
AEROM_STALL_THR = bind_add_param('STALL_THR', 23, 40)
--[[
// @Param: AEROM_STALL_PIT
// @DisplayName: Stall turn pitch threshold
// @Description: Pitch threashold for moving to final stage of stall turn
// @Units: deg
--]]
AEROM_STALL_PIT = bind_add_param('STALL_PIT', 24, -20)
-- 25 was AEROM_KE_TC
--[[
// @Param: AEROM_KE_RUDD
// @DisplayName: KnifeEdge Rudder
// @Description: Percent of rudder normally uses to sustain knife-edge at trick speed
// @Units: %
--]]
AEROM_KE_RUDD = bind_add_param('KE_RUDD', 26, 25)
--[[
// @Param: AEROM_KE_RUDD_LK
// @DisplayName: KnifeEdge Rudder lookahead
// @Description: Time to look ahead in the path to calculate rudder correction for bank angle
// @Units: s
--]]
AEROM_KE_RUDD_LK = bind_add_param('KE_RUDD_LK', 27, 0.25)
--[[
// @Param: AEROM_ALT_ABORT
// @DisplayName: Altitude Abort
// @Description: Maximum allowable loss in altitude during a trick or sequence from its starting altitude.
// @Units: m
--]]
AEROM_ALT_ABORT = bind_add_param('ALT_ABORT',28,15)
--[[
// @Param: AEROM_TS_P
// @DisplayName: Timesync P gain
// @Description: This controls how rapidly two aircraft are brought back into time sync
--]]
AEROM_TS_P = bind_add_param('TS_P', 29, 0.33)
--[[
// @Param: AEROM_TS_I
// @DisplayName: Timesync I gain
// @Description: This controls how rapidly two aircraft are brought back into time sync
--]]
AEROM_TS_I = bind_add_param('TS_I', 30, 0.33)
--[[
// @Param: AEROM_TS_SPDMAX
// @DisplayName: Timesync speed max
// @Description: This sets the maximum speed adjustment for time sync between aircraft
// @Units: m/s
--]]
AEROM_TS_SPDMAX = bind_add_param('TS_SPDMAX', 31, 0.0)
--[[
// @Param: AEROM_TS_RATE
// @DisplayName: Timesync rate of send of NAMED_VALUE_FLOAT data
// @Description: This sets the rate we send data for time sync between aircraft
// @Units: Hz
--]]
AEROM_TS_RATE = bind_add_param('TS_RATE', 32, 4.0)
--[[
// @Param: AEROM_MIS_ANGLE
// @DisplayName: Mission angle
// @Description: When set to a non-zero value, this is the assumed direction of the mission. Otherwise the waypoint angle is used
// @Units: deg
--]]
AEROM_MIS_ANGLE = bind_add_param('MIS_ANGLE', 33, 0.0)
--[[
// @Param: AEROM_OPTIONS
// @DisplayName: Aerobatic options
// @Description: Options to control aerobatic behavior
// @Bitmask: 0:UseRTLOnAbort, 1:AddAtToMessages, 2:DualAircraftSynchronised
// @Units: deg
--]]
AEROM_OPTIONS = bind_add_param('OPTIONS', 34, 0.0)
local OPTIONS = { ABORT_RTL=(1<<0), MSG_ADD_AT=(1<<1), DUAL_AIRCRAFT=(1<<2) }
--[[
return true if an option is set
--]]
function option_set(option)
local options = math.floor(AEROM_OPTIONS:get())
return options & option ~= 0
end
-- cope with old param values
if AEROM_ANG_ACCEL:get() < 100 and AEROM_ANG_ACCEL:get() > 0 then
AEROM_ANG_ACCEL:set_and_save(3000)
end
if AEROM_ANG_TC:get() > 1.0 then
AEROM_ANG_TC:set_and_save(0.2)
end
ACRO_ROLL_RATE = Parameter("ACRO_ROLL_RATE")
ACRO_YAW_RATE = Parameter('ACRO_YAW_RATE')
AIRSPEED_MIN = Parameter("AIRSPEED_MIN")
SCALING_SPEED = Parameter("SCALING_SPEED")
SYSID_THISMAV = Parameter("SYSID_THISMAV")
GRAVITY_MSS = 9.80665
--[[
list of attributes that can be added to a path element
--]]
local path_attribs = { "roll_ref", "set_orient", "rate_override", "thr_boost", "pos_corr", "message", "shift_xy", "timestamp", "pos_gain_mul" }
--[[
Aerobatic tricks on a switch support - allows for tricks to be initiated outside AUTO mode
--]]
-- 2nd param table for tricks on a switch
local PARAM_TABLE_KEY2 = 71
local PARAM_TABLE_PREFIX2 = "TRIK"
assert(param:add_table(PARAM_TABLE_KEY2, PARAM_TABLE_PREFIX2, 63), 'could not add param table2')
-- add a parameter and bind it to a variable in table2
function bind_add_param2(name, idx, default_value)
assert(param:add_param(PARAM_TABLE_KEY2, idx, name, default_value), string.format('could not add param %s', name))
return Parameter(PARAM_TABLE_PREFIX2 .. name)
end
--[[
// @Param: TRIK_ENABLE
// @DisplayName: Tricks on Switch Enable
// @Description: Enables Tricks on Switch. TRIK params hidden until enabled
--]]
local TRIK_ENABLE = bind_add_param2("_ENABLE", 1, 0)
local TRICKS = nil
local TRIK_SEL_FN = nil
local TRIK_ACT_FN = nil
local TRIK_COUNT = nil
local function TrickDef(id, arg1, arg2, arg3, arg4)
local self = {}
self.id = id
self.args = {arg1, arg2, arg3, arg4}
return self
end
-- constrain a value between limits
local function constrain(v, vmin, vmax)
if v < vmin then
v = vmin
end
if v > vmax then
v = vmax
end
return v
end
local function sq(x)
return x*x
end
local last_trick_action_state = nil
if TRIK_ENABLE:get() > 0 then
--[[
// @Param: TRIK_SEL_FN
// @DisplayName: Trik Selection Scripting Function
// @Description: Setting an RC channel's _OPTION to this value will use it for trick selection
// @Range: 301 307
--]]
TRIK_SEL_FN = bind_add_param2("_SEL_FN", 2, 301)
--[[
// @Param: TRIK_ACT_FN
// @DisplayName: Trik Action Scripting Function
// @Description: Setting an RC channel's _OPTION to this value will use it for trick action (abort,announce,execute)
// @Range: 301 307
--]]
TRIK_ACT_FN = bind_add_param2("_ACT_FN", 3, 300)
--[[
// @Param: TRIK_COUNT
// @DisplayName: Trik Count
// @Description: Number of tricks which can be selected over the range of the trik selection RC channel
// @Range: 1 11
--]]
TRIK_COUNT = bind_add_param2("_COUNT", 4, 3)
TRICKS = {}
last_trick_action_state = rc:get_aux_cached(TRIK_ACT_FN:get())
-- setup parameters for tricks
local count = math.floor(constrain(TRIK_COUNT:get(),1,11))
for i = 1, count do
local k = 5*i
local prefix = string.format("%u", i)
TRICKS[i] = TrickDef(bind_add_param2(prefix .. "_ID", k+0, -1),
bind_add_param2(prefix .. "_ARG1", k+1, 30),
bind_add_param2(prefix .. "_ARG2", k+2, 0),
bind_add_param2(prefix .. "_ARG3", k+3, 0),
bind_add_param2(prefix .. "_ARG4", k+4, 0))
end
gcs:send_text(MAV_SEVERITY.ALERT, string.format("Enabled %u aerobatic tricks", TRIK_COUNT:get()))
end
local NAV_SCRIPT_TIME = 42702
local MODE_AUTO = 10
local MODE_RTL = 11
local LOOP_RATE = 40
local DO_JUMP = 177
local k_throttle = 70
local NAME_FLOAT_RATE = 2
local AIRSPEED_CRUISE = Parameter("AIRSPEED_CRUISE")
local last_id = 0
local current_task = nil
local last_named_float_t = 0
local last_named_float_send_t = 0
local path_var = {}
local function wrap_360(angle)
local res = math.fmod(angle, 360.0)
if res < 0 then
res = res + 360.0
end
return res
end
local function wrap_180(angle)
local res = wrap_360(angle)
if res > 180 then
res = res - 360
end
return res
end
local function wrap_pi(angle)
local angle_deg = math.deg(angle)
local angle_wrapped = wrap_180(angle_deg)
return math.rad(angle_wrapped)
end
local function wrap_2pi(angle)
local angle_deg = math.deg(angle)
local angle_wrapped = wrap_360(angle_deg)
return math.rad(angle_wrapped)
end
--[[
calculate an alpha for a first order low pass filter
--]]
local function calc_lowpass_alpha(dt, time_constant)
local rc = time_constant/(math.pi*2)
return dt/(dt+rc)
end
--[[ get the c.y element of a quaternion, which gives
the projection of the vehicle y axis in the down direction
This is equal to sin(roll)*cos(pitch)
--]]
local function get_quat_dcm_c_y(q)
local q1 = q:q1()
local q2 = q:q2()
local q3 = q:q3()
local q4 = q:q4()
local q3q4 = q3 * q4
local q1q2 = q1 * q2
return 2*(q3q4 + q1q2)
end
--[[ get the c.y element of the DCM body to earth matrix, which gives
the projection of the vehicle y axis in the down direction
This is equal to sin(roll)*cos(pitch)
--]]
local function get_ahrs_dcm_c_y()
return get_quat_dcm_c_y(ahrs:get_quaternion())
end
-- a PI controller implemented as a Lua object
local function PI_controller(kP,kI,iMax,min,max)
-- the new instance. You can put public variables inside this self
-- declaration if you want to
local self = {}
-- private fields as locals
local _kP = kP or 0.0
local _kI = kI or 0.0
local _iMax = iMax
local _min = min
local _max = max
local _last_t = nil
local _I = 0
local _P = 0
local _total = 0
local _counter = 0
local _target = 0
local _current = 0
-- update the controller.
function self.update(target, current)
local now = millis():tofloat() * 0.001
if not _last_t then
_last_t = now
end
local dt = now - _last_t
_last_t = now
local err = target - current
_counter = _counter + 1
local P = _kP * err
if ((_total < _max and _total > _min) or (_total >= _max and err < 0) or (_total <= _min and err > 0)) then
_I = _I + _kI * err * dt
end
if _iMax then
_I = constrain(_I, -_iMax, iMax)
end
local I = _I
local ret = P + I
_target = target
_current = current
_P = P
ret = constrain(ret, _min, _max)
_total = ret
return ret
end
-- reset integrator to an initial value
function self.reset(integrator)
_I = integrator
end
function self.set_I(I)
_kI = I
end
function self.set_P(P)
_kP = P
end
function self.set_Imax(Imax)
_iMax = Imax
end
-- log the controller internals
function self.log(name, add_total)
-- allow for an external addition to total
logger.write(name,'Targ,Curr,P,I,Total,Add','ffffff',_target,_current,_P,_I,_total,add_total)
end
-- return the instance
return self
end
local function speed_controller(kP_param,kI_param, kFF_pitch_param, Imax, min, max)
local self = {}
local kFF_pitch = kFF_pitch_param
local PI = PI_controller(kP_param:get(), kI_param:get(), Imax, min, max)
function self.update(target, anticipated_pitch_rad)
local current_speed = ahrs:get_velocity_NED():length()
local throttle = PI.update(target, current_speed)
local FF = math.sin(anticipated_pitch_rad)*kFF_pitch:get()
PI.log("AESP", FF)
return throttle + FF
end
function self.reset()
PI.reset(0)
local temp_throttle = self.update(ahrs:get_velocity_NED():length(), 0)
local current_throttle = SRV_Channels:get_output_scaled(k_throttle)
PI.reset(current_throttle-temp_throttle)
end
return self
end
local speed_PI = speed_controller(SPD_P, SPD_I, THR_PIT_FF, 100.0, 0.0, 100.0)
local function speed_adjust_controller(kP_param, kI_param)
local self = {}
local spd_max = AEROM_TS_SPDMAX:get()
local PI = PI_controller(kP_param:get(), kI_param:get(), spd_max, -spd_max, spd_max)
function self.update(spd_error)
local adjustment = PI.update(0, spd_error)
PI.log("AESA", 0)
return adjustment
end
function self.reset()
PI.reset(0)
end
return self
end
local speed_adjustment_PI = speed_adjust_controller(AEROM_TS_P, AEROM_TS_I)
function sgn(x)
local eps = 0.000001
if (x > eps) then
return 1.0
elseif x < eps then
return -1.0
else
return 0.0
end
end
local function get_wp_location(i)
local m = mission:get_item(i)
local loc = Location()
loc:lat(m:x())
loc:lng(m:y())
loc:relative_alt(true)
loc:terrain_alt(false)
loc:origin_alt(false)
loc:alt(math.floor(m:z()*100))
return loc
end
local function resolve_jump(i)
local m = mission:get_item(i)
while m:command() == DO_JUMP do
i = math.floor(m:param1())
m = mission:get_item(i)
end
return i
end
--[[
Wrapper to construct a Vector3f{x, y, z} from (x, y, z)
--]]
local function makeVector3f(x, y, z)
local vec = Vector3f()
vec:x(x)
vec:y(y)
vec:z(z)
return vec
end
--[[
get quaternion rotation between vector1 and vector2
with thanks to https://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another
--]]
local function vectors_to_quat_rotation(vector1, vector2)
local v1 = vector1:copy()
local v2 = vector2:copy()
v1:normalize()
v2:normalize()
local dot = v1:dot(v2)
local a = v1:cross(v2)
local w = 1.0 + dot
local q = Quaternion()
q:q1(w)
q:q2(a:x())
q:q3(a:y())
q:q4(a:z())
q:normalize()
return q
end
--[[
get path rate from two tangents and delta time
--]]
local function tangents_to_rate(t1, t2, dt)
local q_delta = vectors_to_quat_rotation(t1, t2)
local rate_rads = Vector3f()
q_delta:to_axis_angle(rate_rads)
rate_rads = rate_rads:scale(1.0/dt)
return rate_rads
end
--[[
create a class that inherits from a base class
--]]
local function inheritsFrom(baseClass, _name)
local new_class = { name = _name }
local class_mt = { __index = new_class }
function new_class:create()
local newinst = {}
setmetatable( newinst, class_mt )
return newinst
end
if nil ~= baseClass then
setmetatable( new_class, { __index = baseClass } )
end
return new_class
end
--[[
return a quaternion for a roll, pitch, yaw (321 euler sequence) attitude
--]]
function qorient(roll_deg, pitch_deg, yaw_deg)
local q = Quaternion()
q:from_euler(math.rad(roll_deg), math.rad(pitch_deg), math.rad(yaw_deg))
return q
end
--[[
rotate a vector by a quaternion
--]]
local function quat_earth_to_body(quat, v)
local v2 = v:copy()
quat:earth_to_body(v2)
return v2
end
--[[
rotate a vector by a inverse quaternion
--]]
local function quat_body_to_earth(quat, v)
local v2 = v:copy()
quat:inverse():earth_to_body(v2)
return v2
end
--[[
copy a quaternion
--]]
local function quat_copy(q)
return q:inverse():inverse()
end
--[[
trajectory building blocks. We have two types of building blocks,
roll blocks and path blocks. These are combined to give composite paths
--]]
--[[
roll component that goes through a fixed total angle at a fixed roll rate
--]]
local _roll_angle = inheritsFrom(nil, 'roll_angle')
function _roll_angle:get_roll(t)
if self.angle == nil then
return 0
end
return self.angle * t
end
function roll_angle(angle)
local self = _roll_angle:create()
if angle ~= 0 then
self.angle = angle
end
return self
end
--[[
roll component that banks to _angle over AEROM_ENTRY_RATE
degrees/s, then holds that angle, then banks back to zero at
AEROM_ENTRY_RATE degrees/s
--]]
local _roll_angle_entry_exit = inheritsFrom(nil, "roll_angle_entry_exit")
function _roll_angle_entry_exit:get_roll(t, time_s)
local entry_s = math.abs(self.angle) / AEROM_ENTRY_RATE:get()
local entry_t = entry_s / time_s
if entry_t > 0.5 then
entry_t = 0.5
end
if t <= 0 then
return 0
end
if t < entry_t then
return self.angle * t / entry_t
end
if t < 1.0 - entry_t then
return self.angle
end
if self.angle == 0 or t >= 1.0 then
return 0
end
return (1.0 - ((t - (1.0 - entry_t)) / entry_t)) * self.angle
end
function roll_angle_entry_exit(angle)
local self = _roll_angle_entry_exit:create()
self.angle = angle
return self
end
--[[
roll component that banks to _angle over AEROM_ENTRY_RATE
degrees/s, then holds that angle
--]]
local _roll_angle_entry = inheritsFrom(nil, "roll_angle_entry")
function _roll_angle_entry:get_roll(t, time_s)
local entry_s = math.abs(self.angle) / AEROM_ENTRY_RATE:get()
local entry_t = entry_s / time_s
if entry_t > 0.5 then
entry_t = 0.5
end
if t < entry_t then
return self.angle * t / entry_t
end
return self.angle
end
function roll_angle_entry(angle)
local self = _roll_angle_entry:create()
self.angle = angle
return self
end
--[[
roll component that holds angle until the end of the subpath, then
rolls back to 0 at the AEROM_ENTRY_RATE
--]]
local _roll_angle_exit = inheritsFrom(nil, "roll_angle_exit")
function _roll_angle_exit:get_roll(t, time_s)
local entry_s = math.abs(self.angle) / AEROM_ENTRY_RATE:get()
local entry_t = entry_s / time_s
if t < 1.0 - entry_t then
return 0
end
if self.angle == 0 then
return 0
end
return ((t - (1.0 - entry_t)) / entry_t) * self.angle
end
function roll_angle_exit(angle)
local self = _roll_angle_exit:create()
self.angle = angle
return self
end
--[[
implement a sequence of rolls, specified as a list of {proportion, roll_angle} pairs
--]]
local _roll_sequence = inheritsFrom(nil, "roll_sequence")
function _roll_sequence:get_roll(t)
for i = 1, #self.seq do
if t <= self.end_t[i] then
local t2 = (t - self.start_t[i])/(self.seq[i][1]/self.total)
return self.start_ang[i] + t2 * self.seq[i][2]
end
end
-- we've gone past the end
return self.start_ang[#self.seq] + self.seq[#self.seq][2]
end
function roll_sequence(seq)
local self = _roll_sequence:create()
self.seq = seq
self.total = 0.0
self.end_t = {}
self.start_t = {}
self.start_ang = {}
for i = 1, #seq do
self.total = self.total + seq[i][1]
end
local t = 0.0
local angle = 0.0
for i = 1, #seq do
self.start_t[i] = t
self.start_ang[i] = angle
angle = angle + seq[i][2]
t = t + seq[i][1]/self.total
self.end_t[i] = t
end
return self
end
--[[ given a path function get_pos() calculate the extents of the path
along the X axis as a tuple
--]]
local function get_extents_x(obj)
local p = obj:get_pos(0)
local min_x = p:x()
local max_x = min_x
for t=0, 1, 0.02 do
p = obj:get_pos(t)
min_x = math.min(min_x, p:x())
max_x = math.max(max_x, p:x())
end
return { min_x, max_x }
end
--[[
all path components inherit from PathComponent
--]]
local _PathComponent = inheritsFrom(nil)
function _PathComponent:get_pos(t)
return makeVector3f(0, 0, 0)
end
function _PathComponent:get_length()
return 0
end
function _PathComponent:get_final_orientation()
return Quaternion()
end
function _PathComponent:get_roll_correction(t)
return 0
end
function _PathComponent:get_attribute(t, attrib)
return self[attrib]
end
function _PathComponent:get_extents_x()
if self.extents ~= nil then
return self.extents
end
self.extents = get_extents_x(self)
return self.extents
end
--[[
path component that does a straight horizontal line
--]]
local _path_straight = inheritsFrom(_PathComponent, "path_straight")
function _path_straight:get_pos(t)
return makeVector3f(self.distance*t, 0, 0)
end
function _path_straight:get_length()
return self.distance
end
function path_straight(distance)
local self = _path_straight:create()
self.distance = distance
return self
end
--[[
path component that does a straight line then reverses direction
--]]
local _path_reverse = inheritsFrom(_PathComponent, "path_reverse")
function _path_reverse:get_pos(t)
if t < 0.5 then
return makeVector3f(self.distance*t*2, 0, 0)
else
return makeVector3f(self.distance-(self.distance*(t-0.5)*2), 0, 0)
end
end
function _path_reverse:get_length()
return self.distance*2
end
function path_reverse(distance)
local self = _path_reverse:create()
self.distance = distance
return self
end
--[[
path component that aligns to within the aerobatic box
--]]
local _path_align_box = inheritsFrom(_PathComponent, "path_align_box")
function _path_align_box:get_pos(t)
return makeVector3f(self.distance*t, 0, 0)
end
function _path_align_box:get_length()
return self.distance
end
function _path_align_box:set_next_extents(extents, start_pos, start_orientation)
local box_half = AEROM_BOX_WIDTH:get()/2
local start_x = start_pos:x()
local next_max_x = extents[2]
if math.abs(math.deg(start_orientation:get_euler_yaw())) > 90 then
-- we are on a reverse path
self.distance = (box_half * self.alignment) + start_x - next_max_x
else
-- we are on a forward path
self.distance = (box_half * self.alignment) - start_x - next_max_x
end
self.distance = math.max(self.distance, 0.01)
end
function path_align_box(alignment)
local self = _path_align_box:create()
self.distance = nil
self.alignment = alignment
return self
end
--[[
path component that aligns so the center of the next maneuver is
centered within the aerobatic box
--]]
local _path_align_center = inheritsFrom(_PathComponent, "path_align_center")
function _path_align_center:get_pos(t)
return makeVector3f(self.distance*t, 0, 0)
end
function _path_align_center:get_length()
return self.distance
end
function _path_align_center:set_next_extents(extents, start_pos, start_orientation)
local start_x = start_pos:x()
local next_mid_x = (extents[1]+extents[2])*0.5
if math.abs(math.deg(start_orientation:get_euler_yaw())) > 90 then
-- we are on a reverse path
self.distance = start_x - next_mid_x
else
-- we are on a forward path
self.distance = - start_x - next_mid_x
end
self.distance = math.max(self.distance, 0.01)
end
function path_align_center()
local self = _path_align_center:create()
self.distance = nil
return self
end
--[[
path component that does a vertical arc over a given angle
--]]
local _path_vertical_arc = inheritsFrom(_PathComponent, "path_vertical_arc")
function _path_vertical_arc:get_pos(t)
local t2ang = wrap_2pi(t * math.rad(self.angle))
return makeVector3f(math.abs(self.radius)*math.sin(t2ang), 0, -self.radius*(1.0 - math.cos(t2ang)))
end
function _path_vertical_arc:get_length()
return math.abs(self.radius) * 2 * math.pi * math.abs(self.angle) / 360.0
end
function _path_vertical_arc:get_final_orientation()
local q = Quaternion()
q:from_axis_angle(makeVector3f(0,1,0), sgn(self.radius)*math.rad(wrap_180(self.angle)))
q:normalize()
return q
end
function path_vertical_arc(radius, angle)
local self = _path_vertical_arc:create()
self.radius = radius
self.angle = angle
return self
end
--[[
path component that does a horizontal arc over a given angle
--]]
local _path_horizontal_arc = inheritsFrom(_PathComponent, "path_horizontal_arc")
function _path_horizontal_arc:get_pos(t)
local t2ang = t * math.rad(self.angle)
return makeVector3f(math.abs(self.radius)*math.sin(t2ang), self.radius*(1.0 - math.cos(t2ang)), -self.height_gain*t)
end
function _path_horizontal_arc:get_length()
local circumference = 2 * math.pi * math.abs(self.radius)
local full_circle_height_gain = self.height_gain * 360.0 / math.abs(self.angle)
local helix_length = math.sqrt(full_circle_height_gain*full_circle_height_gain + circumference*circumference)
return helix_length * math.abs(self.angle) / 360.0
end
function _path_horizontal_arc:get_final_orientation()
local q = Quaternion()
q:from_axis_angle(makeVector3f(0,0,1), sgn(self.radius)*math.rad(self.angle))
return q
end
--[[
roll correction for the rotation caused by height gain
--]]
function _path_horizontal_arc:get_roll_correction(t)
if self.height_gain == 0 then
return 0
end
local gamma=math.atan(self.height_gain*(360/self.angle)/(2*math.pi*self.radius))
return -t*self.angle*math.sin(gamma)
end
function path_horizontal_arc(radius, angle, height_gain)
local self = _path_horizontal_arc:create()
self.radius = radius
self.angle = angle
self.height_gain = height_gain or 0
return self
end
--[[
path component that does a cylinder for a barrel roll
--]]
local _path_cylinder = inheritsFrom(_PathComponent, "path_cylinder")
function _path_cylinder:get_pos(t)
local t2ang = t * self.num_spirals * math.pi * 2
local v = makeVector3f(self.length*t, math.abs(self.radius)*math.sin(t2ang+math.pi), -math.abs(self.radius)*(1.0 - math.cos(t2ang)))
local qrot = Quaternion()
qrot:from_axis_angle(makeVector3f(0,0,1), (0.5*math.pi)-self.gamma)
v = quat_earth_to_body(qrot, v)
if self.radius < 0 then
-- mirror for reverse radius
v:y(-v:y())
end
return v
end
function _path_cylinder:get_length()
local circumference = 2 * math.pi * math.abs(self.radius)
local length_per_spiral = self.length / self.num_spirals
local helix_length = math.sqrt(length_per_spiral*length_per_spiral + circumference*circumference)
return helix_length * self.num_spirals
end
--[[
roll correction for the rotation caused by the path
--]]
function _path_cylinder:get_roll_correction(t)
return sgn(self.radius)*t*360*math.sin(self.gamma)*self.num_spirals
end
function path_cylinder(radius, length, num_spirals)
local self = _path_cylinder:create()
self.radius = radius
self.length = length
self.num_spirals = num_spirals
self.gamma = math.atan((length/num_spirals)/(2*math.pi*math.abs(radius)))
return self
end
--[[
a Path has the methods of both RollComponent and
PathComponent allowing for a complete description of a subpath
--]]
local _Path = inheritsFrom(nil)
function _Path:get_roll(t, time_s)
return wrap_180(self.roll_component:get_roll(t, time_s))
end
function _Path:get_roll_correction(t)
return self.path_component:get_roll_correction(t)
end
function _Path:get_pos(t)
return self.path_component:get_pos(t)
end
function _Path:get_length()
return self.path_component:get_length()
end
function _Path:get_final_orientation()
return self.path_component:get_final_orientation()
end
function _Path:get_attribute(t, attrib)
return self[attrib]
end
function _Path:set_next_extents(extents, start_pos, start_orientation)
self.path_component:set_next_extents(extents, start_pos, start_orientation)
end
local function Path(path_component, roll_component)
local self = _Path:create()
self.name = string.format("%s|%s", path_component.name, roll_component.name)
assert(path_component)
assert(roll_component)
self.path_component = path_component
self.roll_component = roll_component
return self
end
--[[
componse multiple sub-paths together to create a full trajectory
--]]
local _path_composer = inheritsFrom(nil)
-- return the subpath with index i. Used to cope with two ways of calling path_composer
function _path_composer:subpath(i)
if i == self.cache_i then
return self.cache_sp
end
self.cache_i = i
local sp = self.subpaths[i]
if sp.name then
-- we are being called with a list of Path objects
self.cache_sp = sp
else
-- we are being called with a list function/argument tuples
local args = self.subpaths[i][2]
self.cache_sp = self.subpaths[i][1](args[1], args[2], args[3], args[4], self.start_pos[i], self.start_orientation[i])
-- copy over path attributes
for _, v in pairs(path_attribs) do
self.cache_sp[v] = self.subpaths[i][v]
end
end
return self.cache_sp
end
function _path_composer:end_time(i)
local proportion = self.lengths[i] / self.total_length
return self.start_time[i] + proportion
end
function _path_composer:get_subpath_t(t)
if self.last_subpath_t[1] == t then
-- use cached value
return self.last_subpath_t[2], self.last_subpath_t[3]
end
local i = 1
while t >= self:end_time(i) and i < self.num_sub_paths do
i = i + 1
end
local proportion = self.lengths[i]/self.total_length
local subpath_t = (t - self.start_time[i]) / proportion
self.last_subpath_t = { t, subpath_t, i }
local sp = self:subpath(i)
if i > self.highest_i and t < 1.0 and t > 0 then
self.highest_i = i
if sp.message ~= nil then
local msg = sp.message
if option_set(OPTIONS.MSG_ADD_AT) then
msg = "@" .. msg
end
gcs:send_text(MAV_SEVERITY.ALERT, msg)
end
if AEROM_DEBUG:get() > 0 then
gcs:send_text(MAV_SEVERITY.ALERT, string.format("starting %s[%d] %s", self.name, i, sp.name))
end
end
return subpath_t, i
end
-- return position at time t
function _path_composer:get_pos(t)
local subpath_t, i = self:get_subpath_t(t)
local sp = self:subpath(i)
return quat_earth_to_body(self.start_orientation[i], sp:get_pos(subpath_t)) + self.start_pos[i]
end
-- return angle for the composed path at time t
function _path_composer:get_roll(t, time_s)
local subpath_t, i = self:get_subpath_t(t)
local speed = target_groundspeed()
local sp = self:subpath(i)
local angle = sp:get_roll(subpath_t, self.lengths[i]/speed)
return angle + self.start_angle[i]
end
function _path_composer:get_roll_correction(t)
local subpath_t, i = self:get_subpath_t(t)
local sp = self:subpath(i)
return sp:get_roll_correction(subpath_t) + (self.start_roll_correction[i] or 0)
end
function _path_composer:get_length()
return self.total_length
end
function _path_composer:get_final_orientation()
return self.final_orientation
end
function _path_composer:get_attribute(t, attrib)
local subpath_t, i = self:get_subpath_t(t)
local sp = self:subpath(i)
return sp[attrib] or sp:get_attribute(subpath_t, attrib)
end
function _path_composer:get_extents_x()
if self.extents ~= nil then
return self.extents
end
self.extents = get_extents_x(self)
return self.extents
end
function _path_composer:calculate_timestamps()
self.timestamp_start = {}
self.timestamp_start[1] = 0.0
self.have_timestamps = false
for i = 1, self.num_sub_paths do
local sp = self:subpath(i)
local timestamp = sp.timestamp
if timestamp then
self.timestamp_start[i] = timestamp
self.have_timestamps = true
end
end
if self.have_timestamps then
local tstart = 0.0
for i = 2, self.num_sub_paths do
if not self.timestamp_start[i] then
-- find the next element with a timestamp, getting total length
local length_sum = self:subpath(i-1):get_length()
for j = i, self.num_sub_paths do
if self.timestamp_start[j] then
--gcs:send_text(MAV_SEVERITY.ALERT, string.format("found %u %u %.3f ts=%.3f", i, j, length_sum, tstart))
for k = i, j-1 do
local len = self:subpath(k):get_length()
self.timestamp_start[k] = tstart + len / length_sum
--gcs:send_text(MAV_SEVERITY.ALERT, string.format("ts[%u] %.3f %.2f/%.2f", k, self.timestamp_start[k], len, length_sum))
end
break
end
length_sum = length_sum + self:subpath(j):get_length()
end
else
tstart = self.timestamp_start[i]
end
end
self.timestamp_start[self.num_sub_paths+1] = self.timestamp_start[self.num_sub_paths]+1.0
end
self.patht_start = {}
self.patht_start[1] = 0.0
self.patht_start[self.num_sub_paths+1] = 1.0
local total_length = self:get_length()
for i = 2, self.num_sub_paths do
self.patht_start[i] = self.patht_start[i-1] + self:subpath(i-1):get_length() / total_length
end
if self.have_timestamps then
gcs:send_text(MAV_SEVERITY.INFO,"Calculated timestamps")
end
end
function _path_composer:patht_to_timestamp(path_t)
path_t = constrain(path_t, 0.0, 1.0)
if not self.have_timestamps then
return path_t
end
for i = 1, self.num_sub_paths do
if self.patht_start[i+1] >= path_t then
local dt = path_t - self.patht_start[i]
local p = dt / (self.patht_start[i+1] - self.patht_start[i])
return self.timestamp_start[i] + p * (self.timestamp_start[i+1] - self.timestamp_start[i])
end
end
return self.timestamp_start[self.num_sub_paths+1]
end
function _path_composer:timestamp_to_patht(tstamp)
if not self.have_timestamps then
return tstamp
end
tstamp = constrain(tstamp, 0.0, self.timestamp_start[self.num_sub_paths+1])
for i = 1, self.num_sub_paths do
if self.timestamp_start[i+1] >= tstamp then
local dt = tstamp - self.timestamp_start[i]
local p = dt / (self.timestamp_start[i+1] - self.timestamp_start[i])
return self.patht_start[i] + p * (self.patht_start[i+1] - self.patht_start[i])
end
end
return 1.0
end
--[[
get the time that the next segment starts
--]]
function _path_composer:get_next_segment_start(t)
local subpath_t, i = self:get_subpath_t(t)
local sp = self:subpath(i)
if sp.get_next_segment_start ~= nil then
return self.start_time[i] + (sp:get_next_segment_start(subpath_t) * (self:end_time(i) - self.start_time[i]))
end
return self:end_time(i)
end
local function path_composer(name, subpaths)
local self = _path_composer:create()
self.name = name
self.subpaths = subpaths
self.lengths = {}
self.start_time = {}
self.start_orientation = {}
self.start_pos = {}
self.start_angle = {}
self.start_roll_correction = {}
self.total_length = 0
self.num_sub_paths = #subpaths
self.last_subpath_t = { -1, 0, 0 }
self.highest_i = 0
local orientation = Quaternion()
local pos = makeVector3f(0,0,0)
local angle = 0
local roll_correction = 0
local speed = target_groundspeed()
for i = 1, self.num_sub_paths do
-- accumulate orientation, position and angle
self.start_orientation[i] = quat_copy(orientation)
self.start_pos[i] = pos:copy()
self.start_angle[i] = angle
if roll_correction ~= 0 then
self.start_roll_correction[i] = roll_correction
end
local sp = self:subpath(i)
self.lengths[i] = sp:get_length()
if self.lengths[i] == nil and i < self.num_sub_paths then
local sp2 = self:subpath(i+1)
local next_extents = sp2:get_extents_x()
if next_extents ~= nil then
sp:set_next_extents(next_extents, self.start_pos[i], self.start_orientation[i])
self.lengths[i] = sp:get_length()
-- solidify this subpath now that it has its length calculated
self.subpaths[i] = sp
end
end
self.total_length = self.total_length + self.lengths[i]
local spos = quat_earth_to_body(orientation, sp:get_pos(1.0))
pos = pos + spos
orientation = sp:get_final_orientation() * orientation
orientation:normalize()
angle = angle + sp:get_roll(1.0, self.lengths[i]/speed)
roll_correction = roll_correction + sp:get_roll_correction(1.0)
if sp.set_orient ~= nil then
-- override orientation at this point in the sequence
orientation = sp.set_orient
end
if sp.roll_ref ~= nil then
local q = Quaternion()
q:from_axis_angle(makeVector3f(1,0,0), math.rad(sp.roll_ref))
orientation = orientation * q
orientation:normalize()
end
end
-- get our final orientation, including roll
self.final_orientation = quat_copy(orientation)
local q = Quaternion()
q:from_axis_angle(makeVector3f(1,0,0), math.rad(wrap_180(angle)))
self.final_orientation = q * self.final_orientation
-- work out the proportion of the total time we will spend in each sub path
local total_time = 0
for i = 1, self.num_sub_paths do
self.start_time[i] = total_time
local proportion = self.lengths[i]/self.total_length
total_time = total_time + proportion
end
return self
end
--[[
make a list of Path() objects from a list of PathComponent, RollComponent pairs
--]]
function make_paths(name, paths)
local p = {}
for i = 1, #paths do
if paths[i][2] == nil then
p[i] = paths[i][1]
else
p[i] = Path(paths[i][1], paths[i][2])
end
-- copy over path attributes
for _, v in pairs(path_attribs) do
if paths[i][v] ~= nil then
p[i][v] = paths[i][v]
end
end
end
return path_composer(name, p)
end
--[[
composed trajectories, does as individual aerobatic maneuvers
--]]
function climbing_circle(radius, height, bank_angle, arg4)
return make_paths("climbing_circle", {
{ path_horizontal_arc(radius, 360, height), roll_angle_entry_exit(bank_angle) },
})
end
function half_climbing_circle(radius, height, bank_angle, arg4)
return make_paths("half_climbing_circle", {
{ path_horizontal_arc(radius, 180, height), roll_angle_entry_exit(bank_angle) },
})
end
function partial_circle(radius, bank_angle, angle)
return make_paths("partial_circle", {
{ path_horizontal_arc(radius, angle, 0), roll_angle_entry_exit(bank_angle) },
})
end
function loop(radius, bank_angle, num_loops, arg4)
if not num_loops or num_loops <= 0 then
num_loops = 1
end
return make_paths("loop", {
{ path_vertical_arc(radius, 360*num_loops), roll_angle_entry_exit(bank_angle) },
})
end
function straight_roll(length, num_rolls, arg3, arg4)
return make_paths("straight_roll", {
{ path_straight(length), roll_angle(num_rolls*360) },
})
end
--[[
fly straight until we are distance meters from the composite path
origin in the maneuver frame along the X axis. If we are already
past that position then return immediately
--]]
function straight_align(distance, arg2, arg3, arg4, start_pos, start_orientation)
local d2 = distance - start_pos:x()
local v = quat_earth_to_body(start_orientation, makeVector3f(d2, 0, 0))
local len = math.max(v:x(),0.01)
return make_paths("straight_align", {
{ path_straight(len), roll_angle(0) },
})
end
--[[
fly straight so that the next maneuver in the sequence ends at
the given proportion of the aerobatic box
--]]
function align_box(alignment, arg2, arg3, arg4)
return Path(path_align_box(alignment), roll_angle(0))
end
--[[
fly straight so that the next maneuver in the sequence is centered
in the aerobatic box
--]]
function align_center(arg1, arg2, arg3, arg4)
return Path(path_align_center(), roll_angle(0))
end
function immelmann_turn(r, arg2, arg3, arg4)
local rabs = math.abs(r)
return make_paths("immelmann_turn", {
{ path_vertical_arc(r, 180), roll_angle(0) },
{ path_straight(rabs), roll_angle(180) },
})
end
-- immelmann with max roll rate
function immelmann_turn_fast(r, arg2, arg3, arg4)
local roll_time = 180.0 / ACRO_ROLL_RATE:get()
local roll_dist = target_groundspeed() * roll_time
return make_paths("immelmann_turn_fast", {
{ path_vertical_arc(r, 180), roll_angle(0) },
{ path_straight(roll_dist), roll_angle(180) },
})
end
function humpty_bump(r, h, arg3, arg4)
assert(h >= 2*r)
local rabs = math.abs(r)
return make_paths("humpty_bump", {
{ path_vertical_arc(r, 90), roll_angle(0) },
{ path_straight((h-2*rabs)/3), roll_angle(0) },
{ path_straight((h-2*rabs)/3), roll_angle(180) },
{ path_straight((h-2*rabs)/3), roll_angle(0) },
{ path_vertical_arc(-r, 180), roll_angle(0) },
{ path_straight(h-2*rabs), roll_angle(0) },
{ path_vertical_arc(-r, 90), roll_angle(0) },
{ path_straight(2*rabs), roll_angle(0) },
})
end
function laydown_humpty(r, h, arg3, arg4)
assert(h >= 2*r)
local rabs = math.abs(r)
return make_paths("laydown_humpty", {
{ path_vertical_arc(r, 45), roll_angle(0) },
{ path_straight((h-2*rabs)/3), roll_angle(0) },
{ path_straight((h-2*rabs)/3), roll_angle(-90), roll_ref=90 },
{ path_straight((h-2*rabs)/3), roll_angle(0) },
{ path_vertical_arc(r, 180), roll_angle(0) },
{ path_straight((h-2*rabs)/3), roll_angle(0) },
{ path_straight((h-2*rabs)/3), roll_angle(90), roll_ref=-90 },
{ path_straight((h-2*rabs)/3), roll_angle(0) },
{ path_vertical_arc(-r, 45), roll_angle(0), roll_ref=180},
})
end
function split_s(r, arg2, arg3, arg4)
local rabs = math.abs(r)
return make_paths("split_s", {
{ path_straight(rabs/2), roll_angle(180) },
{ path_vertical_arc(-r, 180), roll_angle(0) },
})
end
function upline_45(r, height_gain, arg3, arg4)
--local h = (height_gain - 2*r*(1.0-math.cos(math.rad(45))))/math.sin(math.rad(45))
local h = (height_gain - (2 * r) + (2 * r * math.cos(math.rad(45)))) / math.cos(math.rad(45))
assert(h >= 0)
return make_paths("upline_45", {
{ path_vertical_arc(r, 45), roll_angle(0) },
{ path_straight(h), roll_angle(0) },
{ path_vertical_arc(-r, 45), roll_angle(0) },
})
end
function upline_20(r, height_gain, arg3, arg4)
local h = (height_gain - 2*r*(1.0-math.cos(math.rad(20))))/math.sin(math.rad(20))
assert(h >= 0)
return make_paths("upline_45", {
{ path_vertical_arc(r, 20), roll_angle(0) },
{ path_straight(h), roll_angle(0) },
{ path_vertical_arc(-r, 20), roll_angle(0) },
})
end
function downline_45(r, height_loss, arg3, arg4)
local h = (height_loss - 2*r*(1.0-math.cos(math.rad(45))))/math.sin(math.rad(45))
assert(h >= 0)
return make_paths("downline_45", {
{ path_vertical_arc(-r, 45), roll_angle(0) },
{ path_straight(h), roll_angle(0) },
{ path_vertical_arc(r, 45), roll_angle(0) },
})
end
function rolling_circle(radius, num_rolls, arg3, arg4)
return make_paths("rolling_circle", {
{ path_horizontal_arc(radius, 360), roll_angle(360*num_rolls), thr_boost=true },
})
end
function barrel_roll(radius, length, num_spirals, arg4)
local gamma_deg = math.deg(math.atan((length/num_spirals)/(2*math.pi*math.abs(radius))))
local speed = target_groundspeed()
local bank = math.deg(math.atan((speed*speed) / (radius * GRAVITY_MSS)))
local radius2 = radius/(1.0 - math.cos(math.rad(90-gamma_deg)))
return make_paths("barrel_roll", {
{ path_horizontal_arc(-radius2, 90-gamma_deg, 0), roll_angle_entry_exit(-bank) },
{ path_cylinder(radius, length, num_spirals), roll_angle(0) },
{ path_horizontal_arc(radius2, 90-gamma_deg, 0), roll_angle_entry_exit(bank) },
})
end
function side_step(displacement, length, arg3, arg4)
local speed = target_groundspeed()
local radius = (displacement*displacement + length*length)/(4*displacement)
local angle = math.deg(2*math.atan(displacement, length))
local sign = sgn(displacement)
local bank = math.deg(math.atan((speed*speed) / (radius * GRAVITY_MSS)))
return make_paths("side_step",{
{path_horizontal_arc(sign*radius, angle, 0), roll_angle_entry_exit(sign*bank)},
{path_horizontal_arc(-sign*radius, angle, 0) , roll_angle_entry_exit(-sign*bank)},
})
end
function straight_flight(length, bank_angle, arg3, arg4)
return make_paths("straight_flight", {
{ path_straight(length), roll_angle_entry_exit(bank_angle) },
})
end
function straight_hold(length, bank_angle, arg3, arg4)
return make_paths("straight_hold", {
{ path_straight(length), roll_angle_entry(bank_angle) },
})
end
function scale_figure_eight(r, bank_angle, arg3, arg4)
local rabs = math.abs(r)
return make_paths("scale_figure_eight", {
{ path_straight(rabs), roll_angle(0) },
{ path_horizontal_arc(r, 90), roll_angle_entry_exit(bank_angle) },
{ path_horizontal_arc(-r, 360), roll_angle_entry_exit(-bank_angle) },
{ path_horizontal_arc(r, 270), roll_angle_entry_exit(bank_angle) },
{ path_straight(3*rabs), roll_angle(0) },
})
end
function figure_eight(r, bank_angle, arg3, arg4)
local rabs = math.abs(r)
return make_paths("figure_eight", {
{ path_straight(rabs*math.sqrt(2)), roll_angle(0) },
{ path_horizontal_arc(r, 225), roll_angle_entry_exit(bank_angle) },
{ path_straight(2*rabs), roll_angle(0) },
{ path_horizontal_arc(-r, 270), roll_angle_entry_exit(-bank_angle) },
{ path_straight(2*rabs), roll_angle(0) },
{ path_horizontal_arc(r, 45), roll_angle_entry_exit(bank_angle) },
})
end
--[[
perform a rudder over maneuver
--]]
function rudder_over(_direction, _min_speed)
local self = {}
local direction = _direction
local min_speed = _min_speed
local reached_speed = false
local kick_started = false
local pitch2_done = false
local descent_done = false
local target_q = nil
local initial_q = nil
local last_t = nil
local initial_z = nil
local desired_direction = nil
--[[
the update() method is called during the rudder over, it
should return true when the maneuver is completed
--]]
function self.update(path, t, target_speed)
if descent_done then
-- we're all done
return true
end
local ahrs_quat = ahrs:get_quaternion()
local ahrs_pos = ahrs:get_relative_position_NED_origin()
local ahrs_gyro = ahrs:get_gyro()
local now = millis():tofloat() * 0.001
if target_q == nil then
-- initialising
initial_z = ahrs_pos:z()
target_q = quat_copy(ahrs_quat)
initial_q = quat_copy(target_q)
last_t = now
end
local dt = now - last_t
last_t = now
local error_quat = ahrs_quat:inverse() * target_q
local rate_rads = Vector3f()
error_quat:to_axis_angle(rate_rads)
local tc = ROLL_CORR_TC:get()
local rate_dps = rate_rads:scale(math.deg(1)/tc)
-- use user set throttle for achieving the stall
local throttle = AEROM_STALL_THR:get()
local pitch_deg = math.deg(ahrs:get_pitch())
if reached_speed and not kick_started and math.abs(math.deg(ahrs_gyro:z())) > ACRO_YAW_RATE:get()/3 then
kick_started = true
end
if kick_started then
-- when we have established some yaw rate cut the throttle to minimum
throttle = AEROM_THR_MIN:get()
end
vehicle:set_target_throttle_rate_rpy(throttle, rate_dps:x(), rate_dps:y(), rate_dps:z())
log_pose('POSM', ahrs_pos, ahrs:get_quaternion())
log_pose('POST', ahrs_pos, target_q)
local current_speed_up = -ahrs:get_velocity_NED():z()
if not reached_speed and current_speed_up <= min_speed then
reached_speed = true
end
if not reached_speed then
return false
end
-- integrate desired attitude through yaw
local q_rate_rads = makeVector3f(0,0,ahrs_gyro:z())
if pitch2_done then
-- stop adding yaw
q_rate_rads:z(0)
end
local rotation = Quaternion()
rotation:from_angular_velocity(q_rate_rads, dt)
target_q = target_q * rotation
target_q:normalize()
--[[
override rudder to maximum, basing PWM on the MIN/MAX of the channel
according to the desired direction
--]]
if desired_direction == nil then
desired_direction = direction
if desired_direction == 0 then
local c_y = get_ahrs_dcm_c_y()
if c_y > 0 then
desired_direction = 1
else
desired_direction = -1
end
end
end
if not pitch2_done then
vehicle:set_rudder_offset(desired_direction * 100, false)
else
vehicle:set_rudder_offset(0, true)
end
if not kick_started then
return false
end
-- see if we are nose down
if kick_started and pitch_deg < AEROM_STALL_PIT:get() and not pitch2_done then
-- lock onto a descent path
pitch2_done = true
target_q = initial_q * qorient(0, 0, 180)
--[[ correct the attitude to the opposite correction that we
had at the start of the slowdown, so we fight the wind on
the way down
--]]
local error_q = initial_q:inverse() * qorient(0, 90, math.deg(initial_q:get_euler_yaw()))
local error_pitch = error_q:get_euler_pitch()
local error_yaw = error_q:get_euler_yaw()
target_q = target_q * qorient(0, math.deg(-2*error_pitch), math.deg(2*error_yaw))
target_q:normalize()
return false
end
if not pitch2_done or ahrs_pos:z() < initial_z then
-- haven't finished the descent
return false
end
-- all done, update state
descent_done = true
path_var.tangent = path_var.tangent:scale(-1)
path_var.path_t = path:get_next_segment_start(t)
path_var.accumulated_orientation_rel_ef = path_var.accumulated_orientation_rel_ef * qorient(0,0,180)
path_var.last_time = now
path_var.last_ang_rate_dps = ahrs_gyro:scale(math.deg(1))
path_var.pos = rotate_path(path, path_var.path_t, path_var.initial_ori, path_var.initial_ef_pos)
-- ensure that the path will move fwd on the next step
path_var.pos:z(path_var.pos:z()-10)
return false
end
return self
end
--[[
stall turn is not really correct, as we don't fully stall. Needs to be
reworked
--]]
function stall_turn(radius, height, direction, min_speed)
local h = height - radius
assert(h >= 0)
return make_paths("stall_turn", {
{ path_vertical_arc(radius, 90), roll_angle(0) },
{ path_straight(h), roll_angle(0) },
{ path_reverse(h/4), roll_angle(0), rate_override=rudder_over(direction,min_speed), set_orient=qorient(0,-90,0) },
{ path_straight(h), roll_angle(0), pos_corr=0.5, shift_xy=true },
{ path_vertical_arc(-radius, 90), roll_angle(0), set_orient=qorient(0,0,180) },
})
end
--[[
takeoff controller
--]]
function takeoff_controller(_distance, _thr_slew)
local self = {}
local start_time = 0
local start_pos = nil
local thr_slew = _thr_slew
local distance = _distance
local all_done = false
local initial_yaw_deg = math.deg(ahrs:get_yaw())
local yaw_correction_tconst = 1.0
gcs:send_text(MAV_SEVERITY.INFO,string.format("Takeoff init"))
--[[
the update() method is called during the rudder over, it
should return true when the maneuver is completed
--]]
function self.update(path, t, target_speed)
if all_done then
return true
end
local now = millis():tofloat() * 0.001
local ahrs_pos = ahrs:get_relative_position_NED_origin()
if start_time == 0 then
gcs:send_text(MAV_SEVERITY.INFO,string.format("Takeoff start"))
start_time = now
start_pos = ahrs_pos
end
local throttle = constrain(thr_slew * (now - start_time), 0, 100)
local yaw_deg = math.deg(ahrs:get_yaw())
local yaw_err_deg = wrap_180(yaw_deg - initial_yaw_deg)
local targ_yaw_rate = -yaw_err_deg / yaw_correction_tconst
vehicle:set_target_throttle_rate_rpy(throttle, 0, 0, targ_yaw_rate)
vehicle:set_rudder_offset(0, true)
local dist_moved = (ahrs_pos - start_pos):length()
if dist_moved > distance then
gcs:send_text(MAV_SEVERITY.INFO,string.format("Takeoff complete dist=%.1f", dist_moved))
path_var.path_t = path:get_next_segment_start(t)
path_var.last_time = now
path_var.last_ang_rate_dps = ahrs:get_gyro():scale(math.deg(1))
path_var.pos = rotate_path(path, path_var.path_t, path_var.initial_ori, path_var.initial_ef_pos)
all_done = true
end
return false
end
return self
end
--[[
stall turn is not really correct, as we don't fully stall. Needs to be
reworked
--]]
function takeoff(dist, height, thr_slew)
local angle_deg = 20
local dist_per_arc = 3*dist/8
local radius = dist_per_arc / math.sin(math.rad(angle_deg))
local h1 = dist_per_arc * (1.0 - math.cos(math.rad(angle_deg)))
local line_h = constrain(height - 2*h1, 0, height)
local line_len = (line_h - 2*radius*(1.0-math.cos(math.rad(angle_deg))))/math.sin(math.rad(angle_deg))
return make_paths("takeoff", {
{ path_straight(dist/4), roll_angle(0), rate_override=takeoff_controller(dist/4, thr_slew) },
{ path_vertical_arc(radius, 20), roll_angle(0), pos_gain_mul=0.3 },
{ path_straight(line_len), roll_angle(0), pos_gain_mul=0.5 },
{ path_vertical_arc(-radius, 20), roll_angle(0), pos_gain_mul=0.5 },
})
end
function half_cuban_eight(r, arg2, arg3, arg4)
local rabs = math.abs(r)
return make_paths("half_cuban_eight", {
{ path_straight(2*rabs*math.sqrt(2)), roll_angle(0) },
{ path_vertical_arc(r, 225), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(180) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_vertical_arc(-r, 45), roll_angle(0) },
})
end
function cuban_eight(r, arg2, arg3, arg4)
local rabs = math.abs(r)
return make_paths("cuban_eight", {
{ path_straight(rabs*math.sqrt(2)), roll_angle(0) },
{ path_vertical_arc(r, 225), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(180) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_vertical_arc(-r, 270), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(180) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_vertical_arc(r, 45), roll_angle(0) },
})
end
function half_reverse_cuban_eight(r, arg2, arg3, arg4)
local rabs = math.abs(r)
return make_paths("half_reverse_cuban_eight", {
{ path_vertical_arc(r, 45), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_straight(2*rabs/3), roll_angle(180) },
{ path_straight(2*rabs/3), roll_angle(0) },
{ path_vertical_arc(-r, 225), roll_angle(0) },
})
end
function horizontal_rectangle(total_length, total_width, r, bank_angle)
local l = total_length - 2*r
local w = total_width - 2*r
return make_paths("horizontal_rectangle", {
{ path_straight(0.5*l), roll_angle(0) },
{ path_horizontal_arc(r, 90), roll_angle_entry_exit(bank_angle)},
{ path_straight(w), roll_angle(0) },
{ path_horizontal_arc(r, 90), roll_angle_entry_exit(bank_angle) },
{ path_straight(l), roll_angle(0) },
{ path_horizontal_arc(r, 90), roll_angle_entry_exit(bank_angle) },
{ path_straight(w), roll_angle(0) },
{ path_horizontal_arc(r, 90), roll_angle_entry_exit(bank_angle) },
{ path_straight(0.5*l), roll_angle(0) },
})
end
function vertical_aerobatic_box(total_length, total_width, r, bank_angle)
local l = total_length - 2*r
local w = total_width - 2*r
return make_paths("vertical_aerobatic_box", {
{ path_straight(0.5*l), roll_angle_entry(bank_angle) },
{ path_vertical_arc(r, 90), roll_angle(0) },
{ path_straight(w), roll_angle(0) },
{ path_vertical_arc(r, 90), roll_angle(0) },
{ path_straight(l), roll_angle(0) },
{ path_vertical_arc(r, 90), roll_angle(0) },
{ path_straight(w), roll_angle(0) },
{ path_vertical_arc(r, 90), roll_angle(0) },
{ path_straight(0.5*l), roll_angle_exit(-bank_angle) },
})
end
--[[
a multi-point roll
- length = total length of straight flight
- N = number of points of roll for full 360
- hold_frac = proportion of each segment to hold attitude, will use 0.2 if 0
- num_points = number of points of the N point roll to do, will use N if 0
Note that num_points can be greater than N, for example do 6 points
of a 4 point roll, resulting in inverted flight
--]]
function multi_point_roll(length, N, hold_frac, num_points)
if hold_frac <= 0 then
hold_frac = 0.2
end
if num_points <= 0 then
num_points = N
end
--[[
construct a roll sequence to use over the full length
--]]
local seq = {}
local roll_frac = 1.0 - hold_frac
for i = 1, num_points do
seq[#seq+1] = { roll_frac, 360 / N }
if i < num_points then
seq[#seq+1] = { hold_frac, 0 }
end
end
return make_paths("multi_point_roll", {{ path_straight(length), roll_sequence(seq) }})
end
function eight_point_roll(length, arg2, arg3, arg4)
return multi_point_roll(length, 8, 0.5)
end
function procedure_turn(radius, bank_angle, step_out, arg4)
local rabs = math.abs(radius)
return make_paths("procedure_turn", {
{ path_horizontal_arc(radius, 90), roll_angle_entry_exit(bank_angle) },
{ path_straight(step_out), roll_angle(0) },
{ path_horizontal_arc(-radius, 270), roll_angle_entry_exit(-bank_angle) },
{ path_straight(3*rabs), roll_angle(0) },
})
end
---------------------------------------------------
--[[
target speed is taken as max of target airspeed and current 3D
velocity at the start of the maneuver
--]]
function target_groundspeed()
return math.max(ahrs:get_EAS2TAS()*AIRSPEED_CRUISE:get(), ahrs:get_velocity_NED():length())
end
--[[
get ground course from AHRS
--]]
function get_ground_course_deg()
local vned = ahrs:get_velocity_NED()
return wrap_180(math.deg(math.atan(vned:y(), vned:x())))
end
--args:
-- path_f: path function returning position
-- t: normalised [0, 1] time
-- arg1, arg2: arguments for path function
-- orientation: maneuver frame orientation
--returns: requested position, angle and speed in maneuver frame
function rotate_path(path_f, t0, orientation, offset)
local t = constrain(t0, 0, 1)
local point = path_f:get_pos(t)
local angle = path_f:get_roll(t)
local roll_correction = path_f:get_roll_correction(t)
local attrib = {}
for _, v in pairs(path_attribs) do
attrib[v] = path_f:get_attribute(t, v)
end
point = point + path_var.path_shift
point = quat_earth_to_body(orientation, point)
local scale = AEROM_PATH_SCALE:get()
point = point:scale(math.abs(scale))
if scale < 0 then
-- we need to mirror the path
point:y(-point:y())
roll_correction = -roll_correction
angle = -angle
-- compensate path orientation for the mirroring
local orient = orientation:inverse()
point = quat_body_to_earth((orient * orient), point)
end
return point+offset, math.rad(angle+roll_correction), attrib
end
--Given vec1, vec2, returns an (rotation axis, angle) tuple that rotates vec1 to be parallel to vec2
--If vec1 and vec2 are already parallel, returns a zero vector and zero angle
--Note that the rotation will not be unique.
function vectors_to_rotation(vector1, vector2)
local axis = vector1:cross(vector2)
if axis:length() < 0.00001 then
local vec = Vector3f()
vec:x(1)
return vec, 0
end
axis:normalize()
local angle = vector1:angle(vector2)
return axis, angle
end
--returns Quaternion
function vectors_to_rotation_w_roll(vector1, vector2, roll)
local axis, angle = vectors_to_rotation(vector1, vector2)
local vector_rotation = Quaternion()
vector_rotation:from_axis_angle(axis, angle)
local roll_rotation = Quaternion()
roll_rotation:from_euler(roll, 0, 0)
local total_rot = vector_rotation*roll_rotation
return to_axis_and_angle(total_rot)
end
--Given vec1, vec2, returns an angular velocity tuple that rotates vec1 to be parallel to vec2
--If vec1 and vec2 are already parallel, returns a zero vector and zero angle
function vectors_to_angular_rate(vector1, vector2, time_constant)
local axis, angle = vectors_to_rotation(vector1, vector2)
local angular_velocity = angle/time_constant
return axis:scale(angular_velocity)
end
function vectors_to_angular_rate_w_roll(vector1, vector2, time_constant, roll)
local axis, angle = vectors_to_rotation_w_roll(vector1, vector2, roll)
local angular_velocity = angle/time_constant
return axis:scale(angular_velocity)
end
-- convert a quaternion to axis angle form
function to_axis_and_angle(quat)
local axis_angle = Vector3f()
quat:to_axis_angle(axis_angle)
local angle = axis_angle:length()
if angle < 0.00001 then
return makeVector3f(1.0, 0.0, 0.0), 0.0
end
return axis_angle:scale(1.0/angle), angle
end
--projects x onto the othogonal subspace of span(unit_v)
function ortho_proj(x, unit_v)
local temp_x = unit_v:cross(x)
return unit_v:cross(temp_x)
end
-- log a pose from position and quaternion attitude
function log_pose(logname, pos, quat)
local loc = ahrs:get_origin():copy()
loc:offset(pos:x(),pos:y())
logger.write(logname, 'px,py,pz,q1,q2,q3,q4,r,p,y,Lat,Lon', 'ffffffffffLL',
pos:x(),
pos:y(),
pos:z(),
quat:q1(),
quat:q2(),
quat:q3(),
quat:q4(),
math.deg(quat:get_euler_roll()),
math.deg(quat:get_euler_pitch()),
math.deg(quat:get_euler_yaw()),
loc:lat(),
loc:lng())
end
--[[
get GPS week and MS, coping with crossing a week boundary
--]]
function get_gps_times()
local gps_last_fix_ms1 = gps:last_fix_time_ms(0)
local gps_week = gps:time_week(0)
local gps_week_ms = gps:time_week_ms(0)
local gps_last_fix_ms2 = gps:last_fix_time_ms(0)
local now_ms = millis()
if gps_last_fix_ms2 ~= gps_last_fix_ms1 then
-- we got a new fix while requesting the values. fetch again,
-- and assume we won't get another fix during these calls
gps_week = gps:time_week(0)
gps_week_ms = gps:time_week_ms(0)
end
gps_week_ms = gps_week_ms + (now_ms - gps_last_fix_ms2)
return gps_week, gps_week_ms
end
function log_position(logname, loc, quat)
local gps_week, gps_week_ms = get_gps_times()
logger.write(logname, 'I,GWk,GMS,Lat,Lon,Alt,R,P,Y',
'BHILLffff',
'#--DU----',
'---GG----',
SYSID_THISMAV:get(),
gps_week,
gps_week_ms,
loc:lat(),
loc:lng(),
loc:alt()*0.01,
math.deg(quat:get_euler_roll()),
math.deg(quat:get_euler_pitch()),
math.deg(quat:get_euler_yaw()))
end
--[[
check if a number is Nan.
--]]
function isNaN(value)
-- NaN is lua is not equal to itself
return value ~= value
end
function Vec3IsNaN(v)
return isNaN(v:x()) or isNaN(v:y()) or isNaN(v:z())
end
function qIsNaN(q)
return isNaN(q:q1()) or isNaN(q:q2()) or isNaN(q:q3()) or isNaN(q:q4())
end
--[[
return the body y projection down, this is the c.y element of the equivalent rotation matrix
--]]
function quat_projection_ground_plane(q)
local q1q2 = q:q1() * q:q2()
local q3q4 = q:q3() * q:q4()
return 2.0 * (q3q4 + q1q2)
end
--[[
calculate rudder offset
--]]
function calculate_rudder_offset(ahrs_quat, ahrs_gyro, airspeed_constrained)
--[[
look ahead for what our y projection will be at AEROM_KE_RUDD_LK
seconds forward in time
--]]
local qchange = Quaternion()
qchange:from_angular_velocity(ahrs_gyro, -AEROM_KE_RUDD_LK:get())
local qnew = qchange * ahrs_quat
local airspeed_scaling = SCALING_SPEED:get()/airspeed_constrained
local y_projection = get_quat_dcm_c_y(qnew:inverse())
local rudder_ofs = -y_projection * AEROM_KE_RUDD:get() * sq(airspeed_scaling)
rudder_ofs = constrain(rudder_ofs, -100, 100)
return rudder_ofs
end
--[[
convert a table of bytes to a lua string
--]]
function bytes_to_string(bytes)
local ret = {}
for _, b in ipairs(bytes) do
if b == 0 then
break
end
table.insert(ret, string.char(b))
end
return table.concat(ret)
end
--[[
a lua implementation of the jitter correction algorithm from libraries/AP_RTC
note that the use of a 32 bit float lua number for a uint32_t
milliseconds means we lose accuracy over time. At 9 hours we have
an accuracy of about 1 millisecond
--]]
local function JitterCorrection(_max_lag_ms, _convergence_loops)
local self = {}
local max_lag_ms = _max_lag_ms
local convergence_loops = _convergence_loops
local link_offset_ms = 0
local min_sample_ms = 0
local initialised = false
local min_sample_counter = 0
function self.correct_offboard_timestamp_msec(offboard_ms, local_ms)
local diff_ms = local_ms - offboard_ms
if not initialised or diff_ms < link_offset_ms then
--[[
this message arrived from the remote system with a
timestamp that would imply the message was from the
future. We know that isn't possible, so we adjust down the
correction value
--]]
link_offset_ms = diff_ms
initialised = true
end
local estimate_ms = offboard_ms + link_offset_ms
if estimate_ms + max_lag_ms < local_ms then
--[[
this implies the message came from too far in the past. clamp the lag estimate
to assume the message had maximum lag
--]]
estimate_ms = local_ms - max_lag_ms
link_offset_ms = estimate_ms - offboard_ms
end
if min_sample_counter == 0 then
min_sample_ms = diff_ms
end
min_sample_counter = (min_sample_counter+1)
if diff_ms < min_sample_ms then
min_sample_ms = diff_ms
end
if min_sample_counter == convergence_loops then
--[[
we have the requested number of samples of the transport
lag for convergence. To account for long term clock drift
we set the diff we will use in future to this value
--]]
link_offset_ms = min_sample_ms
min_sample_counter = 0
end
return estimate_ms
end
return self
end
--[[
import mavlink support for NAMED_VALUE_FLOAT, only used for
DUAL_AIRCRAFT operation
--]]
local function mavlink_receiver()
local self = {}
local mavlink_msgs = require("mavlink_msgs")
local NAMED_VALUE_FLOAT_msgid = mavlink_msgs.get_msgid("NAMED_VALUE_FLOAT")
local msg_map = {}
local jitter_correction = JitterCorrection(5000, 100)
msg_map[NAMED_VALUE_FLOAT_msgid] = "NAMED_VALUE_FLOAT"
-- initialise mavlink rx with number of messages, and buffer depth
mavlink.init(1, 10)
-- register message id to receive
mavlink.register_rx_msgid(NAMED_VALUE_FLOAT_msgid)
--[[
get a NAMED_VALUE_FLOAT incoming message, handling jitter correction
--]]
function self.get_named_value_float()
local msg,_,timestamp_ms = mavlink.receive_chan()
if msg then
local parsed_msg = mavlink_msgs.decode(msg, msg_map)
if (parsed_msg ~= nil) and (parsed_msg.msgid == NAMED_VALUE_FLOAT_msgid) then
-- convert remote timestamp to local timestamp with jitter correction
local time_boot_ms = jitter_correction.correct_offboard_timestamp_msec(parsed_msg.time_boot_ms, timestamp_ms:toint())
local value = parsed_msg.value
local name = bytes_to_string(parsed_msg.name)
return time_boot_ms, name, value, parsed_msg.sysid
end
end
return nil
end
return self
end
local mavlink_handler = nil
if option_set(OPTIONS.DUAL_AIRCRAFT) then
mavlink_handler = mavlink_receiver()
end
--[[
handle NAMED_VALUE_FLOAT from another vehicle to sync our schedules
--]]
function handle_speed_adjustment()
local local_t = millis():tofloat() * 0.001
local named_float_rate = AEROM_TS_RATE:get()
local loc_timestamp = current_task.fn:patht_to_timestamp(path_var.path_t)
if named_float_rate > 0 and loc_timestamp > 1 and local_t - last_named_float_send_t > 1.0/named_float_rate then
last_named_float_send_t = local_t
gcs:send_named_float("PATHT", loc_timestamp)
end
local time_boot_ms, name, remote_timestamp, sysid = mavlink_handler.get_named_value_float()
if not time_boot_ms then
return
end
-- gcs:send_text(MAV_SEVERITY.INFO, string.format("NVF: name='%s' value=%f sysid=%d tbm=%f", name, remote_timestamp, sysid, time_boot_ms))
if name == "PATHT" and sysid ~= SYSID_THISMAV:get() then
local remote_t = time_boot_ms * 0.001
local dt = local_t - remote_t
local rem_patht = current_task.fn:timestamp_to_patht(remote_timestamp)
local adjusted_rem_path_t = rem_patht + dt / path_var.total_time
local dist_err = (path_var.path_t - adjusted_rem_path_t) * path_var.total_time * path_var.target_speed
if loc_timestamp > 1 and remote_timestamp > 1 then
path_var.speed_adjustment = speed_adjustment_PI.update(dist_err)
else
path_var.speed_adjustment = 0.0
end
local gps_week, gps_week_ms = get_gps_times()
logger.write("PTHT",
'SysID,GWk,GMS,RemT,LocT,TS,RTS,PT,RPT,Dt,ARPT,DE,SA',
'BHIffffffffff',
'#------------',
'-------------',
sysid,
gps_week, gps_week_ms,
remote_t, local_t,
loc_timestamp,
remote_timestamp,
path_var.path_t, rem_patht,
dt, adjusted_rem_path_t, dist_err, path_var.speed_adjustment)
end
end
path_var.count = 0
function do_path()
local now = millis():tofloat() * 0.001
local ahrs_pos_NED = ahrs:get_relative_position_NED_origin()
local ahrs_pos = ahrs:get_position()
local ahrs_gyro = ahrs:get_gyro()
local ahrs_velned = ahrs:get_velocity_NED()
local ahrs_airspeed = ahrs:airspeed_estimate()
--[[
ahrs_quat is the quaterion which when used with quat_earth_to_body() rotates a vector
from earth to body frame. It needs to be the inverse of ahrs:get_quaternion()
--]]
local ahrs_quat = ahrs:get_quaternion():inverse()
path_var.count = path_var.count + 1
local path = current_task.fn
if not current_task.started then
local initial_yaw_deg = current_task.initial_yaw_deg
current_task.started = true
local speed = target_groundspeed()
path_var.target_speed = speed
path_var.speed_adjustment = 0.0
path_var.length = path:get_length() * math.abs(AEROM_PATH_SCALE:get())
path_var.total_rate_rads_ef = makeVector3f(0.0, 0.0, 0.0)
--assuming constant velocity
path_var.total_time = path_var.length/speed
--deliberately only want yaw component, because the maneuver should be performed relative to the earth, not relative to the initial orientation
path_var.initial_ori = Quaternion()
path_var.initial_ori:from_euler(0, 0, math.rad(initial_yaw_deg))
path_var.initial_ori = path_var.initial_ori
path_var.initial_ori:normalize()
path_var.initial_ef_pos = ahrs_pos_NED:copy()
path_var.start_pos = ahrs_pos:copy()
path_var.path_int = path_var.start_pos:copy()
speed_PI.reset()
path_var.accumulated_orientation_rel_ef = path_var.initial_ori
path_var.time_correction = 0.0
path_var.filtered_angular_velocity = Vector3f()
path_var.last_time = now - 1.0/LOOP_RATE
path_var.last_ang_rate_dps = ahrs_gyro:scale(math.deg(1))
path_var.path_t = 0.0
path_var.pos = path_var.initial_ef_pos:copy()
path_var.roll = 0.0
path_var.last_shift_xy = nil
path_var.path_shift = Vector3f()
path_var.ss_angle = 0.0
path_var.ss_angle_filt = 0.0
path_var.last_rate_override = 0
path.highest_i = 0
-- get initial tangent
local p1, _ = rotate_path(path, path_var.path_t + 0.1/(path_var.total_time*LOOP_RATE),
path_var.initial_ori, path_var.initial_ef_pos)
path_var.tangent = p1 - path_var.pos
return true
end
local vel_length = ahrs_velned:length()
local actual_dt = now - path_var.last_time
if actual_dt < 0.25 / LOOP_RATE then
-- the update has been executed too soon
return true
end
path_var.last_time = now
local local_n_dt = (1.0/LOOP_RATE)/path_var.total_time
if path_var.path_t + local_n_dt > 1.0 then
-- all done
return false
end
if option_set(OPTIONS.DUAL_AIRCRAFT) then
handle_speed_adjustment()
end
-- airspeed, assume we don't go below min
local airspeed_constrained = math.max(AIRSPEED_MIN:get(), ahrs_airspeed)
--[[
calculate positions and angles at previous, current and next time steps
--]]
local p0 = path_var.pos:copy()
local r0 = path_var.roll
local p1, _, attrib = rotate_path(path, path_var.path_t + local_n_dt,
path_var.initial_ori, path_var.initial_ef_pos)
local current_measured_pos_ef = ahrs_pos_NED:copy()
if attrib.rate_override ~= nil then
if not attrib.rate_override.update(path, path_var.path_t + local_n_dt, path_var.target_speed) then
-- not done yet
path_var.pos = current_measured_pos_ef
path_var.last_rate_override = now
return true
end
end
--[[
see if this path element has a shift_xy attribute
--]]
local shift_xy = attrib.shift_xy
if shift_xy and not path_var.last_shift_xy then
--[[
we have entered a new sub-element with a shift_xy
--]]
local curpos_mf = quat_body_to_earth(path_var.initial_ori, current_measured_pos_ef)
local pathpos_mf = quat_body_to_earth(path_var.initial_ori, p1)
local shift = curpos_mf - pathpos_mf
shift:z(0)
path_var.path_shift = path_var.path_shift + shift
local shift_ef = quat_earth_to_body(path_var.initial_ori, shift)
p1 = p1 + shift_ef
p0:y(p1:y())
p0:x(p1:x())
end
path_var.last_shift_xy = shift_xy
--[[
get tangents to the path
--]]
local tangent1_ef = path_var.tangent:copy()
local tangent2_ef = p1 - p0
local tv_unit = tangent2_ef:copy()
if tv_unit:length() < 0.00001 then
gcs:send_text(MAV_SEVERITY.EMERGENCY, string.format("path not advancing %f", tv_unit:length()))
end
tv_unit:normalize()
--[[
use actual vehicle velocity to calculate how far along the
path we have progressed
--]]
local v = ahrs_velned:copy()
local path_dist = v:dot(tv_unit)*actual_dt
if path_dist < 0 and path_var.last_rate_override > 0 and now - path_var.last_rate_override > 1 then
gcs:send_text(MAV_SEVERITY.EMERGENCY, string.format("aborting %.2f at %d tv=(%.2f,%.2f,%.2f) vx=%.2f adt=%.2f",
path_dist, path_var.count,
tangent2_ef:x(),
tangent2_ef:y(),
tangent2_ef:z(),
v:x(), actual_dt))
if option_set(OPTIONS.ABORT_RTL) and vehicle:get_mode() == MODE_AUTO then
vehicle:set_mode(MODE_RTL)
end
path_var.last_rate_override = 0
return false
end
local path_t_delta = constrain(path_dist/path_var.length, 0.2*local_n_dt, 4*local_n_dt)
--[[
recalculate the current path position and angle based on actual delta time
--]]
p1, _, _ = rotate_path(path,
constrain(path_var.path_t + path_t_delta, 0, 1),
path_var.initial_ori, path_var.initial_ef_pos)
local last_path_t = path_var.path_t
path_var.path_t = path_var.path_t + path_t_delta
-- tangents needs to be recalculated
tangent2_ef = p1 - p0
tv_unit = tangent2_ef:copy()
tv_unit:normalize()
-- error in position versus current point on the path
local pos_error_ef = current_measured_pos_ef - p1
--[[
calculate a time correction. We first get the projection of
the position error onto the track. This tells us how far we
are ahead or behind on the track
--]]
local path_dist_err_m = tv_unit:dot(pos_error_ef)
-- normalize against the total path length
local path_err_t = path_dist_err_m / path_var.length
-- don't allow the path to go backwards in time, or faster than twice the actual rate
path_err_t = constrain(path_err_t, -0.9*path_t_delta, 2*path_t_delta)
-- correct time to bring us back into sync
path_var.path_t = path_var.path_t + TIME_CORR_P:get() * path_err_t
-- get the path again with the corrected time
local r1
p1, r1, attrib = rotate_path(path,
constrain(path_var.path_t, 0, 1),
path_var.initial_ori, path_var.initial_ef_pos)
-- recalculate the tangent to match the amount we advanced the path time
tangent2_ef = p1 - p0
-- get the real world time corresponding to the quaternion change
local q_change_t = (path_var.path_t - last_path_t) * path_var.total_time
-- low pass filter the demanded roll angle
r1 = path_var.roll + wrap_pi(r1 - path_var.roll)
local alpha = calc_lowpass_alpha(q_change_t, AEROM_ANG_TC:get())
r1 = (1.0 - alpha) * path_var.roll + alpha * r1
r1 = wrap_pi(r1)
path_var.tangent = tangent2_ef:copy()
path_var.pos = p1:copy()
path_var.roll = r1
--[[
calculation of error correction, calculating acceleration
needed to bring us back on the path, and body rates in pitch and
yaw to achieve those accelerations
--]]
-- component of pos_err perpendicular to the current path tangent
local B = ortho_proj(pos_error_ef, tv_unit)
-- derivative of pos_err perpendicular to the current path tangent, assuming tangent is constant
local B_dot = ortho_proj(v, tv_unit)
-- gains for error correction.
local acc_err_ef = B:scale(ERR_CORR_P:get()) + B_dot:scale(ERR_CORR_D:get())
if attrib.pos_gain_mul then
-- allow for reduced gains during some maneuvers like takeoff
acc_err_ef = acc_err_ef:scale(attrib.pos_gain_mul)
end
-- scale by per-maneuver error correction scale factor
acc_err_ef = acc_err_ef:scale(attrib.pos_corr or 1.0)
local acc_err_bf = quat_earth_to_body(ahrs_quat, acc_err_ef)
local TAS = constrain(ahrs:get_EAS2TAS()*airspeed_constrained, 3, 100)
local corr_rate_bf_y_rads = -acc_err_bf:z()/TAS
local corr_rate_bf_z_rads = acc_err_bf:y()/TAS
local cor_ang_vel_bf_rads = makeVector3f(0.0, corr_rate_bf_y_rads, corr_rate_bf_z_rads)
if Vec3IsNaN(cor_ang_vel_bf_rads) then
cor_ang_vel_bf_rads = makeVector3f(0,0,0)
end
local cor_ang_vel_bf_dps = cor_ang_vel_bf_rads:scale(math.deg(1))
if path_var.count < 2 then
cor_ang_vel_bf_dps = Vector3f()
end
--[[
work out body frame path rate, this is based on two adjacent tangents on the path
--]]
local path_rate_ef_rads = tangents_to_rate(tangent1_ef, tangent2_ef, actual_dt)
if Vec3IsNaN(path_rate_ef_rads) then
gcs:send_text(MAV_SEVERITY.EMERGENCY,string.format("path_rate_ef_rads: NaN"))
path_rate_ef_rads = makeVector3f(0,0,0)
end
local path_rate_ef_dps = path_rate_ef_rads:scale(math.deg(1))
if path_var.count < 3 then
-- cope with small initial misalignment
path_rate_ef_dps:z(0)
end
local path_rate_bf_dps = quat_earth_to_body(ahrs_quat, path_rate_ef_dps)
-- set the path roll rate
path_rate_bf_dps:x(math.deg(wrap_pi(r1 - r0)/actual_dt))
--[[
calculate body frame roll rate to achieved the desired roll
angle relative to the maneuver path
--]]
local zero_roll_angle_delta = Quaternion()
zero_roll_angle_delta:from_angular_velocity(path_rate_ef_rads, actual_dt)
path_var.accumulated_orientation_rel_ef = zero_roll_angle_delta*path_var.accumulated_orientation_rel_ef
path_var.accumulated_orientation_rel_ef:normalize()
local mf_axis = quat_earth_to_body(path_var.accumulated_orientation_rel_ef, makeVector3f(1, 0, 0))
local orientation_rel_mf_with_roll_angle = Quaternion()
orientation_rel_mf_with_roll_angle:from_axis_angle(mf_axis, r1)
orientation_rel_ef_with_roll_angle = orientation_rel_mf_with_roll_angle*path_var.accumulated_orientation_rel_ef
--[[
calculate the error correction for the roll versus the desired roll
--]]
local roll_error = orientation_rel_ef_with_roll_angle * ahrs_quat
roll_error:normalize()
local err_axis_ef, err_angle_rad = to_axis_and_angle(roll_error)
local time_const_roll = ROLL_CORR_TC:get()
local err_angle_rate_ef_rads = err_axis_ef:scale(err_angle_rad/time_const_roll)
local err_angle_rate_bf_dps = quat_earth_to_body(ahrs_quat,err_angle_rate_ef_rads):scale(math.deg(1))
-- zero any non-roll components
err_angle_rate_bf_dps:y(0)
err_angle_rate_bf_dps:z(0)
--[[
implement lookahead for path rates
--]]
if AEROM_LKAHD:get() > 0 then
local lookahead = AEROM_LKAHD:get()
local lookahead_vt = lookahead / path_var.total_time
p2 = rotate_path(path,
constrain(path_var.path_t+lookahead_vt, 0, 1),
path_var.initial_ori, path_var.initial_ef_pos)
local tangent3_ef = p2 - p1
local lk_ef_rads = tangents_to_rate(tangent2_ef, tangent3_ef, 0.5*(lookahead+(1.0/LOOP_RATE)))
-- scale for airspeed
lk_ef_rads = lk_ef_rads:scale(sq(vel_length/path_var.target_speed))
local lookahead_bf_rads = quat_earth_to_body(ahrs_quat, lk_ef_rads)
local lookahead_bf_dps = lookahead_bf_rads:scale(math.deg(1))
logger.write('AELK','Py,Ly,Pz,Lz', 'ffff',
path_rate_bf_dps:y(),
lookahead_bf_dps:y(),
path_rate_bf_dps:z(),
lookahead_bf_dps:z())
if not Vec3IsNaN(lookahead_bf_dps) then
path_rate_bf_dps:y(lookahead_bf_dps:y())
path_rate_bf_dps:z(lookahead_bf_dps:z())
end
end
--[[
calculate an additional yaw rate to get us to the right angle of sideslip for knifeedge
--]]
-- local sideslip_rate_bf_dps = calculate_side_slip_aoa(path_rate_bf_dps, ahrs_quat, airspeed_constrained, tv_unit, ahrs_velned, actual_dt)
local sideslip_rate_bf_dps = Vector3f()
--[[
total angular rate is sum of path rate, correction rate and roll correction rate
--]]
local tot_ang_vel_bf_dps = path_rate_bf_dps + cor_ang_vel_bf_dps + err_angle_rate_bf_dps + sideslip_rate_bf_dps
--[[
apply angular accel limit
--]]
local ang_rate_diff_dps = tot_ang_vel_bf_dps - path_var.last_ang_rate_dps
local max_delta_dps = AEROM_ANG_ACCEL:get() * actual_dt
local max_delta_yaw_dps = max_delta_dps
if AEROM_YAW_ACCEL:get() > 0 and
(AEROM_YAW_ACCEL:get() < AEROM_ANG_ACCEL:get() or AEROM_ANG_ACCEL:get() <= 0) then
max_delta_yaw_dps = AEROM_YAW_ACCEL:get() * actual_dt
end
if max_delta_dps > 0 then
ang_rate_diff_dps:x(constrain(ang_rate_diff_dps:x(), -max_delta_dps, max_delta_dps))
ang_rate_diff_dps:y(constrain(ang_rate_diff_dps:y(), -max_delta_dps, max_delta_dps))
end
if max_delta_yaw_dps > 0 then
ang_rate_diff_dps:z(constrain(ang_rate_diff_dps:z(), -max_delta_yaw_dps, max_delta_yaw_dps))
end
tot_ang_vel_bf_dps = path_var.last_ang_rate_dps + ang_rate_diff_dps
path_var.last_ang_rate_dps = tot_ang_vel_bf_dps
--[[
calculate a rudder offset for knife-edge
--]]
local rudder_offset_pct = 0
if AEROM_KE_RUDD:get() > 0 then
rudder_offset_pct = calculate_rudder_offset(ahrs_quat, ahrs_gyro, airspeed_constrained)
end
--[[
log POSM is pose-measured, POST is pose-track, POSB is pose-track without the roll
--]]
log_pose('POSM', current_measured_pos_ef, ahrs_quat:inverse())
log_pose('POST', p1, orientation_rel_ef_with_roll_angle)
logger.write('AETM', 'T,Terr,QCt,Adt','ffff',
path_var.path_t,
path_err_t,
q_change_t,
actual_dt)
logger.write('AERT','Cx,Cy,Cz,Px,Py,Pz,Ex,Tx,Ty,Tz,Perr,Aerr,Yff,Rofs', 'ffffffffffffff',
cor_ang_vel_bf_dps:x(), cor_ang_vel_bf_dps:y(), cor_ang_vel_bf_dps:z(),
path_rate_bf_dps:x(), path_rate_bf_dps:y(), path_rate_bf_dps:z(),
err_angle_rate_bf_dps:x(),
tot_ang_vel_bf_dps:x(), tot_ang_vel_bf_dps:y(), tot_ang_vel_bf_dps:z(),
pos_error_ef:length(),
wrap_180(math.deg(err_angle_rad)),
sideslip_rate_bf_dps:z(),
rudder_offset_pct)
--log_pose('POSB', p1, path_var.accumulated_orientation_rel_ef)
--[[
run the throttle based speed controller
get the anticipated pitch at the throttle lookahead time
we use the maximum of the current path pitch and the anticipated pitch
--]]
local qchange = Quaternion()
qchange:from_angular_velocity(path_rate_ef_rads, AEROM_THR_LKAHD:get())
local qnew = qchange * orientation_rel_ef_with_roll_angle
local anticipated_pitch_rad = math.max(qnew:get_euler_pitch(), orientation_rel_ef_with_roll_angle:get_euler_pitch())
local throttle = speed_PI.update(path_var.target_speed + path_var.speed_adjustment, anticipated_pitch_rad)
local thr_min = AEROM_THR_MIN:get()
if attrib.thr_boost then
thr_min = math.max(thr_min, AEROM_THR_BOOST:get())
end
throttle = constrain(throttle, thr_min, 100.0)
if isNaN(throttle) or Vec3IsNaN(tot_ang_vel_bf_dps) then
gcs:send_text(MAV_SEVERITY.EMERGENCY,string.format("Path NaN - aborting"))
return false
end
vehicle:set_target_throttle_rate_rpy(throttle, tot_ang_vel_bf_dps:x(), tot_ang_vel_bf_dps:y(), tot_ang_vel_bf_dps:z())
vehicle:set_rudder_offset(rudder_offset_pct, true)
if now - last_named_float_t > 1.0 / NAME_FLOAT_RATE then
last_named_float_t = now
gcs:send_named_float("PERR", pos_error_ef:length())
end
local alt_error = (current_measured_pos_ef - path_var.initial_ef_pos):z()
if alt_error > AEROM_ALT_ABORT:get() then
gcs:send_text(MAV_SEVERITY.EMERGENCY,"Too low altitude, aborting")
return false
end
return true
end
--[[
an object defining a path
--]]
function PathFunction(fn, name)
local self = {}
self.fn = fn
self.name = name
return self
end
local last_preload = nil
local command_table = {}
command_table[1] = PathFunction(figure_eight, "Figure Eight")
command_table[2] = PathFunction(loop, "Loop")
command_table[3] = PathFunction(horizontal_rectangle, "Horizontal Rectangle")
command_table[4] = PathFunction(climbing_circle, "Climbing Circle")
command_table[5] = PathFunction(vertical_aerobatic_box, "Vertical Box")
command_table[6] = PathFunction(immelmann_turn_fast, "Immelmann Fast")
command_table[7] = PathFunction(straight_roll, "Axial Roll")
command_table[8] = PathFunction(rolling_circle, "Rolling Circle")
command_table[9] = PathFunction(half_cuban_eight, "Half Cuban Eight")
command_table[10]= PathFunction(half_reverse_cuban_eight, "Half Reverse Cuban Eight")
command_table[11]= PathFunction(cuban_eight, "Cuban Eight")
command_table[12]= PathFunction(humpty_bump, "Humpty Bump")
command_table[13]= PathFunction(straight_flight, "Straight Flight")
command_table[14]= PathFunction(scale_figure_eight, "Scale Figure Eight")
command_table[15]= PathFunction(immelmann_turn, "Immelmann Turn")
command_table[16]= PathFunction(split_s, "Split-S")
command_table[17]= PathFunction(upline_45, "Upline-45")
command_table[18]= PathFunction(downline_45, "Downline-45")
command_table[19]= PathFunction(stall_turn, "Stall Turn")
command_table[20]= PathFunction(procedure_turn, "Procedure Turn")
command_table[23]= PathFunction(half_climbing_circle, "Half Climbing Circle")
command_table[25]= PathFunction(laydown_humpty, "Laydown Humpty")
command_table[26]= PathFunction(barrel_roll, "Barrel Roll")
command_table[27]= PathFunction(straight_flight, "Straight Hold")
command_table[28]= PathFunction(partial_circle, "Partial Circle")
command_table[31]= PathFunction(multi_point_roll, "Multi Point Roll")
command_table[32]= PathFunction(side_step, "Side Step")
--[[
a table of function available in loadable tricks
--]]
local load_table = {}
load_table["loop"] = loop
load_table["horizontal_rectangle"] = horizontal_rectangle
load_table["climbing_circle"] = climbing_circle
load_table["vertical_aerobatic_box"] = vertical_aerobatic_box
load_table["immelmann_turn_fast"] = immelmann_turn_fast
load_table["straight_roll"] = straight_roll
load_table["rolling_circle"] = rolling_circle
load_table["half_cuban_eight"] = half_cuban_eight
load_table["half_reverse_cuban_eight"] = half_reverse_cuban_eight
load_table["cuban_eight"] = cuban_eight
load_table["humpty_bump"] = humpty_bump
load_table["straight_flight"] = straight_flight
load_table["scale_figure_eight"] = scale_figure_eight
load_table["immelmann_turn"] = immelmann_turn
load_table["split_s"] = split_s
load_table["upline_45"] = upline_45
load_table["upline_20"] = upline_20
load_table["takeoff"] = takeoff
load_table["downline_45"] = downline_45
load_table["stall_turn"] = stall_turn
load_table["procedure_turn"] = procedure_turn
load_table["two_point_roll"] = two_point_roll
load_table["half_climbing_circle"] = half_climbing_circle
load_table["laydown_humpty"] = laydown_humpty
load_table["straight_align"] = straight_align
load_table["figure_eight"] = figure_eight
load_table["barrel_roll"] = barrel_roll
load_table["straight_hold"] = straight_hold
load_table["partial_circle"] = partial_circle
load_table["multi_point_roll"] = multi_point_roll
load_table["side_step"] = side_step
load_table["align_box"] = align_box
load_table["align_center"] = align_center
--[[
interpret an attribute value, coping with special cases
--]]
function interpret_attrib(v)
if v == "true" then
return true
end
if v == "false" then
return false
end
-- could be a number
local n = tonumber(v)
if n ~= nil then
return n
end
-- assume a string
return v
end
--[[
parse a function definition in a txt load file, adding it to the load table so
it can be used in schedules
--]]
function parse_function(line, file)
_, _, funcname = string.find(line, "^function%s*([%w_]+).*$")
if not funcname then
gcs:send_text(MAV_SEVERITY.ERROR, string.format("Parse error: %s", line))
return
end
local funcstr = line .. "\n"
while true do
line = file:read()
if not line then
gcs:send_text(MAV_SEVERITY.ERROR, string.format("Error: end of file in %s", funcname))
break
end
funcstr = funcstr .. line .. "\n"
if string.sub(line,1,3) == "end" then
break
end
end
local f, errloc, err = load(funcstr, funcname, "t", _ENV)
if not f then
gcs:send_text(MAV_SEVERITY.ERROR,string.format("Error %s: %s", errloc, err))
return
end
-- fun the function code, which creates the function
local success
success, err = pcall(f)
if not success then
gcs:send_text(MAV_SEVERITY.ERROR,string.format("Error %s: %s", funcname, err))
end
load_table[funcname] = _ENV[funcname]
end
--[[
load a trick description from a text file
--]]
function load_trick(id)
if command_table[id] ~= nil then
-- already have it
return
end
-- look in 3 possible locations for the trick, coping with SITL and real boards
local trickdirs = { "APM/scripts/", "scripts/", "./" }
local file = nil
local fname = string.format("trick%u.txt", id)
local filename = nil
for i = 1, #trickdirs do
filename = trickdirs[i] .. fname
file = io.open(filename, "r")
if file then
break
end
end
if file == nil then
gcs:send_text(MAV_SEVERITY.ERROR,string.format("Failed to open %s", fname))
return
end
local name = string.format("Trick%u", id)
local attrib = {}
local paths = {}
while true do
local line = file:read()
if not line then
break
end
-- trim trailing spaces
line = string.gsub(line, '^(.-)%s*$', '%1')
local _, _, cmd, arg1, arg2, arg3, arg4 = string.find(line, "^([%w_:]+)%s*([-.%d]*)%s*([-.%d]*)%s*([-.%d]*)%s*([-.%d]*)")
if cmd == "" or cmd == nil or string.sub(cmd,1,1) == "#" then
-- ignore comments
goto continue
elseif cmd == "name:" then
_, _, name = string.find(line, "^name:%s*([%w_]+)$")
elseif string.sub(cmd,-1) == ":" then
_, _, a, s = string.find(line, "^([%w_]+):%s*([%w_:%s-]+)$")
if a ~= nil then
attrib[a] = interpret_attrib(s)
else
gcs:send_text(MAV_SEVERITY.ERROR, string.format("Bad line: '%s'", line))
end
elseif cmd == "function" then
parse_function(line, file)
elseif cmd ~= nil then
arg1 = tonumber(arg1) or 0
arg2 = tonumber(arg2) or 0
arg3 = tonumber(arg3) or 0
arg4 = tonumber(arg4) or 0
local f = load_table[cmd]
if f == nil then
gcs:send_text(MAV_SEVERITY.ERROR,string.format("Unknown command '%s' in %s", cmd, fname))
else
paths[#paths+1] = { f, { arg1, arg2, arg3, arg4 }}
for k, v in pairs(attrib) do
paths[#paths][k] = v
end
attrib = {}
end
end
::continue::
end
local pc = path_composer(name, paths)
gcs:send_text(MAV_SEVERITY.INFO, string.format("Loaded trick%u '%s'", id, name))
command_table[id] = PathFunction(pc, name)
logger:log_file_content(filename)
calculate_timestamps(command_table[id])
end
function calculate_timestamps(pc)
pc.fn:calculate_timestamps()
end
function PathTask(fn, name, id, initial_yaw_deg, arg1, arg2, arg3, arg4)
local self = {}
if type(fn) == "table" then
self.fn = fn
else
self.fn = fn(arg1, arg2, arg3, arg4)
end
self.name = name
self.id = id
self.initial_yaw_deg = initial_yaw_deg
self.started = false
return self
end
--[[
see if we should prepare for an upcoming trick
--]]
function check_preload_trick()
local idx = mission:get_current_nav_index()
if idx == last_preload then
return
end
last_preload = idx
local m = mission:get_item(idx+1)
if not m then
return
end
if m:command() ~= NAV_SCRIPT_TIME then
return
end
cmdid = m:param1()
if command_table[cmdid] == nil then
load_trick(cmdid)
end
end
-- see if an auto mission item needs to be run
function check_auto_mission()
id, cmd, arg1, arg2, arg3, arg4 = vehicle:nav_script_time()
if not id then
check_preload_trick()
return
end
if id ~= last_id then
-- we've started a new command
current_task = nil
last_id = id
local initial_yaw_deg = get_ground_course_deg()
load_trick(cmd)
if command_table[cmd] == nil then
gcs:send_text(MAV_SEVERITY.ERROR, string.format("Trick %u not found", cmd))
return
end
gcs:send_text(MAV_SEVERITY.INFO, string.format("Starting %s!", command_table[cmd].name ))
-- work out yaw between previous WP and next WP
local cnum = mission:get_current_nav_index()
if AEROM_MIS_ANGLE:get() == 0 then
-- find previous nav waypoint
local loc_prev = ahrs:get_location()
if cnum > 1 then
loc_prev = get_wp_location(cnum-1)
local i= cnum-1
while get_wp_location(i):lat() == 0 and get_wp_location(i):lng() == 0 do
i = i-1
loc_prev = get_wp_location(i)
end
end
-- find next nav waypoint
local loc_next = get_wp_location(cnum+1)
i = cnum+1
while get_wp_location(i):lat() == 0 and get_wp_location(i):lng() == 0 do
i = i+1
loc_next = get_wp_location(resolve_jump(i))
end
local wp_yaw_deg = math.deg(loc_prev:get_bearing(loc_next))
if math.abs(wrap_180(initial_yaw_deg - wp_yaw_deg)) > 90 and cnum > 1 then
gcs:send_text(MAV_SEVERITY.INFO, string.format("Doing turnaround! iyaw=%.1f wyaw=%.1f", initial_yaw_deg, wp_yaw_deg))
wp_yaw_deg = wrap_180(wp_yaw_deg + 180)
end
initial_yaw_deg = wp_yaw_deg
else
initial_yaw_deg = AEROM_MIS_ANGLE:get()
end
current_task = PathTask(command_table[cmd].fn, command_table[cmd].name,
id, initial_yaw_deg, arg1, arg2, arg3, arg4)
end
end
local trick_sel_chan = nil
local last_trick_selection = nil
--[[
get selected trick. Trick numbers are 1 .. TRIK_COUNT. A value of 0 is invalid
--]]
function get_trick_selection()
if trick_sel_chan == nil then
trick_sel_chan = rc:find_channel_for_option(TRIK_SEL_FN:get())
if trick_sel_chan == nil then
return 0
end
end
-- get trick selection based on selection channel input and number of tricks
local i = math.floor(TRIK_COUNT:get() * constrain(0.5*(trick_sel_chan:norm_input_ignore_trim()+1),0,0.999)+1)
if TRICKS[i] == nil then
return 0
end
return i
end
--[[
check for running a trick
--]]
function check_trick()
local selection = get_trick_selection()
local action = rc:get_aux_cached(TRIK_ACT_FN:get())
if action == 0 and current_task ~= nil then
gcs:send_text(MAV_SEVERITY.ALERT,string.format("Trick aborted"))
current_task = nil
last_trick_selection = nil
-- use invalid mode to disable script control
vehicle:nav_scripting_enable(255)
return
end
if selection == 0 then
return
end
if action == 1 and selection ~= last_trick_selection then
local id = TRICKS[selection].id:get()
if id == -1 then
gcs:send_text(MAV_SEVERITY.ERROR,string.format("Trick %u not setup",selection))
last_trick_selection = selection
return
end
load_trick(id)
if command_table[id] ~= nil then
local cmd = command_table[id]
gcs:send_text(MAV_SEVERITY.INFO, string.format("Trick %u selected (%s)", selection, cmd.name))
last_trick_selection = selection
return
end
end
if current_task ~= nil then
-- let the task finish
return
end
if action ~= last_trick_action_state then
last_trick_selection = selection
last_trick_action_state = action
if selection == 0 then
gcs:send_text(MAV_SEVERITY.ALERT, string.format("No trick selected"))
return
end
local id = TRICKS[selection].id:get()
if id == -1 then
gcs:send_text(MAV_SEVERITY.ALERT,string.format("Trick %u not setup",selection))
last_trick_selection = selection
return
end
load_trick(id)
if command_table[id] == nil then
gcs:send_text(MAV_SEVERITY.ALERT, string.format("Invalid trick ID %u", id))
return
end
local cmd = command_table[id]
if action == 1 then
gcs:send_text(MAV_SEVERITY.INFO, string.format("Trick %u selected (%s)", selection, cmd.name))
end
if action == 2 then
last_trick_selection = nil
local current_mode = vehicle:get_mode()
if not vehicle:nav_scripting_enable(current_mode) then
gcs:send_text(MAV_SEVERITY.ALERT, string.format("Tricks not available in mode"))
return
end
gcs:send_text(MAV_SEVERITY.INFO, string.format("Trick %u started (%s)", selection, cmd.name))
local initial_yaw_deg = get_ground_course_deg()
current_task = PathTask(cmd.fn,
cmd.name,
nil,
initial_yaw_deg,
TRICKS[selection].args[1]:get(),
TRICKS[selection].args[2]:get(),
TRICKS[selection].args[3]:get(),
TRICKS[selection].args[4]:get())
end
end
end
function update()
if ahrs:get_velocity_NED() == nil or ahrs:get_EAS2TAS() == nil or ahrs:get_relative_position_NED_origin() == nil then
-- don't start till we have valid ahrs estimates
return update, 1000.0/LOOP_RATE
end
if vehicle:get_mode() == MODE_AUTO then
if arming:is_armed() then
log_position("VEH", ahrs:get_location(), ahrs:get_quaternion())
end
check_auto_mission()
elseif TRICKS ~= nil then
check_trick()
end
if current_task ~= nil then
if not do_path() then
gcs:send_text(MAV_SEVERITY.INFO, string.format("Finishing %s!", current_task.name))
if current_task.id ~= nil then
vehicle:nav_script_time_done(current_task.id)
else
-- use invalid mode to disable script control
vehicle:nav_scripting_enable(255)
end
current_task = nil
end
end
return update, 1000.0/LOOP_RATE
end
gcs:send_text(MAV_SEVERITY.INFO, string.format("Loaded plane_aerobatics.lua"))
return update()