Skip to content

Commit

Permalink
Merge pull request #582 from dalathegreat/feature/LEAF-30-reset-SOH
Browse files Browse the repository at this point in the history
Feature: Add degradation data reset for 30kWH LEAF batteries
  • Loading branch information
dalathegreat authored Nov 16, 2024
2 parents d5f310c + 10a7cfe commit 32e75ba
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 17 deletions.
241 changes: 224 additions & 17 deletions Software/src/battery/NISSAN-LEAF-BATTERY.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,17 @@ static int16_t battery2_temp_polled_max = 0;
static int16_t battery2_temp_polled_min = 0;
#endif // DOUBLE_BATTERY

void print_with_units(char* header, int value, char* units) {
Serial.print(header);
Serial.print(value);
Serial.print(units);
}
// Clear SOH values
static uint8_t stateMachineClearSOH = 0xFF;
static uint32_t incomingChallenge = 0xFFFFFFFF;
static uint8_t solvedChallenge[8];
static bool challengeFailed = false;

CAN_frame LEAF_CLEAR_SOH = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x79B,
.data = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};

void update_values_battery() { /* This function maps all the values fetched via CAN to the correct parameters used for modbus */
/* Start with mapping all values */
Expand Down Expand Up @@ -334,17 +340,18 @@ void update_values_battery() { /* This function maps all the values fetched via
datalayer_extended.nissanleaf.HeatingStop = battery_Heating_Stop;
datalayer_extended.nissanleaf.HeatingStart = battery_Heating_Start;
datalayer_extended.nissanleaf.HeaterSendRequest = battery_Batt_Heater_Mail_Send_Request;

/*Finally print out values to serial if configured to do so*/
#ifdef DEBUG_VIA_USB
Serial.println("Values from battery");
print_with_units("Real SOC%: ", (battery_SOC * 0.1), "% ");
print_with_units(", GIDS: ", battery_GIDS, " (x77Wh) ");
print_with_units(", Battery gen: ", LEAF_battery_Type, " ");
print_with_units(", Has heater: ", battery_HeatExist, " ");
print_with_units(", Max cell voltage: ", battery_min_max_voltage[1], "mV ");
print_with_units(", Min cell voltage: ", battery_min_max_voltage[0], "mV ");
#endif
datalayer_extended.nissanleaf.CryptoChallenge = incomingChallenge;
datalayer_extended.nissanleaf.SolvedChallengeMSB =
((solvedChallenge[7] << 24) | (solvedChallenge[6] << 16) | (solvedChallenge[5] << 8) | solvedChallenge[4]);
datalayer_extended.nissanleaf.SolvedChallengeLSB =
((solvedChallenge[3] << 24) | (solvedChallenge[2] << 16) | (solvedChallenge[1] << 8) | solvedChallenge[0]);
datalayer_extended.nissanleaf.challengeFailed = challengeFailed;

// Update requests from webserver datalayer
if (datalayer_extended.nissanleaf.UserRequestSOHreset) {
stateMachineClearSOH = 0; //Start the statemachine
datalayer_extended.nissanleaf.UserRequestSOHreset = false;
}
}

#ifdef DOUBLE_BATTERY
Expand Down Expand Up @@ -603,7 +610,7 @@ void receive_can_battery2(CAN_frame rx_frame) {
}
}

if (stop_battery_query) { //Leafspy is active, stop our own polling
if (stop_battery_query) { //Leafspy/Service request is active, stop our own polling
break;
}

Expand Down Expand Up @@ -840,6 +847,22 @@ void receive_can_battery(CAN_frame rx_frame) {
hold_off_with_polling_10seconds = 10; //Polling is paused for 100s
break;
case 0x7BB:

// This section checks if we are doing a SOH reset towards BMS
if (stateMachineClearSOH < 255) {
//Intercept the messages based on state machine
if (rx_frame.data.u8[0] == 0x06) { // Incoming challenge data!
// BMS should reply with (challenge) 06 67 65 (02 DD 86 43) FF
incomingChallenge = ((rx_frame.data.u8[3] << 24) | (rx_frame.data.u8[4] << 16) | (rx_frame.data.u8[5] << 8) |
rx_frame.data.u8[6]);
}
//Error checking
if ((rx_frame.data.u8[0] == 0x03) && (rx_frame.data.u8[1] == 0x7F)) {
challengeFailed = true;
}
break;
}

//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];
Expand Down Expand Up @@ -1120,6 +1143,10 @@ void send_can_battery() {
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
previousMillis100 = currentMillis;

if (stateMachineClearSOH < 255) { // Enter the ClearSOH statemachine only if we request it
clearSOH();
}

//When battery requests heating pack status change, ack this
if (battery_Batt_Heater_Mail_Send_Request) {
LEAF_50B.data.u8[6] = 0x20; //Batt_Heater_Mail_Send_OK
Expand Down Expand Up @@ -1223,6 +1250,186 @@ uint16_t Temp_fromRAW_to_F(uint16_t temperature) { //This function feels horrib
return static_cast<uint16_t>(1094 + (309 - temperature) * 2.5714285714285715);
}

void clearSOH(void) {
stop_battery_query = true;
hold_off_with_polling_10seconds = 10; // Active battery polling is paused for 100 seconds

switch (stateMachineClearSOH) {
case 0: // Wait until polling actually stops
challengeFailed = false;
stateMachineClearSOH = 1;
break;
case 1: // Set CAN_PROCESS_FLAG to 0xC0
LEAF_CLEAR_SOH.data = {0x02, 0x10, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
// BMS should reply 02 50 C0 FF FF FF FF FF
stateMachineClearSOH = 2;
break;
case 2: // Set something ?
LEAF_CLEAR_SOH.data = {0x02, 0x3E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
// BMS should reply 7E FF FF FF FF FF FF
stateMachineClearSOH = 3;
break;
case 3: // Request challenge to solve
LEAF_CLEAR_SOH.data = {0x02, 0x27, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
// BMS should reply with (challenge) 06 67 65 (02 DD 86 43) FF
stateMachineClearSOH = 4;
break;
case 4: // Send back decoded challenge data
decodeChallengeData(incomingChallenge, solvedChallenge);
LEAF_CLEAR_SOH.data = {
0x10, 0x0A, 0x27, 0x66, solvedChallenge[0], solvedChallenge[1], solvedChallenge[2], solvedChallenge[3]};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
// BMS should reply 7BB 8 30 01 00 FF FF FF FF FF // Proceed with more data (PID ACK)
stateMachineClearSOH = 5;
break;
case 5: // Reply with even more decoded challenge data
LEAF_CLEAR_SOH.data = {
0x21, solvedChallenge[4], solvedChallenge[5], solvedChallenge[6], solvedChallenge[7], 0x00, 0x00, 0x00};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
// BMS should reply 02 67 66 FF FF FF FF FF // Thank you for the data
stateMachineClearSOH = 6;
break;
case 6: // Check if solved data was OK
LEAF_CLEAR_SOH.data = {0x03, 0x31, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
//7BB 8 03 71 03 01 FF FF FF FF // If all is well, BMS replies with 03 71 03 01.
//Incase you sent wrong challenge, you get 03 7f 31 12
stateMachineClearSOH = 7;
break;
case 7: // Reset SOH% request
LEAF_CLEAR_SOH.data = {0x03, 0x31, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
//7BB 8 03 71 03 02 FF FF FF FF // 03 71 03 02 means that BMS accepted command.
//7BB 03 7f 31 12 means your challenge was wrong, so command ignored
stateMachineClearSOH = 8;
break;
case 8: // Please proceed with resetting SOH
LEAF_CLEAR_SOH.data = {0x02, 0x10, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can(&LEAF_CLEAR_SOH, can_config.battery);
// 7BB 8 02 50 81 FF FF FF FF FF // SOH reset OK
stateMachineClearSOH = 255;
break;
default:
break;
}
}

unsigned int CyclicXorHash16Bit(unsigned int param_1, unsigned int param_2) {
bool bVar1;
unsigned int uVar2, uVar3, uVar4, uVar5, uVar6, uVar7, uVar8, uVar9, uVar10, uVar11, iVar12;

param_1 = param_1 & 0xffff;
param_2 = param_2 & 0xffff;
uVar10 = 0xffff;
iVar12 = 2;
do {
uVar2 = param_2;
if ((param_1 & 1) == 1) {
uVar2 = param_1 >> 1;
}
uVar3 = param_2;
if ((param_1 >> 1 & 1) == 1) {
uVar3 = param_1 >> 2;
}
uVar4 = param_2;
if ((param_1 >> 2 & 1) == 1) {
uVar4 = param_1 >> 3;
}
uVar5 = param_2;
if ((param_1 >> 3 & 1) == 1) {
uVar5 = param_1 >> 4;
}
uVar6 = param_2;
if ((param_1 >> 4 & 1) == 1) {
uVar6 = param_1 >> 5;
}
uVar7 = param_2;
if ((param_1 >> 5 & 1) == 1) {
uVar7 = param_1 >> 6;
}
uVar11 = param_1 >> 7;
uVar8 = param_2;
if ((param_1 >> 6 & 1) == 1) {
uVar8 = uVar11;
}
param_1 = param_1 >> 8;
uVar9 = param_2;
if ((uVar11 & 1) == 1) {
uVar9 = param_1;
}
uVar10 =
(((((((((((((((uVar10 & 0x7fff) << 1 ^ uVar2) & 0x7fff) << 1 ^ uVar3) & 0x7fff) << 1 ^ uVar4) & 0x7fff) << 1 ^
uVar5) &
0x7fff)
<< 1 ^
uVar6) &
0x7fff)
<< 1 ^
uVar7) &
0x7fff)
<< 1 ^
uVar8) &
0x7fff)
<< 1 ^
uVar9;
bVar1 = iVar12 != 1;
iVar12 = iVar12 + -1;
} while (bVar1);
return uVar10;
}
unsigned int ComputeMaskedXorProduct(unsigned int param_1, unsigned int param_2, unsigned int param_3) {
return (param_3 ^ 0x7F88 | param_2 ^ 0x8FE7) * ((param_1 & 0xffff) >> 8 ^ param_1 & 0xff) & 0xffff;
}

short ShortMaskedSumAndProduct(short param_1, short param_2) {
unsigned short uVar1;

uVar1 = param_2 + param_1 * 0x0006 & 0xff;
return (uVar1 + param_1) * (uVar1 + param_2);
}

unsigned int MaskedBitwiseRotateMultiply(unsigned int param_1, unsigned int param_2) {
unsigned int uVar1;

param_1 = param_1 & 0xffff;
param_2 = param_2 & 0xffff;
uVar1 = param_2 & (param_1 | 0x0006) & 0xf;
return ((unsigned int)param_1 >> uVar1 | param_1 << (0x10 - uVar1 & 0x1f)) *
(param_2 << uVar1 | (unsigned int)param_2 >> (0x10 - uVar1 & 0x1f)) &
0xffff;
}

unsigned int CryptAlgo(unsigned int param_1, unsigned int param_2, unsigned int param_3) {
unsigned int uVar1, uVar2, iVar3, iVar4;

uVar1 = MaskedBitwiseRotateMultiply(param_2, param_3);
uVar2 = ShortMaskedSumAndProduct(param_2, param_3);
uVar1 = ComputeMaskedXorProduct(param_1, uVar1, uVar2);
uVar2 = ComputeMaskedXorProduct(param_1, uVar2, uVar1);
iVar3 = CyclicXorHash16Bit(uVar1, 0x8421);
iVar4 = CyclicXorHash16Bit(uVar2, 0x8421);
return iVar4 + iVar3 * 0x10000;
}

void decodeChallengeData(unsigned int incomingChallenge, unsigned char* solvedChallenge) {
unsigned int uVar1, uVar2;

uVar1 = CryptAlgo(0x54e9, 0x3afd, incomingChallenge >> 0x10);
uVar2 = CryptAlgo(incomingChallenge & 0xffff, incomingChallenge >> 0x10, 0x54e9);
*solvedChallenge = (unsigned char)uVar1;
solvedChallenge[1] = (unsigned char)uVar2;
solvedChallenge[2] = (unsigned char)((unsigned int)uVar2 >> 8);
solvedChallenge[3] = (unsigned char)((unsigned int)uVar1 >> 8);
solvedChallenge[4] = (unsigned char)((unsigned int)uVar2 >> 0x10);
solvedChallenge[5] = (unsigned char)((unsigned int)uVar1 >> 0x10);
solvedChallenge[6] = (unsigned char)((unsigned int)uVar2 >> 0x18);
solvedChallenge[7] = (unsigned char)((unsigned int)uVar1 >> 0x18);
return;
}

void setup_battery(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Nissan LEAF battery", 63);
datalayer.system.info.battery_protocol[63] = '\0';
Expand Down
8 changes: 8 additions & 0 deletions Software/src/battery/NISSAN-LEAF-BATTERY.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@ uint16_t Temp_fromRAW_to_F(uint16_t temperature);
bool is_message_corrupt(CAN_frame rx_frame);
void setup_battery(void);
void transmit_can(CAN_frame* tx_frame, int interface);
void clearSOH(void);
//Cryptographic functions
void decodeChallengeData(unsigned int SeedInput, unsigned char* Crypt_Output_Buffer);
unsigned int CyclicXorHash16Bit(unsigned int param_1, unsigned int param_2);
unsigned int ComputeMaskedXorProduct(unsigned int param_1, unsigned int param_2, unsigned int param_3);
short ShortMaskedSumAndProduct(short param_1, short param_2);
unsigned int MaskedBitwiseRotateMultiply(unsigned int param_1, unsigned int param_2);
unsigned int CryptAlgo(unsigned int param_1, unsigned int param_2, unsigned int param_3);

#endif
15 changes: 15 additions & 0 deletions Software/src/datalayer/datalayer_extended.h
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,21 @@ typedef struct {
/** bool */
/** Heat request sent*/
bool HeaterSendRequest = false;
/** bool */
/** User requesting SOH reset via WebUI*/
bool UserRequestSOHreset = false;
/** bool */
/** True if the crypto challenge response from BMS is signalling a failed attempt*/
bool challengeFailed = false;
/** uint32_t */
/** Cryptographic challenge to be solved */
uint32_t CryptoChallenge = 0;
/** uint32_t */
/** Solution for crypto challenge, MSBs */
uint32_t SolvedChallengeMSB = 0;
/** uint32_t */
/** Solution for crypto challenge, LSBs */
uint32_t SolvedChallengeLSB = 0;

} DATALAYER_INFO_NISSAN_LEAF;

Expand Down
14 changes: 14 additions & 0 deletions Software/src/devboard/webserver/advanced_battery_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,11 @@ String advanced_battery_processor(const String& var) {
content += "<h4>Heating stopped: " + String(datalayer_extended.nissanleaf.HeatingStop) + "</h4>";
content += "<h4>Heating started: " + String(datalayer_extended.nissanleaf.HeatingStart) + "</h4>";
content += "<h4>Heating requested: " + String(datalayer_extended.nissanleaf.HeaterSendRequest) + "</h4>";
content += "<button onclick='askResetSOH()'>Reset degradation data</button>";
content += "<h4>CryptoChallenge: " + String(datalayer_extended.nissanleaf.CryptoChallenge) + "</h4>";
content += "<h4>SolvedChallenge: " + String(datalayer_extended.nissanleaf.SolvedChallengeMSB) +
String(datalayer_extended.nissanleaf.SolvedChallengeLSB) + "</h4>";
content += "<h4>Challenge failed: " + String(datalayer_extended.nissanleaf.challengeFailed) + "</h4>";
#endif

#ifdef RENAULT_ZOE_GEN2_BATTERY
Expand Down Expand Up @@ -388,6 +393,15 @@ String advanced_battery_processor(const String& var) {
content += "</div>";

content += "<script>";
content +=
"function askResetSOH() { if (window.confirm('Are you sure you want to reset degradation data? "
"Note this should only be used on 2011-2017 24/30kWh batteries!')) { "
"resetSOH(); } }";
content += "function resetSOH() {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/resetSOH', true);";
content += " xhr.send();";
content += "}";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
return content;
Expand Down
10 changes: 10 additions & 0 deletions Software/src/devboard/webserver/webserver.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "webserver.h"
#include <Preferences.h>
#include "../../datalayer/datalayer.h"
#include "../../datalayer/datalayer_extended.h"
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
#include "../utils/events.h"
#include "../utils/led_handler.h"
Expand Down Expand Up @@ -231,6 +232,15 @@ void init_webserver() {
}
});

// Route for resetting SOH on Nissan LEAF batteries
server.on("/resetSOH", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.nissanleaf.UserRequestSOHreset = true;
request->send(200, "text/plain", "Updated successfully");
});

#ifdef TEST_FAKE_BATTERY
// Route for editing FakeBatteryVoltage
server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) {
Expand Down

0 comments on commit 32e75ba

Please sign in to comment.