Merge from main and fix conflicts

This commit is contained in:
Jaakko Haakana 2025-06-02 23:55:36 +03:00
commit c968f13a79
11 changed files with 181 additions and 39 deletions

View file

@ -458,6 +458,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)
@ -709,7 +739,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);

View file

@ -93,7 +93,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,
@ -163,7 +163,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

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;
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:

View file

@ -213,6 +213,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,
@ -233,6 +234,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,

View file

@ -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%.

View file

@ -49,6 +49,24 @@ String cellmonitor_processor(const String& var) {
content += "<div id='graph'></div>";
// Display single hovered value
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
content += "</div>";
@ -65,6 +83,25 @@ String cellmonitor_processor(const String& var) {
content += "<div id='graph2'></div>";
// Display single hovered value
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
content += "</div>";
@ -83,6 +120,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));";
@ -113,20 +159,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');"
"});"
@ -143,20 +196,20 @@ String cellmonitor_processor(const String& var) {
"cell.id = `cellIndex${index}`;"
"let cellContent = `Cell ${index + 1}<br>${mV} mV`;"
"if (mV < 3000) {"
"cellContent = `<span class='low-voltage'>${cellContent}</span>`;"
" cellContent = `<span class='low-voltage'>${cellContent}</span>`;"
"}"
"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');"
"});"
@ -198,6 +251,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));";
@ -226,20 +288,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');"
"});"
@ -263,13 +331,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');"
"});"
@ -284,7 +352,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<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`"
"}";

View file

@ -3,6 +3,12 @@
#include "../datalayer/datalayer.h"
#include "../include.h"
//#define SEND_0 //If defined, the messages will have ID ending with 0 (useful for some inverters)
#define SEND_1 //If defined, the messages will have ID ending with 1 (useful for some inverters)
#define INVERT_LOW_HIGH_BYTES //If defined, certain frames will have inverted low/high bytes \
//useful for some inverters like Sofar that report the voltages incorrect otherwise
#define SET_30K_OFFSET //If defined, current values are sent with a 30k offest (useful for ferroamp)
void FerroampCanInverter::
update_values() { //This function maps all the values fetched from battery CAN to the correct CAN messages
//There are more mappings that could be added, but this should be enough to use as a starting point

View file

@ -20,12 +20,6 @@ class FerroampCanInverter : public CanInverterProtocol {
void send_system_data();
void send_setup_info();
//#define SEND_0 //If defined, the messages will have ID ending with 0 (useful for some inverters)
#define SEND_1 //If defined, the messages will have ID ending with 1 (useful for some inverters)
#define INVERT_LOW_HIGH_BYTES //If defined, certain frames will have inverted low/high bytes \
//useful for some inverters like Sofar that report the voltages incorrect otherwise
#define SET_30K_OFFSET //If defined, current values are sent with a 30k offest (useful for ferroamp)
/* Some inverters need to see a specific amount of cells/modules to emulate a specific Pylon battery.
Change the following only if your inverter is generating fault codes about voltage range */
static const int TOTAL_CELL_AMOUNT = 120; //Adjust this parameter in steps of 120 to add another 14,2kWh of capacity
@ -51,14 +45,14 @@ Change the following only if your inverter is generating fault codes about volta
.ID = 0x7320,
.data = {(TOTAL_CELL_AMOUNT & 0xFF), (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES,
CELLS_PER_MODULE, (uint8_t)(VOLTAGE_LEVEL & 0x00FF), (uint8_t)(VOLTAGE_LEVEL >> 8),
AH_CAPACITY, (uint8_t)(AH_CAPACITY >> 8)}};
(uint8_t)(AH_CAPACITY & 0x00FF), (uint8_t)(AH_CAPACITY >> 8)}};
CAN_frame PYLON_7321 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7321,
.data = {(TOTAL_CELL_AMOUNT & 0xFF), (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES,
CELLS_PER_MODULE, (uint8_t)(VOLTAGE_LEVEL & 0x00FF), (uint8_t)(VOLTAGE_LEVEL >> 8),
AH_CAPACITY, (uint8_t)(AH_CAPACITY >> 8)}};
(uint8_t)(AH_CAPACITY & 0x00FF), (uint8_t)(AH_CAPACITY >> 8)}};
CAN_frame PYLON_4210 = {.FD = false,
.ext_ID = true,
.DLC = 8,

View file

@ -3,6 +3,12 @@
#include "../datalayer/datalayer.h"
#include "../include.h"
#define SEND_0 //If defined, the messages will have ID ending with 0 (useful for some inverters)
//#define SEND_1 //If defined, the messages will have ID ending with 1 (useful for some inverters)
#define INVERT_LOW_HIGH_BYTES //If defined, certain frames will have inverted low/high bytes \
//useful for some inverters like Sofar that report the voltages incorrect otherwise
//#define SET_30K_OFFSET //If defined, current values are sent with a 30k offest (useful for ferroamp)
void PylonInverter::
update_values() { //This function maps all the values fetched from battery CAN to the correct CAN messages

View file

@ -20,12 +20,6 @@ class PylonInverter : public CanInverterProtocol {
void send_system_data();
void send_setup_info();
#define SEND_0 //If defined, the messages will have ID ending with 0 (useful for some inverters)
//#define SEND_1 //If defined, the messages will have ID ending with 1 (useful for some inverters)
#define INVERT_LOW_HIGH_BYTES //If defined, certain frames will have inverted low/high bytes \
//useful for some inverters like Sofar that report the voltages incorrect otherwise
//#define SET_30K_OFFSET //If defined, current values are sent with a 30k offest (useful for ferroamp)
/* Some inverters need to see a specific amount of cells/modules to emulate a specific Pylon battery.
Change the following only if your inverter is generating fault codes about voltage range */
static const int TOTAL_CELL_AMOUNT = 120;
@ -52,14 +46,14 @@ class PylonInverter : public CanInverterProtocol {
.ID = 0x7320,
.data = {TOTAL_CELL_AMOUNT, (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES,
CELLS_PER_MODULE, (uint8_t)(VOLTAGE_LEVEL & 0x00FF), (uint8_t)(VOLTAGE_LEVEL >> 8),
AH_CAPACITY, (uint8_t)(AH_CAPACITY >> 8)}};
(uint8_t)(AH_CAPACITY & 0x00FF), (uint8_t)(AH_CAPACITY >> 8)}};
CAN_frame PYLON_7321 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7321,
.data = {TOTAL_CELL_AMOUNT, (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES,
CELLS_PER_MODULE, (uint8_t)(VOLTAGE_LEVEL & 0x00FF), (uint8_t)(VOLTAGE_LEVEL >> 8),
AH_CAPACITY, (uint8_t)(AH_CAPACITY >> 8)}};
(uint8_t)(AH_CAPACITY & 0x00FF), (uint8_t)(AH_CAPACITY >> 8)}};
CAN_frame PYLON_4210 = {.FD = false,
.ext_ID = true,
.DLC = 8,