Merge branch 'main' into bugfix/nissan-remaining-capacity

This commit is contained in:
Daniel Öster 2025-06-02 23:31:06 +03:00
commit ca7914df9d
7 changed files with 165 additions and 23 deletions

View file

@ -10,7 +10,7 @@ ci:
repos: repos:
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.3 rev: v20.1.5
hooks: hooks:
- id: clang-format - id: clang-format
args: [-Werror] # change formatting warnings to errors, hook includes -i (Inplace edit) by default args: [-Werror] # change formatting warnings to errors, hook includes -i (Inplace edit) by default

View file

@ -449,6 +449,36 @@ void NissanLeafBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
} }
} }
if (group_7bb == 0x06) //Balancing resistor status
{
if (rx_frame.data.u8[0] == 0x10) { //First frame (10 1A 61 06 [14 55 55 51])
for (int i = 0; i < 8; i++) {
// Byte 4 - 7 (bits 0-31)
for (int byte_i = 0; byte_i < 4; byte_i++) {
battery_balancing_shunts[byte_i * 8 + i] = (rx_frame.data.u8[4 + byte_i] & (1 << i)) >> i;
}
}
}
if (rx_frame.data.u8[0] == 0x21) { // Second frame (21 [50 55 41 2B 56 54 15])
for (int i = 0; i < 8; i++) {
// Byte 1 to 7 (bits 32-87)
for (int byte_i = 0; byte_i < 7; byte_i++) {
battery_balancing_shunts[32 + byte_i * 8 + i] = (rx_frame.data.u8[1 + byte_i] & (1 << i)) >> i;
}
}
}
if (rx_frame.data.u8[0] == 0x22) { //Third frame (22 51 FF FF FF FF FF FF)
for (int i = 0; i < 8; i++) {
// Byte 1 (bits 88-95)
battery_balancing_shunts[88 + i] = (rx_frame.data.u8[1] & (1 << i)) >> i;
}
memcpy(datalayer_battery->status.cell_balancing_status, battery_balancing_shunts, 96 * sizeof(bool));
}
if (rx_frame.data.u8[0] == 0x23) { //Fourth frame (23 FF FF FF FF FF FF FF)
}
}
if (group_7bb == 0x83) //BatteryPartNumber if (group_7bb == 0x83) //BatteryPartNumber
{ {
if (rx_frame.data.u8[0] == 0x10) { //First frame (101A6183334E4B32) if (rx_frame.data.u8[0] == 0x10) { //First frame (101A6183334E4B32)
@ -700,7 +730,7 @@ void NissanLeafBattery::transmit_can(unsigned long currentMillis) {
if (!stop_battery_query) { if (!stop_battery_query) {
// Move to the next group // Move to the next group
PIDindex = (PIDindex + 1) % 6; // 6 = amount of elements in the PIDgroups[] PIDindex = (PIDindex + 1) % 7; // 7 = amount of elements in the PIDgroups[]
LEAF_GROUP_REQUEST.data.u8[2] = PIDgroups[PIDindex]; LEAF_GROUP_REQUEST.data.u8[2] = PIDgroups[PIDindex];
transmit_can_frame(&LEAF_GROUP_REQUEST, can_interface); transmit_can_frame(&LEAF_GROUP_REQUEST, can_interface);

View file

@ -82,7 +82,7 @@ class NissanLeafBattery : public CanBattery {
.ID = 0x1D4, .ID = 0x1D4,
.data = {0x6E, 0x6E, 0x00, 0x04, 0x07, 0x46, 0xE0, 0x44}}; .data = {0x6E, 0x6E, 0x00, 0x04, 0x07, 0x46, 0xE0, 0x44}};
// Active polling messages // Active polling messages
uint8_t PIDgroups[6] = {0x01, 0x02, 0x04, 0x83, 0x84, 0x90}; uint8_t PIDgroups[7] = {0x01, 0x02, 0x04, 0x06, 0x83, 0x84, 0x90};
uint8_t PIDindex = 0; uint8_t PIDindex = 0;
CAN_frame LEAF_GROUP_REQUEST = {.FD = false, CAN_frame LEAF_GROUP_REQUEST = {.FD = false,
.ext_ID = false, .ext_ID = false,
@ -155,7 +155,8 @@ class NissanLeafBattery : public CanBattery {
uint8_t group_7bb = 0; uint8_t group_7bb = 0;
bool stop_battery_query = true; bool stop_battery_query = true;
uint8_t hold_off_with_polling_10seconds = 2; //Paused for 20 seconds on startup uint8_t hold_off_with_polling_10seconds = 2; //Paused for 20 seconds on startup
uint16_t battery_cell_voltages[97]; //array with all the cellvoltages uint16_t battery_cell_voltages[96]; //array with all the cellvoltages
bool battery_balancing_shunts[96]; //array with all the balancing resistors
uint8_t battery_cellcounter = 0; uint8_t battery_cellcounter = 0;
uint16_t battery_min_max_voltage[2]; //contains cell min[0] and max[1] values in mV uint16_t battery_min_max_voltage[2]; //contains cell min[0] and max[1] values in mV
uint16_t battery_HX = 0; //Internal resistance uint16_t battery_HX = 0; //Internal resistance

View file

@ -108,8 +108,16 @@ void RenaultZoeGen2Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
switch (rx_frame.ID) { switch (rx_frame.ID) {
case 0x18DAF1DB: // LBC Reply from active polling case 0x18DAF1DB: // LBC Reply from active polling
//frame 2 & 3 contains
if (rx_frame.data.u8[0] == 0x10) { //First frame of a group
transmit_can_frame(&ZOE_POLL_FLOW_CONTROL, can_config.battery);
//frame 2 & 3 contains which PID is sent
reply_poll = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
}
if (rx_frame.data.u8[0] < 0x10) { //One line responses
reply_poll = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]; reply_poll = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
}
switch (reply_poll) { switch (reply_poll) {
case POLL_SOC: case POLL_SOC:
@ -200,6 +208,29 @@ void RenaultZoeGen2Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
battery_bms_state = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; battery_bms_state = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break; break;
case POLL_BALANCE_SWITCHES: case POLL_BALANCE_SWITCHES:
if (rx_frame.data.u8[0] == 0x10) {
for (int i = 0; i < 8; i++) {
// Byte 4 - 7 (bits 0-31)
for (int byte_i = 0; byte_i < 4; byte_i++) {
battery_balancing_shunts[byte_i * 8 + i] = (rx_frame.data.u8[4 + byte_i] & (1 << i)) >> i;
}
}
}
if (rx_frame.data.u8[0] == 0x21) {
for (int i = 0; i < 8; i++) {
// Byte 1 to 7 (bits 32-87)
for (int byte_i = 0; byte_i < 7; byte_i++) {
battery_balancing_shunts[32 + byte_i * 8 + i] = (rx_frame.data.u8[1 + byte_i] & (1 << i)) >> i;
}
}
}
if (rx_frame.data.u8[0] == 0x22) {
for (int i = 0; i < 8; i++) {
// Byte 1 (bits 88-95)
battery_balancing_shunts[88 + i] = (rx_frame.data.u8[1] & (1 << i)) >> i;
}
memcpy(datalayer.battery.status.cell_balancing_status, battery_balancing_shunts, 96 * sizeof(bool));
}
battery_balance_switches = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; battery_balance_switches = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break; break;
case POLL_ENERGY_COMPLETE: case POLL_ENERGY_COMPLETE:

View file

@ -205,6 +205,7 @@ class RenaultZoeGen2Battery : public CanBattery {
uint32_t ZOE_376_time_now_s = 1745452800; // Initialized to make the battery think it is April 24, 2025 uint32_t ZOE_376_time_now_s = 1745452800; // Initialized to make the battery think it is April 24, 2025
unsigned long kProductionTimestamp_s = unsigned long kProductionTimestamp_s =
1614454107; // Production timestamp in seconds since January 1, 1970. Production timestamp used: February 25, 2021 at 8:08:27 AM GMT 1614454107; // Production timestamp in seconds since January 1, 1970. Production timestamp used: February 25, 2021 at 8:08:27 AM GMT
bool battery_balancing_shunts[96];
CAN_frame ZOE_373 = { CAN_frame ZOE_373 = {
.FD = false, .FD = false,
@ -225,6 +226,11 @@ class RenaultZoeGen2Battery : public CanBattery {
.DLC = 8, .DLC = 8,
.ID = 0x18DADBF1, .ID = 0x18DADBF1,
.data = {0x03, 0x22, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00}}; .data = {0x03, 0x22, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame ZOE_POLL_FLOW_CONTROL = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x18DADBF1,
.data = {0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
//NVROL Reset //NVROL Reset
CAN_frame ZOE_NVROL_1_18DADBF1 = {.FD = false, CAN_frame ZOE_NVROL_1_18DADBF1 = {.FD = false,
.ext_ID = true, .ext_ID = true,

View file

@ -81,6 +81,11 @@ typedef struct {
* Use with battery.info.number_of_cells to get valid data. * Use with battery.info.number_of_cells to get valid data.
*/ */
uint16_t cell_voltages_mV[MAX_AMOUNT_CELLS]; uint16_t cell_voltages_mV[MAX_AMOUNT_CELLS];
/** All balancing resistors status inside the pack, either on(1) or off(0).
* Use with battery.info.number_of_cells to get valid data.
* Not available for all battery manufacturers.
*/
bool cell_balancing_status[MAX_AMOUNT_CELLS];
/** The "real" SOC reported from the battery, in integer-percent x 100. 9550 = 95.50% */ /** The "real" SOC reported from the battery, in integer-percent x 100. 9550 = 95.50% */
uint16_t real_soc; uint16_t real_soc;
/** The SOC reported to the inverter, in integer-percent x 100. 9550 = 95.50%. /** The SOC reported to the inverter, in integer-percent x 100. 9550 = 95.50%.

View file

@ -46,6 +46,24 @@ String cellmonitor_processor(const String& var) {
content += "<div id='graph'></div>"; content += "<div id='graph'></div>";
// Display single hovered value // Display single hovered value
content += "<div id='valueDisplay'>Value: ...</div>"; content += "<div id='valueDisplay'>Value: ...</div>";
//Legend for graph
content +=
"<span style='color: white; background-color: blue; font-weight: bold; padding: 2px 8px; border-radius: 4px; "
"margin-right: 15px;'>Idle</span>";
bool battery_balancing = false;
for (uint8_t i = 0u; i < datalayer.battery.info.number_of_cells; i++) {
battery_balancing = datalayer.battery.status.cell_balancing_status[i];
if (battery_balancing)
break;
}
if (battery_balancing) {
content +=
"<span style='color: black; background-color: #00FFFF; font-weight: bold; padding: 2px 8px; border-radius: "
"4px; margin-right: 15px;'>Balancing</span>";
}
content +=
"<span style='color: white; background-color: red; font-weight: bold; padding: 2px 8px; border-radius: "
"4px;'>Min/Max</span>";
// Close the block // Close the block
content += "</div>"; content += "</div>";
@ -62,6 +80,25 @@ String cellmonitor_processor(const String& var) {
content += "<div id='graph2'></div>"; content += "<div id='graph2'></div>";
// Display single hovered value // Display single hovered value
content += "<div id='valueDisplay2'>Value: ...</div>"; content += "<div id='valueDisplay2'>Value: ...</div>";
//Legend for graph
content +=
"<span style='color: white; background-color: blue; font-weight: bold; padding: 2px 8px; border-radius: 4px; "
"margin-right: 15px;'>Idle</span>";
bool battery2_balancing = false;
for (uint8_t i = 0u; i < datalayer.battery2.info.number_of_cells; i++) {
battery2_balancing = datalayer.battery2.status.cell_balancing_status[i];
if (battery2_balancing)
break;
}
if (battery2_balancing) {
content +=
"<span style='color: black; background-color: #00FFFF; font-weight: bold; padding: 2px 8px; border-radius: "
"4px; margin-right: 15px;'>Balancing</span>";
}
content +=
"<span style='color: white; background-color: red; font-weight: bold; padding: 2px 8px; border-radius: "
"4px;'>Min/Max</span>";
// Close the block // Close the block
content += "</div>"; content += "</div>";
@ -80,6 +117,15 @@ String cellmonitor_processor(const String& var) {
} }
content += "];"; content += "];";
content += "const balancing = [";
for (uint8_t i = 0u; i < datalayer.battery.info.number_of_cells; i++) {
if (datalayer.battery.status.cell_voltages_mV[i] == 0) {
continue;
}
content += datalayer.battery.status.cell_balancing_status[i] ? "true," : "false,";
}
content += "];";
content += "const min_mv = Math.min(...data) - 20;"; content += "const min_mv = Math.min(...data) - 20;";
content += "const max_mv = Math.max(...data) + 20;"; content += "const max_mv = Math.max(...data) + 20;";
content += "const min_index = data.indexOf(Math.min(...data));"; content += "const min_index = data.indexOf(Math.min(...data));";
@ -110,20 +156,27 @@ String cellmonitor_processor(const String& var) {
"bar.id = `barIndex${index}`;" "bar.id = `barIndex${index}`;"
"bar.style.height = `${mV_limited}px`;" "bar.style.height = `${mV_limited}px`;"
"bar.style.width = `${750/data.length}px`;" "bar.style.width = `${750/data.length}px`;"
"if (balancing[index]) {"
" bar.style.backgroundColor = '#00FFFF';" // Cyan color for balancing
" bar.style.borderColor = '#00FFFF';"
"} else {"
" bar.style.backgroundColor = 'blue';" // Normal blue for non-balancing
" bar.style.borderColor = 'white';"
"}"
"const cell = document.getElementById(`cellIndex${index}`);" "const cell = document.getElementById(`cellIndex${index}`);"
"checkMinMax(cell, bar, index);" "checkMinMax(cell, bar, index);"
"bar.addEventListener('mouseenter', () => {" "bar.addEventListener('mouseenter', () => {"
"valueDisplay.textContent = `Value: ${mV}`;" " valueDisplay.textContent = `Value: ${mV}` + (balancing[index] ? ' (balancing)' : '');"
"bar.style.backgroundColor = `lightblue`;" " bar.style.backgroundColor = balancing[index] ? '#80FFFF' : 'lightblue';"
"cell.style.backgroundColor = `blue`;" " cell.style.backgroundColor = balancing[index] ? '#006666' : 'blue';"
"});" "});"
"bar.addEventListener('mouseleave', () => {" "bar.addEventListener('mouseleave', () => {"
"valueDisplay.textContent = 'Value: ...';" "valueDisplay.textContent = 'Value: ...';"
"bar.style.backgroundColor = `blue`;" "bar.style.backgroundColor = balancing[index] ? '#00FFFF' : 'blue';" // Restore cyan if balancing, else blue
"cell.style.removeProperty('background-color');" "cell.style.removeProperty('background-color');"
"});" "});"
@ -140,20 +193,20 @@ String cellmonitor_processor(const String& var) {
"cell.id = `cellIndex${index}`;" "cell.id = `cellIndex${index}`;"
"let cellContent = `Cell ${index + 1}<br>${mV} mV`;" "let cellContent = `Cell ${index + 1}<br>${mV} mV`;"
"if (mV < 3000) {" "if (mV < 3000) {"
"cellContent = `<span class='low-voltage'>${cellContent}</span>`;" " cellContent = `<span class='low-voltage'>${cellContent}</span>`;"
"}" "}"
"cell.innerHTML = cellContent;" "cell.innerHTML = cellContent;"
"cell.addEventListener('mouseenter', () => {" "cell.addEventListener('mouseenter', () => {"
"let bar = document.getElementById(`barIndex${index}`);" "let bar = document.getElementById(`barIndex${index}`);"
"valueDisplay.textContent = `Value: ${mV}`;" "valueDisplay.textContent = `Value: ${mV}`;"
"bar.style.backgroundColor = `lightblue`;" "bar.style.backgroundColor = balancing[index] ? '#80FFFF' : 'lightblue';" // Lighter cyan if balancing
"cell.style.backgroundColor = `blue`;" "cell.style.backgroundColor = balancing[index] ? '#006666' : 'blue';" // Darker cyan if balancing
"});" "});"
"cell.addEventListener('mouseleave', () => {" "cell.addEventListener('mouseleave', () => {"
"let bar = document.getElementById(`barIndex${index}`);" "let bar = document.getElementById(`barIndex${index}`);"
"bar.style.backgroundColor = `blue`;" "bar.style.backgroundColor = balancing[index] ? '#00FFFF' : 'blue';" // Restore original color
"cell.style.removeProperty('background-color');" "cell.style.removeProperty('background-color');"
"});" "});"
@ -195,6 +248,15 @@ String cellmonitor_processor(const String& var) {
} }
content += "];"; content += "];";
content += "const balancing2 = [";
for (uint8_t i = 0u; i < datalayer.battery2.info.number_of_cells; i++) {
if (datalayer.battery2.status.cell_voltages_mV[i] == 0) {
continue;
}
content += datalayer.battery2.status.cell_balancing_status[i] ? "true," : "false,";
}
content += "];";
content += "const min_mv2 = Math.min(...data2) - 20;"; content += "const min_mv2 = Math.min(...data2) - 20;";
content += "const max_mv2 = Math.max(...data2) + 20;"; content += "const max_mv2 = Math.max(...data2) + 20;";
content += "const min_index2 = data2.indexOf(Math.min(...data2));"; content += "const min_index2 = data2.indexOf(Math.min(...data2));";
@ -223,20 +285,26 @@ String cellmonitor_processor(const String& var) {
"bar2.id = `barIndex2${index2}`;" "bar2.id = `barIndex2${index2}`;"
"bar2.style.height = `${mV_limited2}px`;" "bar2.style.height = `${mV_limited2}px`;"
"bar2.style.width = `${750/data2.length}px`;" "bar2.style.width = `${750/data2.length}px`;"
"if (balancing2[index2]) {"
" bar2.style.backgroundColor = '#00FFFF';" // Cyan color for balancing
" bar2.style.borderColor = '#00FFFF';"
"} else {"
" bar2.style.backgroundColor = 'blue';" // Normal blue for non-balancing
" bar2.style.borderColor = 'white';"
"}"
"const cell2 = document.getElementById(`cellIndex2${index2}`);" "const cell2 = document.getElementById(`cellIndex2${index2}`);"
"checkMinMax2(cell2, bar2, index2);" "checkMinMax2(cell2, bar2, index2);"
"bar2.addEventListener('mouseenter', () => {" "bar2.addEventListener('mouseenter', () => {"
"valueDisplay2.textContent = `Value: ${mV}`;" " valueDisplay2.textContent = `Value: ${mV}` + (balancing[index2] ? ' (balancing)' : '');"
"bar2.style.backgroundColor = `lightblue`;" " bar2.style.backgroundColor = balancing2[index2] ? '#80FFFF' : 'lightblue';"
"cell2.style.backgroundColor = `blue`;" " cell2.style.backgroundColor = balancing2[index2] ? '#006666' : 'blue';"
"});" "});"
"bar2.addEventListener('mouseleave', () => {" "bar2.addEventListener('mouseleave', () => {"
"valueDisplay2.textContent = 'Value: ...';" "valueDisplay2.textContent = 'Value: ...';"
"bar2.style.backgroundColor = `blue`;" "bar2.style.backgroundColor = balancing2[index2] ? '#00FFFF' : 'blue';" // Restore cyan if balancing, else blue
"cell2.style.removeProperty('background-color');" "cell2.style.removeProperty('background-color');"
"});" "});"
@ -260,13 +328,13 @@ String cellmonitor_processor(const String& var) {
"cell2.addEventListener('mouseenter', () => {" "cell2.addEventListener('mouseenter', () => {"
"let bar2 = document.getElementById(`barIndex2${index2}`);" "let bar2 = document.getElementById(`barIndex2${index2}`);"
"valueDisplay2.textContent = `Value: ${mV}`;" "valueDisplay2.textContent = `Value: ${mV}`;"
"bar2.style.backgroundColor = `lightblue`;" "bar2.style.backgroundColor = balancing2[index2] ? '#80FFFF' : 'lightblue';" // Lighter cyan if balancing
"cell2.style.backgroundColor = `blue`;" "cell2.style.backgroundColor = balancing2[index2] ? '#006666' : 'blue';" // Darker cyan if balancing
"});" "});"
"cell2.addEventListener('mouseleave', () => {" "cell2.addEventListener('mouseleave', () => {"
"let bar2 = document.getElementById(`barIndex2${index2}`);" "let bar2 = document.getElementById(`barIndex2${index2}`);"
"bar2.style.backgroundColor = `blue`;" "bar2.style.backgroundColor = balancing2[index2] ? '#00FFFF' : 'blue';" // Restore original color
"cell2.style.removeProperty('background-color');" "cell2.style.removeProperty('background-color');"
"});" "});"
@ -281,7 +349,8 @@ String cellmonitor_processor(const String& var) {
"const max_mv2 = Math.max(...data2);" "const max_mv2 = Math.max(...data2);"
"const cell_dev2 = max_mv2 - min_mv2;" "const cell_dev2 = max_mv2 - min_mv2;"
"const voltVal2 = document.getElementById('voltageValues2');" "const voltVal2 = document.getElementById('voltageValues2');"
"voltVal2.innerHTML = `Max Voltage : ${max_mv2} mV<br>Min Voltage: ${min_mv2} mV<br>Voltage Deviation: " "voltVal2.innerHTML = `Battery #2<br>Max Voltage : ${max_mv2} mV<br>Min Voltage: ${min_mv2} mV<br>Voltage "
"Deviation: "
"${cell_dev2} mV`" "${cell_dev2} mV`"
"}"; "}";