mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-05 10:49:42 +02:00
864 lines
33 KiB
C++
864 lines
33 KiB
C++
#include "NISSAN-LEAF-BATTERY.h"
|
||
#include "ESP32CAN.h"
|
||
#include "CAN_config.h"
|
||
|
||
/* Do not change code below unless you are sure what you are doing */
|
||
static unsigned long previousMillis10 = 0; // will store last time a 10ms CAN Message was send
|
||
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
|
||
static unsigned long previousMillis10s = 0; // will store last time a 1s CAN Message was send
|
||
static const int interval10 = 10; // interval (ms) at which send CAN Messages
|
||
static const int interval100 = 100; // interval (ms) at which send CAN Messages
|
||
static const int interval10s = 10000; // interval (ms) at which send CAN Messages
|
||
uint16_t CANerror = 0; //counter on how many CAN errors encountered
|
||
#define MAX_CAN_FAILURES 5000 //Amount of malformed CAN messages to allow before raising a warning
|
||
static uint8_t CANstillAlive = 12; //counter for checking if CAN is still alive
|
||
static uint8_t errorCode = 0; //stores if we have an error code active from battery control logic
|
||
static uint8_t mprun10r = 0; //counter 0-20 for 0x1F2 message
|
||
static byte mprun10 = 0; //counter 0-3
|
||
static byte mprun100 = 0; //counter 0-3
|
||
|
||
CAN_frame_t LEAF_1F2 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_std,}},.MsgID = 0x1F2,.data = {0x10, 0x64, 0x00, 0xB0, 0x00, 0x1E, 0x00, 0x8F}};
|
||
CAN_frame_t LEAF_50B = {.FIR = {.B = {.DLC = 7,.FF = CAN_frame_std,}},.MsgID = 0x50B,.data = {0x00, 0x00, 0x06, 0xC0, 0x00, 0x00, 0x00}};
|
||
CAN_frame_t LEAF_50C = {.FIR = {.B = {.DLC = 6,.FF = CAN_frame_std,}},.MsgID = 0x50C,.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||
CAN_frame_t LEAF_1D4 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_std,}},.MsgID = 0x1D4,.data = {0x6E, 0x6E, 0x00, 0x04, 0x07, 0x46, 0xE0, 0x44}};
|
||
//These CAN messages need to be sent towards the battery to keep it alive
|
||
|
||
CAN_frame_t LEAF_GROUP_REQUEST = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_std,}},.MsgID = 0x79B,.data = {2, 0x21, 1, 0, 0, 0, 0, 0}};
|
||
const CAN_frame_t LEAF_NEXT_LINE_REQUEST = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_std,}},.MsgID = 0x79B,.data = {0x30, 1, 0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
|
||
// The Li-ion battery controller only accepts a multi-message query. In fact, the LBC transmits many
|
||
// groups: the first one contains lots of High Voltage battery data as SOC, currents, and voltage; the second
|
||
// replies with all the battery’s cells voltages in millivolt, the third and the fifth one are still unknown, the
|
||
// fourth contains the four battery packs temperatures, and the last one tells which cell has the shunt active.
|
||
// There are also two more groups: group 61, which replies with lots of CAN messages (up to 48); here we
|
||
// found the SOH value, and group 84 that replies with the HV battery production serial.
|
||
|
||
static uint8_t crctable[256] = {0,133,143,10,155,30,20,145,179,54,60,185,40,173,167,34,227,102,108,233,120,253,247,114,80,213,223,90,203,78,68,193,67,
|
||
198,204,73,216,93,87,210,240,117,127,250,107,238,228,97,160,37,47,170,59,190,180,49,19,150,156,25,136,13,7,130,134,3,9,
|
||
140,29,152,146,23,53,176,186,63,174,43,33,164,101,224,234,111,254,123,113,244,214,83,89,220,77,200,194,71,197,64,74,207,
|
||
94,219,209,84,118,243,249,124,237,104,98,231,38,163,169,44,189,56,50,183,149,16,26,159,14,139,129,4,137,12,6,131,18,151,
|
||
157,24,58,191,181,48,161,36,46,171,106,239,229,96,241,116,126,251,217,92,86,211,66,199,205,72,202,79,69,192,81,212,222,
|
||
91,121,252,246,115,226,103,109,232,41,172,166,35,178,55,61,184,154,31,21,144,1,132,142,11,15,138,128,5,148,17,27,158,188,
|
||
57,51,182,39,162,168,45,236,105,99,230,119,242,248,125,95,218,208,85,196,65,75,206,76,201,195,70,215,82,88,221,255,122,
|
||
112,245,100,225,235,110,175,42,32,165,52,177,187,62,28,153,147,22,135,2,8,141};
|
||
|
||
//Nissan LEAF battery parameters from constantly sent CAN
|
||
#define ZE0_BATTERY 0
|
||
#define AZE0_BATTERY 1
|
||
#define ZE1_BATTERY 2
|
||
static uint8_t LEAF_Battery_Type = ZE0_BATTERY;
|
||
#define MAX_CELL_VOLTAGE 4250 //Battery is put into emergency stop if one cell goes over this value
|
||
#define MIN_CELL_VOLTAGE 2700 //Battery is put into emergency stop if one cell goes below this value
|
||
#define MAX_CELL_DEVIATION 500 //LED turns yellow on the board if mv delta exceeds this value
|
||
#define WH_PER_GID 77 //One GID is this amount of Watt hours
|
||
#define LB_MAX_SOC 1000 //LEAF BMS never goes over this value. We use this info to rescale SOC% sent to Fronius
|
||
#define LB_MIN_SOC 0 //LEAF BMS never goes below this value. We use this info to rescale SOC% sent to Fronius
|
||
static uint16_t LB_Discharge_Power_Limit = 0; //Limit in kW
|
||
static uint16_t LB_Charge_Power_Limit = 0; //Limit in kW
|
||
static int16_t LB_MAX_POWER_FOR_CHARGER = 0; //Limit in kW
|
||
static int16_t LB_SOC = 500; //0 - 100.0 % (0-1000) The real SOC% in the battery
|
||
static int16_t CalculatedSOC = 0; // Temporary value used for calculating SOC
|
||
static uint16_t LB_TEMP = 0; //Temporary value used in status checks
|
||
static uint16_t LB_Wh_Remaining = 0; //Amount of energy in battery, in Wh
|
||
static uint16_t LB_GIDS = 0;
|
||
static uint16_t LB_MAX = 0;
|
||
static uint16_t LB_Max_GIDS = 273; //Startup in 24kWh mode
|
||
static uint16_t LB_StateOfHealth = 99; //State of health %
|
||
static uint16_t LB_Total_Voltage = 370; //Battery voltage (0-450V)
|
||
static int16_t LB_Current = 0; //Current in A going in/out of battery
|
||
static int16_t LB_Power = 0; //Watts going in/out of battery
|
||
static int16_t LB_HistData_Temperature_MAX = 6; //-40 to 86*C
|
||
static int16_t LB_HistData_Temperature_MIN = 5; //-40 to 86*C
|
||
static int16_t LB_AverageTemperature = 6; //Only available on ZE0, in celcius, -40 to +55
|
||
static uint8_t LB_Relay_Cut_Request = 0; //LB_FAIL
|
||
static uint8_t LB_Failsafe_Status = 0; //LB_STATUS = 000b = normal start Request
|
||
//001b = Main Relay OFF Request
|
||
//010b = Charging Mode Stop Request
|
||
//011b = Main Relay OFF Request
|
||
//100b = Caution Lamp Request
|
||
//101b = Caution Lamp Request & Main Relay OFF Request
|
||
//110b = Caution Lamp Request & Charging Mode Stop Request
|
||
//111b = Caution Lamp Request & Main Relay OFF Request
|
||
static byte LB_Interlock = 1; //Contains info on if HV leads are seated (Note, to use this both HV connectors need to be inserted)
|
||
static byte LB_Full_CHARGE_flag = 0; //LB_FCHGEND , Goes to 1 if battery is fully charged
|
||
static byte LB_MainRelayOn_flag = 0; //No-Permission=0, Main Relay On Permission=1
|
||
static byte LB_Capacity_Empty = 0; //LB_EMPTY, , Goes to 1 if battery is empty
|
||
|
||
// Nissan LEAF battery data from polled CAN messages
|
||
static uint8_t battery_request_idx = 0;
|
||
static uint8_t group_7bb = 0;
|
||
static uint8_t group = 1;
|
||
static uint8_t stop_battery_query = 0;
|
||
static uint16_t cell_voltages[97]; //array with all the cellvoltages
|
||
static uint16_t cellcounter = 0;
|
||
static uint16_t min_max_voltage[2]; //contains cell min[0] and max[1] values in mV
|
||
static uint16_t cell_deviation_mV = 0; //contains the deviation between highest and lowest cell in mV
|
||
static uint16_t HX = 0; //Internal resistance
|
||
static uint16_t insulation = 0; //Insulation resistance
|
||
static int32_t Battery_current_1 = 0; //High Voltage battery current; it’s positive if discharged, negative when charging
|
||
static int32_t Battery_current_2 = 0; //High Voltage battery current; it’s positive if discharged, negative when charging (unclear why two values exist)
|
||
static uint16_t temp_raw_1 = 0;
|
||
static uint8_t temp_raw_2_highnibble = 0;
|
||
static uint16_t temp_raw_2 = 0;
|
||
static uint16_t temp_raw_3 = 0;
|
||
static uint16_t temp_raw_4 = 0;
|
||
static uint16_t temp_raw_max = 0;
|
||
static uint16_t temp_raw_min = 0;
|
||
static int16_t temp_polled_max = 0;
|
||
static int16_t temp_polled_min = 0;
|
||
|
||
|
||
void update_values_leaf_battery()
|
||
{ /* This function maps all the values fetched via CAN to the correct parameters used for modbus */
|
||
bms_status = ACTIVE; //Startout in active mode
|
||
|
||
/* Start with mapping all values */
|
||
|
||
StateOfHealth = (LB_StateOfHealth * 100); //Increase range from 99% -> 99.00%
|
||
|
||
//Calculate the SOC% value to send to Fronius
|
||
CalculatedSOC = LB_SOC;
|
||
CalculatedSOC = LB_MIN_SOC + (LB_MAX_SOC - LB_MIN_SOC) * (CalculatedSOC - MINPERCENTAGE) / (MAXPERCENTAGE - MINPERCENTAGE);
|
||
if (CalculatedSOC < 0){ //We are in the real SOC% range of 0-20%, always set SOC sent to Fronius as 0%
|
||
CalculatedSOC = 0;
|
||
}
|
||
if (CalculatedSOC > 1000){ //We are in the real SOC% range of 80-100%, always set SOC sent to Fronius as 100%
|
||
CalculatedSOC = 1000;
|
||
}
|
||
SOC = (CalculatedSOC * 10); //increase CalculatedSOC range from 0-100.0 -> 100.00
|
||
|
||
battery_voltage = (LB_Total_Voltage*10); //One more decimal needed
|
||
|
||
battery_current = (LB_Current*10); //One more decimal needed
|
||
|
||
capacity_Wh = (LB_Max_GIDS * WH_PER_GID);
|
||
|
||
remaining_capacity_Wh = LB_Wh_Remaining;
|
||
|
||
LB_Power = LB_Total_Voltage * LB_Current;//P = U * I
|
||
stat_batt_power = convert2unsignedint16(LB_Power); //add sign if needed
|
||
|
||
//Update temperature readings. Method depends on which generation LEAF battery is used
|
||
if(LEAF_Battery_Type == ZE0_BATTERY){
|
||
//Since we only have average value, send the minimum as -1.0 degrees below average
|
||
temperature_min = convert2unsignedint16((LB_AverageTemperature * 10)-10); //add sign if negative and increase range
|
||
temperature_max = convert2unsignedint16((LB_AverageTemperature * 10));
|
||
}
|
||
else if(LEAF_Battery_Type == AZE0_BATTERY){
|
||
//Use the value sent constantly via CAN in 5C0 (only available on AZE0)
|
||
temperature_min = convert2unsignedint16((LB_HistData_Temperature_MIN * 10)); //add sign if negative and increase range
|
||
temperature_max = convert2unsignedint16((LB_HistData_Temperature_MAX * 10));
|
||
}
|
||
else
|
||
{ // ZE1 (TODO: Once the muxed value in 5C0 becomes known, switch to using that instead of this complicated polled value)
|
||
if(temp_raw_min != 0) //We have a polled value available
|
||
{
|
||
temp_polled_min = ((Temp_fromRAW_to_F(temp_raw_min) - 320 ) * 5) / 9; //Convert from F to C
|
||
temp_polled_max = ((Temp_fromRAW_to_F(temp_raw_max) - 320 ) * 5) / 9; //Convert from F to C
|
||
if(temp_polled_min < temp_polled_max){ //Catch any edge cases from Temp_fromRAW_to_F function
|
||
temperature_min = convert2unsignedint16((temp_polled_min));
|
||
temperature_max = convert2unsignedint16((temp_polled_max));
|
||
}
|
||
else{
|
||
temperature_min = convert2unsignedint16((temp_polled_max));
|
||
temperature_max = convert2unsignedint16((temp_polled_min));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Define power able to be discharged from battery
|
||
if(LB_Discharge_Power_Limit > 30) { //if >30kW can be pulled from battery
|
||
max_target_discharge_power = 30000; //cap value so we don't go over the Fronius limits
|
||
}
|
||
else{
|
||
max_target_discharge_power = (LB_Discharge_Power_Limit * 1000); //kW to W
|
||
}
|
||
if(SOC == 0){ //Scaled SOC% value is 0.00%, we should not discharge battery further
|
||
max_target_discharge_power = 0;
|
||
}
|
||
|
||
// Define power able to be put into the battery
|
||
if(LB_Charge_Power_Limit > 30){ //if >30kW can be put into the battery
|
||
max_target_charge_power = 30000; //cap value so we don't go over the Fronius limits
|
||
}
|
||
if(LB_Charge_Power_Limit < 0){ //LB_MAX_POWER_FOR_CHARGER can actually go to -10kW
|
||
max_target_charge_power = 0; //cap calue so we dont do under the Fronius limits
|
||
}
|
||
else{
|
||
max_target_charge_power = (LB_Charge_Power_Limit * 1000); //kW to W
|
||
}
|
||
if(SOC == 10000) //Scaled SOC% value is 100.00%
|
||
{
|
||
max_target_charge_power = 0; //No need to charge further, set max power to 0
|
||
}
|
||
|
||
/*Extra safety functions below*/
|
||
if(LB_GIDS < 10) //800Wh left in battery
|
||
{ //Battery is running abnormally low, some discharge logic might have failed. Zero it all out.
|
||
SOC = 0;
|
||
max_target_discharge_power = 0;
|
||
}
|
||
|
||
if(LB_Full_CHARGE_flag)
|
||
{ //Battery reports that it is fully charged stop all further charging incase it hasn't already
|
||
max_target_charge_power = 0;
|
||
}
|
||
|
||
if(LB_Relay_Cut_Request)
|
||
{ //LB_FAIL, BMS requesting shutdown and contactors to be opened
|
||
Serial.println("Battery requesting immediate shutdown and contactors to be opened!");
|
||
//Note, this is sometimes triggered during the night while idle, and the BMS recovers after a while. Removed latching from this scenario
|
||
errorCode = 1;
|
||
max_target_discharge_power = 0;
|
||
max_target_charge_power = 0;
|
||
}
|
||
|
||
if(LB_Failsafe_Status > 0) // 0 is normal, start charging/discharging
|
||
{
|
||
switch(LB_Failsafe_Status)
|
||
{
|
||
case(1):
|
||
//Normal Stop Request
|
||
//This means that battery is fully discharged and it's OK to stop the session. For stationary storage we don't disconnect contactors, so we do nothing here.
|
||
break;
|
||
case(2):
|
||
//Charging Mode Stop Request
|
||
//This means that battery is fully charged and it's OK to stop the session. For stationary storage we don't disconnect contactors, so we do nothing here.
|
||
break;
|
||
case(3):
|
||
//Charging Mode Stop Request & Normal Stop Request
|
||
//Normal stop request. For stationary storage we don't disconnect contactors, so we ignore this.
|
||
break;
|
||
case(4):
|
||
//Caution Lamp Request
|
||
Serial.println("Battery raised caution indicator. Inspect battery status!");
|
||
break;
|
||
case(5):
|
||
//Caution Lamp Request & Normal Stop Request
|
||
bms_status = FAULT;
|
||
errorCode = 2;
|
||
Serial.println("Battery raised caution indicator AND requested discharge stop. Inspect battery status!");
|
||
break;
|
||
case(6):
|
||
//Caution Lamp Request & Charging Mode Stop Request
|
||
bms_status = FAULT;
|
||
errorCode = 3;
|
||
Serial.println("Battery raised caution indicator AND requested charge stop. Inspect battery status!");
|
||
break;
|
||
case(7):
|
||
//Caution Lamp Request & Charging Mode Stop Request & Normal Stop Request
|
||
bms_status = FAULT;
|
||
errorCode = 4;
|
||
Serial.println("Battery raised caution indicator AND requested charge/discharge stop. Inspect battery status!");
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(LB_StateOfHealth < 25)
|
||
{ //Battery is extremely degraded, not fit for secondlifestorage. Zero it all out.
|
||
if(LB_StateOfHealth != 0)
|
||
{ //Extra check to see that we actually have a SOH Value available
|
||
Serial.println("State of health critically low. Battery internal resistance too high to continue. Recycle battery.");
|
||
bms_status = FAULT;
|
||
errorCode = 5;
|
||
max_target_discharge_power = 0;
|
||
max_target_charge_power = 0;
|
||
}
|
||
}
|
||
|
||
#ifdef INTERLOCK_REQUIRED
|
||
if(!LB_Interlock)
|
||
{
|
||
Serial.println("Battery interlock loop broken. Check that high voltage connectors are seated. Battery will be disabled!");
|
||
bms_status = FAULT;
|
||
errorCode = 6;
|
||
SOC = 0;
|
||
max_target_discharge_power = 0;
|
||
max_target_charge_power = 0;
|
||
}
|
||
#endif
|
||
|
||
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
|
||
if(!CANstillAlive)
|
||
{
|
||
bms_status = FAULT;
|
||
errorCode = 7;
|
||
Serial.println("No CAN communication detected for 60s. Shutting down battery control.");
|
||
}
|
||
else
|
||
{
|
||
CANstillAlive--;
|
||
}
|
||
if(CANerror > MAX_CAN_FAILURES) //Also check if we have recieved too many malformed CAN messages. If so, signal via LED
|
||
{
|
||
LEDcolor = YELLOW;
|
||
}
|
||
|
||
/*Finally print out values to serial if configured to do so*/
|
||
if(printValues)
|
||
{
|
||
if(errorCode > 0)
|
||
{
|
||
Serial.print("ERROR CODE ACTIVE IN SYSTEM. NUMBER: ");
|
||
Serial.println(errorCode);
|
||
}
|
||
Serial.print("BMS Status (3=OK): ");
|
||
Serial.println(bms_status);
|
||
switch (bms_char_dis_status)
|
||
{
|
||
case 0:
|
||
Serial.println("Battery Idle");
|
||
break;
|
||
case 1:
|
||
Serial.println("Battery Discharging");
|
||
break;
|
||
case 2:
|
||
Serial.println("Battery Charging");
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
Serial.print("Power: ");
|
||
Serial.print(LB_Power);
|
||
Serial.print(" Max discharge power: ");
|
||
Serial.print(max_target_discharge_power);
|
||
Serial.print(" Max charge power: ");
|
||
Serial.print(max_target_charge_power);
|
||
Serial.print(" SOH%: ");
|
||
Serial.print(StateOfHealth);
|
||
Serial.print(" SOC% to inverter: ");
|
||
Serial.print(SOC);
|
||
Serial.print(" SOC% of battery: ");
|
||
Serial.print(LB_SOC);
|
||
Serial.print(" GIDS: ");
|
||
Serial.println(LB_GIDS);
|
||
Serial.print("LEAF battery gen: ");
|
||
Serial.println(LEAF_Battery_Type);
|
||
Serial.print("Min cell voltage: ");
|
||
Serial.println(min_max_voltage[0]);
|
||
Serial.print("Max cell voltage: ");
|
||
Serial.println(min_max_voltage[1]);
|
||
Serial.print("Cell deviation: ");
|
||
Serial.println(cell_deviation_mV);
|
||
Serial.print("Temperatures; Polled temp min: ");
|
||
Serial.print(temp_polled_min);
|
||
Serial.print(" Polled temp max: ");
|
||
Serial.print(temp_polled_max);
|
||
Serial.print(" 5C0 temp max: ");
|
||
Serial.print(LB_HistData_Temperature_MAX);
|
||
Serial.print(" 5C0 temp min: ");
|
||
Serial.println(LB_HistData_Temperature_MIN);
|
||
Serial.print("Current 1: ");
|
||
Serial.print(Battery_current_1);
|
||
Serial.print(" Current 2: ");
|
||
Serial.println(Battery_current_2);
|
||
}
|
||
}
|
||
|
||
void receive_can_leaf_battery(CAN_frame_t rx_frame)
|
||
{
|
||
switch (rx_frame.MsgID)
|
||
{
|
||
case 0x1DB:
|
||
if(is_message_corrupt(rx_frame)){
|
||
CANerror++;
|
||
break; //Message content malformed, abort reading data from it
|
||
}
|
||
LB_Current = (rx_frame.data.u8[0] << 3) | (rx_frame.data.u8[1] & 0xe0) >> 5;
|
||
if (LB_Current & 0x0400){
|
||
// negative so extend the sign bit
|
||
LB_Current |= 0xf800;
|
||
}
|
||
|
||
LB_Total_Voltage = ((rx_frame.data.u8[2] << 2) | (rx_frame.data.u8[3] & 0xc0) >> 6) / 2;
|
||
|
||
//Collect various data from the BMS
|
||
LB_Relay_Cut_Request = ((rx_frame.data.u8[1] & 0x18) >> 3);
|
||
LB_Failsafe_Status = (rx_frame.data.u8[1] & 0x07);
|
||
LB_MainRelayOn_flag = (byte) ((rx_frame.data.u8[3] & 0x20) >> 5);
|
||
if(LB_MainRelayOn_flag){
|
||
batteryAllowsContactorClosing = 1;
|
||
}
|
||
else{
|
||
batteryAllowsContactorClosing = 0;
|
||
}
|
||
LB_Full_CHARGE_flag = (byte) ((rx_frame.data.u8[3] & 0x10) >> 4);
|
||
LB_Interlock = (byte) ((rx_frame.data.u8[3] & 0x08) >> 3);
|
||
break;
|
||
case 0x1DC:
|
||
if(is_message_corrupt(rx_frame)){
|
||
CANerror++;
|
||
break; //Message content malformed, abort reading data from it
|
||
}
|
||
LB_Discharge_Power_Limit = ((rx_frame.data.u8[0] << 2 | rx_frame.data.u8[1] >> 6) / 4.0);
|
||
LB_Charge_Power_Limit = (((rx_frame.data.u8[1] & 0x3F) << 4 | rx_frame.data.u8[2] >> 4) / 4.0);
|
||
LB_MAX_POWER_FOR_CHARGER = ((((rx_frame.data.u8[2] & 0x0F) << 6 | rx_frame.data.u8[3] >> 2) / 10.0) - 10);
|
||
break;
|
||
case 0x55B:
|
||
if(is_message_corrupt(rx_frame)){
|
||
CANerror++;
|
||
break; //Message content malformed, abort reading data from it
|
||
}
|
||
LB_TEMP = (rx_frame.data.u8[0] << 2 | rx_frame.data.u8[1] >> 6);
|
||
if (LB_TEMP != 0x3ff) { //3FF is unavailable value
|
||
LB_SOC = LB_TEMP;
|
||
}
|
||
break;
|
||
case 0x5BC:
|
||
CANstillAlive = 12; //Indicate that we are still getting CAN messages from the BMS
|
||
|
||
LB_MAX = ((rx_frame.data.u8[5] & 0x10) >> 4);
|
||
if (LB_MAX){
|
||
LB_Max_GIDS = (rx_frame.data.u8[0] << 2) | ((rx_frame.data.u8[1] & 0xC0) >> 6);
|
||
//Max gids active, do nothing
|
||
//Only the 30/40/62kWh packs have this mux
|
||
}
|
||
else{ //Normal current GIDS value is transmitted
|
||
LB_GIDS = (rx_frame.data.u8[0] << 2) | ((rx_frame.data.u8[1] & 0xC0) >> 6);
|
||
LB_Wh_Remaining = (LB_GIDS * WH_PER_GID);
|
||
}
|
||
|
||
if(LEAF_Battery_Type == ZE0_BATTERY){
|
||
LB_AverageTemperature = (rx_frame.data.u8[3] - 40); //In celcius, -40 to +55
|
||
}
|
||
|
||
LB_TEMP = (rx_frame.data.u8[4] >> 1);
|
||
if (LB_TEMP != 0){
|
||
LB_StateOfHealth = LB_TEMP; //Collect state of health from battery
|
||
}
|
||
break;
|
||
case 0x5C0: //This method only works for 2013-2017 AZE0 LEAF packs, the mux is different on other generations
|
||
if(LEAF_Battery_Type == AZE0_BATTERY){
|
||
if ((rx_frame.data.u8[0]>>6) == 1){ // Battery MAX temperature. Effectively has only 7-bit precision, as the bottom bit is always 0.
|
||
LB_HistData_Temperature_MAX = ((rx_frame.data.u8[2] / 2) - 40);
|
||
}
|
||
if ((rx_frame.data.u8[0]>>6) == 3){ // Battery MIN temperature. Effectively has only 7-bit precision, as the bottom bit is always 0.
|
||
LB_HistData_Temperature_MIN = ((rx_frame.data.u8[2] / 2) - 40);
|
||
}
|
||
}
|
||
break;
|
||
case 0x59E:
|
||
//AZE0 2013-2017 or ZE1 2018-2023 battery detected
|
||
//Only detect as AZE0 if not already set as ZE1
|
||
if(LEAF_Battery_Type != ZE1_BATTERY){
|
||
LEAF_Battery_Type = AZE0_BATTERY;
|
||
}
|
||
break;
|
||
case 0x1ED:
|
||
case 0x1C2:
|
||
//ZE1 2018-2023 battery detected!
|
||
LEAF_Battery_Type = ZE1_BATTERY;
|
||
break;
|
||
case 0x79B:
|
||
stop_battery_query = 1; //Someone is trying to read data with Leafspy, stop our own polling!
|
||
break;
|
||
case 0x7BB:
|
||
//First check which group data we are getting
|
||
if (rx_frame.data.u8[0] == 0x10) { //First message of a group
|
||
group_7bb = rx_frame.data.u8[3];
|
||
if (group_7bb != 1 && group_7bb != 2 && group_7bb != 4) { //We are only interested in groups 1,2 and 4
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(!stop_battery_query){
|
||
ESP32Can.CANWriteFrame(&LEAF_NEXT_LINE_REQUEST); //Request the next frame for the group
|
||
}
|
||
|
||
if(group_7bb == 1) //High precision SOC, Current, voltages etc.
|
||
{
|
||
if(rx_frame.data.u8[0] == 0x10){ //First frame
|
||
Battery_current_1 = (rx_frame.data.u8[4] << 24) | (rx_frame.data.u8[5] << 16 | ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]));
|
||
if(Battery_current_1 & 0x8000000 == 0x8000000){
|
||
Battery_current_1 = (( Battery_current_1 | -0x100000000 ) / 1024);
|
||
}
|
||
else{
|
||
Battery_current_1 = (Battery_current_1 / 1024);
|
||
}
|
||
}
|
||
|
||
if(rx_frame.data.u8[0] == 0x21){ //Second frame
|
||
Battery_current_2 = (rx_frame.data.u8[3] << 24) | (rx_frame.data.u8[4] << 16 | ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]));
|
||
if(Battery_current_2 & 0x8000000 == 0x8000000){
|
||
Battery_current_2 = (( Battery_current_2 | -0x100000000 ) / 1024);
|
||
}
|
||
else{
|
||
Battery_current_2 = (Battery_current_2 / 1024);
|
||
}
|
||
}
|
||
|
||
if(rx_frame.data.u8[0] == 0x23){ // Fourth frame
|
||
insulation = (uint16_t) ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]);
|
||
}
|
||
|
||
if(rx_frame.data.u8[0] == 0x24){ // Fifth frame
|
||
HX = (uint16_t) ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]) / 102.4;
|
||
}
|
||
|
||
}
|
||
|
||
if(group_7bb == 2) //Cell Voltages
|
||
{
|
||
if(rx_frame.data.u8[0] == 0x10){ //first frame is anomalous
|
||
battery_request_idx = 0;
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||
break;
|
||
}
|
||
if(rx_frame.data.u8[6] == 0xFF && rx_frame.data.u8[0] == 0x2C){ //Last frame
|
||
//Last frame does not contain any cell data, calculate the result
|
||
min_max_voltage[0] = 9999;
|
||
min_max_voltage[1] = 0;
|
||
for(cellcounter = 0; cellcounter < 96; cellcounter++){
|
||
if(min_max_voltage[0] > cell_voltages[cellcounter]) min_max_voltage[0] = cell_voltages[cellcounter];
|
||
if(min_max_voltage[1] < cell_voltages[cellcounter]) min_max_voltage[1] = cell_voltages[cellcounter];
|
||
}
|
||
|
||
cell_deviation_mV = (min_max_voltage[1] - min_max_voltage[0]);
|
||
|
||
if(cell_deviation_mV > MAX_CELL_DEVIATION){
|
||
LEDcolor = YELLOW;
|
||
Serial.println("HIGH CELL DEVIATION!!! Inspect battery!");
|
||
}
|
||
|
||
if(min_max_voltage[1] >= MAX_CELL_VOLTAGE){
|
||
bms_status = FAULT;
|
||
errorCode = 8;
|
||
Serial.println("CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
|
||
}
|
||
if(min_max_voltage[0] <= MIN_CELL_VOLTAGE){
|
||
bms_status = FAULT;
|
||
errorCode = 9;
|
||
Serial.println("CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
|
||
}
|
||
break;
|
||
}
|
||
|
||
if((rx_frame.data.u8[0] % 2) == 0){ //even frames
|
||
cell_voltages[battery_request_idx++] |= rx_frame.data.u8[1];
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||
} else { //odd frames
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
|
||
cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8);
|
||
}
|
||
}
|
||
|
||
if(group_7bb == 4) { //Temperatures
|
||
if (rx_frame.data.u8[0] == 0x10) { //First message
|
||
temp_raw_1 = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
|
||
temp_raw_2_highnibble = rx_frame.data.u8[7];
|
||
}
|
||
if (rx_frame.data.u8[0] == 0x21) { //Second message
|
||
temp_raw_2 = (temp_raw_2_highnibble << 8) | rx_frame.data.u8[1];
|
||
temp_raw_3 = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
|
||
temp_raw_4 = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7];
|
||
}
|
||
if (rx_frame.data.u8[0] == 0x22) { //Third message
|
||
//All values read, let's figure out the min/max!
|
||
|
||
if(temp_raw_3 == 65535){ //We are on a 2013+ pack that only has three temp sensors.
|
||
//Start with finding max value
|
||
temp_raw_max = temp_raw_1;
|
||
if(temp_raw_2 > temp_raw_max){
|
||
temp_raw_max = temp_raw_2;
|
||
}
|
||
if(temp_raw_4 > temp_raw_max){
|
||
temp_raw_max = temp_raw_4;
|
||
}
|
||
//Then find min
|
||
temp_raw_min = temp_raw_1;
|
||
if(temp_raw_2 < temp_raw_min){
|
||
temp_raw_min = temp_raw_2;
|
||
}
|
||
if(temp_raw_4 < temp_raw_min){
|
||
temp_raw_min = temp_raw_4;
|
||
}
|
||
}
|
||
else{ //All 4 temp sensors available on 2011-2012
|
||
//Start with finding max value
|
||
temp_raw_max = temp_raw_1;
|
||
if(temp_raw_2 > temp_raw_max){
|
||
temp_raw_max = temp_raw_2;
|
||
}
|
||
if(temp_raw_3 > temp_raw_max){
|
||
temp_raw_max = temp_raw_3;
|
||
}
|
||
if(temp_raw_4 > temp_raw_max){
|
||
temp_raw_max = temp_raw_4;
|
||
}
|
||
//Then find min
|
||
temp_raw_min = temp_raw_1;
|
||
if(temp_raw_2 < temp_raw_min){
|
||
temp_raw_min = temp_raw_2;
|
||
}
|
||
if(temp_raw_3 < temp_raw_min){
|
||
temp_raw_min = temp_raw_2;
|
||
}
|
||
if(temp_raw_4 < temp_raw_min){
|
||
temp_raw_min = temp_raw_4;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
void send_can_leaf_battery()
|
||
{
|
||
unsigned long currentMillis = millis();
|
||
// Send 100ms CAN Message
|
||
if (currentMillis - previousMillis100 >= interval100)
|
||
{
|
||
previousMillis100 = currentMillis;
|
||
|
||
ESP32Can.CANWriteFrame(&LEAF_50B); //Always send 50B as a static message (Contains HCM_WakeUpSleepCommand == 11b == WakeUp, and CANMASK = 1)
|
||
|
||
mprun100++;
|
||
if (mprun100 > 3){
|
||
mprun100 = 0;
|
||
}
|
||
|
||
if (mprun100 == 0){
|
||
LEAF_50C.data.u8[3] = 0x00;
|
||
LEAF_50C.data.u8[4] = 0x5D;
|
||
LEAF_50C.data.u8[5] = 0xC8;
|
||
}
|
||
else if(mprun100 == 1){
|
||
LEAF_50C.data.u8[3] = 0x01;
|
||
LEAF_50C.data.u8[4] = 0xB2;
|
||
LEAF_50C.data.u8[5] = 0x31;
|
||
}
|
||
else if(mprun100 == 2){
|
||
LEAF_50C.data.u8[3] = 0x02;
|
||
LEAF_50C.data.u8[4] = 0x5D;
|
||
LEAF_50C.data.u8[5] = 0x63;
|
||
}
|
||
else if(mprun100 == 3){
|
||
LEAF_50C.data.u8[3] = 0x03;
|
||
LEAF_50C.data.u8[4] = 0xB2;
|
||
LEAF_50C.data.u8[5] = 0x9A;
|
||
}
|
||
ESP32Can.CANWriteFrame(&LEAF_50C);
|
||
}
|
||
//Send 10ms message
|
||
if (currentMillis - previousMillis10 >= interval10)
|
||
{
|
||
previousMillis10 = currentMillis;
|
||
|
||
if(mprun10 == 0){
|
||
LEAF_1D4.data.u8[4] = 0x07;
|
||
LEAF_1D4.data.u8[7] = 0x12;
|
||
}
|
||
else if(mprun10 == 1){
|
||
LEAF_1D4.data.u8[4] = 0x47;
|
||
LEAF_1D4.data.u8[7] = 0xD5;
|
||
}
|
||
else if(mprun10 == 2){
|
||
LEAF_1D4.data.u8[4] = 0x87;
|
||
LEAF_1D4.data.u8[7] = 0x19;
|
||
}
|
||
else if(mprun10 == 3){
|
||
LEAF_1D4.data.u8[4] = 0xC7;
|
||
LEAF_1D4.data.u8[7] = 0xDE;
|
||
}
|
||
ESP32Can.CANWriteFrame(&LEAF_1D4);
|
||
|
||
mprun10++;
|
||
if (mprun10 > 3){
|
||
mprun10 = 0;
|
||
}
|
||
|
||
switch(mprun10r)
|
||
{
|
||
case(0):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x00;
|
||
LEAF_1F2.data.u8[7] = 0x8F;
|
||
break;
|
||
case(1):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x01;
|
||
LEAF_1F2.data.u8[7] = 0x80;
|
||
break;
|
||
case(2):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x02;
|
||
LEAF_1F2.data.u8[7] = 0x81;
|
||
break;
|
||
case(3):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x03;
|
||
LEAF_1F2.data.u8[7] = 0x82;
|
||
break;
|
||
case(4):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x00;
|
||
LEAF_1F2.data.u8[7] = 0x8F;
|
||
break;
|
||
case(5): // Set 2
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x01;
|
||
LEAF_1F2.data.u8[7] = 0x84;
|
||
break;
|
||
case(6):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x02;
|
||
LEAF_1F2.data.u8[7] = 0x85;
|
||
break;
|
||
case(7):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x03;
|
||
LEAF_1F2.data.u8[7] = 0x86;
|
||
break;
|
||
case(8):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x00;
|
||
LEAF_1F2.data.u8[7] = 0x83;
|
||
break;
|
||
case(9):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x01;
|
||
LEAF_1F2.data.u8[7] = 0x84;
|
||
break;
|
||
case(10): // Set 3
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x02;
|
||
LEAF_1F2.data.u8[7] = 0x81;
|
||
break;
|
||
case(11):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x03;
|
||
LEAF_1F2.data.u8[7] = 0x82;
|
||
break;
|
||
case(12):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x00;
|
||
LEAF_1F2.data.u8[7] = 0x8F;
|
||
break;
|
||
case(13):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x01;
|
||
LEAF_1F2.data.u8[7] = 0x80;
|
||
break;
|
||
case(14):
|
||
LEAF_1F2.data.u8[3] = 0xB0;
|
||
LEAF_1F2.data.u8[6] = 0x02;
|
||
LEAF_1F2.data.u8[7] = 0x81;
|
||
break;
|
||
case(15): // Set 4
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x03;
|
||
LEAF_1F2.data.u8[7] = 0x86;
|
||
break;
|
||
case(16):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x00;
|
||
LEAF_1F2.data.u8[7] = 0x83;
|
||
break;
|
||
case(17):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x01;
|
||
LEAF_1F2.data.u8[7] = 0x84;
|
||
break;
|
||
case(18):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x02;
|
||
LEAF_1F2.data.u8[7] = 0x85;
|
||
break;
|
||
case(19):
|
||
LEAF_1F2.data.u8[3] = 0xB4;
|
||
LEAF_1F2.data.u8[6] = 0x03;
|
||
LEAF_1F2.data.u8[7] = 0x86;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
ESP32Can.CANWriteFrame(&LEAF_1F2); //Contains (CHG_STA_RQ == 1 == Normal Charge)
|
||
|
||
mprun10r++;
|
||
if(mprun10r > 19){ // 0x1F2 patter repeats after 20 messages,
|
||
mprun10r = 0;
|
||
}
|
||
}
|
||
//Send 10s CAN messages
|
||
if (currentMillis - previousMillis10s >= interval10s)
|
||
{
|
||
previousMillis10s = currentMillis;
|
||
|
||
//Every 10s, ask diagnostic data from the battery. Don't ask if someone is already polling on the bus (Leafspy?)
|
||
if(!stop_battery_query){
|
||
if (group == 1){ // Cycle between group 1, 2, and 4 using bit manipulation
|
||
group = 2;
|
||
}
|
||
else if (group == 2){
|
||
group = 4;
|
||
}
|
||
else if (group == 4){
|
||
group = 1;
|
||
}
|
||
LEAF_GROUP_REQUEST.data.u8[2] = group;
|
||
ESP32Can.CANWriteFrame(&LEAF_GROUP_REQUEST);
|
||
}
|
||
stop_battery_query = 0;
|
||
}
|
||
}
|
||
|
||
uint16_t convert2unsignedint16(uint16_t signed_value)
|
||
{
|
||
if(signed_value < 0){
|
||
return(65535 + signed_value);
|
||
}
|
||
else{
|
||
return signed_value;
|
||
}
|
||
}
|
||
|
||
bool is_message_corrupt(CAN_frame_t rx_frame)
|
||
{
|
||
uint8_t crc = 0;
|
||
for (uint8_t j = 0; j < 7; j++){
|
||
crc = crctable[(crc ^ static_cast<uint8_t>(rx_frame.data.u8[j])) % 256];
|
||
}
|
||
return crc != rx_frame.data.u8[7];
|
||
}
|
||
|
||
uint16_t Temp_fromRAW_to_F(uint16_t temperature)
|
||
{ //This function feels horrible, but apparently works well
|
||
if (temperature == 1021) {
|
||
return 10;
|
||
} else if (temperature >= 589) {
|
||
return static_cast<uint16_t>(1620 - temperature * 1.81);
|
||
} else if (temperature >= 569) {
|
||
return static_cast<uint16_t>(572 + (579 - temperature) * 1.80);
|
||
} else if (temperature >= 558) {
|
||
return static_cast<uint16_t>(608 + (558 - temperature) * 1.6363636363636364);
|
||
} else if (temperature >= 548) {
|
||
return static_cast<uint16_t>(626 + (548 - temperature) * 1.80);
|
||
} else if (temperature >= 537) {
|
||
return static_cast<uint16_t>(644 + (537 - temperature) * 1.6363636363636364);
|
||
} else if (temperature >= 447) {
|
||
return static_cast<uint16_t>(662 + (527 - temperature) * 1.8);
|
||
} else if (temperature >= 438) {
|
||
return static_cast<uint16_t>(824 + (438 - temperature) * 2);
|
||
} else if (temperature >= 428) {
|
||
return static_cast<uint16_t>(842 + (428 - temperature) * 1.80);
|
||
} else if (temperature >= 365) {
|
||
return static_cast<uint16_t>(860 + (419 - temperature) * 2.0);
|
||
} else if (temperature >= 357) {
|
||
return static_cast<uint16_t>(986 + (357 - temperature) * 2.25);
|
||
} else if (temperature >= 348) {
|
||
return static_cast<uint16_t>(1004 + (348 - temperature) * 2);
|
||
} else if (temperature >= 316) {
|
||
return static_cast<uint16_t>(1022 + (340 - temperature) * 2.25);
|
||
}
|
||
return static_cast<uint16_t>(1094 + (309 - temperature) * 2.5714285714285715);
|
||
}
|