/* Copyright 2016 Benjamin Vedder benjamin@vedder.se Copyright 2017 - 2018 Danny Bokma danny@diebie.nl Copyright 2019 - 2020 Kevin Dionne kevin.dionne@ennoid.me This file is part of the VESC/DieBieMS/ENNOID-BMS firmware. The VESC/DieBieMS/ENNOID-BMS firmware is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The VESC/DieBieMS/ENNOID-BMS firmware is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "modTerminal.h" #include "main.h" #include "time.h" #include "rtc.h" #include "modStateOfCharge.h" // Private types typedef struct _terminal_callback_struct { const char *command; const char *help; const char *arg_names; void(*cbf)(int argc, const char **argv); } terminal_callback_struct; // Private variables static terminal_callback_struct callbacks[CALLBACK_LEN]; static int callback_write = 0; extern modConfigGeneralConfigStructTypedef *generalConfig; extern modStateOfChargeStructTypeDef *generalStateOfCharge; extern modPowerElectronicsPackStateTypedef packState; extern OperationalStateTypedef modOperationalStateCurrentState; extern SIM800_Global_Struct SIM800_Struct; extern bool jumpBootloaderTrue; void modTerminalProcessString(char *str) { struct tm tm_PC_Time = {0}; Time_Struct PC_time; uint64_t NewSerial; enum { kMaxArgs = 64 }; int argc = 0; char *argv[kMaxArgs]; char* ptr; char *p2 = strtok(str, " "); while (p2 && argc < kMaxArgs) { argv[argc++] = p2; p2 = strtok(0, " "); } if (argc == 0) { modCommandsPrintf("No command received\n"); return; } if (strcmp(argv[0], "ping") == 0) { modCommandsPrintf("pong\n"); } else if (strcmp(argv[0], "status") == 0) { bool disChargeEnabled = packState.disChargeDesired && packState.disChargeLCAllowed; bool chargeEnabled = packState.chargeDesired && packState.chargeAllowed; /* static float real_capacity = 0; static float previous_capacity = 0; static float previous_SoC = 0; //static float current_SoC = 0; static float soc_diff = 0; static float capacity_diff = 0; static uint8_t SoC_Save_Flag = 0; */ /* * uint8_t Pilot_Status = 0; uint8_t Maximum_Charge_Current_Status = 0; uint8_t Maximum_Voltage_Status = 0; uint8_t Maximum_Temp_Status = 0; uint8_t Maximum_Load_Current_Status = 0; uint8_t Minimum_Voltage_Status = 0; */ modCommandsPrintf(" "); modCommandsPrintf("-----Battery Pack Status-----"); //modPowerElectronicsPackStateHandle->cellModuleBalanceResistorEnableMask modCommandsPrintf("Balance mask : 0x%x ",packState.cellModuleBalanceResistorEnableMask[0]); modCommandsPrintf("Pack voltage Direct : %.2fV",packState.packVoltage); modCommandsPrintf("Pack voltage CVAverage: %.2fV",packState.cellVoltageAverage*generalConfig->noOfCellsSeries); modCommandsPrintf("Pack current : %.2fA",packState.packCurrent); //modCommandsPrintf("LC Load voltage : %.2fV",packState.loCurrentLoadVoltage); //modCommandsPrintf("Low current : %.2fA",packState.loCurrentLoadCurrent); modCommandsPrintf("State of charge : %.1f%%",generalStateOfCharge->generalStateOfCharge); modCommandsPrintf("Remaining capacity : %.2fAh",generalStateOfCharge->remainingCapacityAh); modCommandsPrintf("Real calculated capacity : %fAh",export_real_capacity); //modCommandsPrintf("previous_capacity : %.2fAh",previous_capacity); // modCommandsPrintf("previous_SoC : %.2fAh",previous_SoC); // modCommandsPrintf("soc_diff : %.2fAh",soc_diff); // modCommandsPrintf("capacity_diff : %.2fAh",capacity_diff); // modCommandsPrintf("SoC_Save_Flag : %.2fAh",SoC_Save_Flag); modCommandsPrintf(" "); modCommandsPrintf("Pilot_Status: %s",Pilot_Status ? "Charging" : "No charging"); modCommandsPrintf("Maximum_Charge_Current_Status: %s",Maximum_Charge_Current_Status ? "Good" : "Too high charge current!"); modCommandsPrintf("Maximum_Voltage_Status: %s",Maximum_Voltage_Status ? "Good" : "Too high voltage!"); modCommandsPrintf("Maximum_Temp_Status: %s",Maximum_Temp_Status ? "Good" : "Too high temperature!"); modCommandsPrintf("Maximum_Load_Current_Status: %s",Maximum_Load_Current_Status ? "Good" : "Too high load current!"); modCommandsPrintf("Minimum_Voltage_Status: %s",Minimum_Voltage_Status ? "Good" : "Too low voltage!"); modCommandsPrintf("Deep_Discharge_Status: %s",Deep_Discharge_Status ? "Good" : "Deep discharge!"); modCommandsPrintf("ADC_Current: %d",export_adc_average_res); modCommandsPrintf("Current Zero Value: %f",generalConfig->shuntLCFactor); modCommandsPrintf(" "); modCommandsPrintf("Charge Switch State: %s",charge_switch_state ? "ON" : "OFF"); modCommandsPrintf("Load Switch State: %s",load_switch_state ? "ON" : "OFF"); //generalConfig->shuntLCFactor //export_adc_average_res //export_real_capacity // switch(modOperationalStateCurrentState) { // case OP_STATE_CHARGING: // modCommandsPrintf("Operational state : %s","Charging"); // break; // case OP_STATE_LOAD_ENABLED: // modCommandsPrintf("Operational state : %s","Load enabled"); // break; // case OP_STATE_CHARGED: // modCommandsPrintf("Operational state : %s","Charged"); // break; // case OP_STATE_BALANCING: // modCommandsPrintf("Operational state : %s","Balancing"); // break; // case OP_STATE_ERROR_PRECHARGE: // modCommandsPrintf("Operational state : %s","Pre charge error"); // break; // case OP_STATE_ERROR: // modCommandsPrintf("Operational state : %s","Error"); // break; // case OP_STATE_FORCEON: // modCommandsPrintf("Operational state : %s","Forced on"); // break; // case OP_STATE_POWER_DOWN: // modCommandsPrintf("Operational state : %s","Power down"); // break; // case OP_STATE_EXTERNAL: // modCommandsPrintf("Operational state : %s","External (USB or CAN)"); // break; // default: // modCommandsPrintf("Operational state : %s","Unknown"); // break; // } // modCommandsPrintf("Cell voltage high : %.3fV",packState.cellVoltageHigh); // modCommandsPrintf("Cell voltage low : %.3fV",packState.cellVoltageLow); // modCommandsPrintf("Cell voltage average : %.3fV",packState.cellVoltageAverage); // modCommandsPrintf("Cell voltage mismatch : %.3fV",packState.cellVoltageMisMatch); // modCommandsPrintf("Discharge enabled : %s",disChargeEnabled ? "True" : "False"); // modCommandsPrintf("Charge enabled : %s",chargeEnabled ? "True" : "False"); //ni modCommandsPrintf("Power button pressed : %s",packState.powerButtonActuated ? "True" : "False"); //ni modCommandsPrintf("CAN safety state : %s",packState.safetyOverCANHCSafeNSafe ? "True" : "False"); //ni modCommandsPrintf("---End Battery Pack Status---"); //ni modCommandsPrintf(" "); /* uint8_t cellVoltageLow_Minimum = 0; uint8_t cellVoltageLow_Minimum_Hyst = 0; uint8_t cellVoltageHigh_Maximum = 0; uint8_t cellVoltageHigh_Maximum_Hyst = 0; uint8_t maxChargeCurrent = 0; uint8_t maxLoadCurrent = 0; */ } else if (strcmp(argv[0], "setZeroCurrent") == 0) { modCommandsPrintf(" "); modCommandsPrintf("----- setZeroCurrent -----"); // print temperatures modCommandsPrintf("ADC Value: %.3f V",export_adc_average_res); generalConfig->shuntLCFactor = export_adc_average_res; modCommandsPrintf("Config Zero Value in EEPROM: %.3f V",generalConfig->shuntLCFactor); modCommandsPrintf("Config Zero Value in RAM: %.3f V",currentZero_config); modCommandsPrintf("Current Zero Value Updated! Remember to save EEPROM!"); modCommandsPrintf(" "); /* * Brush_Minimum_SoC = generalConfig->minimalPrechargePercentage; Brush_Default_State = generalConfig->timeoutLCPreCharge; */ } else if (strcmp(argv[0], "Outputs") == 0) { modCommandsPrintf(" "); modCommandsPrintf("----- Outputs -----"); // print temperatures modCommandsPrintf("Output 2 minimum SoC: %.3f",Brush_Minimum_SoC); modCommandsPrintf("Output 2 default state: %d",Brush_Default_State); modCommandsPrintf(" "); }else if (strcmp(argv[0], "limits") == 0) { modCommandsPrintf(" "); modCommandsPrintf("----- Limits -----"); // print temperatures modCommandsPrintf("Minimum Cell Voltage: %.3f V",cellVoltageLow_Minimum); modCommandsPrintf("Minimum Cell Voltage Hyst: %.3f V",cellVoltageLow_Minimum_Hyst); modCommandsPrintf("Maximum Cell Voltage: %.3f V",cellVoltageHigh_Maximum); modCommandsPrintf("Maximum Cell Voltage Hyst: %.3f V",cellVoltageHigh_Maximum_Hyst); modCommandsPrintf(" "); modCommandsPrintf("Maximum Charge Current: %.3f A",maxChargeCurrent); modCommandsPrintf("Maximum Load Current: %.3f A",maxLoadCurrent); modCommandsPrintf(" "); modCommandsPrintf("Maximum Temperature: %.3f C",maxTemperature); modCommandsPrintf("Maximum Temperature Hyst: %.3f C",maxTemperature_Hyst); modCommandsPrintf(" "); } else if (strcmp(argv[0], "sens") == 0) { modCommandsPrintf("----- Sensors -----"); // print temperatures modCommandsPrintf("Sensor[0] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[0]); modCommandsPrintf("Sensor[1] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[1]); modCommandsPrintf("Sensor[2] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[2]); modCommandsPrintf("Sensor[3] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[3]); modCommandsPrintf("Sensor[4] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[4]); modCommandsPrintf("Sensor[5] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[5]); modCommandsPrintf("Sensor[6] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[6]); modCommandsPrintf("Sensor[7] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[7]); modCommandsPrintf("Sensor[8] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[8]); modCommandsPrintf("Sensor[9] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[9]); modCommandsPrintf("Sensor[10] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[10]); modCommandsPrintf("Sensor[11] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[11]); modCommandsPrintf("Sensor[12] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[12]); modCommandsPrintf("Sensor[2] : % 3.1f C - I - 'LTC Internal'",packState.temperatures[2]); modCommandsPrintf("Sensor[3] : % 3.1f C - I - 'STM NTC'",packState.temperatures[3]); modCommandsPrintf("----- End sensors -----"); modCommandsPrintf("----- E=External I=Internal -----"); modCommandsPrintf(" "); } else if (strcmp(argv[0], "cells") == 0) { uint8_t cellPointer = 0; modCommandsPrintf("----- Cell voltages -----"); modCommandsPrintf("Cell in series : %d ",generalConfig->noOfCellsSeries); modCommandsPrintf("Number of ICs : %d ",generalConfig->cellMonitorICCount); modCommandsPrintf("Cells per IC : %d ",generalConfig->noOfCellsPerModule); for(cellPointer = 0 ; cellPointer < generalConfig->noOfCellsSeries ; cellPointer++) { // test 01.11.2021 // //for(cellPointer = 0 ; cellPointer < 20 ; cellPointer++) { modCommandsPrintf("Cell voltage%3d : %.3f V",cellPointer,packState.cellVoltagesIndividual[cellPointer].cellVoltage); } modCommandsPrintf("Cell voltage high : %.3f V",packState.cellVoltageHigh); modCommandsPrintf("Cell voltage low : %.3f V",packState.cellVoltageLow); modCommandsPrintf("Cell voltage average : %.3f V",packState.cellVoltageAverage); modCommandsPrintf("Cell voltage mismatch : %.3f V",packState.cellVoltageMisMatch); modCommandsPrintf("----- End Cell voltages -----"); modCommandsPrintf(" "); } else if (strcmp(argv[0], "config") == 0) { modCommandsPrintf("--- BMS Configuration ---"); //modCommandsPrintf("serialNumber : %u",generalConfig->serialNumber); modCommandsPrintf("NoOfCells : %u",generalConfig->noOfCellsSeries); modCommandsPrintf("batteryCapacity : %.2fAh",generalConfig->batteryCapacity); modCommandsPrintf("cellHardUnderVoltage : %.3fV",generalConfig->cellHardUnderVoltage); modCommandsPrintf("cellHardOverVoltage : %.3fV",generalConfig->cellHardOverVoltage); modCommandsPrintf("cellLCSoftUnderVoltage : %.3fV",generalConfig->cellLCSoftUnderVoltage); modCommandsPrintf("cellSoftOverVoltage : %.3fV",generalConfig->cellSoftOverVoltage); modCommandsPrintf("cellBalanceStart : %.3fV",generalConfig->cellBalanceStart); //modCommandsPrintf("cellBalanceDiffThreshold : %.3fV",generalConfig->cellBalanceDifferenceThreshold); modCommandsPrintf("CAN ID : %u",generalConfig->CANID); modCommandsPrintf("--- End BMS Configuration ---"); modCommandsPrintf(" "); } else if (strcmp(argv[0], "config_default") == 0) { modCommandsPrintf("--Restoring default config--"); if(modConfigStoreDefaultConfig()) modCommandsPrintf("Succesfully restored config, new config wil be used on powercycle (or use config_read to apply it now)."); else modCommandsPrintf("Error restored config."); modCommandsPrintf(" "); } else if (strcmp(argv[0], "config_write") == 0) { modCommandsPrintf("--- Writing config ---"); if(modConfigStoreConfig()) modCommandsPrintf("Succesfully written config."); else modCommandsPrintf("Error writing config."); modCommandsPrintf(" "); } else if (strcmp(argv[0], "config_read") == 0) { modCommandsPrintf("--- Reading config ---"); if(modConfigLoadConfig()) modCommandsPrintf("Succesfully read config."); else modCommandsPrintf("Error reading config."); modCommandsPrintf(" "); } else if (strcmp(argv[0], "config_set_cells") == 0) { modCommandsPrintf("---Setting new cell count---"); if (argc == 2) { uint32_t newNumberOfCells = 0; sscanf(argv[1], "%u", &newNumberOfCells); if(newNumberOfCells < 13 && newNumberOfCells > 2) { modCommandsPrintf("Number of cells is set to: %u.",newNumberOfCells); generalConfig->noOfCellsSeries = newNumberOfCells; } else { modCommandsPrintf("Invalid number of cells (should be anything from 3 to 12)."); } } else { modCommandsPrintf("This command requires one argument."); } modCommandsPrintf(" "); } else if (strcmp(argv[0], "hwinfo") == 0) { modCommandsPrintf("------- BMS Info -------"); modCommandsPrintf("Firmware: %s", FW_REAL_VERSION); modCommandsPrintf("Name : %s", HW_NAME); modCommandsPrintf("UUID: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", STM32_UUID_8[0], STM32_UUID_8[1], STM32_UUID_8[2], STM32_UUID_8[3], STM32_UUID_8[4], STM32_UUID_8[5], STM32_UUID_8[6], STM32_UUID_8[7], STM32_UUID_8[8], STM32_UUID_8[9], STM32_UUID_8[10], STM32_UUID_8[11]); modCommandsPrintf("------- End BMS Info -------"); modCommandsPrintf(" "); } else if (strcmp(argv[0], "reboot") == 0) { modCommandsPrintf("------ Rebooting BMS ------"); NVIC_SystemReset(); } else if (strcmp(argv[0], "bootloader_erase") == 0) { modCommandsPrintf("------ erasing new app space ------"); //ni if(modFlashEraseNewAppData(0x00002000) == HAL_OK) if(1) modCommandsPrintf("--Erase done."); else modCommandsPrintf("--Erase error."); } else if (strcmp(argv[0], "bootloader_jump") == 0) { //ni modFlashJumpToBootloader(); } else if (strcmp(argv[0], "help") == 0) { modCommandsPrintf("------- Start of help -------"); modCommandsPrintf("Valid commands for ENNOID-BMS are:"); modCommandsPrintf("help"); modCommandsPrintf(" Show this help."); modCommandsPrintf("setZeroCurrent"); modCommandsPrintf(" Set Zero for current sensor"); modCommandsPrintf("ping"); modCommandsPrintf(" Print pong here to see if the reply works."); modCommandsPrintf("slave_scan"); modCommandsPrintf(" Scan the I2C devices on the slave."); modCommandsPrintf("status"); modCommandsPrintf(" Print battery measurements summary."); modCommandsPrintf("sens"); modCommandsPrintf(" Print all sensor values."); modCommandsPrintf("cells"); modCommandsPrintf(" Print cell voltage measurements."); modCommandsPrintf("config"); modCommandsPrintf(" Print BMS configuration."); modCommandsPrintf("config_default"); modCommandsPrintf(" Load default BMS configuration."); modCommandsPrintf("config_write"); modCommandsPrintf(" Store current BMS configuration to EEPROM."); modCommandsPrintf("config_read"); modCommandsPrintf(" Read BMS configuration from EEPROM."); modCommandsPrintf("hwinfo"); modCommandsPrintf(" Print some hardware information."); modCommandsPrintf("GSM"); modCommandsPrintf(" Print some GSM information."); modCommandsPrintf("setUnixTime [Value]"); modCommandsPrintf(" Set RTC Time from TimeStamp. [Value] - uint64 unixtime value "); modCommandsPrintf("checkUnixTime"); modCommandsPrintf(" Check actual RTC time "); modCommandsPrintf("setSerialNumber [Value]"); modCommandsPrintf(" Set Serial [Value] - uint64 value "); modCommandsPrintf("checkSerialNumber"); modCommandsPrintf(" Check actual Serial "); modCommandsPrintf("SDcard"); modCommandsPrintf(" Switch to SD storage"); for (int i = 0;i < callback_write;i++) { if (callbacks[i].arg_names) { modCommandsPrintf("%s %s", callbacks[i].command, callbacks[i].arg_names); } else { modCommandsPrintf(callbacks[i].command); } if (callbacks[i].help) { modCommandsPrintf(" %s", callbacks[i].help); } else { modCommandsPrintf(" There is no help available for this command."); } } modCommandsPrintf(" "); } else if (strcmp(argv[0], "GSM") == 0) { uint8_t cellPointer = 0; modCommandsPrintf("----- GSM STATE -----"); modCommandsPrintf("GPRS Connected : %d ", SIM800_Struct.Sim_Main_States.GPRS_Established); ptr = &SIM800_Struct.Sim_Answer.SIM_answer[0]; modCommandsPrintf("Last received msg : %s ", ptr); modCommandsPrintf("----- End GSM STATE -----"); modCommandsPrintf(" "); }else if (strcmp(argv[0], "setUnixTime") == 0) { modCommandsPrintf(" "); modCommandsPrintf("----- set Unix Time -----"); time_t Time_rcvd; Time_rcvd = strtol(argv[1], NULL, 16); tm_PC_Time = *(localtime(&Time_rcvd)); if(Time_rcvd < 0x386D4380){ PC_time.year = 0; PC_time.month = 1; PC_time.day = 1; PC_time.hour = 0; PC_time.minute = 0; PC_time.second = 0; } else { PC_time.year = tm_PC_Time.tm_year+1900-2000; PC_time.month = tm_PC_Time.tm_mon+1; PC_time.day = tm_PC_Time.tm_mday; PC_time.hour = tm_PC_Time.tm_hour; PC_time.minute = tm_PC_Time.tm_min; PC_time.second = tm_PC_Time.tm_sec; } RTC_Set_Values(&PC_time); // print temperatures modCommandsPrintf("RTC Updated!"); modCommandsPrintf("Actual RTC:"); modCommandsPrintf("Year : %d", PC_time.year); modCommandsPrintf("Month : %d", PC_time.month); modCommandsPrintf("Day : %d", PC_time.day); modCommandsPrintf("Hours : %d", PC_time.hour); modCommandsPrintf("Minutes : %d", PC_time.minute); modCommandsPrintf("Seconds : %d", PC_time.second); modCommandsPrintf(" "); }else if (strcmp(argv[0], "checkUnixTime") == 0) { modCommandsPrintf(" "); modCommandsPrintf("----- check Unix Time -----"); RTC_Get_Values(&PC_time); modCommandsPrintf("Actual RTC:"); modCommandsPrintf("Year : %d", PC_time.year); modCommandsPrintf("Month : %d", PC_time.month); modCommandsPrintf("Day : %d", PC_time.day); modCommandsPrintf("Hours : %d", PC_time.hour); modCommandsPrintf("Minutes : %d", PC_time.minute); modCommandsPrintf("Seconds : %d", PC_time.second); modCommandsPrintf("----- end Unix Time -----"); modCommandsPrintf(" "); } else if (strcmp(argv[0], "checkSerialNumber") == 0) { modCommandsPrintf(" "); modCommandsPrintf("----- check Serial -----"); modCommandsPrintf("Actual Serial:"); modCommandsPrintf("Serial : %lu %lu", generalConfig->serialNumber); modCommandsPrintf("Serial : %.8f", generalConfig->notUsedCurrentThreshold); modCommandsPrintf("----- end Serial -----"); modCommandsPrintf(" "); } else if (strcmp(argv[0], "setSerialNumber") == 0) { modCommandsPrintf(" "); modCommandsPrintf("----- Set Serial -----"); NewSerial = strtol(argv[1], NULL, 10); generalConfig->serialNumber = NewSerial; modCommandsPrintf("New Serial : %lu %lu", NewSerial); modCommandsPrintf("----- end Serial -----"); modCommandsPrintf(" "); } else if (strcmp(argv[0], "SDcard") == 0) { HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR11, 1); jumpBootloaderTrue = 1; } else { bool found = false; for (int i = 0;i < callback_write;i++) { if (strcmp(argv[0], callbacks[i].command) == 0) { callbacks[i].cbf(argc, (const char**)argv); found = true; break; } } if (!found) { modCommandsPrintf("Invalid command: %s\n type help to list all available commands\n", argv[0]); } } } /** * Register a custom command callback to the terminal. If the command * is already registered the old command callback will be replaced. * * @param command * The command name. * * @param help * A help text for the command. Can be NULL. * * @param arg_names * The argument names for the command, e.g. [arg_a] [arg_b] * Can be NULL. * * @param cbf * The callback function for the command. */ void modTerminalRegisterCommandCallBack( const char* command, const char *help, const char *arg_names, void(*cbf)(int argc, const char **argv)) { int callback_num = callback_write; for (int i = 0;i < callback_write;i++) { // First check the address in case the same callback is registered more than once. if (callbacks[i].command == command) { callback_num = i; break; } // Check by string comparison. if (strcmp(callbacks[i].command, command) == 0) { callback_num = i; break; } } callbacks[callback_num].command = command; callbacks[callback_num].help = help; callbacks[callback_num].arg_names = arg_names; callbacks[callback_num].cbf = cbf; if (callback_num == callback_write) { callback_write++; if (callback_write >= CALLBACK_LEN) { callback_write = 0; } } } /* defaultConfig.cellBalanceDifferenceThreshold = 0.01f; // Start balancing @ 5mV difference, stop if below */