Jump to content

EScribe Custom Battery Test Python Script


Recommended Posts

Recently I was asked if I would test a couple of AOSO 18650 batteries and give an opinion by one of the organisers on the London and South East Vapers vape meet.  They had already had the usual pulsed constant current tests and other people had reported puff counts and mWh for their normal use pattern. I wanted something that was informative for vapers, nobody vapes at a constant current.  The efficiency of the board also has an effect, here is the current a battery will have to supply for 25,50 and 75 watts for a mod with 85% efficiency like the DNA 75, but the DNA  75 low cut-off is a quarter volt higher so max current would be 32 A.  I have removed the start of the lines as even the best batteries cannot supply that current without that much sag.

[battery_current_85_efficiency] 

The EScribe battery analyser is more realistic as it is pulsed constant power, far more like the way people vape, but with 10 second vape and 30 second gaps you can build up a lot of heat at 75 W.  Looking at https://www.ecigstats.org/global-stats/ average puff time is under 3 seconds, so I though I could do something with the IronPython scripting in EScribe Device Monitor.  This has the additional benefits that anybody with a DNA 75, EScribe V1.2SP5+, a spare dripper deck and some Kanthal can use for free. We could also share results, repeat other's tests to verify them, load them in Device Monitor (or many other apps that handle csv data) to look at details and compare them. (BTW If you record the EScribe Battery Analyzer test you can also load these into Device Monitor)


From the start I wanted puff time, gap and power to be easily changeable settings and to set a max board temp limit, but the more I tested the more I added to my wish list in terms flexibility and what I wanted to measure.  If you look at the script you can see the settings at the top.  With testing at 75 W some of the batteries could not keep doing 3 s puff at full power for very long, but still kept going and 90% for quite a while, so I made that measurement part of the test as well, actually you can add a list of decreasing percentages to record now.  I started testing at 75 W, 3 s puff, 57 s gap and found that after the battery couldn't supply enough power they still had a lot of charge that took far too long to test with 3 s puff every minute.  The solution I came up with was to increase the length of the puff so it still drew the same amount of energy and while not exact it works very well.  Like the limp home mode on your car this is using the DNA's soft limiting where you can at least get something to for your nic craving.  Testing this one battery I saw do this for a long time before doubling the length of the puff so I record that as well.  You can see the DNA's soft limiting in action in Device Monitor when the battery drops to the low cut-off voltage (default 2.75 V) the output power is lowered.

So the test has 3 stages:
1 measure puffs where the battery can supply full power.
2 measure puffs where the battery can supply a given percentage of full power.
3 increasing puff time to compensate for lower watts just noting the point where puff length doubles (I think most people would have stopped vaping by then) and the remaining power which is not really useful for vaping.
(Stage 1 & 2 are exact tests, stage 3 is a little fuzzier just because there is no way to tell the board to take x watt hours. I just extend the puff time by the proportion it fell short on the previous puff as it varies between batteries which is fair and consistent, but I think vapers would be taking more energy at this point.)

Here is a old 25R that is near the end of its life.

[demo1]   

Here are some examples of puffs at different stages (all a AOSO 42 A, 75 W 3 s puff, 57 s gap).

[puff_stages1] 

The first is a fully charged battery, 2nd is the last puff before the sag hits the low cut off. The 3rd it hits low cut off in the middle off the puff, note the yellow line falling. The 4th & 5th the sag is at the limit for the whole puff, you would have swapped the battery or charge by now if you could, but could still get a weaker vape.

[puff_stages2] 

The 2nd pic of puff is where the test is lengthening the puff time trying to keep the yellow energy line at the same level as for the full power puff.


I saves 2 files, the csv that EScribe records and a text file with the summary shown in the message window.

[msg] 

How to run the test
I want to make it clear that this is what is working for me and provided in all good faith, it is your responsibility asses risks and to ensure your personal safety, the safety of anyone or anything else that could be affected, basically only use at your own risk I take no responsibility for the fitness or safety of the script or the process.  Do not use it if you don agree with these conditions.

Make a resistance below 0.5 ohm on your dipper deck as you would for Battery Analyser, I use 4 parallel loops of 0.5 mm Kanthal about the diameter of a tennis ball.  I also you a couple of old computer fans to provide extra cooling at high powers. Make sure the mod is stable and the coils are safely away form kids, pets and anything that could be damaged by heat.

[setup] 

Copy the script into a text file, save and change the file extension to py, I chose to do it this way so it can not pass malware and you can see it before you put it on your PC.  Open the file in a text editor like notepad++ or notepad and adjust the settings at the top, they should be pretty self explanatory.  Set your computers power saving settings so it will not sleep, hibernate or shut down USB for the duration of the test.  Open Device Monitor, click Diagnostics - Disable USB Charging, set the Graph Options - Time Scale to something sensible, 60 seconds to and hour 3600 seconds.  Then click Diagnostics - Run Script and find the file, that's it a message box will pop up when it finishes, close Device Monitor if you want to stop it running.


[CODE]
# DNA 75 Battery test script for use in EScribe V1.2SP5 Device Monitor.
# Disable USB Charging in Device Monitor before running. 
# Ensure your PC will not go into sleep mode while running.
# At high powers your mod and battery can get very hot, take care.
# If the board temp is at the max setting after a rest period the script will keep resting until is cools.
# USE AT YOUR OWN RISK
#
# Thanks to Evolv for providing the free tools to do this.
# Written by VapingBad

# test settings, things you will probably want to customise
batteryModel = '25R'
nominalCellVoltage = 3.7
puffPower = 50
puffTime = 4
restTimeBetweenPuffs = 46
dataCollectionPoints = [90,80]
outputFileFolderPath = r'C:\data'   # no trailing slash
maxPuffTime = 20
maxNumOfMaxTimePuffs = 6
# safety cut-offs
maxBoardTempF = 125
minBatteryVoltsAfterRest = 2.8
maxPuffCount = 1000

# DO NOT ALTER AFTER THIS POINT
# DO NOT ALTER AFTER THIS POINT
# DO NOT ALTER AFTER THIS POINT
import time
import datetime
from System.IO import Path

# set up EScribe Device Monitor tracking, fields plotted and saved in the csv file
if Recorder.IsRecording: 
  Recorder.StopRecording()
ECig.ClearTracking()
ECig.Track('Power')
ECig.Track('Battery Pack')
ECig.Track('Last Puff Energy')
ECig.Track('Last Puff Time')
ECig['Power Set'] = puffPower;
time.sleep(2)

# Homer Simpson guard clauses
if minBatteryVoltsAfterRest < 2.75: 
  minBatteryVoltsAfterRest = 2.75
if maxBoardTempF > 200.0: 
  maxBoardTempF = 200.0
if puffTime > 20.0: 
  puffTime = 20.0
if restTimeBetweenPuffs < 2 * puffTime: 
  restTimeBetweenPuffs = 2 * puffTime
if maxPuffTime < puffTime: 
  maxPuffTime = puffTime
if maxPuffTime > 20.0: 
  maxPuffTime = 20.0

# conversion factors and constants
secondsInAnHour = 3600.0
milliToBaseUnitConversionFactor = 0.001 
lossCompensationFactor = 1.0/0.85
wattHourToMilliAmpHourFactor = 1000.0/nominalCellVoltage
wiggleRoomFactor = 0.98 

# classes
class puffLogger:
  def __init__(self, lable):
    self.lable = lable
  puffs = 0
  energy = 0.0
  volts = 0.0
  test = None
  next = None
  def copyReadings(self, fromThis):
    self.puffs = fromThis.puffs
    self.energy = fromThis.energy
    self.volts = fromThis.volts
  def checkPuff(self, puffEnergy, data):
    if self.test(puffEnergy):
      self.copyReadings(data)
      return self
    else:
      return self.next
 
class compensatingPuffTime:
  def __init__(self, setTime, maxTime = 20.0, maxExtPuffs = 5):
    self.setTime = float(setTime)
    self.maxTime = maxTime
    self.maxExtPuffs = maxExtPuffs
  expectedPuffEnergy = 0.0
  increasedPuffCount = 0
  increaseFactor = 1.0
  reachedLimit = False
  timeDoubledData = puffLogger('Up to double length puff')
  def puffTime(self):
    return self.setTime * self.increaseFactor
  def calcNextPuff(self, lastPuffEnergy, allPuffsData):
    factor = self.increaseFactor * self.expectedPuffEnergy / lastPuffEnergy
    if self.setTime * factor > self.maxTime:
      factor = float(self.maxTime) / self.setTime
      if self.increasedPuffCount >= self.maxExtPuffs:
        self.reachedLimit = True
      else:
        self.increasedPuffCount += 1
    if factor < 1.0:
      factor = 1.0
    if factor > 2.0 and self.timeDoubledData.puffs == 0:
      self.timeDoubledData.copyReadings(allPuffsData)
    self.increaseFactor = factor
  
# variables
date = datetime.datetime.now()
expectedPuffEnergy =  puffPower * puffTime / secondsInAnHour #* wiggleRoomFactor
puffTimeCompensator = compensatingPuffTime(puffTime, maxPuffTime, maxNumOfMaxTimePuffs)
puffTimeCompensator.expectedPuffEnergy = expectedPuffEnergy
globalLogger = puffLogger('Battery capacity')
fullPowerLogger = puffLogger("At full power")
fullPowerLogger.test = lambda x: x >= expectedPuffEnergy * wiggleRoomFactor
# chain puff tests together, when the test fails the next test will take it's place
previous = fullPowerLogger
for p in dataCollectionPoints:
  dp = puffLogger('Up to {}% power'.format(p))
  dp.test = lambda x, y = p: x >= expectedPuffEnergy * wiggleRoomFactor * y / 100
  previous.next = dp
  previous = dp

# make timestamped file names
fileName = 'BatteryTest {0} {1}'.format(batteryModel, '{:%Y%m%d-%H%M}'.format(date))
outputFileNoExtension = Path.Combine(outputFileFolderPath, fileName)
csvFilePath = '{0}.csv'.format(outputFileNoExtension)
txtFilePath = '{0}.txt'.format(outputFileNoExtension)

# ACTUAL BATTERY TEST  
Recorder.Record(csvFilePath)
initialVoltage = ECig['Battery Pack']
currentLogger = fullPowerLogger

while True: 
  if ECig['Board Temperature'] >= maxBoardTempF:
    time.sleep(restTimeBetweenPuffs)
    continue
  ECig.Puff(puffTimeCompensator.puffTime())
  time.sleep(restTimeBetweenPuffs)
  # there is a delay before this value is updated
  lastPuffEnergy = ECig['Last Puff Energy'] * milliToBaseUnitConversionFactor
  globalLogger.volts = ECig['Battery Pack']
  globalLogger.puffs += 1
  globalLogger.energy += lastPuffEnergy 
 
  if currentLogger is not None:
    currentLogger = currentLogger.checkPuff(lastPuffEnergy, globalLogger)
  if currentLogger is None:
    puffTimeCompensator.calcNextPuff(lastPuffEnergy, globalLogger)
  
  if globalLogger.volts <= minBatteryVoltsAfterRest or globalLogger.puffs >= maxPuffCount or puffTimeCompensator.reachedLimit:
    break

if Recorder.IsRecording:
  Recorder.StopRecording()

# format, save and show result summary
resultText = 'Battery test {} {:%c}\nInitial voltage {:.2f} V, Nominal voltage {:.2f} V\nTest: {} W, {} s puffs, {} s rest periods:\n'.format(batteryModel, date, initialVoltage, nominalCellVoltage, puffPower, puffTime, restTimeBetweenPuffs)
writeResult = lambda r: '   {}:\n     Puffs = {}\n     Energy = {:.3f} Wh, {} mAh\n     Resting Voltage = {:.2f} V\n'.format(r.lable, r.puffs, r.energy*lossCompensationFactor, int(r.energy*lossCompensationFactor*wattHourToMilliAmpHourFactor), r.volts)
p = fullPowerLogger
while p is not None:
  resultText += writeResult(p)
  p = p.next
resultText += writeResult(puffTimeCompensator.timeDoubledData)
resultText += writeResult(globalLogger)

with open(txtFilePath, 'w') as file:
  file.write(resultText)

UI.Message(resultText)
[/CODE]

Link to comment
Share on other sites

@VapingBad Let me be the first to say thank you for this. Very helpful, and such clean code. Just looking at your variable names I imagine that you always intended to publish it. Looking at the escribe stats, I'll probably do this myself for my batteries, and my 'mean' for puff seconds, and board efficiency for the 200 vs 75. The stuff I write for myself looks like crap. Is there documentation showing a list of all fields available to use. Please and thanks.

Edit: You should actually share this with Mooch, it's so good. IMHO, as beneficial as his simulated testing.

Link to comment
Share on other sites

Thank's Wayneo, there is a pdf with 1.2SP5 in the EScribe program folder called EScribe.pdf %appdata%\Evolv\EScribe\Escribe.pdf  I hadn't done any python before I read that.  The only tricky bit was a closure using lambdas in a loop, it was easy to tell that was the issue, harder to solve, used the default argument ( lambda x,y=p: ) to force it to evaluate in the loop.

You would need to change

lossCompensationFactor = 1.0/0.85

to
lossCompensationFactor = 1.0/0.98

for the DNA200, you could monitor each cell individually as well
Link to comment
Share on other sites

Changes for V2.1
  • Added a field to capture battery sag 0.1 seconds before the end of the last puff for each stage.
  • Added setting doExtendPuffTimes to enable/disable the variable puff time feature.
  • Changed the file name format to include the power, puff time and rest time EG BatteryTest 25R 50W-4-46 20161220-1039
  • Added link to this thread in comments.
  • Changed to allow rest time to equal puff time, previously it had to be double.
  • Changed the wiggleRoomFactor from 0.98 to 0.99, this just allows a tiny tolerance when checking if a stage is complete.
  • Fixed bug the sleep rest time, I should have added puff time.


I have used the script to discharge batteries for storage with these setting
[CODE]puffPower = 20
puffTime = 2
restTimeBetweenPuffs = 2
doExtendPuffTimes = False
minBatteryVoltsAfterRest = 3.7
[/CODE]

V2.1
[CODE]# DNA 75 Battery test script for use in EScribe V1.2SP5 Device Monitor.
# Disable USB Charging in Device Monitor before running.
# Ensure your PC will not go into sleep mode while running.
# At high powers your mod and battery can get very hot, take care.
# If the board temp is at the max setting after a rest period the script will keep resting until is cools.
# USE AT YOUR OWN RISK
#
# Thanks to Evolv for providing the free tools to do this.
# Written by VapingBad V2.1
# Thread https://forum.evolvapor.com/topic/66169-topic/

# test settings, things you will probably want to customise
puffPower = 50
puffTime = 4
restTimeBetweenPuffs = 46
doExtendPuffTimes = True
dataCollectionPoints = [90,80]
batteryModel = '25R'
nominalCellVoltage = 3.7
outputFileFolderPath = r'C:\data'   # no trailing slash
maxPuffTime = 20
maxNumOfMaxTimePuffs = 6
# safety cut-offs
maxBoardTempF = 125
minBatteryVoltsAfterRest = 2.8
maxPuffCount = 1000

# DO NOT ALTER AFTER THIS POINT
# DO NOT ALTER AFTER THIS POINT
# DO NOT ALTER AFTER THIS POINT
import time
import datetime
from System.IO import Path

# set up EScribe Device Monitor tracking, fields plotted and saved in the csv file
if Recorder.IsRecording:
  Recorder.StopRecording()
ECig.ClearTracking()
ECig.Track('Power')
ECig.Track('Battery Pack')
ECig.Track('Last Puff Energy')
ECig.Track('Last Puff Time')
ECig['Power Set'] = puffPower;
time.sleep(2)

# Homer Simpson guard clauses
if minBatteryVoltsAfterRest < 2.75:
  minBatteryVoltsAfterRest = 2.75
if maxBoardTempF > 200.0:
  maxBoardTempF = 200.0
if puffTime > 20.0:
  puffTime = 20.0
if restTimeBetweenPuffs < puffTime:
  restTimeBetweenPuffs = puffTime
if maxPuffTime < puffTime:
  maxPuffTime = puffTime
if maxPuffTime > 20.0:
  maxPuffTime = 20.0

# conversion factors and constants
secondsInAnHour = 3600.0
milliToBaseUnitConversionFactor = 0.001
lossCompensationFactor = 1.0/0.85
wattHourToMilliAmpHourFactor = 1000.0/nominalCellVoltage
wiggleRoomFactor = 0.99

# classes
class puffLogger:
  def __init__(self, lable):
    self.lable = lable
  puffs = 0
  energy = 0.0
  volts = 0.0
  sag = 0.0
  test = None
  next = None
  def copyReadings(self, fromThis):
    self.puffs = fromThis.puffs
    self.energy = fromThis.energy
    self.volts = fromThis.volts
    self.sag = fromThis.sag
  def checkPuff(self, puffEnergy, data):
    if self.test(puffEnergy):
      self.copyReadings(data)
      return self
    else:
      return self.next
 
class compensatingPuffTime:
  def __init__(self, setPuffTime, maxTime = 20.0, maxExtPuffs = 6):
    self.setPuffTime = float(setPuffTime)
    self.maxTime = maxTime
    self.maxExtPuffs = maxExtPuffs
  expectedPuffEnergy = 0.0
  increasedPuffCount = 0
  increaseFactor = 1.0
  reachedLimit = False
  timeDoubledData = puffLogger('Up to double length puff')
  def puffTime(self):
    return self.setPuffTime * self.increaseFactor
  def calcNextPuff(self, lastPuffEnergy, allPuffsData):
    factor = self.increaseFactor * self.expectedPuffEnergy / lastPuffEnergy
    if self.setPuffTime * factor > self.maxTime:
      factor = float(self.maxTime) / self.setPuffTime
      if self.increasedPuffCount >= self.maxExtPuffs:
        self.reachedLimit = True
      else:
        self.increasedPuffCount += 1
    if factor < 1.0:
      factor = 1.0
    if factor > 2.0 and self.timeDoubledData.puffs == 0:
      self.timeDoubledData.copyReadings(allPuffsData)
    self.increaseFactor = factor
 
# variables
date = datetime.datetime.now()
timeToReadSagBeforeEndOfPuff = 0.1
expectedPuffEnergy =  puffPower * puffTime / secondsInAnHour #* wiggleRoomFactor
puffTimeCompensator = compensatingPuffTime(puffTime, maxPuffTime, maxNumOfMaxTimePuffs)
puffTimeCompensator.expectedPuffEnergy = expectedPuffEnergy
globalLogger = puffLogger('Battery capacity')
fullPowerLogger = puffLogger("At full power")
fullPowerLogger.test = lambda x: x >= expectedPuffEnergy * wiggleRoomFactor
# chain puff tests together, when the test fails the next test will take it's place
previous = fullPowerLogger
for p in dataCollectionPoints:
  dp = puffLogger('Up to {}% power'.format(p))
  dp.test = lambda x, y = p: x >= expectedPuffEnergy * wiggleRoomFactor * y / 100
  previous.next = dp
  previous = dp

# make timestamped file names
fileName = 'BatteryTest {} {}W-{}-{} {}'.format(batteryModel, puffPower, puffTime, restTimeBetweenPuffs, '{:%Y%m%d-%H%M}'.format(date))
outputFileNoExtension = Path.Combine(outputFileFolderPath, fileName)
csvFilePath = '{}.csv'.format(outputFileNoExtension)
txtFilePath = '{}.txt'.format(outputFileNoExtension)

# ACTUAL BATTERY TEST  
Recorder.Record(csvFilePath)
initialVoltage = ECig['Battery Pack']
currentLogger = fullPowerLogger

while True:
  if ECig['Board Temperature'] >= maxBoardTempF:
    time.sleep(restTimeBetweenPuffs)
    continue
  ECig.Puff(puffTimeCompensator.puffTime())
  time.sleep(puffTimeCompensator.puffTime() - timeToReadSagBeforeEndOfPuff)
  globalLogger.sag = globalLogger.volts - ECig['Battery Pack']
  time.sleep(restTimeBetweenPuffs)
  lastPuffEnergy = ECig['Last Puff Energy'] * milliToBaseUnitConversionFactor
  globalLogger.volts = ECig['Battery Pack']
  globalLogger.puffs += 1
  globalLogger.energy += lastPuffEnergy
 
  if currentLogger is not None:
    currentLogger = currentLogger.checkPuff(lastPuffEnergy, globalLogger)
  if doExtendPuffTimes and currentLogger is None:
    puffTimeCompensator.calcNextPuff(lastPuffEnergy, globalLogger)
 
  if globalLogger.volts <= minBatteryVoltsAfterRest or globalLogger.puffs >= maxPuffCount or puffTimeCompensator.reachedLimit:
    break

if Recorder.IsRecording:
  Recorder.StopRecording()

# format, save and show result summary
resultText = 'Battery test {} {:%c}\nInitial voltage {:.2f} V, Nominal voltage {:.2f} V\nTest: {} W, {} s puffs, {} s rest periods:\n'.format(batteryModel, date, initialVoltage, nominalCellVoltage, puffPower, puffTime, restTimeBetweenPuffs)
writeResult = lambda r: '   {}:\n     Puffs = {}\n     Energy = {:.3f} Wh, {} mAh\n     Max Voltage Sag = {:.3f} \n     Resting Voltage = {:.2f} V\n'.format(r.lable, r.puffs, r.energy*lossCompensationFactor, int(r.energy*lossCompensationFactor*wattHourToMilliAmpHourFactor), r.sag, r.volts)
p = fullPowerLogger
while p is not None:
  resultText += writeResult(p)
  p = p.next
resultText += writeResult(puffTimeCompensator.timeDoubledData)
resultText += writeResult(globalLogger)

with open(txtFilePath, 'w') as file:
  file.write(resultText)

UI.Message(resultText)[/CODE]
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...