-- x-quad-cg-allocation.lua: Adjust the control allocation matrix for offset CoG.
--
-- WARNING: This script is applicable only to X-type quadrotors and quadplanes.
--
-- How To Use
--   1. Place this script in the "scripts" directory.
--   2. Set FRAME_CLASS or Q_FRAME_CLASS to 17 to enable the dynamic scriptable mixer.
--   3. Enable Lua scripting via the SCR_ENABLE parameter.
--   4. Reboot.
--   5. Fly the vehicle.
--   6. Adjust the value of the CGA_RATIO parameter.
--
-- How It Works
--   1. The control allocation matrix is adjusted for thrust and pitch based on the ??? parameter value.

--[[
Global definitions.
--]]
local MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7}
local SCRIPT_NAME = "CoG adjust script"
local LOOP_RATE_HZ = 10
local last_warning_time_ms = uint32_t() -- Time we last sent a warning message to the user.
local WARNING_DEADTIME_MS = 1000 -- How often the user should be warned.
local is_mixer_matrix_static = false
local is_mixer_matrix_dynamic = false
local last_ratio = 1

-- State machine states.
local FSM_STATE = {
    INACTIVE = 0,
    INITIALIZE = 1,
    ACTIVE = 2,
    FINISHED = 3
}
local current_state = FSM_STATE.INACTIVE
local next_state = FSM_STATE.INACTIVE


--[[
New parameter declarations
--]]
local PARAM_TABLE_KEY = 139
local PARAM_TABLE_PREFIX = "CGA_"

-- Bind a parameter to a variable.
function bind_param(name)
    return Parameter(name)
end

-- Add a parameter and bind it to a variable.
function bind_add_param(name, idx, default_value)
    assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('Could not add param %s', name))
    return bind_param(PARAM_TABLE_PREFIX .. name)
end

-- Add param table.
assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 1), SCRIPT_NAME .. ': Could not add param table.')

--[[
  // @Param: CGA_RATIO
  // @DisplayName: CoG adjustment ratio
  // @Description: The ratio between the front and back motor outputs during steady-state hover. Positive when the CoG is in front of the motors midpoint (front motors work harder).
  // @Range: 0.5 2
  // @User: Advanced
--]]
CGA_RATIO = bind_add_param('RATIO', 1, 1)

-- Bindings to existing parameters

--[[
Potential additions:
--]]
-- Warn the user, throttling the message rate.
function warn_user(msg, severity)
    severity = severity or MAV_SEVERITY.WARNING -- Optional severity argument.
    if millis() - last_warning_time_ms > WARNING_DEADTIME_MS then
            gcs:send_text(severity, SCRIPT_NAME .. ": " .. msg)
        last_warning_time_ms = millis()
    end
end

-- Decide if the given ratio value makes sense.
function sanitize_ratio(ratio)
    if (ratio < 0.5) or (ratio > 2) then
        warn_user("CGA_RATIO value out of bounds.", MAV_SEVERITY.ERROR)
        CGA_RATIO:set(1.0)
        return 1.0 -- Return default.
    else
        return ratio
    end
end

-- Normalize the throttle factors to max 1
function normalize_throttle(factors)
    -- Find maximum value.
    local max_factor = 0
    for _, factor in ipairs(factors) do
        max_factor = math.max(max_factor, factor)
    end
    -- Adjust all values by it.
    normalized_factors = {}
    for _, factor in ipairs(factors) do
        table.insert(normalized_factors, factor / max_factor)
    end
    return normalized_factors
end

-- Calculate the thrust factors given a ratio.
function build_factors(ratio)
    local r1 = 2.0/(1+ratio)
    local r2 = 2.0*ratio/(1+ratio)
    local quad_x_factors = {r2, r1, r2, r1}
    return normalize_throttle(quad_x_factors)
end

-- Adjust the dynamic motor mixer.
function update_dynamic_mixer(ratio)

    Motors_dynamic:add_motor(0, 1)
    Motors_dynamic:add_motor(1, 3)
    Motors_dynamic:add_motor(2, 4)
    Motors_dynamic:add_motor(3, 2)

    factors = motor_factor_table()

    -- Roll stays as-is.
    factors:roll(0, -0.5)
    factors:roll(1,  0.5)
    factors:roll(2,  0.5)
    factors:roll(3, -0.5)

    -- Pitch stays as-is.
    factors:pitch(0, 0.5)
    factors:pitch(1, -0.5)
    factors:pitch(2, 0.5)
    factors:pitch(3, -0.5)

    -- Yaw stays as-is.
    factors:yaw(0,  0.5)
    factors:yaw(1,  0.5)
    factors:yaw(2, -0.5)
    factors:yaw(3, -0.5)

    -- Throttle is modulated.
    throttle_factors = build_factors(ratio)
    factors:throttle(0, throttle_factors[1])
    factors:throttle(1, throttle_factors[2])
    factors:throttle(2, throttle_factors[3])
    factors:throttle(3, throttle_factors[4])

    Motors_dynamic:load_factors(factors)

    if not Motors_dynamic:init(4) then
        warn_user("Failed to initialize motor matrix!", MAV_SEVERITY.EMERGENCY)
    else
        if ratio ~= last_ratio then
            warn_user("Set ratio to " .. tostring(ratio), MAV_SEVERITY.INFO)
            last_ratio = ratio
        end
    end
    motors:set_frame_string("Dynamic CoM adjust")

end

-- Adjust the static motor mixer.
function update_static_mixer(ratio)
    MotorsMatrix:add_motor_raw(0,-0.5, 0.5, 0.5, 2)
    MotorsMatrix:add_motor_raw(1, 0.5,-0.5, 0.5, 4)
    MotorsMatrix:add_motor_raw(2, 0.5, 0.5,-0.5, 1)
    MotorsMatrix:add_motor_raw(3,-0.5,-0.5,-0.5, 3)

    throttle_factors = build_factors(ratio)
    MotorsMatrix:set_throttle_factor(0, throttle_factors[1])
    MotorsMatrix:set_throttle_factor(1, throttle_factors[2])
    MotorsMatrix:set_throttle_factor(2, throttle_factors[3])
    MotorsMatrix:set_throttle_factor(3, throttle_factors[4])

    if not MotorsMatrix:init(4) then
        warn_user("Failed to initialize motor matrix!", MAV_SEVERITY.EMERGENCY)
    else
        if ratio ~= last_ratio then
            warn_user("Set ratio to " .. tostring(ratio), MAV_SEVERITY.INFO)
            last_ratio = ratio
        end
    end
    motors:set_frame_string("Static CoM adjust")
end

-- Decide if the UA is a Quad X quadplane.
function inspect_frame_class_fw()

    Q_ENABLE = bind_param("Q_ENABLE")
    Q_FRAME_CLASS = bind_param("Q_FRAME_CLASS")

    if FWVersion:type()==3 then
        -- Test for the validity of the parameters.
        if Q_ENABLE:get()==1 then
            if Q_FRAME_CLASS:get()==15 then
                is_mixer_matrix_static = true
            elseif Q_FRAME_CLASS:get()==17 then
                is_mixer_matrix_dynamic = true
            end
        end
    end
end

-- Decide if the UA is a Quad X multicopter.
function inspect_frame_class_mc()

    FRAME_CLASS = bind_param("FRAME_CLASS")

    if FWVersion:type()==2 then
        if FRAME_CLASS:get()==15 then
            is_mixer_matrix_static = true
        elseif FRAME_CLASS:get()==17 then
            is_mixer_matrix_dynamic = true
        end
    end
end

--[[
Activation conditions
--]]
-- Check for script activating conditions here.
-- Check frame types.
function can_start()
    result = is_mixer_matrix_static or is_mixer_matrix_dynamic
    return result
end

--[[
State machine
--]]
function fsm_step()
    if current_state == FSM_STATE.INACTIVE then
        if can_start() then
            next_state = FSM_STATE.INITIALIZE
        else
            next_state = FSM_STATE.FINISHED
            warn_user("Could not find scriptable mixer", MAV_SEVERITY.ERROR)
        end

    elseif current_state == FSM_STATE.INITIALIZE then
        if is_mixer_matrix_static then
            local ratio = sanitize_ratio(CGA_RATIO:get())
            update_static_mixer(ratio)
            next_state = FSM_STATE.FINISHED
        else
            next_state = FSM_STATE.ACTIVE
        end

    elseif current_state == FSM_STATE.ACTIVE then
        -- Assert the parameter limits.
        local ratio = sanitize_ratio(CGA_RATIO:get())
        -- Create the control allocation matrix parameters.
        update_dynamic_mixer(ratio)

    else
        gcs:send_text(MAV_SEVERITY.CRITICAL, "Unexpected FSM state!")
    end

    current_state = next_state
end

-- Check once on boot if the frame type is suitable for this script.
pcall(inspect_frame_class_mc)
pcall(inspect_frame_class_fw)
gcs:send_text(MAV_SEVERITY.INFO, SCRIPT_NAME .. string.format(" loaded."))

-- Wrapper around update() to catch errors.
function protected_wrapper()
  local success, err = pcall(fsm_step)
  if not success then
    gcs:send_text(MAV_SEVERITY.EMERGENCY, "Internal Error: " .. err)
    return protected_wrapper, 1000
  end
  if current_state ~= FSM_STATE.FINISHED then
    return protected_wrapper, 1000.0/LOOP_RATE_HZ
  end
end

-- Start running update loop
return protected_wrapper()
