mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-04 10:19:29 +02:00
BMW iX: add functionality to close and open contactors, including webserver buttons to close and open contactors
This commit is contained in:
parent
eb293b38d4
commit
f3c250368a
5 changed files with 489 additions and 34 deletions
|
@ -6,11 +6,13 @@
|
||||||
#include "BMW-IX-BATTERY.h"
|
#include "BMW-IX-BATTERY.h"
|
||||||
|
|
||||||
/* Do not change code below unless you are sure what you are doing */
|
/* Do not change code below unless you are sure what you are doing */
|
||||||
|
static unsigned long previousMillis10 = 0; // will store last time a 20ms CAN Message was send
|
||||||
static unsigned long previousMillis20 = 0; // will store last time a 20ms CAN Message was send
|
static unsigned long previousMillis20 = 0; // will store last time a 20ms CAN Message was send
|
||||||
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
|
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
|
||||||
static unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was send
|
static unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was send
|
||||||
static unsigned long previousMillis500 = 0; // will store last time a 500ms CAN Message was send
|
static unsigned long previousMillis500 = 0; // will store last time a 500ms CAN Message was send
|
||||||
static unsigned long previousMillis640 = 0; // will store last time a 600ms CAN Message was send
|
static unsigned long previousMillis640 = 0; // will store last time a 600ms CAN Message was send
|
||||||
|
static unsigned long previousMillis1000 = 0; // will store last time a 600ms CAN Message was send
|
||||||
static unsigned long previousMillis10000 = 0; // will store last time a 10000ms CAN Message was send
|
static unsigned long previousMillis10000 = 0; // will store last time a 10000ms CAN Message was send
|
||||||
|
|
||||||
#define ALIVE_MAX_VALUE 14 // BMW CAN messages contain alive counter, goes from 0...14
|
#define ALIVE_MAX_VALUE 14 // BMW CAN messages contain alive counter, goes from 0...14
|
||||||
|
@ -19,6 +21,27 @@ enum CmdState { SOH, CELL_VOLTAGE_MINMAX, SOC, CELL_VOLTAGE_CELLNO, CELL_VOLTAGE
|
||||||
|
|
||||||
static CmdState cmdState = SOC;
|
static CmdState cmdState = SOC;
|
||||||
|
|
||||||
|
static bool battery_awake = false;
|
||||||
|
|
||||||
|
bool contactorCloseReq = false;
|
||||||
|
|
||||||
|
struct ContactorCloseRequestStruct {
|
||||||
|
bool previous;
|
||||||
|
bool present;
|
||||||
|
} ContactorCloseRequest = {false, false};
|
||||||
|
|
||||||
|
struct ContactorStateStruct {
|
||||||
|
bool closed;
|
||||||
|
bool open;
|
||||||
|
};
|
||||||
|
ContactorStateStruct ContactorState = {false, true};
|
||||||
|
|
||||||
|
struct InverterContactorCloseRequestStruct {
|
||||||
|
bool previous;
|
||||||
|
bool present;
|
||||||
|
};
|
||||||
|
InverterContactorCloseRequestStruct InverterContactorCloseRequest = {false, false};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
SME output:
|
SME output:
|
||||||
0x12B8D087 5000ms - Extended ID
|
0x12B8D087 5000ms - Extended ID
|
||||||
|
@ -89,13 +112,17 @@ CAN_frame BMWiX_12B8D087 = {.FD = true,
|
||||||
.data = {0xFC, 0xFF}}; // 5000ms SME output - Static values
|
.data = {0xFC, 0xFF}}; // 5000ms SME output - Static values
|
||||||
*/
|
*/
|
||||||
|
|
||||||
CAN_frame BMWiX_16E = {
|
CAN_frame BMWiX_16E = {.FD = true,
|
||||||
.FD = true,
|
.ext_ID = false,
|
||||||
.ext_ID = false,
|
.DLC = 8,
|
||||||
.DLC = 8,
|
.ID = 0x16E,
|
||||||
.ID = 0x16E,
|
.data = {0x00, // Almost any possible number in 0x00 and 0xFF
|
||||||
// .data = {TODO:, TODO:, TODO: 0xC8 or 0xC9, 0xFF, TODO:, 0xC9, TODO:, TODO:, }
|
0xA0, // Almost any possible number in 0xA0 and 0xAF
|
||||||
}; // CCU output
|
0xC9, 0xFF,
|
||||||
|
0x60, // FIXME: find out what this value represents
|
||||||
|
0xC9,
|
||||||
|
0x3A, // 0x3A to close contactors, 0x33 to open contactors
|
||||||
|
0xF7}}; // 0xF7 to close contactors, 0xF0 to open contactors // CCU output.
|
||||||
|
|
||||||
CAN_frame BMWiX_188 = {.FD = true,
|
CAN_frame BMWiX_188 = {.FD = true,
|
||||||
.ext_ID = false,
|
.ext_ID = false,
|
||||||
|
@ -127,12 +154,12 @@ CAN_frame BMWiX_21D = {
|
||||||
// .data = {TODO:, TODO:, TODO:, 0xFF, 0xFF, 0xFF, 0xFF, TODO:}
|
// .data = {TODO:, TODO:, TODO:, 0xFF, 0xFF, 0xFF, 0xFF, TODO:}
|
||||||
}; // FIXME:(add transmitter node) output - request heating and air conditioning system 1
|
}; // FIXME:(add transmitter node) output - request heating and air conditioning system 1
|
||||||
|
|
||||||
CAN_frame BMWiX_276 = {
|
CAN_frame BMWiX_276 = {.FD = true,
|
||||||
.FD = true,
|
.ext_ID = false,
|
||||||
.ext_ID = false,
|
.DLC = 8,
|
||||||
.DLC = 8,
|
.ID = 0x276,
|
||||||
.ID = 0x276,
|
.data = {0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
.data = {0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC}}; // 5000ms BDC output - vehicle condition
|
0xFD}}; // BDC output - vehicle condition. Used for contactor closing
|
||||||
|
|
||||||
CAN_frame BMWiX_2ED = {
|
CAN_frame BMWiX_2ED = {
|
||||||
.FD = true,
|
.FD = true,
|
||||||
|
@ -228,8 +255,13 @@ CAN_frame BMWiX_510 = {
|
||||||
.ext_ID = false,
|
.ext_ID = false,
|
||||||
.DLC = 8,
|
.DLC = 8,
|
||||||
.ID = 0x510,
|
.ID = 0x510,
|
||||||
.data = {0x40, 0x10, 0x00, 0x00, 0x00, 0x80, 0x00,
|
.data = {
|
||||||
0x00}}; // 100ms BDC output - Values change in car logs, these bytes are the most common
|
0x40, 0x10,
|
||||||
|
0x04, // 0x02 at contactor closing, afterwards 0x04 and 0x10, 0x00 to open contactors
|
||||||
|
0x00, 0x00,
|
||||||
|
0x80, // 0x00 at start of contactor closing, changing to 0x80, afterwards 0x80
|
||||||
|
0x01,
|
||||||
|
0x00}}; // 100ms BDC output - Values change in car logs, these bytes are the most common. Used for contactor closing
|
||||||
|
|
||||||
CAN_frame BMWiX_6D = {
|
CAN_frame BMWiX_6D = {
|
||||||
.FD = true,
|
.FD = true,
|
||||||
|
@ -420,8 +452,6 @@ CAN_frame BMWiX_6F4_CELL_TEMP = {.FD = true,
|
||||||
.data = {0x07, 0x03, 0x22, 0xE5, 0xCA}};
|
.data = {0x07, 0x03, 0x22, 0xE5, 0xCA}};
|
||||||
//Request Data CAN End
|
//Request Data CAN End
|
||||||
|
|
||||||
static bool battery_awake = false;
|
|
||||||
|
|
||||||
//Setup UDS values to poll for
|
//Setup UDS values to poll for
|
||||||
CAN_frame* UDS_REQUESTS100MS[] = {&BMWiX_6F4_REQUEST_CELL_TEMP,
|
CAN_frame* UDS_REQUESTS100MS[] = {&BMWiX_6F4_REQUEST_CELL_TEMP,
|
||||||
&BMWiX_6F4_REQUEST_SOC,
|
&BMWiX_6F4_REQUEST_SOC,
|
||||||
|
@ -495,6 +525,9 @@ const unsigned long STALE_PERIOD =
|
||||||
|
|
||||||
static uint8_t current_cell_polled = 0;
|
static uint8_t current_cell_polled = 0;
|
||||||
|
|
||||||
|
static uint16_t counter_10ms = 0; // max 65535 --> 655.35 seconds
|
||||||
|
static uint8_t counter_100ms = 0; // max 255 --> 25.5 seconds
|
||||||
|
|
||||||
// Function to check if a value has gone stale over a specified time period
|
// Function to check if a value has gone stale over a specified time period
|
||||||
bool isStale(int16_t currentValue, uint16_t& lastValue, unsigned long& lastChangeTime) {
|
bool isStale(int16_t currentValue, uint16_t& lastValue, unsigned long& lastChangeTime) {
|
||||||
unsigned long currentTime = millis();
|
unsigned long currentTime = millis();
|
||||||
|
@ -629,13 +662,53 @@ void update_values_battery() { //This function maps all the values fetched via
|
||||||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
|
void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
|
||||||
battery_awake = true;
|
battery_awake = true;
|
||||||
switch (rx_frame.ID) {
|
switch (rx_frame.ID) {
|
||||||
case 0x112:
|
case 0x12B8D087:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x1D2:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x20B:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x2E2:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x31F:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x3EA:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x453:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x486:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x49C:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x4A1:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x4BB:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x4D0:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x507:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x587:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
break;
|
break;
|
||||||
case 0x607: //SME responds to UDS requests on 0x607
|
case 0x607: //SME responds to UDS requests on 0x607
|
||||||
|
|
||||||
if (rx_frame.DLC > 6 && rx_frame.data.u8[0] == 0xF4 && rx_frame.data.u8[1] == 0x10 &&
|
if (rx_frame.DLC > 6 && rx_frame.data.u8[0] == 0xF4 && rx_frame.data.u8[1] == 0x10 &&
|
||||||
rx_frame.data.u8[2] == 0xE3 && rx_frame.data.u8[3] == 0x62 && rx_frame.data.u8[4] == 0xE5) {
|
rx_frame.data.u8[2] == 0xE3 && rx_frame.data.u8[3] == 0x62 && rx_frame.data.u8[4] == 0xE5) {
|
||||||
//First of multi frame data - Parse the first frame
|
//First of multi frame data - Parse the first frame
|
||||||
|
@ -785,7 +858,7 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
|
||||||
(rx_frame.data.u8[8] << 8 | rx_frame.data.u8[9]) == 10000) { //Qualifier Invalid Mode - Request Reboot
|
(rx_frame.data.u8[8] << 8 | rx_frame.data.u8[9]) == 10000) { //Qualifier Invalid Mode - Request Reboot
|
||||||
#ifdef DEBUG_LOG
|
#ifdef DEBUG_LOG
|
||||||
logging.println("Cell MinMax Qualifier Invalid - Requesting BMS Reset");
|
logging.println("Cell MinMax Qualifier Invalid - Requesting BMS Reset");
|
||||||
#endif
|
#endif // DEBUG_LOG
|
||||||
//set_event(EVENT_BATTERY_VALUE_UNAVAILABLE, (millis())); //Eventually need new Info level event type
|
//set_event(EVENT_BATTERY_VALUE_UNAVAILABLE, (millis())); //Eventually need new Info level event type
|
||||||
transmit_can_frame(&BMWiX_6F4_REQUEST_HARD_RESET, can_config.battery);
|
transmit_can_frame(&BMWiX_6F4_REQUEST_HARD_RESET, can_config.battery);
|
||||||
} else { //Only ingest values if they are not the 10V Error state
|
} else { //Only ingest values if they are not the 10V Error state
|
||||||
|
@ -832,38 +905,94 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
|
||||||
battery_serial_number = strtoul(numberString, NULL, 10);
|
battery_serial_number = strtoul(numberString, NULL, 10);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 0x7AB:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0x8F:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
case 0xD0D087:
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void transmit_can_battery(unsigned long currentMillis) {
|
void transmit_can_battery(unsigned long currentMillis) {
|
||||||
|
// We can always send CAN as the iX BMS will wake up on vehicle comms
|
||||||
|
if (currentMillis - previousMillis10 >= INTERVAL_10_MS) {
|
||||||
|
previousMillis10 = currentMillis;
|
||||||
|
ContactorCloseRequest.present = contactorCloseReq;
|
||||||
|
// Detect edge
|
||||||
|
if (ContactorCloseRequest.previous == false && ContactorCloseRequest.present == true) {
|
||||||
|
// Rising edge detected
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Rising edge detected. Resetting 10ms counter.");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
counter_10ms = 0; // reset counter
|
||||||
|
} else if (ContactorCloseRequest.previous == true && ContactorCloseRequest.present == false) {
|
||||||
|
// Dropping edge detected
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Dropping edge detected. Resetting 10ms counter.");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
counter_10ms = 0; // reset counter
|
||||||
|
}
|
||||||
|
ContactorCloseRequest.previous = ContactorCloseRequest.present;
|
||||||
|
HandleBmwIxCloseContactorsRequest(counter_10ms);
|
||||||
|
HandleBmwIxOpenContactorsRequest(counter_10ms);
|
||||||
|
counter_10ms++;
|
||||||
|
|
||||||
//if (battery_awake) { //We can always send CAN as the iX BMS will wake up on vehicle comms
|
// prevent counter overflow: 2^16-1 = 65535
|
||||||
|
if (counter_10ms == 65535) {
|
||||||
|
counter_10ms = 1; // set to 1, to differentiate the counter being set to 0 by the functions above
|
||||||
|
}
|
||||||
|
}
|
||||||
// Send 100ms CAN Message
|
// Send 100ms CAN Message
|
||||||
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
|
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
|
||||||
previousMillis100 = currentMillis;
|
previousMillis100 = currentMillis;
|
||||||
|
HandleIncomingInverterRequest();
|
||||||
|
|
||||||
//Loop through and send a different UDS request each cycle
|
//Loop through and send a different UDS request once the contactors are closed
|
||||||
uds_req_id_counter = increment_uds_req_id_counter(uds_req_id_counter);
|
if (contactorCloseReq == true &&
|
||||||
transmit_can_frame(UDS_REQUESTS100MS[uds_req_id_counter], can_config.battery);
|
ContactorState.closed ==
|
||||||
|
true) { // Do not send unless the contactors are requested to be closed and are closed, as sending these does not allow the contactors to close
|
||||||
|
uds_req_id_counter = increment_uds_req_id_counter(uds_req_id_counter);
|
||||||
|
transmit_can_frame(UDS_REQUESTS100MS[uds_req_id_counter],
|
||||||
|
can_config.battery); // FIXME: sending these does not allow the contactors to close
|
||||||
|
} else { // FIXME: hotfix: If contactors are not requested to be closed, ensure the battery is reported as alive, even if no CAN messages are received
|
||||||
|
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep contactors closed if needed
|
||||||
|
BmwIxKeepContactorsClosed(counter_100ms);
|
||||||
|
counter_100ms++;
|
||||||
|
if (counter_100ms == 140) {
|
||||||
|
counter_100ms = 0; // reset counter every 14 seconds
|
||||||
|
}
|
||||||
|
|
||||||
//Send SME Keep alive values 100ms
|
//Send SME Keep alive values 100ms
|
||||||
transmit_can_frame(&BMWiX_510, can_config.battery);
|
//transmit_can_frame(&BMWiX_510, can_config.battery);
|
||||||
}
|
}
|
||||||
// Send 200ms CAN Message
|
// Send 200ms CAN Message
|
||||||
if (currentMillis - previousMillis200 >= INTERVAL_200_MS) {
|
if (currentMillis - previousMillis200 >= INTERVAL_200_MS) {
|
||||||
previousMillis200 = currentMillis;
|
previousMillis200 = currentMillis;
|
||||||
|
|
||||||
//Send SME Keep alive values 200ms
|
//Send SME Keep alive values 200ms
|
||||||
BMWiX_C0.data.u8[0] = increment_C0_counter(BMWiX_C0.data.u8[0]); //Keep Alive 1
|
//BMWiX_C0.data.u8[0] = increment_C0_counter(BMWiX_C0.data.u8[0]); //Keep Alive 1
|
||||||
transmit_can_frame(&BMWiX_C0, can_config.battery);
|
//transmit_can_frame(&BMWiX_C0, can_config.battery);
|
||||||
|
}
|
||||||
|
// Send 1000ms CAN Message
|
||||||
|
if (currentMillis - previousMillis1000 >= INTERVAL_1_S) {
|
||||||
|
previousMillis1000 = currentMillis;
|
||||||
|
|
||||||
|
HandleIncomingUserRequest();
|
||||||
}
|
}
|
||||||
// Send 10000ms CAN Message
|
// Send 10000ms CAN Message
|
||||||
if (currentMillis - previousMillis10000 >= INTERVAL_10_S) {
|
if (currentMillis - previousMillis10000 >= INTERVAL_10_S) {
|
||||||
previousMillis10000 = currentMillis;
|
previousMillis10000 = currentMillis;
|
||||||
transmit_can_frame(&BMWiX_6F4_REQUEST_BALANCING_START2, can_config.battery);
|
//transmit_can_frame(&BMWiX_6F4_REQUEST_BALANCING_START2, can_config.battery);
|
||||||
transmit_can_frame(&BMWiX_6F4_REQUEST_BALANCING_START, can_config.battery);
|
//transmit_can_frame(&BMWiX_6F4_REQUEST_BALANCING_START, can_config.battery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -872,7 +1001,7 @@ void setup_battery(void) { // Performs one time setup at startup
|
||||||
datalayer.system.info.battery_protocol[63] = '\0';
|
datalayer.system.info.battery_protocol[63] = '\0';
|
||||||
|
|
||||||
//Reset Battery at bootup
|
//Reset Battery at bootup
|
||||||
transmit_can_frame(&BMWiX_6F4_REQUEST_HARD_RESET, can_config.battery);
|
//transmit_can_frame(&BMWiX_6F4_REQUEST_HARD_RESET, can_config.battery);
|
||||||
|
|
||||||
//Before we have started up and detected which battery is in use, use 108S values
|
//Before we have started up and detected which battery is in use, use 108S values
|
||||||
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
|
||||||
|
@ -883,4 +1012,211 @@ void setup_battery(void) { // Performs one time setup at startup
|
||||||
datalayer.system.status.battery_allows_contactor_closing = true;
|
datalayer.system.status.battery_allows_contactor_closing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
void HandleIncomingUserRequest(void) {
|
||||||
|
// Debug user request to open or close the contactors
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.print("User request: contactor close: ");
|
||||||
|
logging.print(datalayer_extended.bmwix.UserRequestContactorClose);
|
||||||
|
logging.print(" User request: contactor open: ");
|
||||||
|
logging.println(datalayer_extended.bmwix.UserRequestContactorOpen);
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
if ((datalayer_extended.bmwix.UserRequestContactorClose == false) &&
|
||||||
|
(datalayer_extended.bmwix.UserRequestContactorOpen == false)) {
|
||||||
|
// do nothing
|
||||||
|
} else if ((datalayer_extended.bmwix.UserRequestContactorClose == true) &&
|
||||||
|
(datalayer_extended.bmwix.UserRequestContactorOpen == false)) {
|
||||||
|
BmwIxCloseContactors();
|
||||||
|
// set user request to false
|
||||||
|
datalayer_extended.bmwix.UserRequestContactorClose = false;
|
||||||
|
} else if ((datalayer_extended.bmwix.UserRequestContactorClose == false) &&
|
||||||
|
(datalayer_extended.bmwix.UserRequestContactorOpen == true)) {
|
||||||
|
BmwIxOpenContactors();
|
||||||
|
// set user request to false
|
||||||
|
datalayer_extended.bmwix.UserRequestContactorOpen = false;
|
||||||
|
} else if ((datalayer_extended.bmwix.UserRequestContactorClose == true) &&
|
||||||
|
(datalayer_extended.bmwix.UserRequestContactorOpen == true)) {
|
||||||
|
// these flasgs should not be true at the same time, therefore open contactors, as that is the safest state
|
||||||
|
BmwIxOpenContactors();
|
||||||
|
// set user request to false
|
||||||
|
datalayer_extended.bmwix.UserRequestContactorClose = false;
|
||||||
|
datalayer_extended.bmwix.UserRequestContactorOpen = false;
|
||||||
|
// print error, as both these flags shall not be true at the same time
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println(
|
||||||
|
"Error: user requested contactors to close and open at the same time. Contactors have been opened.");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleIncomingInverterRequest(void) {
|
||||||
|
InverterContactorCloseRequest.present = datalayer.system.status.inverter_allows_contactor_closing;
|
||||||
|
// Detect edge
|
||||||
|
if (InverterContactorCloseRequest.previous == false && InverterContactorCloseRequest.present == true) {
|
||||||
|
// Rising edge detected
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Inverter requests to close contactors");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
BmwIxCloseContactors();
|
||||||
|
} else if (InverterContactorCloseRequest.previous == true && InverterContactorCloseRequest.present == false) {
|
||||||
|
// Falling edge detected
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Inverter requests to open contactors");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
BmwIxOpenContactors();
|
||||||
|
} // else: do nothing
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
InverterContactorCloseRequest.previous = InverterContactorCloseRequest.present;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BmwIxCloseContactors(void) {
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Closing contactors");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
contactorCloseReq = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BmwIxOpenContactors(void) {
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Opening contactors");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
contactorCloseReq = false;
|
||||||
|
counter_100ms = 0; // reset counter, such that keep contactors closed message sequence starts from the beginning
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleBmwIxCloseContactorsRequest(uint16_t counter_10ms) {
|
||||||
|
if (contactorCloseReq == true) { // Only when contactor close request is set to true
|
||||||
|
if (ContactorState.closed == false &&
|
||||||
|
ContactorState.open ==
|
||||||
|
true) { // Only when the following commands have not been completed yet, because it shall not be run when commands have already been run, AND only when contactor open commands have finished
|
||||||
|
// Initially 0x510[2] needs to be 0x02, and 0x510[5] needs to be 0x00
|
||||||
|
BMWiX_510.data = {0x40, 0x10,
|
||||||
|
0x02, // 0x02 at contactor closing, afterwards 0x04 and 0x10, 0x00 to open contactors
|
||||||
|
0x00, 0x00,
|
||||||
|
0x00, // 0x00 at start of contactor closing, changing to 0x80, afterwards 0x80
|
||||||
|
0x01, // 0x01 at contactor closing
|
||||||
|
0x00}; // Explicit declaration, to prevent modification by other functions
|
||||||
|
BMWiX_16E.data = {
|
||||||
|
0x00, // Almost any possible number in 0x00 and 0xFF
|
||||||
|
0xA0, // Almost any possible number in 0xA0 and 0xAF
|
||||||
|
0xC9, 0xFF, 0x60,
|
||||||
|
0xC9, 0x3A, 0xF7}; // Explicit declaration of default values, to prevent modification by other functions
|
||||||
|
|
||||||
|
if (counter_10ms == 0) {
|
||||||
|
// @0 ms
|
||||||
|
transmit_can_frame(&BMWiX_510, can_config.battery);
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Transmitted 0x510 - 1/6");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
} else if (counter_10ms == 5) {
|
||||||
|
// @50 ms
|
||||||
|
transmit_can_frame(&BMWiX_276, can_config.battery);
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Transmitted 0x276 - 2/6");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
} else if (counter_10ms == 10) {
|
||||||
|
// @100 ms
|
||||||
|
BMWiX_510.data.u8[2] = 0x04; // TODO: check if needed
|
||||||
|
transmit_can_frame(&BMWiX_510, can_config.battery);
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Transmitted 0x510 - 3/6");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
} else if (counter_10ms == 20) {
|
||||||
|
// @200 ms
|
||||||
|
BMWiX_510.data.u8[2] = 0x10; // TODO: check if needed
|
||||||
|
BMWiX_510.data.u8[5] = 0x80; // needed to close contactors
|
||||||
|
transmit_can_frame(&BMWiX_510, can_config.battery);
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Transmitted 0x510 - 4/6");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
} else if (counter_10ms == 30) {
|
||||||
|
// @300 ms
|
||||||
|
BMWiX_16E.data.u8[0] = 0x6A;
|
||||||
|
BMWiX_16E.data.u8[1] = 0xAD;
|
||||||
|
transmit_can_frame(&BMWiX_16E, can_config.battery);
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Transmitted 0x16E - 5/6");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
} else if (counter_10ms == 50) {
|
||||||
|
// @500 ms
|
||||||
|
BMWiX_16E.data.u8[0] = 0x03;
|
||||||
|
BMWiX_16E.data.u8[1] = 0xA9;
|
||||||
|
transmit_can_frame(&BMWiX_16E, can_config.battery);
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Transmitted 0x16E - 6/6");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
ContactorState.closed = true;
|
||||||
|
ContactorState.open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BmwIxKeepContactorsClosed(uint8_t counter_100ms) {
|
||||||
|
if ((ContactorState.closed == true) && (ContactorState.open == false)) {
|
||||||
|
BMWiX_510.data = {0x40, 0x10,
|
||||||
|
0x04, // 0x02 at contactor closing, afterwards 0x04 and 0x10, 0x00 to open contactors
|
||||||
|
0x00, 0x00,
|
||||||
|
0x80, // 0x00 at start of contactor closing, changing to 0x80, afterwards 0x80
|
||||||
|
0x01, // 0x01 at contactor closing
|
||||||
|
0x00}; // Explicit declaration, to prevent modification by other functions
|
||||||
|
BMWiX_16E.data = {0x00, // Almost any possible number in 0x00 and 0xFF
|
||||||
|
0xA0, // Almost any possible number in 0xA0 and 0xAF
|
||||||
|
0xC9, 0xFF, 0x60,
|
||||||
|
0xC9, 0x3A, 0xF7}; // Explicit declaration, to prevent modification by other functions
|
||||||
|
|
||||||
|
if (counter_100ms == 0) {
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Sending keep contactors closed messages started");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
// @0 ms
|
||||||
|
transmit_can_frame(&BMWiX_510, can_config.battery);
|
||||||
|
} else if (counter_100ms == 7) {
|
||||||
|
// @ 730 ms
|
||||||
|
BMWiX_16E.data.u8[0] = 0x8C;
|
||||||
|
BMWiX_16E.data.u8[1] = 0xA0;
|
||||||
|
transmit_can_frame(&BMWiX_16E, can_config.battery);
|
||||||
|
} else if (counter_100ms == 24) {
|
||||||
|
// @2380 ms
|
||||||
|
transmit_can_frame(&BMWiX_510, can_config.battery);
|
||||||
|
} else if (counter_100ms == 29) {
|
||||||
|
// @ 2900 ms
|
||||||
|
BMWiX_16E.data.u8[0] = 0x02;
|
||||||
|
BMWiX_16E.data.u8[1] = 0xA7;
|
||||||
|
transmit_can_frame(&BMWiX_16E, can_config.battery);
|
||||||
|
#ifdef DEBUG_LOG
|
||||||
|
logging.println("Sending keep contactors closed messages finished");
|
||||||
|
#endif // DEBUG_LOG
|
||||||
|
} else if (counter_100ms == 140) {
|
||||||
|
// @14000 ms
|
||||||
|
// reset counter (outside of this function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleBmwIxOpenContactorsRequest(uint16_t counter_10ms) {
|
||||||
|
if (contactorCloseReq == false) { // if contactors are not requested to be closed, they are requested to be opened
|
||||||
|
if (ContactorState.open == false) { // only if contactors are not open yet
|
||||||
|
// message content to quickly open contactors
|
||||||
|
if (counter_10ms == 0) {
|
||||||
|
// @0 ms (0.00) RX0 510 [8] 40 10 00 00 00 80 00 00
|
||||||
|
BMWiX_510.data = {0x40, 0x10, 0x00, 0x00,
|
||||||
|
0x00, 0x80, 0x00, 0x00}; // Explicit declaration, to prevent modification by other functions
|
||||||
|
transmit_can_frame(&BMWiX_510, can_config.battery);
|
||||||
|
// set back to default values
|
||||||
|
BMWiX_510.data = {0x40, 0x10, 0x04, 0x00, 0x00, 0x80, 0x01, 0x00}; // default values
|
||||||
|
} else if (counter_10ms == 6) {
|
||||||
|
// @60 ms (0.06) RX0 16E [8] E6 A4 C8 FF 60 C9 33 F0
|
||||||
|
BMWiX_16E.data = {0xE6, 0xA4, 0xC8, 0xFF,
|
||||||
|
0x60, 0xC9, 0x33, 0xF0}; // Explicit declaration, to prevent modification by other functions
|
||||||
|
transmit_can_frame(&BMWiX_16E, can_config.battery);
|
||||||
|
// set back to default values
|
||||||
|
BMWiX_16E.data = {0x00, 0xA0, 0xC9, 0xFF, 0x60, 0xC9, 0x3A, 0xF7}; // default values
|
||||||
|
ContactorState.closed = false;
|
||||||
|
ContactorState.open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // BMW_IX_BATTERY
|
||||||
|
|
|
@ -19,4 +19,69 @@
|
||||||
void setup_battery(void);
|
void setup_battery(void);
|
||||||
void transmit_can_frame(CAN_frame* tx_frame, int interface);
|
void transmit_can_frame(CAN_frame* tx_frame, int interface);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle incoming user request to close or open contactors
|
||||||
|
*
|
||||||
|
* @param[in] void
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void HandleIncomingUserRequest(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle incoming inverter request to close or open contactors.alignas
|
||||||
|
*
|
||||||
|
* This function uses the "inverter_allows_contactor_closing" flag from the datalayer, to determine if CAN messages shall be sent to the battery to close or open the contactors.
|
||||||
|
*
|
||||||
|
* @param[in] void
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void HandleIncomingInverterRequest(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Close contactors of the BMW iX battery
|
||||||
|
*
|
||||||
|
* @param[in] void
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void BmwIxCloseContactors(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle close contactors requests for the BMW iX battery
|
||||||
|
*
|
||||||
|
* @param[in] counter_10ms Counter that increments by 1, every 10ms
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void HandleBmwIxCloseContactorsRequest(uint16_t counter_10ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Keep contactors of the BMW iX battery closed
|
||||||
|
*
|
||||||
|
* @param[in] counter_100ms Counter that increments by 1, every 100 ms
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void BmwIxKeepContactorsClosed(uint8_t counter_100ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Open contactors of the BMW iX battery
|
||||||
|
*
|
||||||
|
* @param[in] void
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void BmwIxOpenContactors(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle open contactors requests for the BMW iX battery
|
||||||
|
*
|
||||||
|
* @param[in] counter_10ms Counter that increments by 1, every 10ms
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void HandleBmwIxOpenContactorsRequest(uint16_t counter_10ms);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -41,6 +41,9 @@ typedef struct {
|
||||||
} DATALAYER_INFO_BOLTAMPERA;
|
} DATALAYER_INFO_BOLTAMPERA;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
/** User requesting contactor open or close via WebUI*/
|
||||||
|
bool UserRequestContactorClose = false;
|
||||||
|
bool UserRequestContactorOpen = false;
|
||||||
/** uint16_t */
|
/** uint16_t */
|
||||||
/** Terminal 30 - 12V SME Supply Voltage */
|
/** Terminal 30 - 12V SME Supply Voltage */
|
||||||
uint16_t T30_Voltage = 0;
|
uint16_t T30_Voltage = 0;
|
||||||
|
|
|
@ -58,6 +58,8 @@ String advanced_battery_processor(const String& var) {
|
||||||
#endif //BOLT_AMPERA_BATTERY
|
#endif //BOLT_AMPERA_BATTERY
|
||||||
|
|
||||||
#ifdef BMW_IX_BATTERY
|
#ifdef BMW_IX_BATTERY
|
||||||
|
content += "<button onclick='askContactorClose()'>Close Contactors</button>";
|
||||||
|
content += "<button onclick='askContactorOpen()'>Open Contactors</button>";
|
||||||
content +=
|
content +=
|
||||||
"<h4>Battery Voltage after Contactor: " + String(datalayer_extended.bmwix.battery_voltage_after_contactor) +
|
"<h4>Battery Voltage after Contactor: " + String(datalayer_extended.bmwix.battery_voltage_after_contactor) +
|
||||||
" dV</h4>";
|
" dV</h4>";
|
||||||
|
@ -1492,6 +1494,7 @@ String advanced_battery_processor(const String& var) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
content += "</div>";
|
content += "</div>";
|
||||||
|
|
||||||
content += "<script>";
|
content += "<script>";
|
||||||
content +=
|
content +=
|
||||||
"function askTeslaClearIsolation() { if (window.confirm('Are you sure you want to clear any active isolation "
|
"function askTeslaClearIsolation() { if (window.confirm('Are you sure you want to clear any active isolation "
|
||||||
|
@ -1504,6 +1507,7 @@ String advanced_battery_processor(const String& var) {
|
||||||
content += "}";
|
content += "}";
|
||||||
content += "function goToMainPage() { window.location.href = '/'; }";
|
content += "function goToMainPage() { window.location.href = '/'; }";
|
||||||
content += "</script>";
|
content += "</script>";
|
||||||
|
|
||||||
content += "<script>";
|
content += "<script>";
|
||||||
content +=
|
content +=
|
||||||
"function askTeslaResetBMS() { if (window.confirm('Are you sure you want to reset the "
|
"function askTeslaResetBMS() { if (window.confirm('Are you sure you want to reset the "
|
||||||
|
@ -1516,6 +1520,7 @@ String advanced_battery_processor(const String& var) {
|
||||||
content += "}";
|
content += "}";
|
||||||
content += "function goToMainPage() { window.location.href = '/'; }";
|
content += "function goToMainPage() { window.location.href = '/'; }";
|
||||||
content += "</script>";
|
content += "</script>";
|
||||||
|
|
||||||
content += "<script>";
|
content += "<script>";
|
||||||
content +=
|
content +=
|
||||||
"function askResetCrash() { if (window.confirm('Are you sure you want to reset crash data? "
|
"function askResetCrash() { if (window.confirm('Are you sure you want to reset crash data? "
|
||||||
|
@ -1540,6 +1545,33 @@ String advanced_battery_processor(const String& var) {
|
||||||
content += "}";
|
content += "}";
|
||||||
content += "function goToMainPage() { window.location.href = '/'; }";
|
content += "function goToMainPage() { window.location.href = '/'; }";
|
||||||
content += "</script>";
|
content += "</script>";
|
||||||
|
|
||||||
|
content += "<script>";
|
||||||
|
content +=
|
||||||
|
"function askContactorClose() { if (window.confirm('Are you sure you want to tirgger "
|
||||||
|
"a contactor close request?')) { "
|
||||||
|
"bmwIxCloseContactorRequest(); } }";
|
||||||
|
content += "function bmwIxCloseContactorRequest() {";
|
||||||
|
content += " var xhr = new XMLHttpRequest();";
|
||||||
|
content += " xhr.open('GET', '/bmwIxCloseContactorRequest', true);";
|
||||||
|
content += " xhr.send();";
|
||||||
|
content += "}";
|
||||||
|
content += "function goToMainPage() { window.location.href = '/'; }";
|
||||||
|
content += "</script>";
|
||||||
|
|
||||||
|
content += "<script>";
|
||||||
|
content +=
|
||||||
|
"function askContactorOpen() { if (window.confirm('Are you sure you want to tirgger "
|
||||||
|
"a contactor open request?')) { "
|
||||||
|
"bmwIxOpenContactorRequest(); } }";
|
||||||
|
content += "function bmwIxOpenContactorRequest() {";
|
||||||
|
content += " var xhr = new XMLHttpRequest();";
|
||||||
|
content += " xhr.open('GET', '/bmwIxOpenContactorRequest', true);";
|
||||||
|
content += " xhr.send();";
|
||||||
|
content += "}";
|
||||||
|
content += "function goToMainPage() { window.location.href = '/'; }";
|
||||||
|
content += "</script>";
|
||||||
|
|
||||||
content += "<script>";
|
content += "<script>";
|
||||||
content +=
|
content +=
|
||||||
"function askResetSOH() { if (window.confirm('Are you sure you want to reset degradation data? "
|
"function askResetSOH() { if (window.confirm('Are you sure you want to reset degradation data? "
|
||||||
|
@ -1552,6 +1584,7 @@ String advanced_battery_processor(const String& var) {
|
||||||
content += "}";
|
content += "}";
|
||||||
content += "function goToMainPage() { window.location.href = '/'; }";
|
content += "function goToMainPage() { window.location.href = '/'; }";
|
||||||
content += "</script>";
|
content += "</script>";
|
||||||
|
|
||||||
content += "<script>";
|
content += "<script>";
|
||||||
content +=
|
content +=
|
||||||
"function Volvo_askEraseDTC() { if (window.confirm('Are you sure you want to erase DTCs?')) { "
|
"function Volvo_askEraseDTC() { if (window.confirm('Are you sure you want to erase DTCs?')) { "
|
||||||
|
@ -1585,6 +1618,7 @@ String advanced_battery_processor(const String& var) {
|
||||||
content += "}";
|
content += "}";
|
||||||
content += "function goToMainPage() { window.location.href = '/'; }";
|
content += "function goToMainPage() { window.location.href = '/'; }";
|
||||||
content += "</script>";
|
content += "</script>";
|
||||||
|
|
||||||
// Additial functions added
|
// Additial functions added
|
||||||
content += "<script>";
|
content += "<script>";
|
||||||
content += "function exportLog() { window.location.href = '/export_log'; }";
|
content += "function exportLog() { window.location.href = '/export_log'; }";
|
||||||
|
|
|
@ -599,6 +599,24 @@ void init_webserver() {
|
||||||
request->send(200, "text/plain", "Updated successfully");
|
request->send(200, "text/plain", "Updated successfully");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route for closing BMW iX Contactors
|
||||||
|
server.on("/bmwIxCloseContactorRequest", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||||
|
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
|
||||||
|
return request->requestAuthentication();
|
||||||
|
}
|
||||||
|
datalayer_extended.bmwix.UserRequestContactorClose = true;
|
||||||
|
request->send(200, "text/plain", "Updated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route for opening BMW iX Contactors
|
||||||
|
server.on("/bmwIxOpenContactorRequest", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||||
|
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
|
||||||
|
return request->requestAuthentication();
|
||||||
|
}
|
||||||
|
datalayer_extended.bmwix.UserRequestContactorOpen = true;
|
||||||
|
request->send(200, "text/plain", "Updated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
// Route for resetting SOH on Nissan LEAF batteries
|
// Route for resetting SOH on Nissan LEAF batteries
|
||||||
server.on("/resetSOH", HTTP_GET, [](AsyncWebServerRequest* request) {
|
server.on("/resetSOH", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||||
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
|
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
|
||||||
|
@ -1207,15 +1225,14 @@ String processor(const String& var) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content += "<h4>Automatic contactor closing allowed:</h4>";
|
content += "<h4>Battery allows contactor closing: ";
|
||||||
content += "<h4>Battery: ";
|
|
||||||
if (datalayer.system.status.battery_allows_contactor_closing == true) {
|
if (datalayer.system.status.battery_allows_contactor_closing == true) {
|
||||||
content += "<span>✓</span>";
|
content += "<span>✓</span>";
|
||||||
} else {
|
} else {
|
||||||
content += "<span style='color: red;'>✕</span>";
|
content += "<span style='color: red;'>✕</span>";
|
||||||
}
|
}
|
||||||
|
|
||||||
content += " Inverter: ";
|
content += " Inverter allows contactor closing: ";
|
||||||
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
|
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
|
||||||
content += "<span>✓</span></h4>";
|
content += "<span>✓</span></h4>";
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue