diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72a4193e..ab929dc3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ ci: repos: - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.3 + rev: v20.1.5 hooks: - id: clang-format args: [-Werror] # change formatting warnings to errors, hook includes -i (Inplace edit) by default diff --git a/Software/src/battery/NISSAN-LEAF-BATTERY.cpp b/Software/src/battery/NISSAN-LEAF-BATTERY.cpp index 6f5f089b..0a35831d 100644 --- a/Software/src/battery/NISSAN-LEAF-BATTERY.cpp +++ b/Software/src/battery/NISSAN-LEAF-BATTERY.cpp @@ -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 (rx_frame.data.u8[0] == 0x10) { //First frame (101A6183334E4B32) @@ -700,7 +730,7 @@ void NissanLeafBattery::transmit_can(unsigned long currentMillis) { if (!stop_battery_query) { // 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]; transmit_can_frame(&LEAF_GROUP_REQUEST, can_interface); diff --git a/Software/src/battery/NISSAN-LEAF-BATTERY.h b/Software/src/battery/NISSAN-LEAF-BATTERY.h index 18cff8a9..2044bb84 100644 --- a/Software/src/battery/NISSAN-LEAF-BATTERY.h +++ b/Software/src/battery/NISSAN-LEAF-BATTERY.h @@ -82,7 +82,7 @@ class NissanLeafBattery : public CanBattery { .ID = 0x1D4, .data = {0x6E, 0x6E, 0x00, 0x04, 0x07, 0x46, 0xE0, 0x44}}; // 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; CAN_frame LEAF_GROUP_REQUEST = {.FD = false, .ext_ID = false, @@ -155,7 +155,8 @@ class NissanLeafBattery : public CanBattery { uint8_t group_7bb = 0; bool stop_battery_query = true; 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; uint16_t battery_min_max_voltage[2]; //contains cell min[0] and max[1] values in mV uint16_t battery_HX = 0; //Internal resistance diff --git a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.cpp b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.cpp index cbfe4667..c7a1b4a6 100644 --- a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.cpp +++ b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.cpp @@ -108,8 +108,16 @@ void RenaultZoeGen2Battery::handle_incoming_can_frame(CAN_frame rx_frame) { datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; switch (rx_frame.ID) { case 0x18DAF1DB: // LBC Reply from active polling - //frame 2 & 3 contains - reply_poll = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]; + + 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]; + } switch (reply_poll) { 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]; break; 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]; break; case POLL_ENERGY_COMPLETE: diff --git a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h index 79ca65fa..31e4cfde 100644 --- a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h +++ b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h @@ -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 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 + bool battery_balancing_shunts[96]; CAN_frame ZOE_373 = { .FD = false, @@ -225,6 +226,11 @@ class RenaultZoeGen2Battery : public CanBattery { .DLC = 8, .ID = 0x18DADBF1, .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 CAN_frame ZOE_NVROL_1_18DADBF1 = {.FD = false, .ext_ID = true, diff --git a/Software/src/datalayer/datalayer.h b/Software/src/datalayer/datalayer.h index 2fdd84f4..f4860502 100644 --- a/Software/src/datalayer/datalayer.h +++ b/Software/src/datalayer/datalayer.h @@ -81,6 +81,11 @@ typedef struct { * Use with battery.info.number_of_cells to get valid data. */ 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% */ uint16_t real_soc; /** The SOC reported to the inverter, in integer-percent x 100. 9550 = 95.50%. diff --git a/Software/src/devboard/webserver/cellmonitor_html.cpp b/Software/src/devboard/webserver/cellmonitor_html.cpp index f37f5d9d..5aaa9a56 100644 --- a/Software/src/devboard/webserver/cellmonitor_html.cpp +++ b/Software/src/devboard/webserver/cellmonitor_html.cpp @@ -46,6 +46,24 @@ String cellmonitor_processor(const String& var) { content += "
"; // Display single hovered value content += "
Value: ...
"; + //Legend for graph + content += + "Idle"; + 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 += + "Balancing"; + } + content += + "Min/Max"; // Close the block content += ""; @@ -62,6 +80,25 @@ String cellmonitor_processor(const String& var) { content += "
"; // Display single hovered value content += "
Value: ...
"; + //Legend for graph + content += + "Idle"; + + 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 += + "Balancing"; + } + content += + "Min/Max"; // Close the block content += ""; @@ -80,6 +117,15 @@ String cellmonitor_processor(const String& var) { } 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 max_mv = Math.max(...data) + 20;"; content += "const min_index = data.indexOf(Math.min(...data));"; @@ -110,20 +156,27 @@ String cellmonitor_processor(const String& var) { "bar.id = `barIndex${index}`;" "bar.style.height = `${mV_limited}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}`);" "checkMinMax(cell, bar, index);" "bar.addEventListener('mouseenter', () => {" - "valueDisplay.textContent = `Value: ${mV}`;" - "bar.style.backgroundColor = `lightblue`;" - "cell.style.backgroundColor = `blue`;" + " valueDisplay.textContent = `Value: ${mV}` + (balancing[index] ? ' (balancing)' : '');" + " bar.style.backgroundColor = balancing[index] ? '#80FFFF' : 'lightblue';" + " cell.style.backgroundColor = balancing[index] ? '#006666' : 'blue';" "});" "bar.addEventListener('mouseleave', () => {" "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');" "});" @@ -140,20 +193,20 @@ String cellmonitor_processor(const String& var) { "cell.id = `cellIndex${index}`;" "let cellContent = `Cell ${index + 1}
${mV} mV`;" "if (mV < 3000) {" - "cellContent = `${cellContent}`;" + " cellContent = `${cellContent}`;" "}" "cell.innerHTML = cellContent;" "cell.addEventListener('mouseenter', () => {" "let bar = document.getElementById(`barIndex${index}`);" "valueDisplay.textContent = `Value: ${mV}`;" - "bar.style.backgroundColor = `lightblue`;" - "cell.style.backgroundColor = `blue`;" + "bar.style.backgroundColor = balancing[index] ? '#80FFFF' : 'lightblue';" // Lighter cyan if balancing + "cell.style.backgroundColor = balancing[index] ? '#006666' : 'blue';" // Darker cyan if balancing "});" "cell.addEventListener('mouseleave', () => {" "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');" "});" @@ -195,6 +248,15 @@ String cellmonitor_processor(const String& var) { } 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 max_mv2 = Math.max(...data2) + 20;"; content += "const min_index2 = data2.indexOf(Math.min(...data2));"; @@ -223,20 +285,26 @@ String cellmonitor_processor(const String& var) { "bar2.id = `barIndex2${index2}`;" "bar2.style.height = `${mV_limited2}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}`);" "checkMinMax2(cell2, bar2, index2);" "bar2.addEventListener('mouseenter', () => {" - "valueDisplay2.textContent = `Value: ${mV}`;" - "bar2.style.backgroundColor = `lightblue`;" - "cell2.style.backgroundColor = `blue`;" + " valueDisplay2.textContent = `Value: ${mV}` + (balancing[index2] ? ' (balancing)' : '');" + " bar2.style.backgroundColor = balancing2[index2] ? '#80FFFF' : 'lightblue';" + " cell2.style.backgroundColor = balancing2[index2] ? '#006666' : 'blue';" "});" "bar2.addEventListener('mouseleave', () => {" "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');" "});" @@ -260,13 +328,13 @@ String cellmonitor_processor(const String& var) { "cell2.addEventListener('mouseenter', () => {" "let bar2 = document.getElementById(`barIndex2${index2}`);" "valueDisplay2.textContent = `Value: ${mV}`;" - "bar2.style.backgroundColor = `lightblue`;" - "cell2.style.backgroundColor = `blue`;" + "bar2.style.backgroundColor = balancing2[index2] ? '#80FFFF' : 'lightblue';" // Lighter cyan if balancing + "cell2.style.backgroundColor = balancing2[index2] ? '#006666' : 'blue';" // Darker cyan if balancing "});" "cell2.addEventListener('mouseleave', () => {" "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');" "});" @@ -281,7 +349,8 @@ String cellmonitor_processor(const String& var) { "const max_mv2 = Math.max(...data2);" "const cell_dev2 = max_mv2 - min_mv2;" "const voltVal2 = document.getElementById('voltageValues2');" - "voltVal2.innerHTML = `Max Voltage : ${max_mv2} mV
Min Voltage: ${min_mv2} mV
Voltage Deviation: " + "voltVal2.innerHTML = `Battery #2
Max Voltage : ${max_mv2} mV
Min Voltage: ${min_mv2} mV
Voltage " + "Deviation: " "${cell_dev2} mV`" "}";