#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 } 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*/ #ifdef DEBUG_VIA_USB 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); #endif } 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]); cell_max_voltage = min_max_voltage[1]; cell_min_voltage = 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(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(1620 - temperature * 1.81); } else if (temperature >= 569) { return static_cast(572 + (579 - temperature) * 1.80); } else if (temperature >= 558) { return static_cast(608 + (558 - temperature) * 1.6363636363636364); } else if (temperature >= 548) { return static_cast(626 + (548 - temperature) * 1.80); } else if (temperature >= 537) { return static_cast(644 + (537 - temperature) * 1.6363636363636364); } else if (temperature >= 447) { return static_cast(662 + (527 - temperature) * 1.8); } else if (temperature >= 438) { return static_cast(824 + (438 - temperature) * 2); } else if (temperature >= 428) { return static_cast(842 + (428 - temperature) * 1.80); } else if (temperature >= 365) { return static_cast(860 + (419 - temperature) * 2.0); } else if (temperature >= 357) { return static_cast(986 + (357 - temperature) * 2.25); } else if (temperature >= 348) { return static_cast(1004 + (348 - temperature) * 2); } else if (temperature >= 316) { return static_cast(1022 + (340 - temperature) * 2.25); } return static_cast(1094 + (309 - temperature) * 2.5714285714285715); }