--[[
   battery state of charge (SOC) estimator based on resting voltage

   See Tools/scripts/battery_fit.py for a tool to calculate the coefficients from a log
--]]

local MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7}

local PARAM_TABLE_KEY = 14
local PARAM_TABLE_PREFIX = "BATT_SOC"

-- bind a parameter to a variable
function bind_param(name)
   local p = Parameter()
   assert(p:init(name), string.format('could not find %s parameter', name))
   return p
end

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

-- setup quicktune specific parameters
assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 32), 'could not add param table')

--[[
  // @Param: BATT_SOC_COUNT
  // @DisplayName: Count of SOC estimators
  // @Description: Number of battery SOC estimators
  // @Range: 0 4
  // @User: Standard
--]]
local BATT_SOC_COUNT     = bind_add_param('_COUNT', 1, 0)

if BATT_SOC_COUNT:get() <= 0 then
   return
end

--[[
  // @Param: BATT_SOC1_IDX
  // @DisplayName: Battery estimator index
  // @Description: Battery estimator index
  // @Range: 0 4
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC1_NCELL
  // @DisplayName: Battery estimator cell count
  // @Description: Battery estimator cell count
  // @Range: 0 48
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC1_C1
  // @DisplayName: Battery estimator coefficient1
  // @Description: Battery estimator coefficient1
  // @Range: 100 200
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC1_C2
  // @DisplayName: Battery estimator coefficient2
  // @Description: Battery estimator coefficient2
  // @Range: 2 5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC1_C3
  // @DisplayName: Battery estimator coefficient3
  // @Description: Battery estimator coefficient3
  // @Range: 0.01 0.5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC1_C4
  // @DisplayName: Battery estimator coefficient4
  // @Description: Battery estimator coefficient4
  // @Range: 5 100
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC2_IDX
  // @DisplayName: Battery estimator index
  // @Description: Battery estimator index
  // @Range: 0 4
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC2_NCELL
  // @DisplayName: Battery estimator cell count
  // @Description: Battery estimator cell count
  // @Range: 0 48
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC2_C1
  // @DisplayName: Battery estimator coefficient1
  // @Description: Battery estimator coefficient1
  // @Range: 100 200
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC2_C2
  // @DisplayName: Battery estimator coefficient2
  // @Description: Battery estimator coefficient2
  // @Range: 2 5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC2_C3
  // @DisplayName: Battery estimator coefficient3
  // @Description: Battery estimator coefficient3
  // @Range: 0.01 0.5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC2_C4
  // @DisplayName: Battery estimator coefficient4
  // @Description: Battery estimator coefficient4
  // @Range: 5 100
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC3_IDX
  // @DisplayName: Battery estimator index
  // @Description: Battery estimator index
  // @Range: 0 4
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC3_NCELL
  // @DisplayName: Battery estimator cell count
  // @Description: Battery estimator cell count
  // @Range: 0 48
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC3_C1
  // @DisplayName: Battery estimator coefficient1
  // @Description: Battery estimator coefficient1
  // @Range: 100 200
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC3_C2
  // @DisplayName: Battery estimator coefficient2
  // @Description: Battery estimator coefficient2
  // @Range: 2 5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC3_C3
  // @DisplayName: Battery estimator coefficient3
  // @Description: Battery estimator coefficient3
  // @Range: 0.01 0.5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC3_C4
  // @DisplayName: Battery estimator coefficient4
  // @Description: Battery estimator coefficient4
  // @Range: 5 100
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC4_IDX
  // @DisplayName: Battery estimator index
  // @Description: Battery estimator index
  // @Range: 0 4
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC4_NCELL
  // @DisplayName: Battery estimator cell count
  // @Description: Battery estimator cell count
  // @Range: 0 48
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC4_C1
  // @DisplayName: Battery estimator coefficient1
  // @Description: Battery estimator coefficient1
  // @Range: 100 200
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC4_C2
  // @DisplayName: Battery estimator coefficient2
  // @Description: Battery estimator coefficient2
  // @Range: 2 5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC4_C3
  // @DisplayName: Battery estimator coefficient3
  // @Description: Battery estimator coefficient3
  // @Range: 0.01 0.5
  // @User: Standard
--]]

--[[
  // @Param: BATT_SOC4_C4
  // @DisplayName: Battery estimator coefficient4
  // @Description: Battery estimator coefficient4
  // @Range: 5 100
  // @User: Standard
--]]

local params = {}
local last_armed_ms = uint32_t(0)

--[[
   add parameters for an estimator
--]]
function add_estimator(i)
   id = string.format("%u_", i)
   pidx = 2+(i-1)*5
   params[i] = {}
   params[i]['IDX']   = bind_add_param(id .. "IDX",     pidx+0, 0)
   params[i]['NCELL'] = bind_add_param(id .. "NCELL", pidx+1, 0)
   params[i]['C1']    = bind_add_param(id .. "C1", pidx+2, 111.56)
   params[i]['C2']    = bind_add_param(id .. "C2", pidx+3, 3.65)
   params[i]['C3']    = bind_add_param(id .. "C3", pidx+4, 0.205)

   -- place the 4th coefficients at the end for backwards compatibility
   c4_pidx = 2+4*5+(i-1)
   params[i]['C4']    = bind_add_param(id .. "C4", c4_pidx, 80.0)
end

-- limit to 4 estimators even if the parameter specifies more
local count = math.min(math.floor(BATT_SOC_COUNT:get() or 0), 4)
for i = 1, count do
   add_estimator(i)
end

local function constrain(v, vmin, vmax)
   return math.max(math.min(v, vmax), vmin)
end

--[[
   simple model of state of charge versus resting voltage.
   With thanks to Roho for the form of the equation
   https://electronics.stackexchange.com/questions/435837/calculate-battery-percentage-on-lipo-battery

   Adjusted to also fit other battery chemistries such as Li-ion.
--]]
local function SOC_model(cell_volt, c1, c2, c3, c4)
    local soc = c1*(1.0-1.0/(1+(cell_volt/c2)^c4)^c3)
    return constrain(soc, 0, 100)
end

--[[
   update one estimator
--]]
local function update_estimator(i)
   local idx = math.floor(params[i]['IDX']:get())
   local ncell = math.floor(params[i]['NCELL']:get())
   if idx <= 0 or ncell <= 0 then
      return
   end
   local C1 = params[i]['C1']:get()
   local C2 = params[i]['C2']:get()
   local C3 = params[i]['C3']:get()
   local C4 = params[i]['C4']:get()
   local num_batts = battery:num_instances()
   if idx > num_batts then
      return
   end
   local voltR = battery:voltage_resting_estimate(idx-1)
   local soc = SOC_model(voltR/ncell, C1, C2, C3, C4)
   battery:reset_remaining(idx-1, soc)
end

--[[
   main update function, called at 1Hz
--]]
function update()
   local now_ms = millis()
   if arming:is_armed() then
      last_armed_ms = now_ms
      return update, 1000
   end
   -- don't update for 10s after disarm, to get logging of charge recovery
   if now_ms - last_armed_ms < 10000 then
      return update, 1000
   end
   for i = 1, #params do
      update_estimator(i)
   end
   return update, 1000
end

gcs:send_text(MAV_SEVERITY.INFO, string.format("Loaded BattEstimate for %u batteries", #params))

-- start running update loop
return update, 1000
