battery_profiling.lua

require("lib/strict")
local json = require("lib/dkjson")
local logger = require("modules/logger")

-- Battery profiling

otii.clear()

-- The profiling alternates between the two currents defined here
-- A current difference of 1mA results in a 1 Ohm internal resistance resolution
--  and difference of 0.5A increases the resolution to 2mOhms.
-- For 18650 Li-Ion cells toggling between 2.0A and 1.5A has worked well.
-- For coin cells, choose a profile with average discharge current that will discharge the
--  battery in roughly 30 days. Average = (Battery capacity / 30*24 ). 4mA for 1s and 300uA
--  for 179s is a good profile for CR2032 and 4mA for 1s and 780uA for 179s is a good profile for CR2450.
-- For Alkaline, 160mA for 30s and 80mA for 90s is a good profile.
-- Change the profile so it fits your application

-- For sub-1 Ohm measurements it is strongly recommended to connect the Sense+ pin to the
--  positive battery terminal (in current sink mode 4wire cannot make use of Sense-)

local currlow = 300e-6
local timelow = 179.0
local currhigh = 4e-3
local timehigh = 1.0
local start_voltage = 3.0
local min_ocv_voltage = 2.0
local fourwire = false

local max_iterations = 20000

-- Additional hard cutoff if OCV does not drop below min_ocv_voltage
-- (current sinking is disabled when voltage drops below 0.5V)
local cutoff_voltage = 0.6

local battery = {   model = "Insert model here",
                    manufacturer = "Insert manufacture here",
                    voltage = start_voltage,
                    cutoffvoltage = min_ocv_voltage,
                    voltageunit = "V",
                    maxtemperature = 60,
                    worktemperature = 20,
                    mintemperature = -20,
                    temperatureunit = "°C",
                    size = "Insert size here",
                    sizeunit = "mm",
                    weight = 0,
                    weightunit = "g",
                    capacity = 0,
                    capacityunit = "mAh" }

-- Current load profile
local current_profile = {{
  current = currhigh,  -- A
  duration = timehigh, -- s
}, {
  current = currlow,  -- A
  duration = timelow, -- s
}}

local currdiff = currhigh - currlow
local recstart = os.date("%Y%m%d%H%M%S")
local recstop = {}

local table = {}

local actualocv = {}

-- Open active project if exists,
-- otherwise create a new project.
local project = otii.get_active_project()
if not project then
    project = otii.create_project()
    assert(project, "cannot create project")
end

-- Open all available Arc devices.
local devices = otii.get_devices("Arc")
assert(#devices > 0, "No available devices")
local arc = {}
local logs = {}
for i, _ in ipairs(devices) do
  arc[i] = otii.open_device(devices[i].id)
  assert(arc[i] ~= nil, "Error opening Arc device")
  local logpath = string.format("%s/batteries/Log-%d-%s-%s.txt", otii.get_otii_dir(), i, devices[i].name, recstart)
  logs[i] = io.open(logpath, "w")
  assert(logs[i] ~= nil, "Cannot create file " .. logpath)
  arc[i]:calibrate()
  otii.msleep(1000)
  arc[i]:set_power_regulation("current")
  arc[i]:set_main_current(0)
  arc[i]:set_max_current(5.0)
  arc[i]:enable_4wire(fourwire)
  arc[i]:enable_channel("mv", true)
  arc[i]:enable_channel("mc", true)
  arc[i]:set_battery_profile(current_profile)
  arc[i]:enable_main(true)
  otii.msleep(5)
  local fourwiretext = string.format("%s: 4-wire mode is %s", devices[i].name, arc[i]:get_4wire())
  otii.writeln(fourwiretext)
  logs[i]:write(fourwiretext, "\n")
  local profiletext = string.format("Discharge profile %.3f A for %.3f s, %.3f A for %.3f s", currhigh, timehigh, currlow, timelow)
  logs[i]:write(profiletext, "\n")
  table[i] = {}
end

function cleanup()
  project:stop()
  project:enable_main_power(false)
  for i, _ in ipairs(devices) do
    arc[i]:enable_battery_profiling(false)
    arc[i]:set_main_current(0)
    arc[i]:set_power_regulation("voltage")
    arc[i]:close()

    if recstop[i] == nil then recstop[i] = os.date("%Y%m%d%H%M%S") end
    local curlowentry = { current = currlow * 1000, time = timelow }
    local curhighentry = { current = currhigh * 1000, time = timehigh }
    local dischargeprofile = { low = curlowentry, high = curhighentry }
    local dischargetable = {current = 0, currentunit = "mA", dischargeprofile = {dischargeprofile}, table = table[i], starttime = recstart, stoptime = recstop[i]}
    battery.capacity = math.floor(100*table[i][#table[i]].capacity+0.5)/100
    local output = { battery = battery, dischargetables = {dischargetable} };
    otii.writeln(json.encode(output, { indent = true }));

    local battery_profile_path = string.format("%s/batteries/dischargeprofiles/Batt-profile-%s-%dmA-%d-%s-%s.json", otii.get_otii_dir(), battery.model, math.floor(currhigh*1e3), i, devices[i].name, recstart)
    local file = io.open(battery_profile_path, "w")
    assert(file ~= nil, "Cannot create file " .. battery_profile_path)
    file:write(json.encode(output, {indent = true}))
    file:close()
    logs[i]:close()
  end
  project:save(string.format("%s/batteries/dischargeprofiles/%s-%dmA-%s.otii", otii.get_otii_dir(),battery.model, math.floor(currhigh*1e3), recstart))
end

otii.on_stop(cleanup)

otii.writeln("Otii Arc battery discharge profiling")

project:start()

-- Retrieve actual OCV, then enable profiling
for i, _ in ipairs(devices) do
  actualocv[i] = arc[i]:get_value("mv")
  local ocvtext = string.format("%s: Measured initial OCV=%.3f V", devices[i].name, actualocv[i])
  otii.writeln(ocvtext)
  logs[i]:write(ocvtext, "\n")
  logs[i]:write("VoltLowLoad, VoltHighLoad, OCV, IntRes, mAh, CalcmAh\n")
  arc[i]:enable_battery_profiling(true)
end

local iteration = {}
local laststepvalue = {}
local voltageok = {}
for i, _ in ipairs(devices) do
  iteration[i] = -1
  laststepvalue[i] = 0
  voltageok[i] = 1
end

-- Iterate until exit condition is met
while true do
  local alldone = 1
  for i, _ in ipairs(devices) do
    if voltageok[i] == 1 then
      logs[i]:flush()
      alldone = 0
      local battery_data = arc[i]:wait_for_battery_data(600)
      if battery_data ~= nil then
        if battery_data.iteration ~= iteration[i] then
          if battery_data.iteration >= max_iterations then
            alldone = 1
            break
          end
          iteration[i] = battery_data.iteration
          otii.writeln(string.format("Arc %d Iteration %d", i, iteration[i] + 1))
        end

        otii.writeln("step" .. battery_data.step .. " " .. battery_data.voltage .. " " .. battery_data.discharge)
        if battery_data.step == 0 then
          laststepvalue[i] = battery_data.voltage
        else
          local voltdiff = battery_data.voltage - laststepvalue[i]
          local intres = voltdiff / currdiff
          local ocv = battery_data.voltage + intres * currlow
          if ocv > actualocv[i] then
            otii.writeln(string.format("%s: Clamping calculated OCV to initial measured OCV (%.3f V > %.3f V)", devices[i].name, ocv, actualocv[i]))
            ocv = actualocv[i]
            -- Reduce intres here?
          end
          local mAh = battery_data.discharge * 1000 / 3600
          local calcmAh = tonumber(string.format("%.3f", (iteration[i] + 1) * 1000 * (currhigh*timehigh + currlow*timelow) / 60 / 60))
          otii.writeln(string.format("%s: OCV=%.3f V Res=%.3f Ohm (%.3f %.3f) %.3fmAh %.3f", devices[i].name, ocv, intres, battery_data.voltage, laststepvalue[i], mAh, calcmAh))
          logs[i]:write(string.format("%.3f, %.3f, %.3f, %.3f, %.3f, %.3f\n", battery_data.voltage, laststepvalue[i], ocv, intres, mAh, calcmAh))

          local ltable = table[i]
          ltable[#ltable + 1] = {
            voltage = tonumber(string.format("%.3f", ocv)),
            resistance = tonumber(string.format("%.3f", intres)),
            capacity = mAh
          }

          if ocv < min_ocv_voltage then
            otii.writeln("OCV (" .. tostring(ocv) .. ") below min voltage (" .. tostring(min_ocv_voltage) .. "), stopping measurement")
            voltageok[i] = 0
          end
        end

        if battery_data.voltage < cutoff_voltage then
          otii.writeln("Voltage (" .. tostring(battery_data.voltage) .. ") below cut-off voltage (" .. tostring(cutoff_voltage) .. "), stopping measurement")
          voltageok[i] = 0
        end

        if voltageok[i] == 0 then
          arc[i]:enable_battery_profiling(false)
          arc[i]:set_main_current(0)
          recstop[i] = os.date("%Y%m%d%H%M%S")
        end
      end
    end
  end
  if alldone == 1 then
    break
  end
end

cleanup()
generated by LDoc 1.4.6 Last updated 2021-10-13 21:46:39