Help with reliable way to measure battery capacity left

Hi all!

I have built an e-scooter from a regular one!

I am now trying to add a current sensor, so that I can measure current draw on the battery. I am not sure how to come up with a reliable and steady way to measure battery capacity left, with voltage and current though. I feel like it's very simple, but I just can't seem to figure it out for some reason :man_facepalming:.

I do have a version of battery percentage left, miles left, and minutes left, but it is unreliable due to voltage drop when using the motor.

Thanks in advance for any ideas!

My current code:

#include "DFRobot_GDL.h"
#include <XPT2046_Touchscreen.h>
#include <SPI.h>
#include "DFRobot_GNSS.h"
#include "DFRobot_BMI160.h"
#include "DFRobot_AHT20.h"
#include "DFRobot_GR10_30.h"
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>
#include "settings.h"

//Testing
bool testing = false;

//Voltage Conversion for Speed
bool voltconversion = true;

// PWM properties for motor driver
const int freq = 5000;
const int speedChannel = 0;
const int resolution = 8;

//throttle
int throttleoffvalue = 0;
int throttleonvalue = 4095;
int throttleval = 0;
int lastthrottleval = 0;

//Touch coordinates for calibration
#define touchxmin 290
#define touchxmax 3750
#define touchymin 300
#define touchymax 3700
#define backcolor COLOR_RGB565_WHITE
#define erasecolor backcolor//COLOR_RGB565_LGRAY
#define backgroundcolor COLOR_RGB565_CYAN

//gnss / display
long readgnssinc = 1000;//time between GNSS readings
unsigned long lastreadgnss = 0;
long readdisplayinc = 1000;//time between display updates
unsigned long lastdisplay = 0;
sTim_t utc;
sTim_t date;
sLonLat_t latitude;
sLonLat_t longitude;
double altitude;
uint8_t satelites;
double knots;
double course;

//AHT20
float temperature = 0;
float humidity = 0;
float HI = 0;

//WS2812
#define NUMPIXELS 14
float lastcoursereading = 0;
float coursechangetime = 1000;
float lastcoursereadtime = 0;
float courseturnsignalinc = 10; //degrees of turning for turning signal to turn on
float coursechange = 0;
float lastspeedreading = 0;
float speedchangetime = 1000;
float lastspeedreadtime = 0;
float speedbrakeinc = -0.08;//decrease in speed before brake light turns on
int brake[] = {5,6,7,8,9};
int footlight[] = {10,11,12,13};
unsigned long startmillis = 0;
int animationcountleft = 1;
int animationcountright = 1;
bool leftturnsignal = false;
bool rightturnsignal = false;
float speeddiferrence = 0;
bool headlight = false;
int panimationcount = 0;
bool pup = true;
bool ledsoff = false;

//Password
bool locked = false;//true
uint16_t gestures;
int passwordlength = 3;
#define EEPROM_SIZE passwordlength
int passc = 0;//pass count

//battery
float batteryvolts = 0;
int batteryreadings = 10;//number of readings to average, more for a smoother more reliable response
long readbatteryint = 20;//time in milliseconds between battery readings
unsigned long lastbatreading = 0;
long R1 = 15000;//15k ohms
long R2 = 1000;//1.1k ohms
int curbatread = 0;
int batsum = 0;
#define batteryamps 5
#define batteryvoltage 36 //voltage and amp hours of battery, or uncomment "batterywattage" below instead
//#define batterywattage 180
#define motorwattage 350
#define maxcruisespeed 15 //mph //20

//gyro
float lastgyroreading = 0;
float readgyroint = 1;
float curgyroread = 0;
float gyroreadings = 5;
float gyrosum = 0;
float gyroy = 0;

//current sensor
int currentreadings = 10;//number of readings to average, more for a smoother more reliable response
long readcurrentint = 20;//time in milliseconds between battery readings
unsigned long lastcurreading = 0;
int curcurread = 0;
int cursum = 0;
float current = 0;                // the average
float watts = 0;

bool lastebrakeon = false;
unsigned long lastebrakechange = 0;
bool ebrakeon = false;
bool lockbtnon = false;
bool lastcruiseon = false;
unsigned long lastcruisechange = 0;
bool cruiseon = false;
float cruisesetspeed = 0;
int motorvalue = 0;

int previousotherx1 = 0;
int previousothery1 = 0;
int previousotherx2 = 0;
int previousothery2 = 0;

bool stole = false;
float speedlimit = 0.00;
bool lastturnstat = false;

XPT2046_Touchscreen ts(TOUCH_CS);
DFRobot_ILI9341_240x320_HW_SPI screen(/*dc=*/TFT_DC,/*cs=*/TFT_CS,/*rst=*/TFT_RST);
DFRobot_GNSS_I2C gnss(&Wire,GNSS_DEVICE_ADDR);
DFRobot_BMI160 bmi160;
const int8_t i2c_addr = 0x69;
DFRobot_AHT20 aht20;
DFRobot_GR10_30 gr10_30(/*addr = */GR10_30_DEVICE_ADDR, /*pWire = */&Wire);
Adafruit_NeoPixel pixels(NUMPIXELS, LEDPIN, NEO_GRB + NEO_KHZ800);
//WiFiMulti wifiMulti;

//BMI160
struct dat{
  float x = 0;
  float y = 0;
  float z = 0;
};
dat accel;
struct data{
  float x = 0;
  float y = 0;
  float z = 0;
};
data gyro;
float baseyangle = 0;

void setup() {
  pinMode(speedpin, OUTPUT);
  pinMode(27, INPUT_PULLUP);
  pinMode(cruisepin, OUTPUT);
  digitalWrite(cruisepin, LOW);

  if (voltconversion){
    digitalWrite(speedpin, HIGH);
  }
  else {
    digitalWrite(speedpin, LOW);
  }
  
  Serial.begin(115200);

  pixels.begin();
  pixels.clear();
  pixels.show();

  for (int i=0;i<4;i++){
    pixels.setPixelColor(footlight[i], pixels.Color(255,255,0));
    pixels.show();
  }

  screen.begin();
  screen.setRotation(3);

  pinMode(rpmpin, INPUT);

  EEPROM.begin(EEPROM_SIZE);
  for (int i=0;i<passwordlength;i++){//read password from EEPROM
    password[i] = EEPROM.read(i);    
  }
  
  pinMode(batterypin, INPUT);
  pinMode(throttlepin, INPUT);
  pinMode(brakepin, OUTPUT);
  pinMode(rpmpin, INPUT);
  attachInterrupt(rpmpin, interruptRoutine, FALLING);
  ledcSetup(speedChannel, freq, resolution);
  ledcAttachPin(speedpin, speedChannel);
  ledcWrite(speedChannel, 0);

  if(!gnss.begin()){
    Serial.println("NO GPS device !");
    delay(1000);
  }

  gnss.enablePower();
  gnss.setGnss(eGPS_BeiDou_GLONASS);
  gnss.setRgbOn();

  if (bmi160.softReset() != BMI160_OK){
    Serial.println("bmi160 reset false");
    //while(1);
  }
  if (bmi160.I2cInit(i2c_addr) != BMI160_OK){
    Serial.println("bmi160 init false");
    //while(1);
  }
  readAccelGyro();
  baseyangle = gyro.y;
  //baseyangle = 20;

  uint8_t status;
  if((status = aht20.begin()) != 0){
    Serial.print("AHT20 sensor initialization failed. error status : ");
    Serial.println(status);
    delay(1000);
  }
  
  startGestureSensor();
  ts.begin();
  ts.setRotation(3);
  
  startDisplay();

}

void loop() {
  if ((millis() - lastreadgnss) > readgnssinc){
    readGNSS();
    lastreadgnss = millis();
  }
  if (!ledsoff){
    handleLeds();
  }
  else {
    pixels.clear();
    pixels.show();  
  }
  if (locked){
    lockedloop();
    if (satelites > 8){
      if (knots > 2){
        //ifttt_counter++;
      }
    }
    return;
  }
  if (ts.touched()){
    TS_Point p = ts.getPoint();
    handleTouch(abs(map(p.x,touchxmax,touchxmin,0,320)),abs(map(p.y,touchymax,touchymin,0,240)));
  }
  readAccelGyro();
  readGyro();
  readTempHum();
  readCurrent();
  readBattery();
  readThrottle();
  if (!rightturnsignal && !leftturnsignal){
    if (lastturnstat){
      startDisplay();
      lastturnstat = false;
    }
    if (((millis() - lastdisplay) > readdisplayinc) && (satelites != 0)){
      handleSlowDisplay();
      lastdisplay = millis();
    }
    handleFastDisplay();
    handleDisplayButtons();
    float handleAngleLineValue = (baseyangle*100) - (gyroy*100);
    handleAngleLine(handleAngleLineValue);
  }
  else {
    if (rightturnsignal){
      screen.fillRoundRect(15,38,290,195,12,backgroundcolor);
      screen.setTextSize(14);
      screen.setCursor(100,75);
      screen.setTextColor(COLOR_RGB565_RED);
      screen.print(">");
    }
    else if (leftturnsignal){
      screen.fillRoundRect(15,38,290,195,12,backgroundcolor);
      screen.setTextSize(14);
      screen.setCursor(100,75);
      screen.setTextColor(COLOR_RGB565_RED);
      screen.print("<");
    }
  }
  handleGestures();
  handleMotordriver();
  //handleSpeedLimit();
}

/*void onLockChange()  {
  if (lock == true){
    screen.fillRoundRect(15,50,80,50,12,backgroundcolor);
    screen.setTextSize(2);
    screen.setCursor(20,70);
    screen.setTextColor(COLOR_RGB565_RED);
    screen.print("Locked");
    screen.fillRoundRect(225,50,80,50,12,backgroundcolor);
    screen.setTextSize(2);
    screen.setCursor(235,70);
    screen.setTextColor(COLOR_RGB565_GREEN);
    screen.print("E ON");
    ebrakeon = true;
    locked = true;
  }
  if (lock == false){
    ebrakeon = false;
    locked = false;
  }
}*/

/*void onMaxspeedChange()  {
  speedlimit = maxspeed;
}

void handleSpeedLimit(){
  if (knots > speedlimit){
    throttleval = throttleval/2;
  }
}*/

void handleTouch(int x,int y){
  Serial.print("Touch X = " + String(x));
  Serial.println(" | Touch Y = " + String(y));

  if (x >= 215 && x <= 320){
    if (y < 45){
      changepassword();
    }
    else if (y >= 45 && y <= 104){
      if ((millis() - lastebrakechange) > 400){//wait 1 second between reading button as pressed
        if (knots < 5){
          ebrakeon = !ebrakeon;
          lastebrakechange = millis();
        }
      }
    }  
    else if (y >= 105 && y <= 160){
      if ((millis() - lastcruisechange) > 400){//wait 1 second between reading button as pressed
        cruiseon = !cruiseon;
        cruisesetspeed = knots;
        lastcruisechange = millis();
      }
    }  
  }
  else if (x >= 5 && x <= 105){
    if (y < 45){
      changepassword();
    }
    else if (y >= 45 && y <= 105){
      if (!lockbtnon){
        lockbtnon = true;
      }
      else {
        screen.setTextSize(2);
        screen.setCursor(15,115);
        screen.setTextColor(COLOR_RGB565_RED);
        screen.print("Unlock with gestures!");//display unlock warning
        delay(3000);
        screen.fillRect(14, 114, 290, 20, erasecolor);//erase display unlock warning
      }
    }
  }
}

void handleSlowDisplay(){
  //satelites
  screen.setTextSize(2);
  screen.setCursor(287,15);
  screen.fillRect(286, 13, 26, 18, erasecolor);//backcolor
  if (satelites <= 5){
    screen.setTextColor(COLOR_RGB565_RED);
  }
  else if (satelites > 5 && satelites <= 8){
    screen.setTextColor(COLOR_RGB565_ORANGE);
  }
  else if (satelites > 8){
    screen.setTextColor(COLOR_RGB565_GREEN);
  }
  screen.print(String(satelites));
  //Serial.println(satelites);

  //speed (knots)
  screen.setTextSize(4);
  if (knots >= 10){
    screen.setCursor(100,62);//118,62
    screen.fillRect(92, 58, 132, 37, erasecolor);//117, 57, 90, 30,
  }
  else {
    screen.setCursor(110,62);//118,62
    screen.fillRect(100, 58, 105, 37, erasecolor);//117, 57, 90, 30,
  }
  if (knots <= 1.00){
    screen.setTextColor(COLOR_RGB565_BLUE);
  }
  else if (knots > 1.00 && knots <= 12.00){
    screen.setTextColor(COLOR_RGB565_GREEN);
  }
  else if (knots > 12.00 && knots <= 19.00){
    screen.setTextColor(COLOR_RGB565_ORANGE);
  }
  else {
    screen.setTextColor(COLOR_RGB565_RED);
  }
  screen.print(knots);
  //if (knots >= cruisesetspeed+4.00){//reset cruise if current speed goes above cruisesetspeed + 4
  //  cruiseon = false;
  //}
  
  //temperature
  screen.setTextSize(2);
  screen.setCursor(12,215);
  screen.fillRect(11,214,55,17,erasecolor);
  screen.setTextColor(COLOR_RGB565_GREEN);
  screen.print(temperature,1);//temperature with only one decimal
  screen.drawCircle(64,209,2,COLOR_RGB565_GREEN);//degrees symbol

  //humidity
  screen.setTextSize(2);
  screen.setCursor(270,215);
  screen.fillRect(269,214,40,17,erasecolor);
  screen.setTextColor(COLOR_RGB565_GREEN);
  screen.print(humidity,0);//humidity with no decimals
  screen.print("%");//percentage symbol
  
  //battery
  screen.setTextSize(2);
  screen.setCursor(140,215);
  screen.fillRect(139,214,65,17,erasecolor);
  float batpercent = map(batteryvolts*10,300,420,0,100); //map battery voltage to a percentage value of 0 - 100
  batpercent = constrain(batpercent,0,100);
  if (batpercent > 80.0){
    screen.setTextColor(COLOR_RGB565_GREEN);
  }
  else if (batpercent <= 80.0 && batpercent > 50.0){
    screen.setTextColor(COLOR_RGB565_DGREEN);
  }
  else if (batpercent <= 50.0 && batpercent > 25.0){
    screen.setTextColor(COLOR_RGB565_YELLOW);
  }
  else if (batpercent <= 25.0 && batpercent > 12.0){
    screen.setTextColor(COLOR_RGB565_ORANGE);
  }
  else if (batpercent <= 12.0 && batpercent > 5.0){
    screen.setTextColor(COLOR_RGB565_RED);
  }
  else {
    screen.setTextColor(COLOR_RGB565_RED);
    batterywarning();
  }
  screen.print(batpercent,0);//battery percentage with no decimals
  screen.print("%");//percentage symbol

  //dist to empty; for 36V 5AH (180W) that would be (at 20mph) 8 miles or 0.4 hours
  float batwatts = 0;
  #ifdef batterywattage
    batwatts = batterywattage;
  #else
    batwatts = batteryvoltage * batteryamps;
  #endif
  float bathours = batwatts / motorwattage;
  float batmiles = maxcruisespeed * bathours;
  float batmilesleft = map(batteryvolts*10,300,420,0,batmiles*100);//map function cant map floating values
  batmilesleft = batmilesleft / 100;
  float bathoursleft = map(batteryvolts*10,300,420,0,bathours*100);//map function cant map floating values
  bathoursleft = bathoursleft / 100;
  float batminutesleft = map(bathoursleft*100,0,100,0,600);//map function cant map floating values
  batminutesleft = batminutesleft / 10;
  screen.setTextSize(3);
  screen.setCursor(75,187);
  screen.fillRect(74,186,198,24,erasecolor);
  screen.setTextColor(COLOR_RGB565_BLUE);
  screen.print(abs(batmilesleft),1);//miles with no decimals
  screen.print("m|");//miles symbol
  screen.print(abs(batminutesleft),1);//hours with two decimals
  screen.print("m");
  Serial.print("Battery: ");
  Serial.println(batteryvolts);
  Serial.print("Current: ");
  Serial.println(current);
}

void handleFastDisplay(){
  //Angle
  screen.setTextSize(2);
  screen.setCursor(140,95);
  screen.fillRect(139,94,75,17,erasecolor);
  screen.setTextColor(COLOR_RGB565_GREEN);
  screen.print((baseyangle*100)-(gyroy*100),1);//angle with only one decimal
}

void handleDisplayButtons(){
  //e-brake button
  if (ebrakeon != lastebrakeon){
    if (!ebrakeon){
      screen.fillRoundRect(220,45,90,60,12,erasecolor);//erase
      screen.drawRoundRect(225,50,80,50,12,backgroundcolor);
      screen.setTextSize(2);
      screen.setCursor(235,70);
      screen.setTextColor(COLOR_RGB565_RED);
      screen.print("E OFF");
    }
    else {
      screen.fillRoundRect(225,50,80,50,12,backgroundcolor);
      screen.setTextSize(2);
      screen.setCursor(235,70);
      screen.setTextColor(COLOR_RGB565_GREEN);
      screen.print("E ON");
    }
    lastebrakeon = ebrakeon;
  }
  if (!lockbtnon){
    screen.fillRoundRect(12,45,90,60,12,erasecolor);//erase
    screen.drawRoundRect(15,50,80,50,12,backgroundcolor);
    screen.setTextSize(1);
    screen.setCursor(25,70);
    screen.setTextColor(COLOR_RGB565_GREEN);
    screen.print("Unlocked");
  }
  else if (lockbtnon || locked){
    screen.fillRoundRect(15,50,80,50,12,backgroundcolor);
    screen.setTextSize(2);
    screen.setCursor(20,70);
    screen.setTextColor(COLOR_RGB565_RED);
    screen.print("Locked");
    ebrakeon = true;
    locked = true;
  }
  if (cruiseon != lastcruiseon){
    if (!cruiseon){
      screen.fillRoundRect(220,105,90,60,12,erasecolor);//erase
      screen.drawRoundRect(225,110,80,50,12,backgroundcolor);
      screen.setTextSize(2);
      screen.setCursor(235,130);
      screen.setTextColor(COLOR_RGB565_GREEN);
      screen.print("C OFF");
    }
    else {
      screen.fillRoundRect(225,110,80,50,12,backgroundcolor);
      screen.setTextSize(2);
      screen.setCursor(235,130);
      screen.setTextColor(COLOR_RGB565_ORANGE);
      screen.print("C ON");
    }
    lastcruiseon = cruiseon;
  }
}

void handleAngleLine(float angle1){  
  int x=160,y=140; //center point of the two lines

  screen.drawLine(x, y, previousotherx1, previousothery1, erasecolor);//line going right
  screen.drawLine(x+1, y+1, previousotherx1+1, previousothery1+1, erasecolor);//double thick line
  screen.drawLine(x, y, previousotherx2, previousothery2, erasecolor);//line going left
  screen.drawLine(x+1, y+1, previousotherx2+1, previousothery2+1, erasecolor);//double thick line

  #define DEG2RAD 0.0174532925
  float screenangle1 = 135; //value for right line to make it level
  float screenangle2 = 315; //value for left line to make it level; appears to be one line instead of two separate ones
  float angle2 = angle1;  
  int w=100,h=100; //length of each line; one going right and one going left
 
  #define linecolor COLOR_RGB565_NAVY
  float cosa1 = cos((screenangle1+angle1) * DEG2RAD), sina1 = sin((screenangle1+angle1) * DEG2RAD);
  int otherx1 = x - ((w * cosa1 / 2) - (h * sina1 / 2));
  int othery1 = y - ((h * cosa1 / 2) + (w * sina1 / 2));
  screen.drawLine(x, y, otherx1, othery1, linecolor);//line going right
  screen.drawLine(x+1, y+1, otherx1+1, othery1+1, linecolor);//double thick line
  float cosa2 = cos((screenangle2+angle2) * DEG2RAD), sina2 = sin((screenangle2+angle2) * DEG2RAD);
  int otherx2 = x - ((w * cosa2 / 2) - (h * sina2 / 2));
  int othery2 = y - ((h * cosa2 / 2) + (w * sina2 / 2));
  screen.drawLine(x, y, otherx2, othery2, linecolor);//line going left
  screen.drawLine(x+1, y+1, otherx2+1, othery2+1, linecolor);//double thick line

  previousotherx1 = otherx1;
  previousothery1 = othery1;
  previousotherx2 = otherx2;
  previousothery2 = othery2;
}

void batterywarning(){

}

void changepassword(){
  screen.fillScreen(backcolor);
  screen.fillScreen(COLOR_RGB565_DGRAY);
  screen.fillRoundRect(0, 0, 320, 240, 15, backgroundcolor);
  screen.fillRoundRect(5, 5, 310, 230, 15, backcolor);
  screen.setTextColor(backgroundcolor);
  screen.setTextSize(3);
  screen.setCursor(30, 11);
  screen.print("Change PASS");
  screen.drawFastHLine(10, 35, 300, COLOR_RGB565_GREEN);
  screen.drawFastHLine(10, 36, 300, COLOR_RGB565_GREEN);
  screen.setTextColor(COLOR_RGB565_GREEN);
  screen.setTextSize(1);
  screen.setCursor(40, 100);
  screen.print("Do three gestures to set new password...");
  
  password[0] = 0;
  password[1] = 0;
  password[2] = 0;

  int gesturepasscount = 0;
  redo:
  if(gr10_30.getDataReady()){
    gestures = gr10_30.getGesturesState();
    if(gestures&GESTURE_UP){
      Serial.println("Up");
      password[gesturepasscount] = GESTURE_UP;
    }
    if(gestures&GESTURE_DOWN){
      Serial.println("Down");
      password[gesturepasscount] = GESTURE_DOWN;
    }
    if(gestures&GESTURE_LEFT){
      Serial.println("Left");
      password[gesturepasscount] = GESTURE_LEFT;
    }
    if(gestures&GESTURE_RIGHT){
      Serial.println("Right");
      password[gesturepasscount] = GESTURE_RIGHT;
    }
    if(gestures&GESTURE_FORWARD){
      password[gesturepasscount] = GESTURE_FORWARD;
    }
    if(gestures&GESTURE_BACKWARD){
      password[gesturepasscount] = GESTURE_BACKWARD;
    }
    if(gestures&GESTURE_CLOCKWISE){
      password[gesturepasscount] = GESTURE_CLOCKWISE;
    }
    if(gestures&GESTURE_COUNTERCLOCKWISE){
      password[gesturepasscount] = GESTURE_COUNTERCLOCKWISE;
    }
    if(gestures&GESTURE_WAVE){
      password[gesturepasscount] = GESTURE_WAVE;
    }
    if(gestures&GESTURE_HOVER){
      password[gesturepasscount] = GESTURE_HOVER;
    }
    if(gestures&GESTURE_CLOCKWISE_C){
      password[gesturepasscount] = GESTURE_CLOCKWISE_C;
    }
    if(gestures&GESTURE_COUNTERCLOCKWISE_C){
      password[gesturepasscount] = GESTURE_COUNTERCLOCKWISE_C;
    }
    EEPROM.write(gesturepasscount,password[gesturepasscount]);
    EEPROM.commit();
    gesturepasscount++;
    Serial.println("gesturepasscount: " + String(gesturepasscount));   
  }

  if (gesturepasscount != passwordlength){
    goto redo;
  }
  Serial.println("Finished!");   
  gesturepasscount = 0;
  startDisplay();
  
}

void readThrottle(){
  throttleval = map(analogRead(throttlepin),throttleoffvalue,throttleonvalue,0,4095);
  //Serial.print("Throttle: ");
  //Serial.println(throttleval);
  //Serial.println("Throttle: " + String(throttleval));
}

void readBattery(){
  if ((millis() - lastbatreading) > readbatteryint){
    if (curbatread < batteryreadings){
      batsum = batsum + analogRead(batterypin);
      curbatread++;
    }
    else{
      float batteryread = batsum/batteryreadings;
      float batpinvolt = (batteryread*3.3)/4095;//3.3V and 4095 is the max resolution of ESP32
      if (testing){
        batpinvolt = 2.2;
      }
      batteryvolts = batpinvolt*16.55;//(1000/(15000+1000)); //(R2/(R1+R2));//VOUT = VIN*R2/R1+R2 ; classic equation for voltage dividers
      
      curbatread = 0;
      batsum = 0;

    }
    lastbatreading = millis();
  }
}

void readGyro(){
  /*for (curgyroread=0;curgyroread<gyroreadings;curgyroread++){
    gyrosum = gyrosum + gyro.y;
    delay(readgyroint);
  }
  gyroy = gyrosum/gyroreadings;      
  curgyroread = 0;
  gyrosum = 0;*/
  
  if ((millis() - lastgyroreading) > readgyroint){
    if (curgyroread < gyroreadings){
      gyrosum = gyrosum + gyro.y;
      curgyroread++;
    }
    else{
      gyroy = gyrosum/gyroreadings;      
      curgyroread = 0;
      gyrosum = 0;
    }
    lastgyroreading = millis();
  }
}

void handleGestures(){
  if(gr10_30.getDataReady()){
    gestures = gr10_30.getGesturesState();
    if(gestures&GESTURE_UP){
      Serial.println("Up");
      cruiseon = true;
    }
    if(gestures&GESTURE_DOWN){
      Serial.println("Down");
      //if (cruiseon){
      //  ledsoff = !ledsoff;
      //}
      cruiseon = false;
    }
    if(gestures&GESTURE_LEFT){
      Serial.println("Left");
      if (rightturnsignal){
        rightturnsignal = false;
        pixels.clear();
        pixels.show();
      }
      else {
        leftturnsignal = true;
        lastturnstat = true;
      }
    }
    if(gestures&GESTURE_RIGHT){
      Serial.println("Right");
      if (leftturnsignal){
        leftturnsignal = false;
        pixels.clear();
        pixels.show();
      }
      else {
        rightturnsignal = true;
        lastturnstat = true;
      }
    }
    if(gestures&GESTURE_FORWARD){
      Serial.println("Forward");
      ebrakeon = true;
    }
    if(gestures&GESTURE_BACKWARD){
      Serial.println("Backward");
      ebrakeon = false;
    }
    if(gestures&GESTURE_CLOCKWISE){
      Serial.println("Clockwise");
    }
    if(gestures&GESTURE_COUNTERCLOCKWISE){
      Serial.println("Contrarotate");
    }
    if(gestures&GESTURE_WAVE){
      Serial.println("Wave");
    }
    if(gestures&GESTURE_HOVER){
      Serial.println("Hover");
    }
    if(gestures&GESTURE_CLOCKWISE_C){
      Serial.println("Continuous clockwise");
      headlight = !headlight;
    }
    if(gestures&GESTURE_COUNTERCLOCKWISE_C){
      Serial.println("Continuous counterclockwise");
      screen.fillRoundRect(15,50,80,50,12,backgroundcolor);
      screen.setTextSize(2);
      screen.setCursor(20,70);
      screen.setTextColor(COLOR_RGB565_RED);
      screen.print("Locked");
      screen.fillRoundRect(225,50,80,50,12,backgroundcolor);
      screen.setTextSize(2);
      screen.setCursor(235,70);
      screen.setTextColor(COLOR_RGB565_GREEN);
      screen.print("E ON");
      ebrakeon = true;
      locked = true;
    }
  }
}

void lockedloop(){
  if(gr10_30.getDataReady()){
    gestures = gr10_30.getGesturesState();
    Serial.print("Gesture: ");
    Serial.println(gestures);
    if (gestures&password[passc]){
      Serial.println("Correct!");
      Serial.println(password[passc]);
      passc++;
      Serial.println("PASSC: " + String(passc));
    }
    else {
      Serial.println("Wrong");
      passc=0;
    }
  }
  if (passc >= passwordlength){
    Serial.println("Unlocked!");
    passc=0;
    locked = false;
    //lock = false;
  }
}

void readGNSS(){
  utc = gnss.getUTC();
  date = gnss.getDate();
  latitude = gnss.getLat();
  longitude = gnss.getLon();
  altitude = gnss.getAlt();
  satelites = gnss.getNumSatUsed();
  knots = gnss.getSog();
  course = gnss.getCog();
}

void readAccelGyro(){
  int i = 0;
  int rslt;
  int16_t accelGyro[6]={0}; 
  
  //get both accel and gyro data from bmi160
  //parameter accelGyro is the pointer to store the data
  rslt = bmi160.getAccelGyroData(accelGyro);
  if(rslt == 0){
    for(i=0;i<6;i++){
      if (i<3){
        //the first three are gyro data
        //Serial.print(accelGyro[i]*3.14/180.0);Serial.print("\t");
        if (i==0){accel.x = accelGyro[i]*3.14/180.0;}
        else if (i==1){accel.y = accelGyro[i]*3.14/180.0;}
        else if (i==2){accel.z = accelGyro[i]*3.14/180.0;}
      }else{
        //the following three data are accel data
        //Serial.print(accelGyro[i]/16384.0);Serial.print("\t");
        if (i==3){gyro.x = accelGyro[i]/16384.0;}
        else if (i==4){gyro.y = accelGyro[i]/16384.0;}
        else if (i==5){gyro.z = accelGyro[i]/16384.0;}
      }
    }
    //Serial.println();
  }else{
    Serial.println("err");
  }
}

void readTempHum(){
  if(aht20.startMeasurementReady(/* crcEn = */true)){
    temperature = aht20.getTemperature_F();
    humidity = aht20.getHumidity_RH();
  }
  HI = 0.5 * (temperature + 61.0 + ((temperature-68.0)*1.2) + (humidity*0.094));
  if (HI > 80.0){
    HI = -42.379 + 2.04901523*temperature + 10.14333127*humidity - .22475541*temperature*humidity - .00683783*temperature*temperature - .05481717*humidity*humidity + .00122874*temperature*temperature*humidity + .00085282*temperature*humidity*humidity - .00000199*temperature*temperature*humidity*humidity;
    if ((temperature > 80.0 && temperature < 112.0) && (humidity < 13.0)){
      HI = ((13-humidity)/4)*sqrt((17-abs(temperature-95.0))/17);
    }
    else if ((temperature > 80.0 && temperature < 87.0) && (humidity > 85.0)){
      HI = ((humidity-85)/10) * ((87-temperature)/5);
    }
  }
  temperature = HI;
}

void readCurrent(){
  if ((millis() - lastcurreading) > readcurrentint){
    if (curcurread < currentreadings){
      cursum = cursum + analogRead(currentpin);
      curcurread++;
    }
    else{
      float currentread = cursum/currentreadings;
      float curpinvolt = (currentread*3.3)/4095;//3.3V and 4095 is the max resolution of ESP32
      current = (curpinvolt-1.65)/0.04;
      current = constrain(current,0,50);
      watts = current*batteryvolts;
      watts = constrain(watts,0,500);

      curcurread = 0;
      cursum = 0;
    }
    lastcurreading = millis();
  }
//Data processing:510-raw data from analogRead when the input is 0;
// 5-5v; the first 0.04-0.04V/A(sensitivity); the second 0.04-offset val;
}

void handleMotordriver(){
  //digitalWrite(cruisepin,cruiseon);
  digitalWrite(brakepin,ebrakeon);
  if (voltconversion){
    motorvalue = map(throttleval,3500,0,0,255);
    motorvalue = constrain(motorvalue,0,255);
  }
  else{
    motorvalue = map(throttleval,300,4000,0,255);
    motorvalue = constrain(motorvalue,0,255);
  }
  
  motorvalue = map(motorvalue,255,0,0,255);
  if (motorvalue > 245){
    digitalWrite(cruisepin, HIGH);
  }
  else {
    digitalWrite(cruisepin, LOW);
  }
  if (motorvalue >= 200 && motorvalue <= 245){
    if (abs(motorvalue - lastthrottleval) <= 5){
      motorvalue = lastthrottleval;
    }
    else {
      lastthrottleval = motorvalue;
    }
  }
  else if (motorvalue < 200 && motorvalue > 110){
    if (abs(motorvalue - lastthrottleval) <= 15){
      motorvalue = lastthrottleval;
    }
    else {
      lastthrottleval = motorvalue;
    }
  }
  else if (motorvalue <= 110 && motorvalue > 5){
    motorvalue = map(motorvalue,5,120,32,87);
  }
  if (motorvalue >= 250){motorvalue = 255;} else if (motorvalue <= 5){motorvalue = 0;}
  if ((lastthrottleval - motorvalue) <= -7){
    motorvalue = lastthrottleval - 8;
    lastthrottleval = motorvalue;
  }
  motorvalue = abs(motorvalue);
  
  motorvalue = constrain(motorvalue,0,255);
  if (throttleval == 0){
    motorvalue = 0;
  }

  //Serial.print("Motor val: ");
  //Serial.print(motorvalue);  Serial.print(" | ");
  motorvalue = map(motorvalue,0,255,255,0);
  
  ledcWrite(speedChannel, motorvalue);//pwm signal for speed
}

void handleLeds(){
  if ((millis() - lastcoursereadtime) > coursechangetime){//handle turning signal
    coursechange = course - lastcoursereading;
    lastcoursereading = course; 
    lastcoursereadtime = millis();   
  }
  
  if (leftturnsignal == true || rightturnsignal == true){
    if (leftturnsignal){
      //brakelight(3);
      leftblinker();
    }
    if (rightturnsignal){
      //brakelight(3);
      rightblinker();
    }
  }
  else if (leftturnsignal == false && rightturnsignal == false){
    if (abs(coursechange) > courseturnsignalinc){
      if (coursechange < 0){
        //brakelight(3);
        leftblinker();
      }
      else {
        //brakelight(3);
        rightblinker();
      }
    }
    else {
      if (speeddiferrence >= speedbrakeinc){
        //pixels.clear();
        //pixels.show();
        brakelight(2);
      }
    }
  }

  if ((millis() - lastspeedreadtime) > speedchangetime){//handle brake light
    speeddiferrence = knots - lastspeedreading;
    if (speeddiferrence < speedbrakeinc){
      if (rightturnsignal){
        pixels.setPixelColor(5, pixels.Color(255,0,0));
        pixels.setPixelColor(6, pixels.Color(255,0,0));
        pixels.show();
      }
      else if (leftturnsignal){
        pixels.setPixelColor(9, pixels.Color(255,0,0));
        pixels.setPixelColor(8, pixels.Color(255,0,0));
        pixels.show();
      }
      else {
        brakelight(1);
      }
    }
    //else {
    //  brakelight(1);
    //}
    lastspeedreading = knots;
    lastspeedreadtime = millis();
  }

  if (headlight){
    headlights(true);
  }
  else {
    headlights(false);
  }

  handleFootlight();
}

void leftblinker(){
  int turningsignalcolor = pixels.Color(0, 20, 255);
  if (animationcountleft == 1){
    if ((millis() - startmillis) < 250){
      pixels.setPixelColor(7, turningsignalcolor);
      pixels.show();
    }
    else {
      animationcountleft++;
      startmillis = millis();
    }
  }
  else if (animationcountleft == 2){
    if ((millis() - startmillis) < 250){
      pixels.setPixelColor(7, pixels.Color(0, 0, 0));
      pixels.setPixelColor(6, turningsignalcolor);
      pixels.show();
    }
    else {
      animationcountleft++;
      startmillis = millis();
    }
  }
  if (animationcountleft == 3){
    if ((millis() - startmillis) < 750){
      pixels.setPixelColor(6, pixels.Color(0, 0, 0));
      pixels.setPixelColor(5, turningsignalcolor);
      pixels.show();
    }
    else {
      animationcountleft++;
      startmillis = millis();
    }
  }
  if (animationcountleft == 4){
    if ((millis() - startmillis) < 400){
      pixels.setPixelColor(5, pixels.Color(0, 0, 0));
      pixels.show();
    }
    else {
      animationcountleft = 1;
      startmillis = millis();
    }
  }
}

void rightblinker(){
  int turningsignalcolor = pixels.Color(0, 20, 255);
  if (animationcountright == 1){
    if ((millis() - startmillis) < 250){
      pixels.setPixelColor(7, turningsignalcolor);
      pixels.show();
    }
    else {
      animationcountright++;
      startmillis = millis();
    }
  }
  else if (animationcountright == 2){
    if ((millis() - startmillis) < 250){
      pixels.setPixelColor(7, pixels.Color(0, 0, 0));
      pixels.setPixelColor(8, turningsignalcolor);
      pixels.show();
    }
    else {
      animationcountright++;
      startmillis = millis();
    }
  }
  if (animationcountright == 3){
    if ((millis() - startmillis) < 750){
      pixels.setPixelColor(8, pixels.Color(0, 0, 0));
      pixels.setPixelColor(9, turningsignalcolor);
      pixels.show();
    }
    else {
      animationcountright++;
      startmillis = millis();
    }
  }
  if (animationcountright == 4){
    if ((millis() - startmillis) < 400){
      pixels.setPixelColor(9, pixels.Color(0, 0, 0));
      pixels.show();
    }
    else {
      animationcountright = 1;
      startmillis = millis();
    }
  }
}

void brakelight(int brakelighton){
  if (brakelighton == 1){
    for (int i=0;i<5;i++){
      pixels.setPixelColor(brake[i], pixels.Color(255, 0, 0));//on brake light
    }
    pixels.show();
  }
  else if (brakelighton == 2){
    for (int i=0;i<5;i++){
      pixels.setPixelColor(brake[i], pixels.Color(60, 0, 0));//on brake light
    }
    pixels.show();
  }
  else {
    for (int i=0;i<5;i++){
      pixels.setPixelColor(brake[i], pixels.Color(0, 0, 0));//on brake light
    }
    pixels.show();
  }
}

void headlights(bool on){
  if (on){
    pixels.setPixelColor(0, pixels.Color(255, 255, 255));//brake light on
    pixels.setPixelColor(1, pixels.Color(255, 255, 255));//brake light on
    pixels.setPixelColor(2, pixels.Color(255, 255, 255));//brake light on
    pixels.setPixelColor(3, pixels.Color(255, 255, 255));//brake light on
    pixels.setPixelColor(4, pixels.Color(255, 255, 255));//brake light on
    pixels.show();
  }
  else {
    pixels.setPixelColor(0, pixels.Color(0, 0, 0));//dim brake light
    pixels.setPixelColor(1, pixels.Color(0, 0, 0));//dim brake light
    pixels.setPixelColor(2, pixels.Color(0, 255, 0));//dim brake light
    pixels.setPixelColor(3, pixels.Color(0, 0, 0));//dim brake light
    pixels.setPixelColor(4, pixels.Color(0, 0, 0));//dim brake light
    pixels.show();
  }
}

void handleFootlight(){
  if (locked){
    if (panimationcount > 235 && pup){
      pup = false;
    }
    else if (panimationcount < 0 && !pup){
      pup = true;
    }

    if (pup){
      panimationcount+=8;
    }
    else {
      panimationcount-=8;
    }

    for (int i=0;i<4;i++){
      pixels.setPixelColor(footlight[i], pixels.Color(panimationcount,0,0));
      pixels.show();
    }
    //delayMicroseconds(1);
  }
  else if (knots < 1){
    if (panimationcount > 235 && pup){
      pup = false;
    }
    else if (panimationcount < 25 && !pup){
      pup = true;
    }

    if (pup){
      panimationcount+=8;
    }
    else {
      panimationcount-=8;
    }

    for (int i=0;i<4;i++){
      pixels.setPixelColor(footlight[i], pixels.Color(0,panimationcount,0));
      pixels.show();
    }
    //delayMicroseconds(1);
  }
  else {
    for (int i=0;i<4;i++){
      pixels.setPixelColor(footlight[i], pixels.Color(0,20,255));
      pixels.show();
    }
  }
}

void startDisplay(){
  screen.fillScreen(backcolor);
  screen.setTextWrap(false);

  screen.fillScreen(COLOR_RGB565_DGRAY);
  screen.fillRoundRect(0, 0, 320, 240, 15, backgroundcolor);
  screen.fillRoundRect(5, 5, 310, 230, 15, backcolor);
  
  //title
  screen.setTextColor(backgroundcolor);
  screen.setTextSize(3);
  screen.setCursor(30, 11);
  screen.print("DIY E-Scooter");

  screen.drawFastHLine(10, 35, 300, COLOR_RGB565_GREEN);
  screen.drawFastHLine(10, 36, 300, COLOR_RGB565_GREEN);

  //speed label
  screen.setTextColor(COLOR_RGB565_YELLOW);
  screen.setTextSize(2);
  screen.setCursor(125,40);
  screen.print("Speed");

  //dist and time to empty labels
  screen.setTextColor(COLOR_RGB565_YELLOW);
  screen.setTextSize(2);
  screen.setCursor(91,170);
  screen.print("Dist   Time");

  //boost button
  screen.drawRoundRect(225,50,80,50,12,backgroundcolor);
  screen.setTextSize(2);
  screen.setCursor(235,70);
  screen.setTextColor(COLOR_RGB565_RED);
  screen.print("E OFF");

  //lock button
  screen.drawRoundRect(15,50,80,50,12,backgroundcolor);
  screen.setTextSize(1);
  screen.setCursor(25,70);
  screen.setTextColor(COLOR_RGB565_GREEN);
  screen.print("Unlocked");

  //cruise button
  screen.drawRoundRect(225,110,80,50,12,backgroundcolor);
  screen.setTextSize(2);
  screen.setCursor(235,130);
  screen.setTextColor(COLOR_RGB565_GREEN);
  screen.print("C OFF");
}

void startGestureSensor(){

  if(gr10_30.begin() != 0){
    Serial.println(" Sensor initialize failed!!");
    delay(1000);
  }
  /** Set the gesture to be enabled
 *  GESTURE_UP
 *  GESTURE_DOWN
 *  GESTURE_LEFT
 *  GESTURE_RIGHT
 *  GESTURE_FORWARD
 *  GESTURE_BACKWARD
 *  GESTURE_CLOCKWISE
 *  GESTURE_COUNTERCLOCKWISE
 *  GESTURE_WAVE              It is not suggested to enable rotation gesture (CW/CCW) and wave gesture at the same time.
 *  GESTURE_HOVER             Disable other gestures when hover gesture enables.
 *  GESTURE_UNKNOWN
 *  GESTURE_CLOCKWISE_C
 *  GESTURE_COUNTERCLOCKWISE_C
 */
  gr10_30.enGestures(GESTURE_UP|GESTURE_DOWN|GESTURE_LEFT|GESTURE_RIGHT|GESTURE_FORWARD|GESTURE_BACKWARD|GESTURE_CLOCKWISE|GESTURE_COUNTERCLOCKWISE|GESTURE_CLOCKWISE_C|GESTURE_COUNTERCLOCKWISE_C);
  /**
 * Set the detection window you want, only data collected in the range are valid
 * The largest window is 31, the configured number represents distance from the center to the top, bottom, left and right
 * For example, if the configured distance from top to bottom is 30, then the distance from center to top is 15, and distance from center to bottom is also 15
 * udSize Range of distance from top to bottom      0-31
 * lrSize Range of distance from left to right      0-31
 */
  gr10_30.setUdlrWin(30, 30);//0-31,0-31
  /**
 * Set moving distance that can be recognized as a gesture
 * Distance range 5-25, must be less than distances of the detection window
 */
  gr10_30.setLeftRange(10);
  gr10_30.setRightRange(10);
  gr10_30.setUpRange(10);
  gr10_30.setDownRange(10);
  //gr10_30.setForwardRange(10);
  //gr10_30.setBackwardRange(10);
  /**
 * Set distance of moving forward and backward that can be recognized as a gesture
 * Distance range 1-15
 */
  gr10_30.setForwardRange(10);
  gr10_30.setBackwardRange(10);
/**
 * Set rotation angle that can trigger the gesture
 * count Default is 16 range 0-31
 * count Rotation angle is 22.5 * count
 * count = 16 22.5*count = 360  Rotate 360° to trigger the gesture
 */
  gr10_30.setCwsAngle(/*count*/16);
  gr10_30.setCcwAngle(/*count*/16);
  /**
 * Set degrees of continuous rotation that can trigger the gesture
 * count Default is 4  range 0-31
 * count The degrees of continuous rotation is 22.5 * count
 * For example: count = 4 22.5*count = 90
 * Trigger the clockwise/counterclockwise rotation gesture first, if keep rotating, then the continuous rotation gesture will be triggered once every 90 degrees
 */
  gr10_30.setCwsAngleCount(/*count*/8);
  gr10_30.setCcwAngleCount(/*count*/8);
}

void interruptRoutine(){
  Serial.println("Interrupt...");
}

It isn't. Not if you expect accuracy (you said reliable, I guess that should include it).. Commercial units use a combination of battery voltage and current measurements, both instantaneous and a stored history that is maintained in memory.

Have you looked at any detailed battery specifications? It would help give you clues.

1 Like

It is not at all simple. In order to predict battery capacity accurately, you need a good theoretical model for the battery, experimentally calibrated for each battery, and update the total remaining capacity using data obtained by continuously monitoring voltage and current draw over time.

3 Likes

I imagine temperature comes into play too?

1 Like

Is that why I was racking my brain and drawing a blank :thinking:?

I built the battery for my scooter, and here is the cell data sheet, if that's what you mean:

It seems you and @anon57585045 are saying the same thing! Do you have any hints/examples on how to do this @jremington and @anon57585045 ? Maybe store an array of voltage and current measurements? Or, keep summed values, and divide them by the measurement count? And it sounds like I would have to average out the history, and current voltage and current measurements, to try and predict the remaining battery percentage :thinking:.

Oh, probably @gfvalvo. I do have temperature and humidity measuring on my scooter; im not sure how I would calculate that in though :joy:.

Voltage x Current = Power. Integrate that over time and you have the total energy delivered so far.

Nor do I, but I'm pretty sure it's important ... the temperature anyway.

Yeah, in wattage. I figured that's how. Right now, I am measuring the wattage draw of the motor to try and predict remaining miles and minutes left of the battery.

Watts are a unit of power ... the instantaneous rate of energy production of consumption. You need to integrate that over time to get energy (Watt-seconds i.e. Joules).

1 Like

Ahh, thanks @gfvalvo ! I was thinking I need to somehow figure out wattage use, but per second.

Yes, and battery age. The battery model has to be continuously adjusted.

1 Watt = 1 Joule per second

3 Likes

You will notice the battery capacity is given in Amp hours.
So to calculate the remaining capacity you need to measure the current, to record (and integrate) the charge (amp seconds) used since the last full charge.

The terminal voltage will change during discharge but that doesnt affect the remaining capacity.

The data sheet also shows the rated capacity is temperature dependent; and also the capacity is derated over charge/discharge cycles
Capacity after 300 cycles.
Capacity ≥ 1500mAh (60% of the nominal capacity at 25±2°C)

The datasheet says maximum discharge-current 30A
capacity 2500 mA = 2.5 Ah.
This means it is a battery that is able to discharge at
30Ah / 2.5A = 12C.
The battery can handle pretty high currents.
So measuring mAh will be sufficient.

This is the way all RC- electric airplane-telemetry systems measure it.
Spectrum, LemonRx etc.

This means you measure the amperes and sum up the measured amperes over time. This measuring must be done in short intervals maybe 20 times per second and then summing up all measurements result in ampere multiplied with time
Ah or mAh
measuring the voltage might even create a bigger deviation as the voltagedrop inside the battery at higher currents is significant.

If I fly one of my electric RC-planes with a 3S 2200 mA Lipo that is fully charged to 4.20V (12.6V) and give full throttle which will draw a current of 35A
the voltage drops to 3.9V (11.7V)
Some E-scooters have a simple 4 bars or 5 bars capacity display.
If the E-scooter is just switched on without driving the display shows as an example 75%. If yu give full throttle the display will drop to 50% because of the voltage drop.

Lipos like used in smartphones use a much smaller current.
In this situation the voltage-drop is small. For estimating the left energy the voltage can be taken as in lipos the voltage goes down almost linear with discharging. But in your situation of a lot of changes between low and high currents in an E-scooter measuring the amperes 20 times per second is nescessary.

The simple but un-precise solution is a voltage-display.

best regards Stefan

There is, of course, no direct relation between remaining battery capacity (energy stored) and miles or minutes of driving - because that depends very much on the type of driving!

There are chips available called "coulomb counters" or "gas gauges" - they deal with the integration of power input & output over time...

Thanks everyone for your replies!

Yeah, I realize that, which is why my current idea is very unreliable; almost completely useless actually.

I am doing that right now, to get battery percentage left. It drops though, when the motor is turned on of course.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.