Merge branch 'main' into feature/ioniq-28kWh

This commit is contained in:
Daniel Öster 2025-08-03 20:28:35 +03:00
commit 2d56ab8916
62 changed files with 1370 additions and 1069 deletions

View file

@ -116,6 +116,8 @@ const char* name_for_battery_type(BatteryType type) {
return RenaultZoeGen1Battery::Name;
case BatteryType::RenaultZoe2:
return RenaultZoeGen2Battery::Name;
case BatteryType::SamsungSdiLv:
return SamsungSdiLVBattery::Name;
case BatteryType::SantaFePhev:
return SantaFePhevBattery::Name;
case BatteryType::SimpBms:
@ -215,6 +217,8 @@ Battery* create_battery(BatteryType type) {
return new RenaultZoeGen1Battery();
case BatteryType::RenaultZoe2:
return new RenaultZoeGen2Battery();
case BatteryType::SamsungSdiLv:
return new SamsungSdiLVBattery();
case BatteryType::SantaFePhev:
return new SantaFePhevBattery();
case BatteryType::SimpBms:

View file

@ -44,6 +44,7 @@ void setup_can_shunt();
#include "RENAULT-ZOE-GEN1-BATTERY.h"
#include "RENAULT-ZOE-GEN2-BATTERY.h"
#include "RJXZS-BMS.h"
#include "SAMSUNG-SDI-LV-BATTERY.h"
#include "SANTA-FE-PHEV-BATTERY.h"
#include "SIMPBMS-BATTERY.h"
#include "SONO-BATTERY.h"

View file

@ -43,7 +43,8 @@ enum class BatteryType {
VolvoSpa = 35,
VolvoSpaHybrid = 36,
MgHsPhev = 37,
HyundaiIoniq28 = 38,
SamsungSdiLv = 38,
HyundaiIoniq28 = 39,
Highest
};

View file

@ -123,6 +123,7 @@ void EcmpBattery::update_values() {
datalayer_extended.stellantisECMP.pid_time_spent_over_55c = pid_time_spent_over_55c;
datalayer_extended.stellantisECMP.pid_contactor_closing_counter = pid_contactor_closing_counter;
datalayer_extended.stellantisECMP.pid_date_of_manufacture = pid_date_of_manufacture;
datalayer_extended.stellantisECMP.pid_SOH_cell_1 = pid_SOH_cell_1;
if (battery_InterlockOpen) {
set_event(EVENT_HVIL_FAILURE, 0);
@ -135,6 +136,12 @@ void EcmpBattery::update_values() {
} else {
clear_event(EVENT_12V_LOW);
}
if (pid_reason_open == 7) { //Invalid status
set_event(EVENT_CONTACTOR_OPEN, 0);
} else {
clear_event(EVENT_CONTACTOR_OPEN);
}
}
void EcmpBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
@ -578,8 +585,7 @@ void EcmpBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
(rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
break;
case PID_ENERGY_CAPACITY:
pid_energy_capacity = ((rx_frame.data.u8[4] << 24) | (rx_frame.data.u8[5] << 16) |
(rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
pid_energy_capacity = (rx_frame.data.u8[4] << 16) | (rx_frame.data.u8[5] << 8) | (rx_frame.data.u8[6]);
break;
case PID_HIGH_CELL_NUM:
pid_highest_cell_voltage_num = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
@ -720,6 +726,33 @@ void EcmpBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (incoming_poll) //Multiframe responses
{
case PID_ALL_CELL_SOH:
switch (rx_frame.data.u8[0]) {
case 0x10:
pid_SOH_cell_1 = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]);
break;
case 0x21:
break;
case 0x22:
break;
case 0x23:
break;
case 0x24:
break;
case 0x25:
break;
case 0x26:
break;
case 0x27:
break;
case 0x28:
break;
case 0x29:
break;
default:
break;
}
break;
case PID_ALL_CELL_VOLTAGES:
switch (rx_frame.data.u8[0]) {
case 0x10:
@ -1291,6 +1324,11 @@ void EcmpBattery::transmit_can(unsigned long currentMillis) {
case PID_DATE_OF_MANUFACTURE:
ECMP_POLL.data.u8[2] = (uint8_t)((PID_DATE_OF_MANUFACTURE & 0xFF00) >> 8);
ECMP_POLL.data.u8[3] = (uint8_t)(PID_DATE_OF_MANUFACTURE & 0x00FF);
poll_state = PID_ALL_CELL_SOH;
break;
case PID_ALL_CELL_SOH:
ECMP_POLL.data.u8[2] = (uint8_t)((PID_ALL_CELL_SOH & 0xFF00) >> 8);
ECMP_POLL.data.u8[3] = (uint8_t)(PID_ALL_CELL_SOH & 0x00FF);
poll_state = PID_WELD_CHECK; // Loop back to beginning
break;
default:

View file

@ -136,6 +136,7 @@ class EcmpBattery : public CanBattery {
uint32_t pid_time_spent_over_55c = NOT_SAMPLED_YET;
uint32_t pid_contactor_closing_counter = NOT_SAMPLED_YET;
uint32_t pid_date_of_manufacture = NOT_SAMPLED_YET;
uint16_t pid_SOH_cell_1 = NOT_SAMPLED_YET;
unsigned long previousMillis10 = 0; // will store last time a 10ms CAN Message was sent
unsigned long previousMillis20 = 0; // will store last time a 20ms CAN Message was sent
@ -197,7 +198,7 @@ class EcmpBattery : public CanBattery {
static const uint16_t PID_SW_VERSION_NUM = 0xF195; //Not supported on all batteris
static const uint16_t PID_FACTORY_MODE_CONTROL = 0xD900;
static const uint16_t PID_BATTERY_SERIAL = 0xD901;
static const uint16_t PID_ALL_CELL_SOH = 0xD4B5; //Very long message reply, too much data for this integration
static const uint16_t PID_ALL_CELL_SOH = 0xD4B5;
static const uint16_t PID_AUX_FUSE_STATE = 0xD86C;
static const uint16_t PID_BATTERY_STATE = 0xD811;
static const uint16_t PID_PRECHARGE_SHORT_CIRCUIT = 0xD4D8;

View file

@ -376,6 +376,11 @@ class EcmpHtmlRenderer : public BatteryHtmlRenderer {
? "N/A"
: String(datalayer_extended.stellantisECMP.pid_contactor_closing_counter)) +
" cycles</h4>";
content += "<h4>State of Health Cell-1: " +
(datalayer_extended.stellantisECMP.pid_SOH_cell_1 == 255
? "N/A"
: String(datalayer_extended.stellantisECMP.pid_SOH_cell_1)) +
"</h4>";
return content;
}

View file

@ -48,66 +48,19 @@ void KiaHyundai64Battery::
datalayer_battery_extended->batteryManagementMode = batteryManagementMode;
datalayer_battery_extended->BMS_ign = BMS_ign;
datalayer_battery_extended->batteryRelay = batteryRelay;
//Perform logging if configured to do so
#ifdef DEBUG_LOG
logging.println(); //sepatator
logging.println("Values from battery: ");
logging.print("SOC BMS: ");
logging.print((uint16_t)SOC_BMS / 10.0, 1);
logging.print("% | SOC Display: ");
logging.print((uint16_t)SOC_Display / 10.0, 1);
logging.print("% | SOH ");
logging.print((uint16_t)batterySOH / 10.0, 1);
logging.println("%");
logging.print((int16_t)batteryAmps / 10.0, 1);
logging.print(" Amps | ");
logging.print((uint16_t)batteryVoltage / 10.0, 1);
logging.print(" Volts | ");
logging.print((int16_t)datalayer_battery->status.active_power_W);
logging.println(" Watts");
logging.print("Allowed Charge ");
logging.print((uint16_t)allowedChargePower * 10);
logging.print(" W | Allowed Discharge ");
logging.print((uint16_t)allowedDischargePower * 10);
logging.println(" W");
logging.print("MaxCellVolt ");
logging.print(CellVoltMax_mV);
logging.print(" mV No ");
logging.print(CellVmaxNo);
logging.print(" | MinCellVolt ");
logging.print(CellVoltMin_mV);
logging.print(" mV No ");
logging.println(CellVminNo);
logging.print("TempHi ");
logging.print((int16_t)temperatureMax);
logging.print("°C TempLo ");
logging.print((int16_t)temperatureMin);
logging.print("°C WaterInlet ");
logging.print((int8_t)temperature_water_inlet);
logging.print("°C PowerRelay ");
logging.print((int8_t)powerRelayTemperature * 2);
logging.println("°C");
logging.print("Aux12volt: ");
logging.print((int16_t)leadAcidBatteryVoltage / 10.0, 1);
logging.println("V | ");
logging.print("BmsManagementMode ");
logging.print((uint8_t)batteryManagementMode, BIN);
if (bitRead((uint8_t)BMS_ign, 2) == 1) {
logging.print(" | BmsIgnition ON");
} else {
logging.print(" | BmsIgnition OFF");
}
if (bitRead((uint8_t)batteryRelay, 0) == 1) {
logging.print(" | PowerRelay ON");
} else {
logging.print(" | PowerRelay OFF");
}
logging.print(" | Inverter ");
logging.print(inverterVoltage);
logging.println(" Volts");
#endif
datalayer_battery_extended->inverterVoltage = inverterVoltage;
memcpy(datalayer_battery_extended->ecu_serial_number, ecu_serial_number, sizeof(ecu_serial_number));
memcpy(datalayer_battery_extended->ecu_version_number, ecu_version_number, sizeof(ecu_version_number));
datalayer_battery_extended->cumulative_charge_current_ah = cumulative_charge_current_ah;
datalayer_battery_extended->cumulative_discharge_current_ah = cumulative_discharge_current_ah;
datalayer_battery_extended->cumulative_energy_charged_kWh = cumulative_energy_charged_kWh;
datalayer_battery_extended->cumulative_energy_discharged_kWh = cumulative_energy_discharged_kWh;
datalayer_battery_extended->powered_on_total_time = powered_on_total_time;
datalayer_battery_extended->isolation_resistance_kOhm = isolation_resistance_kOhm;
datalayer_battery_extended->number_of_standard_charging_sessions = number_of_standard_charging_sessions;
datalayer_battery_extended->number_of_fastcharging_sessions = number_of_fastcharging_sessions;
datalayer_battery_extended->accumulated_normal_charging_energy_kWh = accumulated_normal_charging_energy_kWh;
datalayer_battery_extended->accumulated_fastcharging_energy_kWh = accumulated_fastcharging_energy_kWh;
}
void KiaHyundai64Battery::update_number_of_cells() {
@ -129,6 +82,7 @@ void KiaHyundai64Battery::update_number_of_cells() {
void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x4DE:
datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE;
startedUp = true;
break;
case 0x542: //BMS SOC
@ -168,65 +122,118 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
case 0x5D8:
startedUp = true;
//PID data is polled after last message sent from battery every other time:
//PID data is polled after last message sent from battery every other time this 0x5D8 message arrives:
if (holdPidCounter == true) {
holdPidCounter = false;
} else {
holdPidCounter = true;
if (poll_data_pid >= 6) { //polling one of six PIDs at 100ms*2, resolution = 1200ms
poll_data_pid = 0;
}
poll_data_pid++;
if (poll_data_pid == 1) {
transmit_can_frame(&KIA64_7E4_id1);
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_1 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_1 & 0x00FF);
} else if (poll_data_pid == 2) {
transmit_can_frame(&KIA64_7E4_id2);
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_2 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_2 & 0x00FF);
} else if (poll_data_pid == 3) {
transmit_can_frame(&KIA64_7E4_id3);
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_3 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_3 & 0x00FF);
} else if (poll_data_pid == 4) {
transmit_can_frame(&KIA64_7E4_id4);
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_4 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_4 & 0x00FF);
} else if (poll_data_pid == 5) {
transmit_can_frame(&KIA64_7E4_id5);
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_5 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_5 & 0x00FF);
} else if (poll_data_pid == 6) {
transmit_can_frame(&KIA64_7E4_id6);
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_6 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_6 & 0x00FF);
} else if (poll_data_pid == 7) {
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_11 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_11 & 0x00FF);
} else if (poll_data_pid == 8) {
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_ECU_SERIAL & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_ECU_SERIAL & 0x00FF);
} else if (poll_data_pid == 9) {
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_ECU_VERSION & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_ECU_VERSION & 0x00FF);
poll_data_pid = 0;
}
transmit_can_frame(&KIA64_7E4_poll);
}
break;
case 0x7EC: //Data From polled PID group, BigEndian
switch (rx_frame.data.u8[0]) {
case 0x10: //"PID Header"
if (rx_frame.data.u8[4] == poll_data_pid) {
transmit_can_frame(&KIA64_7E4_ack); //Send ack to BMS if the same frame is sent as polled
if (rx_frame.data.u8[0] < 0x10) { //One line response
pid_reply = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
}
if (rx_frame.data.u8[0] == 0x10) { //Multiframe response, send ACK
transmit_can_frame(&KIA64_7E4_ack);
pid_reply = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
}
switch (rx_frame.data.u8[0]) { //Multiframe responses
case 0x10: //Header frame sometimes has data
if (pid_reply == POLL_ECU_SERIAL) {
ecu_serial_number[0] = rx_frame.data.u8[5];
ecu_serial_number[1] = rx_frame.data.u8[6];
ecu_serial_number[2] = rx_frame.data.u8[7];
} else if (pid_reply == POLL_ECU_VERSION) {
ecu_version_number[0] = rx_frame.data.u8[5];
ecu_version_number[1] = rx_frame.data.u8[6];
ecu_version_number[2] = rx_frame.data.u8[7];
}
break;
case 0x21: //First frame in PID group
if (poll_data_pid == 1) {
if (pid_reply == POLL_GROUP_1) {
batteryRelay = rx_frame.data.u8[7];
} else if (poll_data_pid == 2) {
} else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[0] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[1] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[2] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[3] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[4] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[5] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
} else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[32] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[33] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[34] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[35] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[36] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[37] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
} else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[64] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[65] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[66] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[67] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[68] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[69] = (rx_frame.data.u8[7] * 20);
} else if (pid_reply == POLL_GROUP_11) {
number_of_standard_charging_sessions = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
} else if (pid_reply == POLL_ECU_SERIAL) {
ecu_serial_number[3] = rx_frame.data.u8[1];
ecu_serial_number[4] = rx_frame.data.u8[2];
ecu_serial_number[5] = rx_frame.data.u8[3];
ecu_serial_number[6] = rx_frame.data.u8[4];
ecu_serial_number[7] = rx_frame.data.u8[5];
ecu_serial_number[8] = rx_frame.data.u8[6];
ecu_serial_number[9] = rx_frame.data.u8[7];
} else if (pid_reply == POLL_ECU_VERSION) {
ecu_version_number[3] = rx_frame.data.u8[1];
ecu_version_number[4] = rx_frame.data.u8[2];
ecu_version_number[5] = rx_frame.data.u8[3];
ecu_version_number[6] = rx_frame.data.u8[4];
ecu_version_number[7] = rx_frame.data.u8[5];
ecu_version_number[8] = rx_frame.data.u8[6];
ecu_version_number[9] = rx_frame.data.u8[7];
}
break;
case 0x22: //Second datarow in PID group
if (poll_data_pid == 2) {
if (pid_reply == POLL_GROUP_1) {
//battery_max_temperature = rx_frame.data.u8[5];
//battery_min_temperature = rx_frame.data.u8[6];
//module_1_temperature = rx_frame.data.u8[7];
} else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[6] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[7] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[8] = (rx_frame.data.u8[3] * 20);
@ -234,7 +241,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[10] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[11] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[12] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
} else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[38] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[39] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[40] = (rx_frame.data.u8[3] * 20);
@ -242,7 +249,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[42] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[43] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[44] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
} else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[70] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[71] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[72] = (rx_frame.data.u8[3] * 20);
@ -250,15 +257,35 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[74] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[75] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[76] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 6) {
} else if (pid_reply == POLL_GROUP_6) {
batteryManagementMode = rx_frame.data.u8[5];
} else if (pid_reply == POLL_GROUP_11) {
number_of_fastcharging_sessions = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]);
accumulated_normal_charging_energy_kWh = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]);
} else if (pid_reply == POLL_ECU_SERIAL) {
ecu_serial_number[10] = rx_frame.data.u8[1];
ecu_serial_number[11] = rx_frame.data.u8[2];
ecu_serial_number[12] = rx_frame.data.u8[3];
ecu_serial_number[13] = rx_frame.data.u8[4];
ecu_serial_number[14] = rx_frame.data.u8[5];
ecu_serial_number[15] = rx_frame.data.u8[6];
} else if (pid_reply == POLL_ECU_VERSION) {
ecu_version_number[10] = rx_frame.data.u8[1];
ecu_version_number[11] = rx_frame.data.u8[2];
ecu_version_number[12] = rx_frame.data.u8[3];
ecu_version_number[13] = rx_frame.data.u8[4];
ecu_version_number[14] = rx_frame.data.u8[5];
ecu_version_number[15] = rx_frame.data.u8[6];
}
break;
case 0x23: //Third datarow in PID group
if (poll_data_pid == 1) {
if (pid_reply == POLL_GROUP_1) {
//module_2_temperature = rx_frame.data.u8[1];
//module_3_temperature = rx_frame.data.u8[2];
//module_4_temperature = rx_frame.data.u8[3];
temperature_water_inlet = rx_frame.data.u8[6];
CellVoltMax_mV = (rx_frame.data.u8[7] * 20); //(volts *50) *20 =mV
} else if (poll_data_pid == 2) {
} else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[13] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[14] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[15] = (rx_frame.data.u8[3] * 20);
@ -266,7 +293,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[17] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[18] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[19] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
} else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[45] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[46] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[47] = (rx_frame.data.u8[3] * 20);
@ -274,7 +301,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[49] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[50] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[51] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
} else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[77] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[78] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[79] = (rx_frame.data.u8[3] * 20);
@ -282,16 +309,18 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[81] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[82] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[83] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 5) {
} else if (pid_reply == POLL_GROUP_5) {
heatertemp = rx_frame.data.u8[7];
} else if (pid_reply == POLL_GROUP_11) {
accumulated_fastcharging_energy_kWh = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
}
break;
case 0x24: //Fourth datarow in PID group
if (poll_data_pid == 1) {
if (pid_reply == POLL_GROUP_1) {
CellVmaxNo = rx_frame.data.u8[1];
CellVminNo = rx_frame.data.u8[3];
CellVoltMin_mV = (rx_frame.data.u8[2] * 20); //(volts *50) *20 =mV
} else if (poll_data_pid == 2) {
} else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[20] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[21] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[22] = (rx_frame.data.u8[3] * 20);
@ -299,7 +328,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[24] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[25] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[26] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
} else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[52] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[53] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[54] = (rx_frame.data.u8[3] * 20);
@ -307,7 +336,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[56] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[57] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[58] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
} else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[84] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[85] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[86] = (rx_frame.data.u8[3] * 20);
@ -317,25 +346,30 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
if (rx_frame.data.u8[7] > 4) { // Data only valid on 98S
cellvoltages_mv[90] = (rx_frame.data.u8[7] * 20); // Perform extra checks
}
} else if (poll_data_pid == 5) {
} else if (pid_reply == POLL_GROUP_5) {
batterySOH = ((rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3]);
}
break;
case 0x25: //Fifth datarow in PID group
if (poll_data_pid == 2) {
if (pid_reply == POLL_GROUP_1) {
cumulative_charge_current_ah =
((rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
cumulative_discharge_current_ah =
((rx_frame.data.u8[5] << 16) | (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
} else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[27] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[28] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[29] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[30] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[31] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 3) {
} else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[59] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[60] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[61] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[62] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[63] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 4) { // Data only valid on 98S
if (rx_frame.data.u8[1] > 4) { // Perform extra checks
} else if (pid_reply == POLL_GROUP_4) { // Data only valid on 98S
if (rx_frame.data.u8[1] > 4) { // Perform extra checks
cellvoltages_mv[91] = (rx_frame.data.u8[1] * 20);
}
if (rx_frame.data.u8[2] > 4) { // Perform extra checks
@ -350,8 +384,8 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
if (rx_frame.data.u8[5] > 4) { // Perform extra checks
cellvoltages_mv[95] = (rx_frame.data.u8[5] * 20);
}
} else if (poll_data_pid == 5) { // Data only valid on 98S
if (rx_frame.data.u8[4] > 4) { // Perform extra checks
} else if (pid_reply == POLL_GROUP_5) { // Data only valid on 98S
if (rx_frame.data.u8[4] > 4) { // Perform extra checks
cellvoltages_mv[96] = (rx_frame.data.u8[4] * 20);
}
if (rx_frame.data.u8[5] > 4) { // Perform extra checks
@ -360,7 +394,11 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
}
break;
case 0x26: //Sixth datarow in PID group
if (poll_data_pid == 5) {
if (pid_reply == POLL_GROUP_1) {
cumulative_energy_charged_kWh =
((rx_frame.data.u8[2] << 16) | (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]);
cumulative_energy_discharged_HIGH_BYTE = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
} else if (pid_reply == POLL_GROUP_5) {
//We have read all cells, check that content is valid:
for (uint8_t i = 85; i < 97; ++i) {
if (cellvoltages_mv[i] < 300) { // Zero the value if it's below 300
@ -374,17 +412,24 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
}
break;
case 0x27: //Seventh datarow in PID group
if (poll_data_pid == 1) {
if (pid_reply == POLL_GROUP_1) {
cumulative_energy_discharged_kWh = ((cumulative_energy_discharged_HIGH_BYTE << 8) | rx_frame.data.u8[1]);
powered_on_total_time = ((rx_frame.data.u8[2] << 24) | (rx_frame.data.u8[3] << 16) |
(rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
BMS_ign = rx_frame.data.u8[6];
inverterVoltageFrameHigh = rx_frame.data.u8[7];
}
break;
case 0x28: //Eighth datarow in PID group
if (poll_data_pid == 1) {
if (pid_reply == POLL_GROUP_1) {
inverterVoltage = (inverterVoltageFrameHigh << 8) + rx_frame.data.u8[1];
isolation_resistance_kOhm = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
}
break;
default:
break;
}
break;
default:
break;
@ -401,7 +446,8 @@ void KiaHyundai64Battery::transmit_can(unsigned long currentMillis) {
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
previousMillis100 = currentMillis;
if (contactor_closing_allowed == nullptr || *contactor_closing_allowed) {
if ((contactor_closing_allowed == nullptr || *contactor_closing_allowed) &&
datalayer.system.status.inverter_allows_contactor_closing) {
transmit_can_frame(&KIA64_553);
transmit_can_frame(&KIA64_57F);
transmit_can_frame(&KIA64_2A1);
@ -412,7 +458,8 @@ void KiaHyundai64Battery::transmit_can(unsigned long currentMillis) {
if (currentMillis - previousMillis10 >= INTERVAL_10_MS) {
previousMillis10 = currentMillis;
if (contactor_closing_allowed == nullptr || *contactor_closing_allowed) {
if ((contactor_closing_allowed == nullptr || *contactor_closing_allowed) &&
datalayer.system.status.inverter_allows_contactor_closing) {
switch (counter_200) {
case 0:

View file

@ -79,7 +79,8 @@ class KiaHyundai64Battery : public CanBattery {
int16_t batteryAmps = 0;
int16_t temperatureMax = 0;
int16_t temperatureMin = 0;
int16_t poll_data_pid = 0;
uint8_t poll_data_pid = 0;
uint16_t pid_reply = 0;
bool holdPidCounter = false;
uint8_t CellVmaxNo = 0;
uint8_t CellVminNo = 0;
@ -92,6 +93,19 @@ class KiaHyundai64Battery : public CanBattery {
int8_t heatertemp = 0;
int8_t powerRelayTemperature = 0;
bool startedUp = false;
uint8_t ecu_serial_number[16] = {0};
uint8_t ecu_version_number[16] = {0};
uint32_t cumulative_charge_current_ah = 0;
uint32_t cumulative_discharge_current_ah = 0;
uint32_t cumulative_energy_charged_kWh = 0;
uint16_t cumulative_energy_discharged_HIGH_BYTE = 0;
uint32_t cumulative_energy_discharged_kWh = 0;
uint32_t powered_on_total_time = 0;
uint16_t isolation_resistance_kOhm = 0;
uint16_t number_of_standard_charging_sessions = 0;
uint16_t number_of_fastcharging_sessions = 0;
uint16_t accumulated_normal_charging_energy_kWh = 0;
uint16_t accumulated_fastcharging_energy_kWh = 0;
CAN_frame KIA_HYUNDAI_200 = {.FD = false,
.ext_ID = false,
@ -126,42 +140,26 @@ class KiaHyundai64Battery : public CanBattery {
.DLC = 8,
.ID = 0x2A1,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame KIA64_7E4_id1 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 01
CAN_frame KIA64_7E4_id2 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 02
CAN_frame KIA64_7E4_id3 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 03
CAN_frame KIA64_7E4_id4 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 04
CAN_frame KIA64_7E4_id5 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x05, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 05
CAN_frame KIA64_7E4_id6 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 06
CAN_frame KIA64_7E4_poll = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00}};
CAN_frame KIA64_7E4_ack = {
.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Ack frame, correct PID is returned
.data = {0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Ack frame, correct PID is returned
static const int POLL_GROUP_1 = 0x0101;
static const int POLL_GROUP_2 = 0x0102;
static const int POLL_GROUP_3 = 0x0103;
static const int POLL_GROUP_4 = 0x0104;
static const int POLL_GROUP_5 = 0x0105;
static const int POLL_GROUP_6 = 0x0106;
static const int POLL_GROUP_11 = 0x0111;
static const int POLL_ECU_SERIAL = 0xF18C;
static const int POLL_ECU_VERSION = 0xF191;
};
#endif

View file

@ -11,16 +11,45 @@ class KiaHyundai64HtmlRenderer : public BatteryHtmlRenderer {
String get_status_html() {
String content;
auto print_hyundai = [&content](DATALAYER_INFO_KIAHYUNDAI64& data) {
content += "<h4>Cells: " + String(data.total_cell_count) + "S</h4>";
content += "<h4>12V voltage: " + String(data.battery_12V / 10.0, 1) + "</h4>";
content += "<h4>Waterleakage: " + String(data.waterleakageSensor) + "</h4>";
content += "<h4>Temperature, water inlet: " + String(data.temperature_water_inlet) + "</h4>";
content += "<h4>Temperature, power relay: " + String(data.powerRelayTemperature) + "</h4>";
char readableSerialNumber[17]; // One extra space for null terminator
memcpy(readableSerialNumber, data.ecu_serial_number, sizeof(data.ecu_serial_number));
readableSerialNumber[16] = '\0'; // Null terminate the string
char readableVersionNumber[17]; // One extra space for null terminator
memcpy(readableVersionNumber, data.ecu_version_number, sizeof(data.ecu_version_number));
readableVersionNumber[16] = '\0'; // Null terminate the string
content += "<h4>BMS serial number: " + String(readableSerialNumber) + "</h4>";
content += "<h4>BMS software version: " + String(readableVersionNumber) + "</h4>";
content += "<h4>Cells: " + String(data.total_cell_count) + " S</h4>";
content += "<h4>12V voltage: " + String(data.battery_12V / 10.0, 1) + " V</h4>";
content += "<h4>Waterleakage: ";
if (data.waterleakageSensor == 0) {
content += " LEAK DETECTED</h4>";
} else if (data.waterleakageSensor == 164) {
content += " No leakage</h4>";
} else {
content += String(data.waterleakageSensor) + "</h4>";
}
content += "<h4>Temperature, water inlet: " + String(data.temperature_water_inlet) + " &deg;C</h4>";
content += "<h4>Temperature, power relay: " + String(data.powerRelayTemperature) + " &deg;C</h4>";
content += "<h4>Batterymanagement mode: " + String(data.batteryManagementMode) + "</h4>";
content += "<h4>BMS ignition: " + String(data.BMS_ign) + "</h4>";
content += "<h4>Battery relay: " + String(data.batteryRelay) + "</h4>";
content += "<h4>Inverter voltage: " + String(data.inverterVoltage) + " V</h4>";
content += "<h4>Isolation resistance: " + String(data.isolation_resistance_kOhm) + " kOhm</h4>";
content += "<h4>Power on total time: " + String(data.powered_on_total_time) + " s</h4>";
content += "<h4>Fastcharging sessions: " + String(data.number_of_fastcharging_sessions) + " x</h4>";
content += "<h4>Slowcharging sessions: " + String(data.number_of_standard_charging_sessions) + " x</h4>";
content +=
"<h4>Normal charged energy amount: " + String(data.accumulated_normal_charging_energy_kWh) + " kWh</h4>";
content += "<h4>Fastcharged energy amount: " + String(data.accumulated_fastcharging_energy_kWh) + " kWh</h4>";
content += "<h4>Total amount charged energy: " + String(data.cumulative_energy_charged_kWh / 10.0) + " kWh</h4>";
content +=
"<h4>Total amount discharged energy: " + String(data.cumulative_energy_discharged_kWh / 10.0) + " kWh</h4>";
content += "<h4>Cumulative charge current: " + String(data.cumulative_charge_current_ah / 10.0) + " Ah</h4>";
content +=
"<h4>Cumulative discharge current: " + String(data.cumulative_discharge_current_ah / 10.0) + " Ah</h4>";
};
print_hyundai(*kia_datalayer);

View file

@ -0,0 +1,102 @@
#include "SAMSUNG-SDI-LV-BATTERY.h"
#include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
/*
- Baud rate: 500kbps
- Format: CAN2.0A 11 bit identifier
- Data Length: 8byte
- CAN data is transmitted with encoding in little endian low byte first unless stated otherwise.
- Broadcasting period: 500ms
TODO: Implement the error bit handling for easier visualization if the battery stops operating
- alarms_frame0
- alarms_frame1
- protection_frame2
- protection_frame3
*/
void SamsungSdiLVBattery::update_values() {
datalayer.battery.status.real_soc = system_SOC * 100;
datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>(
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
datalayer.battery.status.soh_pptt = system_SOH * 100;
datalayer.battery.status.voltage_dV = system_voltage / 10;
datalayer.battery.status.current_dA = system_current * 10;
datalayer.battery.status.max_charge_power_W = system_voltage * charge_current_limit;
datalayer.battery.status.max_discharge_power_W = system_voltage * discharge_current_limit;
datalayer.battery.status.temperature_min_dC = (int16_t)(minimum_cell_temperature * 10);
datalayer.battery.status.temperature_max_dC = (int16_t)(maximum_cell_temperature * 10);
datalayer.battery.status.cell_max_voltage_mV = maximum_cell_voltage;
datalayer.battery.status.cell_min_voltage_mV = minimum_cell_voltage;
datalayer.battery.info.max_design_voltage_dV = battery_charge_voltage;
datalayer.battery.info.min_design_voltage_dV = battery_discharge_voltage;
}
void SamsungSdiLVBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x500: //Voltage, current, SOC, SOH
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
system_voltage = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
system_current = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
system_SOC = rx_frame.data.u8[4];
system_SOH = rx_frame.data.u8[5];
break;
case 0x501:
alarms_frame0 = rx_frame.data.u8[0];
alarms_frame1 = rx_frame.data.u8[1];
protection_frame2 = rx_frame.data.u8[2];
protection_frame3 = rx_frame.data.u8[3];
break;
case 0x502:
battery_charge_voltage = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
charge_current_limit = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
discharge_current_limit = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
battery_discharge_voltage = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
break;
case 0x503:
maximum_cell_voltage = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
minimum_cell_voltage = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
break;
case 0x504:
maximum_cell_temperature = rx_frame.data.u8[5];
minimum_cell_temperature = rx_frame.data.u8[6];
break;
case 0x505:
system_permanent_failure_status_dry_contact = rx_frame.data.u8[2];
system_permanent_failure_status_fuse_open = rx_frame.data.u8[3];
break;
default:
break;
}
}
void SamsungSdiLVBattery::transmit_can(unsigned long currentMillis) {
//No periodic sending required
}
void SamsungSdiLVBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.system.status.battery_allows_contactor_closing = true;
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
datalayer.battery.info.total_capacity_Wh = 5000;
}

View file

@ -0,0 +1,46 @@
#ifndef SAMSUNG_SDI_LV_BATTERY_H
#define SAMSUNG_SDI_LV_BATTERY_H
#include <Arduino.h>
#include "../datalayer/datalayer.h"
#include "CanBattery.h"
#ifdef SAMSUNG_SDI_LV_BATTERY
#define SELECTED_BATTERY_CLASS SamsungSdiLVBattery
#endif
class SamsungSdiLVBattery : public CanBattery {
public:
virtual void setup(void);
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr const char* Name = "Samsung SDI LV Battery";
private:
static const int MAX_PACK_VOLTAGE_DV = 600; //5000 = 500.0V
static const int MIN_PACK_VOLTAGE_DV = 300;
static const int MAX_CELL_DEVIATION_MV = 250;
static const int MAX_CELL_VOLTAGE_MV = 4200;
static const int MIN_CELL_VOLTAGE_MV = 3000;
uint16_t system_voltage = 50000;
int16_t system_current = 0;
uint8_t system_SOC = 50;
uint8_t system_SOH = 99;
uint16_t battery_charge_voltage = 9999;
uint16_t charge_current_limit = 0;
uint16_t discharge_current_limit = 0;
uint16_t battery_discharge_voltage = 0;
uint8_t alarms_frame0 = 0;
uint8_t alarms_frame1 = 0;
uint8_t protection_frame2 = 0;
uint8_t protection_frame3 = 0;
uint16_t maximum_cell_voltage = 3700;
uint16_t minimum_cell_voltage = 3700;
int8_t maximum_cell_temperature = 0;
int8_t minimum_cell_temperature = 0;
uint8_t system_permanent_failure_status_dry_contact = 0;
uint8_t system_permanent_failure_status_fuse_open = 0;
};
#endif

View file

@ -75,7 +75,6 @@ unsigned long currentTime = 0;
unsigned long lastPowerRemovalTime = 0;
unsigned long bmsPowerOnTime = 0;
const unsigned long powerRemovalInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const unsigned long powerRemovalDuration = 30000; // 30 seconds in milliseconds
const unsigned long bmsWarmupDuration = 3000;
void set(uint8_t pin, bool direction, uint32_t pwm_freq = 0xFFFF) {
@ -306,8 +305,9 @@ void handle_BMSpower() {
}
}
// If power has been removed for 30 seconds, restore the power
if (datalayer.system.status.BMS_reset_in_progress && currentTime - lastPowerRemovalTime >= powerRemovalDuration) {
// If power has been removed for user configured interval (1-59 seconds), restore the power
if (datalayer.system.status.BMS_reset_in_progress &&
currentTime - lastPowerRemovalTime >= datalayer.battery.settings.user_set_bms_reset_duration_ms) {
// Reapply power to the BMS
digitalWrite(bms_power_pin, HIGH);
bmsPowerOnTime = currentTime;

View file

@ -79,6 +79,10 @@ void init_stored_settings() {
if (temp < 16) {
datalayer.battery.settings.sofar_user_specified_battery_id = temp;
}
temp = settings.getUInt("BMSRESETDUR", false);
if (temp != 0) {
datalayer.battery.settings.user_set_bms_reset_duration_ms = temp;
}
#ifdef COMMON_IMAGE
user_selected_battery_type = (BatteryType)settings.getUInt("BATTTYPE", (int)BatteryType::None);
@ -187,6 +191,9 @@ void store_settings() {
if (!settings.putUInt("SOFAR_ID", datalayer.battery.settings.sofar_user_specified_battery_id)) {
set_event(EVENT_PERSISTENT_SAVE_INFO, 12);
}
if (!settings.putUInt("BMSRESETDUR", datalayer.battery.settings.sofar_user_specified_battery_id)) {
set_event(EVENT_PERSISTENT_SAVE_INFO, 13);
}
settings.end(); // Close preferences handle
}

View file

@ -139,6 +139,9 @@ typedef struct {
/** The user specified maximum allowed discharge voltage, in deciVolt. 3000 = 300.0 V */
uint16_t max_user_set_discharge_voltage_dV = BATTERY_MAX_DISCHARGE_VOLTAGE;
/** The user specified BMS reset period. Keeps track on how many milliseconds should we keep power off during daily BMS reset */
uint16_t user_set_bms_reset_duration_ms = 30000;
/** Parameters for keeping track of the limiting factor in the system */
bool user_settings_limit_discharge = false;
bool user_settings_limit_charge = false;

View file

@ -312,6 +312,7 @@ typedef struct {
uint32_t pid_time_spent_over_55c = 0;
uint32_t pid_contactor_closing_counter = 0;
uint32_t pid_date_of_manufacture = 0;
uint16_t pid_SOH_cell_1 = 0;
} DATALAYER_INFO_ECMP;
typedef struct {
@ -350,6 +351,19 @@ typedef struct {
uint8_t batteryManagementMode = 0;
uint8_t BMS_ign = 0;
uint8_t batteryRelay = 0;
uint16_t inverterVoltage = 0;
uint8_t ecu_serial_number[16] = {0};
uint8_t ecu_version_number[16] = {0};
uint32_t cumulative_charge_current_ah = 0;
uint32_t cumulative_discharge_current_ah = 0;
uint32_t cumulative_energy_charged_kWh = 0;
uint32_t cumulative_energy_discharged_kWh = 0;
uint32_t powered_on_total_time = 0;
uint16_t isolation_resistance_kOhm = 0;
uint16_t number_of_standard_charging_sessions = 0;
uint16_t number_of_fastcharging_sessions = 0;
uint16_t accumulated_normal_charging_energy_kWh = 0;
uint16_t accumulated_fastcharging_energy_kWh = 0;
} DATALAYER_INFO_KIAHYUNDAI64;
typedef struct {
@ -750,7 +764,7 @@ typedef struct {
uint16_t BECMsupplyVoltage = 0;
uint16_t BECMBatteryVoltage = 0;
uint16_t BECMBatteryCurrent = 0;
int16_t BECMBatteryCurrent = 0;
uint16_t BECMUDynMaxLim = 0;
uint16_t BECMUDynMinLim = 0;

View file

@ -59,16 +59,31 @@ class Esp32Hal {
return alloc_pins(name, pins[Is]...);
}
// Base case: no more pins
inline bool alloc_pins_ignore_unused_impl(const char* name) {
return alloc_pins(name); // Call with 0 pins
}
// Recursive case: process one pin at a time
template <typename... Rest>
bool alloc_pins_ignore_unused_impl(const char* name, gpio_num_t first, Rest... rest) {
if (first == GPIO_NUM_NC) {
return alloc_pins_ignore_unused_impl(name, rest...);
} else {
return call_alloc_pins_filtered(name, first, rest...);
}
}
// This helper just forwards pins after filtering is done
template <typename... Pins>
bool call_alloc_pins_filtered(const char* name, Pins... pins) {
return alloc_pins(name, pins...);
}
// Entry point
template <typename... Pins>
bool alloc_pins_ignore_unused(const char* name, Pins... pins) {
std::vector<gpio_num_t> valid_pins;
for (gpio_num_t pin : std::vector<gpio_num_t>{static_cast<gpio_num_t>(pins)...}) {
if (pin != GPIO_NUM_NC) {
valid_pins.push_back(pin);
}
}
return alloc_pins_from_vector(name, valid_pins, std::make_index_sequence<sizeof...(pins)>{});
return alloc_pins_ignore_unused_impl(name, static_cast<gpio_num_t>(pins)...);
}
virtual bool always_enable_bms_power() { return false; }

View file

@ -130,7 +130,8 @@ void update_machineryprotection() {
// Battery is fully charged. Dont allow any more power into it
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.reported_soc == 10000) //Scaled SOC% value is 100.00%
if (datalayer.battery.status.reported_soc == 10000 ||
datalayer.battery.status.real_soc == 10000) //Either Scaled OR Real SOC% value is 100.00%
{
if (!battery_full_event_fired) {
set_event(EVENT_BATTERY_FULL, 0);
@ -145,7 +146,8 @@ void update_machineryprotection() {
// Battery is empty. Do not allow further discharge.
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.bms_status == ACTIVE) {
if (datalayer.battery.status.reported_soc == 0) { //Scaled SOC% value is 0.00%
if (datalayer.battery.status.reported_soc == 0 ||
datalayer.battery.status.real_soc == 0) { //Either Scaled OR Real SOC% value is 0.00%, time to stop
if (!battery_empty_event_fired) {
set_event(EVENT_BATTERY_EMPTY, 0);
battery_empty_event_fired = true;

View file

@ -40,6 +40,7 @@ void init_events(void) {
events.entries[EVENT_CAN_CHARGER_MISSING].level = EVENT_LEVEL_INFO;
events.entries[EVENT_CAN_INVERTER_MISSING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CONTACTOR_WELDED].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CONTACTOR_OPEN].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CPU_OVERHEATING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CPU_OVERHEATED].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_WATER_INGRESS].level = EVENT_LEVEL_ERROR;
@ -191,6 +192,8 @@ String get_event_message_string(EVENTS_ENUM_TYPE event) {
return "Inverter not sending messages via CAN for the last 60 seconds. Check wiring!";
case EVENT_CONTACTOR_WELDED:
return "Contactors sticking/welded. Inspect battery with caution!";
case EVENT_CONTACTOR_OPEN:
return "Battery decided to open contactors. Inspect battery!";
case EVENT_CPU_OVERHEATING:
return "Battery-Emulator CPU overheating! Increase airflow/cooling to increase hardware lifespan!";
case EVENT_CPU_OVERHEATED:

View file

@ -22,6 +22,7 @@
XX(EVENT_CAN_NATIVE_TX_FAILURE) \
XX(EVENT_CHARGE_LIMIT_EXCEEDED) \
XX(EVENT_CONTACTOR_WELDED) \
XX(EVENT_CONTACTOR_OPEN) \
XX(EVENT_CPU_OVERHEATING) \
XX(EVENT_CPU_OVERHEATED) \
XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \

View file

@ -2,6 +2,7 @@
#define __LOGGING_H__
#include <inttypes.h>
#include "../../../USER_SETTINGS.h"
#include "Print.h"
#include "types.h"

View file

@ -4,8 +4,45 @@
#include "index_html.h"
#if defined(DEBUG_VIA_WEB) || defined(LOG_TO_SD)
char* strnchr(const char* s, int c, size_t n) {
// Like strchr, but only searches the first 'n' bytes of the string.
if (s == NULL) {
return NULL;
}
// Iterate through the string up to 'n' characters or until a null terminator is found.
for (size_t i = 0; i < n && s[i] != '\0'; ++i) {
if (s[i] == c) {
// Character found, return a pointer to it.
return (char*)&s[i];
}
}
// If the character to be found is the null terminator, and we haven't exceeded
// 'n' bytes, check if the null terminator is at the current position.
if (c == '\0') {
for (size_t i = 0; i < n; ++i) {
if (s[i] == '\0') {
return (char*)&s[i];
}
}
}
// Character not found within the first 'n' bytes.
return NULL;
}
String debug_logger_processor(void) {
String content = String(index_html_header);
String content = String();
// Reserve enough space for the content to avoid reallocations.
if (!content.reserve(1000 + sizeof(datalayer.system.info.logged_can_messages))) {
if (content.reserve(15)) {
content += "Out of memory.";
}
return content;
}
content += index_html_header;
// Page format
content += "<style>";
content += "body { background-color: black; color: white; font-family: Arial, sans-serif; }";
@ -28,7 +65,34 @@ String debug_logger_processor(void) {
// Start a new block for the debug log messages
content += "<PRE style='text-align: left'>";
content += String(datalayer.system.info.logged_can_messages);
size_t offset = datalayer.system.info.logged_can_messages_offset;
// If we're mid-buffer, print the older part first.
if (offset > 0 && offset < (sizeof(datalayer.system.info.logged_can_messages) - 1)) {
// Find the next newline after the current offset. The offset will always be
// before the penultimate character, so (offset + 1) will be the final '\0'
// or earlier.
char* next_newline = strnchr(&datalayer.system.info.logged_can_messages[offset + 1], '\n',
sizeof(datalayer.system.info.logged_can_messages) - offset - 1);
if (next_newline != NULL) {
// We found a newline, so append from the character after that. We check
// the string length to ensure we don't add any intermediate '\0'
// characters.
content.concat(next_newline + 1,
strnlen(next_newline + 1, sizeof(datalayer.system.info.logged_can_messages) - offset - 2));
} else {
// No newline found, so append from the next character after the offset to
// the end of the buffer. We check the string length to ensure we don't
// add any intermediate '\0' characters.
content.concat(&datalayer.system.info.logged_can_messages[offset + 1],
strnlen(&datalayer.system.info.logged_can_messages[offset + 1],
sizeof(datalayer.system.info.logged_can_messages) - offset - 1));
}
}
// Append the first part of the buffer up to the current write offset (which
// points to the first \0).
content.concat(datalayer.system.info.logged_can_messages, offset);
content += "</PRE>";
// Add JavaScript for navigation

View file

@ -402,6 +402,10 @@ String settings_processor(const String& var, BatteryEmulatorSettingsStore& setti
return String(datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV / 1.0, 0);
}
if (var == "BMS_RESET_DURATION") {
return String(datalayer.battery.settings.user_set_bms_reset_duration_ms / 1000.0, 0);
}
if (var == "CHARGER_CLASS") {
if (!charger) {
return "hidden";
@ -544,6 +548,9 @@ const char* getCANInterfaceName(CAN_Interface interface) {
xhr=new
XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/updateMaxDischargeVoltage?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value between 0 and 1000.0');}}}
function editBMSresetDuration(){var value=prompt('Amount of seconds BMS power should be off during periodic daily resets. Requires "Periodic BMS reset" to be enabled. Enter value in seconds (1-59):');if(value!==null){if(value>=1&&value<=59){var
xhr=new XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/updateBMSresetDuration?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value between 1 and 59');}}}
function editTeslaBalAct(){var value=prompt('Enable or disable forced LFP balancing. Makes the battery charge to 101percent. This should be performed once every month, to keep LFP batteries balanced. Ensure battery is fully charged before enabling, and also that you have enough sun or grid power to feed power into the battery while balancing is active. Enter 1 for enabled, 0 for disabled');if(value!==null){if(value==0||value==1){var xhr=new
XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/TeslaBalAct?value='+value,true);xhr.send();}}else{alert('Invalid value. Please enter 1 or 0');}}
@ -792,6 +799,8 @@ const char* getCANInterfaceName(CAN_Interface interface) {
<h4 class='%VOLTAGE_LIMITS_ACTIVE_CLASS%'>Target discharge voltage: %DISCHARGE_VOLTAGE% V </span> <button onclick='editMaxDischargeVoltage()'>Edit</button></h4>
<h4 style='color: white;'>Periodic BMS reset off time: %BMS_RESET_DURATION% s </span><button onclick='editBMSresetDuration()'>Edit</button></h4>
</div>
<div style='background-color: #2E37AD; padding: 10px; margin-bottom: 10px;border-radius: 50px' class="%FAKE_VOLTAGE_CLASS%">

View file

@ -594,14 +594,6 @@ void init_webserver() {
}
});
update_string("/equipmentStop", [](String value) {
if (value == "true" || value == "1") {
setBatteryPause(true, false, true); //Pause battery, do not pause CAN, equipment stop on (store to flash)
} else {
setBatteryPause(false, false, false);
}
});
// Route for editing SOCMin
update_string_setting("/updateSocMin", [](String value) {
datalayer.battery.settings.min_percentage = static_cast<uint16_t>(value.toFloat() * 100);
@ -657,6 +649,11 @@ void init_webserver() {
datalayer.battery.settings.max_user_set_discharge_voltage_dV = static_cast<uint16_t>(value.toFloat() * 10);
});
// Route for editing BMSresetDuration
update_string_setting("/updateBMSresetDuration", [](String value) {
datalayer.battery.settings.user_set_bms_reset_duration_ms = static_cast<uint16_t>(value.toFloat() * 1000);
});
// Route for editing FakeBatteryVoltage
update_string_setting("/updateFakeBatteryVoltage", [](String value) { battery->set_fake_voltage(value.toFloat()); });
@ -1410,7 +1407,7 @@ String processor(const String& var) {
content +=
"var xhr=new "
"XMLHttpRequest();xhr.onload=function() { "
"window.location.reload();};xhr.open('GET','/equipmentStop?stop='+stop,true);xhr.send();";
"window.location.reload();};xhr.open('GET','/equipmentStop?value='+stop,true);xhr.send();";
content += "}";
content += "</script>";

View file

@ -113,16 +113,20 @@ void wifi_monitor() {
if ((hasConnectedBefore && (currentMillis - lastWiFiCheck > current_check_interval)) ||
(!hasConnectedBefore && (currentMillis - lastWiFiCheck > INIT_WIFI_FULL_RECONNECT_INTERVAL))) {
DEBUG_PRINTF("Time to monitor Wi-Fi status: %d, %d, %d, %d, %d\n", hasConnectedBefore, currentMillis, lastWiFiCheck,
current_check_interval, INIT_WIFI_FULL_RECONNECT_INTERVAL);
lastWiFiCheck = currentMillis;
wl_status_t status = WiFi.status();
if (status != WL_CONNECTED) {
// WL_IDLE_STATUS can mean we're connected but haven't yet gotten the IP.
if (status != WL_CONNECTED && status != WL_IDLE_STATUS) {
// Increase the current check interval if it's not at the maximum
if (current_check_interval + STEP_WIFI_CHECK_INTERVAL <= MAX_STEP_WIFI_CHECK_INTERVAL)
if (current_check_interval + STEP_WIFI_CHECK_INTERVAL <= MAX_STEP_WIFI_CHECK_INTERVAL) {
current_check_interval += STEP_WIFI_CHECK_INTERVAL;
#ifdef DEBUG_LOG
logging.println("Wi-Fi not connected, attempting to reconnect...");
#endif
}
DEBUG_PRINTF("Wi-Fi not connected (status=%d), attempting to reconnect\n", status);
// Try WiFi.reconnect() if it was successfully connected at least once
if (hasConnectedBefore) {
lastReconnectAttempt = currentMillis; // Reset reconnection attempt timer
@ -203,7 +207,7 @@ void onWifiConnect(WiFiEvent_t event, WiFiEventInfo_t info) {
clear_event(EVENT_WIFI_DISCONNECT);
set_event(EVENT_WIFI_CONNECT, 0);
connected_once = true;
DEBUG_PRINTF("Wi-Fi connected. RSSI: %d dBm, IP address: %s, SSID: %s\n", -WiFi.RSSI(),
DEBUG_PRINTF("Wi-Fi connected. status: %d, RSSI: %d dBm, IP address: %s, SSID: %s\n", WiFi.status(), -WiFi.RSSI(),
WiFi.localIP().toString().c_str(), WiFi.SSID().c_str());
hasConnectedBefore = true; // Mark as successfully connected at least once
reconnectAttempts = 0; // Reset the attempt counter

View file

@ -154,6 +154,12 @@ void KostalInverterProtocol::update_values() {
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 18); // Last current
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 22); // Should be Avg current(1s)
// Close contactors after 7 battery info frames requested
if (f2_startup_count > 7) {
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
}
// On startup, byte 56 seems to be always 0x00 couple of frames,.
if (f2_startup_count < 9) {
CYCLIC_DATA[56] = 0x00;
@ -226,7 +232,7 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
if (check_kostal_frame_crc(rx_index)) {
incoming_message_counter = RS485_HEALTHY;
if (RS485_RXFRAME[1] == 'c') {
if (RS485_RXFRAME[1] == 'c' && info_sent) {
if (RS485_RXFRAME[6] == 0x47) {
// Set time function - Do nothing.
send_kostal(ACK_FRAME, 8); // ACK
@ -234,12 +240,11 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
if (RS485_RXFRAME[6] == 0x5E) {
// Set State function
if (RS485_RXFRAME[7] == 0x00) {
// Allow contactor closing
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
send_kostal(ACK_FRAME, 8); // ACK
} else if (RS485_RXFRAME[7] == 0x04) {
// INVALID STATE, no ACK sent
send_kostal(ACK_FRAME, 8); // ACK
} else if (RS485_RXFRAME[7] == 0xFF) {
// no ACK sent
} else {
// Battery deep sleep?
send_kostal(ACK_FRAME, 8); // ACK
@ -250,7 +255,7 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
//Reverse polarity, do nothing
} else {
int code = RS485_RXFRAME[6] + RS485_RXFRAME[7] * 0x100;
if (code == 0x44a) {
if (code == 0x44a && info_sent) {
//Send cyclic data
// TODO: Probably not a good idea to use the battery object here like this.
if (battery) {
@ -274,13 +279,12 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
tmpframe[38] = calculate_kostal_crc(tmpframe, 38);
null_stuffer(tmpframe, 40);
send_kostal(tmpframe, 40);
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing (battery_info) -> true");
info_sent = true;
if (!startupMillis) {
startupMillis = currentMillis;
}
}
if (code == 0x353) {
if (code == 0x353 && info_sent) {
//Send battery error/status
byte tmpframe[9]; //copy values to prevent data manipulation during rewrite/crc calculation
memcpy(tmpframe, STATUS_FRAME, 9);

View file

@ -40,8 +40,7 @@ class KostalInverterProtocol : public Rs485InverterProtocol {
uint8_t incoming_message_counter = RS485_HEALTHY;
int8_t f2_startup_count = 0;
boolean B1_delay = false;
unsigned long B1_last_millis = 0;
boolean info_sent = false;
unsigned long currentMillis;
unsigned long startupMillis = 0;
unsigned long contactorMillis = 0;

View file

@ -61,12 +61,35 @@ void SmaBydHInverter::
SMA_4D8.data.u8[6] = STOP_STATE;
}
//Lifetime charged energy amount
SMA_458.data.u8[0] = (datalayer.battery.status.total_charged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[1] = (datalayer.battery.status.total_charged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[2] = (datalayer.battery.status.total_charged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[3] = (datalayer.battery.status.total_charged_battery_Wh & 0x000000FF);
//Lifetime discharged energy amount
SMA_458.data.u8[4] = (datalayer.battery.status.total_discharged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[5] = (datalayer.battery.status.total_discharged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[6] = (datalayer.battery.status.total_discharged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[7] = (datalayer.battery.status.total_discharged_battery_Wh & 0x000000FF);
//Error bits
if (datalayer.system.status.battery_allows_contactor_closing) {
SMA_158.data.u8[2] = 0xAA;
} else {
SMA_158.data.u8[2] = 0x6A;
}
//Highest battery temperature
SMA_518.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SMA_518.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Lowest battery temperature
SMA_518.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SMA_518.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Sum of all cellvoltages
SMA_518.data.u8[4] = (datalayer.battery.status.voltage_dV >> 8);
SMA_518.data.u8[5] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Cell min/max voltage (mV / 25)
SMA_518.data.u8[6] = (datalayer.battery.status.cell_min_voltage_mV / 25);
SMA_518.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV / 25);
control_contactor_led();

View file

@ -60,6 +60,30 @@ void SmaBydHvsInverter::
SMA_4D8.data.u8[6] = STOP_STATE;
}
//Highest battery temperature
SMA_518.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SMA_518.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Lowest battery temperature
SMA_518.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SMA_518.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Sum of all cellvoltages
SMA_518.data.u8[4] = (datalayer.battery.status.voltage_dV >> 8);
SMA_518.data.u8[5] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Cell min/max voltage (mV / 25)
SMA_518.data.u8[6] = (datalayer.battery.status.cell_min_voltage_mV / 25);
SMA_518.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV / 25);
//Lifetime charged energy amount
SMA_458.data.u8[0] = (datalayer.battery.status.total_charged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[1] = (datalayer.battery.status.total_charged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[2] = (datalayer.battery.status.total_charged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[3] = (datalayer.battery.status.total_charged_battery_Wh & 0x000000FF);
//Lifetime discharged energy amount
SMA_458.data.u8[4] = (datalayer.battery.status.total_discharged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[5] = (datalayer.battery.status.total_discharged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[6] = (datalayer.battery.status.total_discharged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[7] = (datalayer.battery.status.total_discharged_battery_Wh & 0x000000FF);
//Error bits
if (datalayer.system.status.battery_allows_contactor_closing) {
SMA_158.data.u8[2] = 0xAA;
@ -247,7 +271,7 @@ void SmaBydHvsInverter::transmit_can(unsigned long currentMillis) {
// Increment message index and wrap around if needed
batch_send_index++;
if (transmit_can_init == false) {
if (transmit_can_init == false) { //We completed sending the batches
batch_send_index = 0;
}
}

View file

@ -66,7 +66,7 @@ class SmaBydHvsInverter : public SmaInverterBase {
.ext_ID = false,
.DLC = 8,
.ID = 0x518,
.data = {0x01, 0x4A, 0x01, 0x25, 0xFF, 0xFF, 0xFF, 0xFF}};
.data = {0x01, 0x4A, 0x01, 0x25, 0x10, 0x10, 0xFF, 0xFF}};
// Pairing/Battery setup information
@ -79,7 +79,7 @@ class SmaBydHvsInverter : public SmaInverterBase {
.ext_ID = false,
.DLC = 8,
.ID = 0x598,
.data = {0x00, 0x01, 0x0F, 0x2C, 0x5C, 0x98, 0xB6, 0xEE}};
.data = {0x00, 0x01, 0x0F, 0x2C, 0x5C, 0x98, 0xB6, 0xEE}}; //B0-4 Serial, rest unknown
CAN_frame SMA_5D8 = {.FD = false,
.ext_ID = false,
.DLC = 8,

View file

@ -64,6 +64,30 @@ void SmaTripowerInverter::
SMA_4D8.data.u8[6] = READY_STATE;
}
//Highest battery temperature
SMA_518.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SMA_518.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Lowest battery temperature
SMA_518.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SMA_518.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Sum of all cellvoltages
SMA_518.data.u8[4] = (datalayer.battery.status.voltage_dV >> 8);
SMA_518.data.u8[5] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Cell min/max voltage (mV / 25)
SMA_518.data.u8[6] = (datalayer.battery.status.cell_min_voltage_mV / 25);
SMA_518.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV / 25);
//Lifetime charged energy amount
SMA_458.data.u8[0] = (datalayer.battery.status.total_charged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[1] = (datalayer.battery.status.total_charged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[2] = (datalayer.battery.status.total_charged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[3] = (datalayer.battery.status.total_charged_battery_Wh & 0x000000FF);
//Lifetime discharged energy amount
SMA_458.data.u8[4] = (datalayer.battery.status.total_discharged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[5] = (datalayer.battery.status.total_discharged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[6] = (datalayer.battery.status.total_discharged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[7] = (datalayer.battery.status.total_discharged_battery_Wh & 0x000000FF);
control_contactor_led();
// Check if Enable line is working. If we go too long without any input, raise an event

View file

@ -38,7 +38,7 @@ It is also deployed in these registries:
- Arduino Library Registry: [https://github.com/arduino/library-registry](https://github.com/arduino/library-registry)
- ESP Component Registry [https://components.espressif.com/components/esp32async/espasyncbebserver/](https://components.espressif.com/components/esp32async/espasyncbebserver/)
- ESP Component Registry [https://components.espressif.com/components/esp32async/espasyncwebserver](https://components.espressif.com/components/esp32async/espasyncwebserver)
- PlatformIO Registry: [https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer](https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer)
@ -72,6 +72,19 @@ lib_deps =
ESP32Async/ESPAsyncWebServer
```
### LibreTiny (BK7231N/T, RTL8710B, etc.)
Version 1.9.1 or newer is required.
```ini
[env:stable]
platform = libretiny @ ^1.9.1
lib_ldf_mode = chain
lib_deps =
ESP32Async/AsyncTCP
ESP32Async/ESPAsyncWebServer
```
### Unofficial dependencies
**AsyncTCPSock**
@ -100,7 +113,7 @@ platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = rpipicow
board_build.core = earlephilhower
lib_deps =
ayushsharma82/RPAsyncTCP@^1.3.1
ayushsharma82/RPAsyncTCP@^1.3.2
ESP32Async/ESPAsyncWebServer
lib_ignore =
lwIP_ESPHost

View file

@ -1,6 +1,6 @@
{
"name": "ESPAsyncWebServer",
"version": "3.7.2",
"version": "3.7.10",
"description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.",
"keywords": "http,async,websocket,webserver",
"homepage": "https://github.com/ESP32Async/ESPAsyncWebServer",
@ -18,14 +18,18 @@
"platforms": [
"espressif32",
"espressif8266",
"raspberrypi"
"raspberrypi",
"libretiny"
],
"dependencies": [
{
"owner": "ESP32Async",
"name": "AsyncTCP",
"version": "^3.3.6",
"platforms": "espressif32"
"version": "^3.4.5",
"platforms": [
"espressif32",
"libretiny"
]
},
{
"owner": "ESP32Async",
@ -40,7 +44,7 @@
{
"owner": "ayushsharma82",
"name": "RPAsyncTCP",
"version": "^1.3.1",
"version": "^1.3.2",
"platforms": "raspberrypi"
}
],

View file

@ -1,6 +1,6 @@
name=ESP Async WebServer
includes=ESPAsyncWebServer.h
version=3.7.2
version=3.7.10
author=ESP32Async
maintainer=ESP32Async
sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040

View file

@ -193,7 +193,7 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A
AsyncEventSourceClient::~AsyncEventSourceClient() {
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lockmq);
std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif
_messageQueue.clear();
close();
@ -211,7 +211,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
#ifdef ESP32
// length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq);
std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif
_messageQueue.emplace_back(message, len);
@ -241,7 +241,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
#ifdef ESP32
// length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq);
std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif
_messageQueue.emplace_back(std::move(msg));
@ -261,7 +261,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) {
#ifdef ESP32
// Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq);
std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif
// adjust in-flight len
@ -290,7 +290,7 @@ void AsyncEventSourceClient::_onPoll() {
if (_messageQueue.size()) {
#ifdef ESP32
// Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq);
std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif
_runQueue();
}
@ -367,7 +367,7 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient *client) {
return;
}
#ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock);
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif
_clients.emplace_back(client);
if (_connectcb) {
@ -382,7 +382,7 @@ void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient *client) {
_disconnectcb(client);
}
#ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock);
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif
for (auto i = _clients.begin(); i != _clients.end(); ++i) {
if (i->get() == client) {
@ -398,10 +398,15 @@ void AsyncEventSource::close() {
// iterator should remain valid even when AsyncEventSource::_handleDisconnect()
// is called very early
#ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock);
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif
for (const auto &c : _clients) {
if (c->connected()) {
/**
* @brief: Fix self-deadlock by using recursive_mutex instead.
* Due to c->close() shall call the callback function _onDisconnect()
* The calling flow _onDisconnect() --> _handleDisconnect() --> deadlock
*/
c->close();
}
}
@ -412,7 +417,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
size_t aql = 0;
uint32_t nConnectedClients = 0;
#ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock);
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif
if (!_clients.size()) {
return 0;
@ -430,7 +435,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
AsyncEvent_SharedData_t shared_msg = std::make_shared<String>(generateEventMessage(message, event, id, reconnect));
#ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock);
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif
size_t hits = 0;
size_t miss = 0;
@ -446,7 +451,7 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c
size_t AsyncEventSource::count() const {
#ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock);
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif
size_t n_clients{0};
for (const auto &i : _clients) {

View file

@ -6,8 +6,13 @@
#include <Arduino.h>
#ifdef ESP32
#if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#ifdef LIBRETINY
#ifdef round
#undef round
#endif
#endif
#include <mutex>
#ifndef SSE_MAX_QUEUED_MESSAGES
#define SSE_MAX_QUEUED_MESSAGES 32
@ -129,7 +134,7 @@ private:
size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer
std::list<AsyncEventSourceMessage> _messageQueue;
#ifdef ESP32
mutable std::mutex _lockmq;
mutable std::recursive_mutex _lockmq;
#endif
bool _queueMessage(const char *message, size_t len);
bool _queueMessage(AsyncEvent_SharedData_t &&msg);
@ -230,7 +235,7 @@ private:
#ifdef ESP32
// Same as for individual messages, protect mutations of _clients list
// since simultaneous access from different tasks is possible
mutable std::mutex _client_queue_lock;
mutable std::recursive_mutex _client_queue_lock;
#endif
ArEventHandlerFunction _connectcb = nullptr;
ArEventHandlerFunction _disconnectcb = nullptr;

View file

@ -113,53 +113,77 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons
void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) {
if (_onRequest) {
// GET request:
if (request->method() == HTTP_GET) {
JsonVariant json;
_onRequest(request, json);
return;
} else if (request->_tempObject != NULL) {
}
// POST / PUT / ... requests:
// check if JSON body is too large, if it is, don't deserialize
if (request->contentLength() > _maxContentLength) {
#ifdef ESP32
log_e("Content length exceeds maximum allowed");
#endif
request->send(413);
return;
}
if (request->_tempObject == NULL) {
// there is no body
request->send(400);
return;
}
#if ARDUINOJSON_VERSION_MAJOR == 5
DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((uint8_t *)(request->_tempObject));
if (json.success()) {
DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject);
if (json.success()) {
#elif ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#else
JsonDocument jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
JsonDocument jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif
_onRequest(request, json);
return;
}
_onRequest(request, json);
} else {
// error parsing the body
request->send(400);
}
request->send(_contentLength > _maxContentLength ? 413 : 400);
} else {
request->send(500);
}
}
void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (_onRequest) {
_contentLength = total;
if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) {
request->_tempObject = malloc(total);
// ignore callback if size is larger than maxContentLength
if (total > _maxContentLength) {
return;
}
if (index == 0) {
// this check allows request->_tempObject to be initialized from a middleware
if (request->_tempObject == NULL) {
request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string
if (request->_tempObject == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
log_e("Failed to allocate");
#endif
request->abort();
return;
request->abort();
return;
}
}
}
if (request->_tempObject != NULL) {
memcpy((uint8_t *)(request->_tempObject) + index, data, len);
uint8_t *buffer = (uint8_t *)request->_tempObject;
memcpy(buffer + index, data, len);
}
}
}

View file

@ -79,7 +79,6 @@ protected:
String _uri;
WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest;
size_t _contentLength;
#if ARDUINOJSON_VERSION_MAJOR == 6
size_t maxJsonBufferSize;
#endif

View file

@ -3,30 +3,32 @@
#include "ESPAsyncWebServer.h"
AsyncWebHeader::AsyncWebHeader(const String &data) {
const AsyncWebHeader AsyncWebHeader::parse(const char *data) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers
// In HTTP/1.X, a header is a case-insensitive name followed by a colon, then optional whitespace which will be ignored, and finally by its value
if (!data) {
return;
return AsyncWebHeader(); // nullptr
}
int index = data.indexOf(':');
if (index < 0) {
return;
if (data[0] == '\0') {
return AsyncWebHeader(); // empty string
}
_name = data.substring(0, index);
_value = data.substring(index + 2);
}
String AsyncWebHeader::toString() const {
String str;
if (str.reserve(_name.length() + _value.length() + 2)) {
str.concat(_name);
str.concat((char)0x3a);
str.concat((char)0x20);
str.concat(_value);
str.concat(asyncsrv::T_rn);
} else {
#ifdef ESP32
log_e("Failed to allocate");
#endif
}
return str;
if (strchr(data, '\n') || strchr(data, '\r')) {
return AsyncWebHeader(); // Invalid header format
}
char *colon = strchr(data, ':');
if (!colon) {
return AsyncWebHeader(); // separator not found
}
if (colon == data) {
return AsyncWebHeader(); // Header name cannot be empty
}
char *startOfValue = colon + 1; // Skip the colon
// skip one optional whitespace after the colon
if (*startOfValue == ' ') {
startOfValue++;
}
String name;
name.reserve(colon - data);
name.concat(data, colon - data);
return AsyncWebHeader(name, String(startOfValue));
}

View file

@ -0,0 +1,85 @@
#include "ESPAsyncWebServer.h"
/**
* @brief Sends a file from the filesystem to the client, with optional gzip compression and ETag-based caching.
*
* This method serves files over HTTP from the provided filesystem. If a compressed version of the file
* (with a `.gz` extension) exists and uncompressed version does not exist, it serves the compressed file.
* It also handles ETag caching using the CRC32 value from the gzip trailer, responding with `304 Not Modified`
* if the client's `If-None-Match` header matches the generated ETag.
*
* @param fs Reference to the filesystem (SPIFFS, LittleFS, etc.).
* @param path Path to the file to be served.
* @param contentType Optional MIME type of the file to be sent.
* If contentType is "" it will be obtained from the file extension
* @param download If true, forces the file to be sent as a download.
* @param callback Optional template processor for dynamic content generation.
* Templates will not be processed in compressed files.
*
* @note If neither the file nor its compressed version exists, responds with `404 Not Found`.
*/
void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) {
// Check uncompressed file first
if (fs.exists(path)) {
send(beginResponse(fs, path, contentType, download, callback));
return;
}
// Handle compressed version
const String gzPath = path + asyncsrv::T__gz;
File gzFile = fs.open(gzPath, "r");
// Compressed file not found or invalid
if (!gzFile.seek(gzFile.size() - 8)) {
send(404);
gzFile.close();
return;
}
// ETag validation
if (this->hasHeader(asyncsrv::T_INM)) {
// Generate server ETag from CRC in gzip trailer
uint8_t crcInTrailer[4];
gzFile.read(crcInTrailer, 4);
char serverETag[9];
_getEtag(crcInTrailer, serverETag);
// Compare with client's ETag
const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM);
if (inmHeader && inmHeader->value() == serverETag) {
gzFile.close();
this->send(304); // Not Modified
return;
}
}
// Send compressed file response
gzFile.close();
send(beginResponse(fs, path, contentType, download, callback));
}
/**
* @brief Generates an ETag string from a 4-byte trailer
*
* This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes.
*
* @param trailer[4] Input array of 4 bytes to convert to hexadecimal
* @param serverETag Output buffer to store the ETag
* Must be pre-allocated with minimum 9 bytes (8 hex + 1 null terminator)
*/
void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) {
static constexpr char hexChars[] = "0123456789ABCDEF";
uint32_t data;
memcpy(&data, trailer, 4);
serverETag[0] = hexChars[(data >> 4) & 0x0F];
serverETag[1] = hexChars[data & 0x0F];
serverETag[2] = hexChars[(data >> 12) & 0x0F];
serverETag[3] = hexChars[(data >> 8) & 0x0F];
serverETag[4] = hexChars[(data >> 20) & 0x0F];
serverETag[5] = hexChars[(data >> 16) & 0x0F];
serverETag[6] = hexChars[(data >> 28)];
serverETag[7] = hexChars[(data >> 24) & 0x0F];
serverETag[8] = '\0';
}

View file

@ -12,7 +12,7 @@ extern "C" {
/** Minor version number (x.X.x) */
#define ASYNCWEBSERVER_VERSION_MINOR 7
/** Patch version number (x.x.X) */
#define ASYNCWEBSERVER_VERSION_PATCH 2
#define ASYNCWEBSERVER_VERSION_PATCH 10
/**
* Macro to convert version number into an integer

View file

@ -17,6 +17,8 @@
#include <rom/ets_sys.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266)
#include <Hash.h>
#elif defined(LIBRETINY)
#include <mbedtls/sha1.h>
#endif
using namespace asyncsrv;
@ -333,7 +335,7 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async
AsyncWebSocketClient::~AsyncWebSocketClient() {
{
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lock);
std::lock_guard<std::recursive_mutex> lock(_lock);
#endif
_messageQueue.clear();
_controlQueue.clear();
@ -351,7 +353,7 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
_lastMessageTime = millis();
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lock);
std::unique_lock<std::recursive_mutex> lock(_lock);
#endif
if (!_controlQueue.empty()) {
@ -362,6 +364,14 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
_controlQueue.pop_front();
_status = WS_DISCONNECTED;
if (_client) {
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true);
}
return;
@ -385,7 +395,7 @@ void AsyncWebSocketClient::_onPoll() {
}
#ifdef ESP32
std::unique_lock<std::mutex> lock(_lock);
std::unique_lock<std::recursive_mutex> lock(_lock);
#endif
if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) {
_runQueue();
@ -415,21 +425,21 @@ void AsyncWebSocketClient::_runQueue() {
bool AsyncWebSocketClient::queueIsFull() const {
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lock);
std::lock_guard<std::recursive_mutex> lock(_lock);
#endif
return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED);
}
size_t AsyncWebSocketClient::queueLen() const {
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lock);
std::lock_guard<std::recursive_mutex> lock(_lock);
#endif
return _messageQueue.size();
}
bool AsyncWebSocketClient::canSend() const {
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lock);
std::lock_guard<std::recursive_mutex> lock(_lock);
#endif
return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES;
}
@ -440,7 +450,7 @@ bool AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t *data, si
}
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lock);
std::lock_guard<std::recursive_mutex> lock(_lock);
#endif
_controlQueue.emplace_back(opcode, data, len, mask);
@ -458,7 +468,7 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
}
#ifdef ESP32
std::lock_guard<std::mutex> lock(_lock);
std::unique_lock<std::recursive_mutex> lock(_lock);
#endif
if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) {
@ -466,6 +476,14 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
_status = WS_DISCONNECTED;
if (_client) {
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true);
}
@ -551,6 +569,7 @@ void AsyncWebSocketClient::_onTimeout(uint32_t time) {
void AsyncWebSocketClient::_onDisconnect() {
// Serial.println("onDis");
_client = nullptr;
_server->_handleDisconnect(this);
}
void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
@ -857,6 +876,16 @@ AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request)
return &_clients.back();
}
void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) {
const auto client_id = client->id();
const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [client_id](const AsyncWebSocketClient &c) {
return c.id() == client_id;
});
if (iter != std::end(_clients)) {
_clients.erase(iter);
}
}
bool AsyncWebSocket::availableForWriteAll() {
return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) {
return c.queueIsFull();
@ -1300,11 +1329,20 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket
}
k.concat(key);
k.concat(WS_STR_UUID);
#ifdef LIBRETINY
mbedtls_sha1_context ctx;
mbedtls_sha1_init(&ctx);
mbedtls_sha1_starts(&ctx);
mbedtls_sha1_update(&ctx, (const uint8_t *)k.c_str(), k.length());
mbedtls_sha1_finish(&ctx, hash);
mbedtls_sha1_free(&ctx);
#else
SHA1Builder sha1;
sha1.begin();
sha1.add((const uint8_t *)k.c_str(), k.length());
sha1.calculate();
sha1.getBytes(hash);
#endif
#endif
base64_encodestate _state;
base64_init_encodestate(&_state);

View file

@ -5,8 +5,14 @@
#define ASYNCWEBSOCKET_H_
#include <Arduino.h>
#ifdef ESP32
#if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#ifdef LIBRETINY
#ifdef round
#undef round
#endif
#endif
#include <mutex>
#ifndef WS_MAX_QUEUED_MESSAGES
#define WS_MAX_QUEUED_MESSAGES 32
@ -152,7 +158,7 @@ private:
uint32_t _clientId;
AwsClientStatus _status;
#ifdef ESP32
mutable std::mutex _lock;
mutable std::recursive_mutex _lock;
#endif
std::deque<AsyncWebSocketControl> _controlQueue;
std::deque<AsyncWebSocketMessage> _messageQueue;
@ -291,7 +297,7 @@ private:
String _url;
std::list<AsyncWebSocketClient> _clients;
uint32_t _cNextId;
AwsEventHandler _eventHandler{nullptr};
AwsEventHandler _eventHandler;
AwsHandshakeHandler _handshakeHandler;
bool _enabled;
#ifdef ESP32
@ -305,8 +311,8 @@ public:
PARTIALLY_ENQUEUED = 2,
} SendStatus;
explicit AsyncWebSocket(const char *url) : _url(url), _cNextId(1), _enabled(true) {}
AsyncWebSocket(const String &url) : _url(url), _cNextId(1), _enabled(true) {}
explicit AsyncWebSocket(const char *url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {}
AsyncWebSocket(const String &url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {}
~AsyncWebSocket(){};
const char *url() const {
return _url.c_str();
@ -385,6 +391,7 @@ public:
return _cNextId++;
}
AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request);
void _handleDisconnect(AsyncWebSocketClient *client);
void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final;
@ -413,4 +420,86 @@ public:
}
};
class AsyncWebSocketMessageHandler {
public:
AwsEventHandler eventHandler() const {
return _handler;
}
void onConnect(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> onConnect) {
_onConnect = onConnect;
}
void onDisconnect(std::function<void(AsyncWebSocket *server, uint32_t clientId)> onDisconnect) {
_onDisconnect = onDisconnect;
}
/**
* Error callback
* @param reason null-terminated string
* @param len length of the string
*/
void onError(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> onError) {
_onError = onError;
}
/**
* Complete message callback
* @param data pointer to the data (binary or null-terminated string). This handler expects the user to know which data type he uses.
*/
void onMessage(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> onMessage) {
_onMessage = onMessage;
}
/**
* Fragmented message callback
* @param data pointer to the data (binary or null-terminated string), will be null-terminated. This handler expects the user to know which data type he uses.
*/
// clang-format off
void onFragment(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> onFragment) {
_onFragment = onFragment;
}
// clang-format on
private:
// clang-format off
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> _onConnect;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> _onError;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> _onMessage;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> _onFragment;
std::function<void(AsyncWebSocket *server, uint32_t clientId)> _onDisconnect;
// clang-format on
// this handler is meant to only support 1-frame messages (== unfragmented messages)
AwsEventHandler _handler = [this](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_CONNECT) {
if (_onConnect) {
_onConnect(server, client);
}
} else if (type == WS_EVT_DISCONNECT) {
if (_onDisconnect) {
_onDisconnect(server, client->id());
}
} else if (type == WS_EVT_ERROR) {
if (_onError) {
_onError(server, client, *((uint16_t *)arg), (const char *)data, len);
}
} else if (type == WS_EVT_DATA) {
AwsFrameInfo *info = (AwsFrameInfo *)arg;
if (info->opcode == WS_TEXT) {
data[len] = 0;
}
if (info->final && info->index == 0 && info->len == len) {
if (_onMessage) {
_onMessage(server, client, data, len);
}
} else {
if (_onFragment) {
_onFragment(server, client, info, data, len);
}
}
}
};
};
#endif /* ASYNCWEBSOCKET_H_ */

View file

@ -4,9 +4,10 @@
#ifndef _ESPAsyncWebServer_H_
#define _ESPAsyncWebServer_H_
#include "Arduino.h"
#include <Arduino.h>
#include <FS.h>
#include <lwip/tcpbase.h>
#include "FS.h"
#include <algorithm>
#include <deque>
#include <functional>
@ -14,16 +15,13 @@
#include <unordered_map>
#include <vector>
#ifdef ESP32
#if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <HTTP_Method.h>
#include <WiFi.h>
#include <http_parser.h>
#else
#error Platform not supported
@ -131,12 +129,20 @@ private:
String _value;
public:
AsyncWebHeader() {}
AsyncWebHeader(const AsyncWebHeader &) = default;
AsyncWebHeader(AsyncWebHeader &&) = default;
AsyncWebHeader(const char *name, const char *value) : _name(name), _value(value) {}
AsyncWebHeader(const String &name, const String &value) : _name(name), _value(value) {}
AsyncWebHeader(const String &data);
#ifndef ESP8266
[[deprecated("Use AsyncWebHeader::parse(data) instead")]]
#endif
AsyncWebHeader(const String &data)
: AsyncWebHeader(parse(data)){};
AsyncWebHeader &operator=(const AsyncWebHeader &) = default;
AsyncWebHeader &operator=(AsyncWebHeader &&other) = default;
const String &name() const {
return _name;
@ -144,7 +150,18 @@ public:
const String &value() const {
return _value;
}
String toString() const;
// returns true if the header is valid
operator bool() const {
return _name.length();
}
static const AsyncWebHeader parse(const String &data) {
return parse(data.c_str());
}
static const AsyncWebHeader parse(const char *data);
};
/*
@ -180,6 +197,7 @@ class AsyncWebServerRequest {
using FS = fs::FS;
friend class AsyncWebServer;
friend class AsyncCallbackWebHandler;
friend class AsyncFileResponse;
private:
AsyncClient *_client;
@ -251,6 +269,8 @@ private:
void _send();
void _runMiddlewareChain();
static void _getEtag(uint8_t trailer[4], char *serverETag);
public:
File _tempFile;
void *_tempObject;
@ -363,13 +383,7 @@ public:
send(beginResponse(code, contentType, content, len, callback));
}
void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) {
if (fs.exists(path) || (!download && fs.exists(path + asyncsrv::T__gz))) {
send(beginResponse(fs, path, contentType, download, callback));
} else {
send(404);
}
}
void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr);
void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) {
send(fs, path, contentType.c_str(), download, callback);
}
@ -462,7 +476,9 @@ public:
}
AsyncWebServerResponse *beginChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncWebServerResponse *beginChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncWebServerResponse *beginChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) {
return beginChunkedResponse(contentType.c_str(), callback, templateCallback);
}
AsyncResponseStream *beginResponseStream(const char *contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE);
AsyncResponseStream *beginResponseStream(const String &contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) {
@ -531,6 +547,9 @@ public:
* @return const AsyncWebParameter*
*/
const AsyncWebParameter *getParam(size_t num) const;
const AsyncWebParameter *getParam(int num) const {
return num < 0 ? nullptr : getParam((size_t)num);
}
size_t args() const {
return params();
@ -545,9 +564,15 @@ public:
#ifdef ESP8266
const String &arg(const __FlashStringHelper *data) const; // get request argument value by F(name)
#endif
const String &arg(size_t i) const; // get request argument value by number
const String &arg(size_t i) const; // get request argument value by number
const String &arg(int i) const {
return i < 0 ? emptyString : arg((size_t)i);
};
const String &argName(size_t i) const; // get request argument name by number
bool hasArg(const char *name) const; // check if argument exists
const String &argName(int i) const {
return i < 0 ? emptyString : argName((size_t)i);
};
bool hasArg(const char *name) const; // check if argument exists
bool hasArg(const String &name) const {
return hasArg(name.c_str());
};
@ -556,6 +581,9 @@ public:
#endif
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const;
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(int i) const {
return i < 0 ? emptyString : pathArg((size_t)i);
}
// get request header value by name
const String &header(const char *name) const;
@ -567,8 +595,14 @@ public:
const String &header(const __FlashStringHelper *data) const; // get request header value by F(name)
#endif
const String &header(size_t i) const; // get request header value by number
const String &header(size_t i) const; // get request header value by number
const String &header(int i) const {
return i < 0 ? emptyString : header((size_t)i);
};
const String &headerName(size_t i) const; // get request header name by number
const String &headerName(int i) const {
return i < 0 ? emptyString : headerName((size_t)i);
};
size_t headers() const; // get header count
@ -590,6 +624,9 @@ public:
#endif
const AsyncWebHeader *getHeader(size_t num) const;
const AsyncWebHeader *getHeader(int num) const {
return num < 0 ? nullptr : getHeader((size_t)num);
};
const std::list<AsyncWebHeader> &getHeaders() const {
return _headers;
@ -1011,6 +1048,10 @@ public:
setContentType(type.c_str());
}
void setContentType(const char *type);
bool addHeader(AsyncWebHeader &&header, bool replaceExisting = true);
bool addHeader(const AsyncWebHeader &header, bool replaceExisting = true) {
return header && addHeader(header.name(), header.value(), replaceExisting);
}
bool addHeader(const char *name, const char *value, bool replaceExisting = true);
bool addHeader(const String &name, const String &value, bool replaceExisting = true) {
return addHeader(name.c_str(), value.c_str(), replaceExisting);
@ -1069,6 +1110,15 @@ public:
void begin();
void end();
tcp_state state() const {
#ifdef ESP8266
// ESPAsyncTCP and RPAsyncTCP methods are not corrected declared with const for immutable ones.
return static_cast<tcp_state>(const_cast<AsyncWebServer *>(this)->_server.status());
#else
return static_cast<tcp_state>(_server.status());
#endif
}
#if ASYNC_TCP_SSL_ENABLED
void onSslFileRequest(AcSSlFileHandler cb, void *arg);
void beginSecure(const char *cert, const char *private_key_file, const char *password);

View file

@ -172,7 +172,11 @@ void AsyncLoggingMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNex
return;
}
_out->print(F("* Connection from "));
#ifndef LIBRETINY
_out->print(request->client()->remoteIP().toString());
#else
_out->print(request->client()->remoteIP());
#endif
_out->print(':');
_out->println(request->client()->remotePort());
_out->print('>');

View file

@ -19,7 +19,6 @@ class AsyncStaticWebHandler : public AsyncWebHandler {
private:
bool _getFile(AsyncWebServerRequest *request) const;
bool _searchFile(AsyncWebServerRequest *request, const String &path);
uint8_t _countBits(const uint8_t value) const;
protected:
FS _fs;

View file

@ -187,15 +187,6 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
return found;
}
uint8_t AsyncStaticWebHandler::_countBits(const uint8_t value) const {
uint8_t w = value;
uint8_t n;
for (n = 0; w != 0; n++) {
w &= w - 1;
}
return n;
}
void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
// Get the filename from request->_tempObject and free it
String filename((char *)request->_tempObject);
@ -218,11 +209,14 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
char buf[len];
char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10);
etag = ret ? String(ret) : String(request->_tempFile.size());
#elif defined(LIBRETINY)
long val = lw ^ request->_tempFile.size();
etag = String(val);
#else
etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp
#endif
} else {
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
etag = String(request->_tempFile.size());
#else
etag = request->_tempFile.size();

View file

@ -22,10 +22,10 @@ enum {
};
AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c)
: _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), _url(), _host(),
_contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false),
_expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(),
_itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) {
: _client(c), _server(s), _handler(NULL), _response(NULL), _onDisconnectfn(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY),
_url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false),
_isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0),
_itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) {
c->onError(
[](void *r, AsyncClient *c, int8_t error) {
(void)c;
@ -341,10 +341,10 @@ bool AsyncWebServerRequest::_parseReqHead() {
}
bool AsyncWebServerRequest::_parseReqHeader() {
int index = _temp.indexOf(':');
if (index) {
String name(_temp.substring(0, index));
String value(_temp.substring(index + 2));
AsyncWebHeader header = AsyncWebHeader::parse(_temp);
if (header) {
const String &name = header.name();
const String &value = header.value();
if (name.equalsIgnoreCase(T_Host)) {
_host = value;
} else if (name.equalsIgnoreCase(T_Content_Type)) {
@ -392,9 +392,9 @@ bool AsyncWebServerRequest::_parseReqHeader() {
_reqconntype = RCT_EVENT;
}
}
_headers.emplace_back(name, value);
_headers.emplace_back(std::move(header));
}
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
// Ancient PRI core does not have String::clear() method 8-()
_temp = emptyString;
#else
@ -419,7 +419,7 @@ void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) {
_params.emplace_back(name, urlDecode(value), true);
}
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
// Ancient PRI core does not have String::clear() method 8-()
_temp = emptyString;
#else

View file

@ -10,7 +10,7 @@
#undef max
#endif
#include "literals.h"
#include <StreamString.h>
#include <cbuf.h>
#include <memory>
#include <vector>
@ -157,7 +157,7 @@ public:
class AsyncResponseStream : public AsyncAbstractResponse, public Print {
private:
StreamString _content;
std::unique_ptr<cbuf> _content;
public:
AsyncResponseStream(const char *contentType, size_t bufferSize);
@ -168,6 +168,12 @@ public:
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data);
/**
* @brief Returns the number of bytes available in the stream.
*/
size_t available() const {
return _content->available();
}
using Print::write;
};

View file

@ -134,6 +134,30 @@ bool AsyncWebServerResponse::headerMustBePresentOnce(const String &name) {
return false;
}
bool AsyncWebServerResponse::addHeader(AsyncWebHeader &&header, bool replaceExisting) {
if (!header) {
return false; // invalid header
}
for (auto i = _headers.begin(); i != _headers.end(); ++i) {
if (i->name().equalsIgnoreCase(header.name())) {
// header already set
if (replaceExisting) {
// remove, break and add the new one
_headers.erase(i);
break;
} else if (headerMustBePresentOnce(i->name())) { // we can have only one header with that name
// do not update
return false;
} else {
break; // accept multiple headers with the same name
}
}
}
// header was not found found, or existing one was removed
_headers.emplace_back(std::move(header));
return true;
}
bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) {
for (auto i = _headers.begin(); i != _headers.end(); ++i) {
if (i->name().equalsIgnoreCase(name)) {
@ -595,6 +619,16 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size
* File Response
* */
/**
* @brief Sets the content type based on the file path extension
*
* This method determines the appropriate MIME content type for a file based on its
* file extension. It supports both external content type functions (if available)
* and an internal mapping of common file extensions to their corresponding MIME types.
*
* @param path The file path string from which to extract the extension
* @note The method modifies the internal _contentType member variable
*/
void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#if HAVE_EXTERN_GET_Content_Type_FUNCTION
#ifndef ESP8266
@ -604,41 +638,47 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#endif
_contentType = getContentType(path);
#else
if (path.endsWith(T__html)) {
const char *cpath = path.c_str();
const char *dot = strrchr(cpath, '.');
if (!dot) {
_contentType = T_text_plain;
return;
}
if (strcmp(dot, T__html) == 0 || strcmp(dot, T__htm) == 0) {
_contentType = T_text_html;
} else if (path.endsWith(T__htm)) {
_contentType = T_text_html;
} else if (path.endsWith(T__css)) {
} else if (strcmp(dot, T__css) == 0) {
_contentType = T_text_css;
} else if (path.endsWith(T__json)) {
_contentType = T_application_json;
} else if (path.endsWith(T__js)) {
} else if (strcmp(dot, T__js) == 0) {
_contentType = T_application_javascript;
} else if (path.endsWith(T__png)) {
} else if (strcmp(dot, T__json) == 0) {
_contentType = T_application_json;
} else if (strcmp(dot, T__png) == 0) {
_contentType = T_image_png;
} else if (path.endsWith(T__gif)) {
_contentType = T_image_gif;
} else if (path.endsWith(T__jpg)) {
_contentType = T_image_jpeg;
} else if (path.endsWith(T__ico)) {
} else if (strcmp(dot, T__ico) == 0) {
_contentType = T_image_x_icon;
} else if (path.endsWith(T__svg)) {
} else if (strcmp(dot, T__svg) == 0) {
_contentType = T_image_svg_xml;
} else if (path.endsWith(T__eot)) {
_contentType = T_font_eot;
} else if (path.endsWith(T__woff)) {
_contentType = T_font_woff;
} else if (path.endsWith(T__woff2)) {
} else if (strcmp(dot, T__jpg) == 0) {
_contentType = T_image_jpeg;
} else if (strcmp(dot, T__gif) == 0) {
_contentType = T_image_gif;
} else if (strcmp(dot, T__woff2) == 0) {
_contentType = T_font_woff2;
} else if (path.endsWith(T__ttf)) {
} else if (strcmp(dot, T__woff) == 0) {
_contentType = T_font_woff;
} else if (strcmp(dot, T__ttf) == 0) {
_contentType = T_font_ttf;
} else if (path.endsWith(T__xml)) {
} else if (strcmp(dot, T__eot) == 0) {
_contentType = T_font_eot;
} else if (strcmp(dot, T__xml) == 0) {
_contentType = T_text_xml;
} else if (path.endsWith(T__pdf)) {
} else if (strcmp(dot, T__pdf) == 0) {
_contentType = T_application_pdf;
} else if (path.endsWith(T__zip)) {
} else if (strcmp(dot, T__zip) == 0) {
_contentType = T_application_zip;
} else if (path.endsWith(T__gz)) {
} else if (strcmp(dot, T__gz) == 0) {
_contentType = T_application_x_gzip;
} else {
_contentType = T_text_plain;
@ -646,40 +686,73 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#endif
}
/**
* @brief Constructor for AsyncFileResponse that handles file serving with compression support
*
* This constructor creates an AsyncFileResponse object that can serve files from a filesystem,
* with automatic fallback to gzip-compressed versions if the original file is not found.
* It also handles ETag generation for caching and supports both inline and download modes.
*
* @param fs Reference to the filesystem object used to open files
* @param path Path to the file to be served (without compression extension)
* @param contentType MIME type of the file content (empty string for auto-detection)
* @param download If true, file will be served as download attachment; if false, as inline content
* @param callback Template processor callback for dynamic content processing
*/
AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) {
_code = 200;
_path = path;
// Try to open the uncompressed version first
_content = fs.open(path, fs::FileOpenMode::read);
if (_content.available()) {
_path = path;
_contentLength = _content.size();
} else {
// Try to open the compressed version (.gz)
_path = path + asyncsrv::T__gz;
_content = fs.open(_path, fs::FileOpenMode::read);
_contentLength = _content.size();
if (!download && !fs.exists(_path) && fs.exists(_path + T__gz)) {
_path = _path + T__gz;
addHeader(T_Content_Encoding, T_gzip, false);
_callback = nullptr; // Unable to process zipped templates
_sendContentLength = true;
_chunked = false;
if (_content.seek(_contentLength - 8)) {
addHeader(T_Content_Encoding, T_gzip, false);
_callback = nullptr; // Unable to process zipped templates
_sendContentLength = true;
_chunked = false;
// Add ETag and cache headers
uint8_t crcInTrailer[4];
_content.read(crcInTrailer, sizeof(crcInTrailer));
char serverETag[9];
AsyncWebServerRequest::_getEtag(crcInTrailer, serverETag);
addHeader(T_ETag, serverETag, true);
addHeader(T_Cache_Control, T_no_cache, true);
_content.seek(0);
} else {
// File is corrupted or invalid
_code = 404;
return;
}
}
_content = fs.open(_path, fs::FileOpenMode::read);
_contentLength = _content.size();
if (strlen(contentType) == 0) {
if (*contentType != '\0') {
_setContentTypeFromPath(path);
} else {
_contentType = contentType;
}
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26 + path.length() - filenameStart];
char *filename = (char *)path.c_str() + filenameStart;
if (download) {
// set filename and force download
// Extract filename from path and set as download attachment
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26 + path.length() - filenameStart];
char *filename = (char *)path.c_str() + filenameStart;
snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename);
addHeader(T_Content_Disposition, buf, false);
} else {
// set filename and force rendering
snprintf_P(buf, sizeof(buf), PSTR("inline"));
// Serve file inline (display in browser)
addHeader(T_Content_Disposition, PSTR("inline"), false);
}
addHeader(T_Content_Disposition, buf, false);
_code = 200;
}
AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
@ -820,7 +893,9 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
_code = 200;
_contentLength = 0;
_contentType = contentType;
if (!_content.reserve(bufferSize)) {
// internal buffer will be null on allocation failure
_content = std::unique_ptr<cbuf>(new cbuf(bufferSize));
if (bufferSize && _content->size() < bufferSize) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
@ -828,14 +903,26 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
}
size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) {
return _content.readBytes((char *)buf, maxLen);
return _content->read((char *)buf, maxLen);
}
size_t AsyncResponseStream::write(const uint8_t *data, size_t len) {
if (_started()) {
return 0;
}
size_t written = _content.write(data, len);
if (len > _content->room()) {
size_t needed = len - _content->room();
_content->resizeAdd(needed);
// log a warning if allocation failed, but do not return: keep writing the bytes we can
// with _content->write: if len is more than the available size in the buffer, only
// the available size will be written
if (len > _content->room()) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
}
}
size_t written = _content->write((const char *)data, len);
_contentLength += written;
return written;
}

View file

@ -4,10 +4,18 @@
#include "ESPAsyncWebServer.h"
#include "WebHandlerImpl.h"
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#else
#error Platform not supported
#endif
using namespace asyncsrv;
bool ON_STA_FILTER(AsyncWebServerRequest *request) {
#ifndef CONFIG_IDF_TARGET_ESP32H2
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
return WiFi.localIP() == request->client()->localIP();
#else
return false;
@ -15,7 +23,7 @@ bool ON_STA_FILTER(AsyncWebServerRequest *request) {
}
bool ON_AP_FILTER(AsyncWebServerRequest *request) {
#ifndef CONFIG_IDF_TARGET_ESP32H2
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
return WiFi.localIP() != request->client()->localIP();
#else
return false;

View file

@ -213,6 +213,12 @@
# pragma GCC system_header
# endif
#endif
#ifdef true
# undef true
#endif
#ifdef false
# undef false
#endif
#define ARDUINOJSON_CONCAT_(A, B) A##B
#define ARDUINOJSON_CONCAT2(A, B) ARDUINOJSON_CONCAT_(A, B)
#define ARDUINOJSON_CONCAT3(A, B, C) \
@ -239,11 +245,11 @@
#define ARDUINOJSON_BIN2ALPHA_1111() P
#define ARDUINOJSON_BIN2ALPHA_(A, B, C, D) ARDUINOJSON_BIN2ALPHA_##A##B##C##D()
#define ARDUINOJSON_BIN2ALPHA(A, B, C, D) ARDUINOJSON_BIN2ALPHA_(A, B, C, D)
#define ARDUINOJSON_VERSION "7.4.1"
#define ARDUINOJSON_VERSION "7.4.2"
#define ARDUINOJSON_VERSION_MAJOR 7
#define ARDUINOJSON_VERSION_MINOR 4
#define ARDUINOJSON_VERSION_REVISION 1
#define ARDUINOJSON_VERSION_MACRO V741
#define ARDUINOJSON_VERSION_REVISION 2
#define ARDUINOJSON_VERSION_MACRO V742
#ifndef ARDUINOJSON_VERSION_NAMESPACE
# define ARDUINOJSON_VERSION_NAMESPACE \
ARDUINOJSON_CONCAT5( \

View file

@ -300,7 +300,7 @@ class AsyncServer : public AsyncSocketBase
void setNoDelay(bool nodelay) { _noDelay = nodelay; }
bool getNoDelay() { return _noDelay; }
uint8_t status();
uint8_t status() const;
protected:
uint16_t _port;