Merge remote-tracking branch 'origin/main' into feature/automatic-precharge

This commit is contained in:
mvgalen 2025-01-14 08:40:24 +01:00
commit fbd9fe6900
147 changed files with 15677 additions and 12777 deletions

View file

@ -73,6 +73,8 @@ jobs:
- BYD_KOSTAL_RS485
- BYD_MODBUS
- FOXESS_CAN
- GROWATT_HV_CAN
- GROWATT_LV_CAN
- PYLON_CAN
- PYLON_LV_CAN
- SCHNEIDER_CAN

View file

@ -75,6 +75,7 @@ jobs:
- BYD_KOSTAL_RS485
- BYD_MODBUS
- FOXESS_CAN
- GROWATT_LV_CAN
- PYLON_CAN
- PYLON_LV_CAN
- SCHNEIDER_CAN

View file

@ -59,6 +59,8 @@ jobs:
- BYD_KOSTAL_RS485
- BYD_MODBUS
- FOXESS_CAN
- GROWATT_HV_CAN
- GROWATT_LV_CAN
- PYLON_CAN
- PYLON_LV_CAN
- SCHNEIDER_CAN
@ -68,6 +70,7 @@ jobs:
- SMA_TRIPOWER_CAN
- SOFAR_CAN
- SOLAX_CAN
- SUNGROW_CAN
- SERIAL_LINK_TRANSMITTER
- NISSANLEAF_CHARGER # Last element is not an inverter, but good to also test if the charger compiles
# These are the supported hardware platforms for which the code will be compiled.

View file

@ -85,8 +85,8 @@ This code uses the following excellent libraries:
- [eModbus/eModbus](https://github.com/eModbus/eModbus) MIT-License
- [knolleary/pubsubclient](https://github.com/knolleary/pubsubclient) MIT-License
- [mackelec/SerialDataLink](https://github.com/mackelec/SerialDataLink)
- [me-no-dev/AsyncTCP](https://github.com/me-no-dev/AsyncTCP) LGPL-3.0 license
- [me-no-dev/ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer)
- [mathieucarbou/AsyncTCPsock](https://github.com/mathieucarbou/AsyncTCPSock) LGPL-3.0 license
- [mathieucarbou/ESPAsyncWebServer](https://github.com/mathieucarbou/ESPAsyncWebServer) LGPL-3.0 license
- [miwagner/ESP32-Arduino-CAN](https://github.com/miwagner/ESP32-Arduino-CAN/) MIT-License
- [pierremolinaro/acan2515](https://github.com/pierremolinaro/acan2515) MIT-License
- [pierremolinaro/acan2517FD](https://github.com/pierremolinaro/acan2517FD) MIT-License

View file

@ -8,6 +8,7 @@
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "src/communication/can/comm_can.h"
#include "src/communication/contactorcontrol/comm_contactorcontrol.h"
#include "src/communication/equipmentstopbutton/comm_equipmentstopbutton.h"
@ -20,6 +21,7 @@
#include "src/devboard/utils/events.h"
#include "src/devboard/utils/led_handler.h"
#include "src/devboard/utils/logging.h"
#include "src/devboard/utils/timer.h"
#include "src/devboard/utils/value_mapping.h"
#include "src/include.h"
#include "src/lib/YiannisBourkelis-Uptime-Library/src/uptime.h"
@ -52,7 +54,7 @@
#endif // WIFI
// The current software version, shown on webserver
const char* version_number = "8.1.dev";
const char* version_number = "8.2.dev";
// Interval settings
uint16_t intervalUpdateValues = INTERVAL_1_S; // Interval at which to update inverter values / Modbus registers
@ -385,6 +387,12 @@ void update_calculated_values() {
datalayer.battery.status.active_power_W =
(datalayer.battery.status.current_dA * (datalayer.battery.status.voltage_dV / 100));
#ifdef DOUBLE_BATTERY
/* Calculate active power based on voltage and current for battery 2*/
datalayer.battery2.status.active_power_W =
(datalayer.battery2.status.current_dA * (datalayer.battery2.status.voltage_dV / 100));
#endif // DOUBLE_BATTERY
if (datalayer.battery.settings.soc_scaling_active) {
/** SOC Scaling
*
@ -434,10 +442,6 @@ void update_calculated_values() {
}
#ifdef DOUBLE_BATTERY
/* Calculate active power based on voltage and current*/
datalayer.battery2.status.active_power_W =
(datalayer.battery2.status.current_dA * (datalayer.battery2.status.voltage_dV / 100));
// Calculate the scaled remaining capacity in Wh
if (datalayer.battery2.info.total_capacity_Wh > 0 && datalayer.battery2.status.real_soc > 0) {
calc_max_capacity =

View file

@ -47,8 +47,10 @@ const char* mqtt_object_id_prefix =
"be_"; // Custom prefix for MQTT object ID. Previously, the prefix was automatically set to "esp32-XXXXXX_"
const char* mqtt_device_name =
"Battery Emulator"; // Custom device name in Home Assistant. Previously, the name was automatically set to "BatteryEmulator_esp32-XXXXXX"
#endif // MQTT_MANUAL_TOPIC_OBJECT_NAME
#endif // USE_MQTT
const char* ha_device_id =
"battery-emulator"; // Custom device ID in Home Assistant. Previously, the ID was always "battery-emulator"
#endif // MQTT_MANUAL_TOPIC_OBJECT_NAME
#endif // USE_MQTT
#ifdef EQUIPMENT_STOP_BUTTON
// Equipment stop button behavior. Use NC button for safety reasons.

View file

@ -30,6 +30,7 @@
//#define RENAULT_TWIZY_BATTERY
//#define RENAULT_ZOE_GEN1_BATTERY
//#define RENAULT_ZOE_GEN2_BATTERY
//#define SONO_BATTERY
//#define SANTA_FE_PHEV_BATTERY
//#define STELLANTIS_ECMP_BATTERY
//#define TESLA_MODEL_3Y_BATTERY
@ -44,6 +45,8 @@
//#define BYD_KOSTAL_RS485 //Enable this line to emulate a "BYD 11kWh HVM battery" over Kostal RS485
//#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU
//#define FOXESS_CAN //Enable this line to emulate a "HV2600/ECS4100 battery" over CAN bus
//#define GROWATT_HV_CAN //Enable this line to emulate a "Growatt High Voltage v1.10 battery" over CAN bus
//#define GROWATT_LV_CAN //Enable this line to emulate a "48V Growatt Low Voltage battery" over CAN bus
//#define PYLON_LV_CAN //Enable this line to emulate a "48V Pylontech battery" over CAN bus
//#define PYLON_CAN //Enable this line to emulate a "High Voltage Pylontech battery" over CAN bus
//#define SCHNEIDER_CAN //Enable this line to emulate a "Schneider Version 2: SE BMS" over CAN bus
@ -53,6 +56,7 @@
//#define SMA_TRIPOWER_CAN //Enable this line to emulate a "SMA Home Storage battery" over CAN bus
//#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus
//#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus
//#define SUNGROW_CAN //Enable this line to emulate a "Sungrow SBR064" over CAN bus
/* Select hardware used for Battery-Emulator */
//#define HW_LILYGO
@ -66,6 +70,8 @@
//#define CONTACTOR_CONTROL_DOUBLE_BATTERY //Enable this line to have the emulator hardware control secondary set of contactors for double battery setups (See wiki for pins)
//#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM for CONTACTOR_CONTROL, which lowers power consumption and heat generation. CONTACTOR_CONTROL must be enabled.
//#define NC_CONTACTORS //Enable this line to control normally closed contactors. CONTACTOR_CONTROL must be enabled for this option. Extremely rare setting!
//#define PERIODIC_BMS_RESET //Enable to have the emulator powercycle the connected battery every 24hours via GPIO. Useful for some batteries like Nissan LEAF
//#define REMOTE_BMS_RESET //Enable to allow the emulator to remotely trigger a powercycle of the battery via MQTT. Useful for some batteries like Nissan LEAF
/* Shunt/Contactor settings */
//#define BMW_SBOX // SBOX relay control & battery current/voltage measurement

View file

@ -34,6 +34,10 @@ void setup_can_shunt();
#include "CHADEMO-SHUNTS.h"
#endif
#ifdef SONO_BATTERY
#include "SONO-BATTERY.h"
#endif
#ifdef STELLANTIS_ECMP_BATTERY
#include "ECMP-BATTERY.h"
#endif

View file

@ -196,6 +196,7 @@ static uint8_t BMW_1D0_counter = 0;
static uint8_t BMW_13E_counter = 0;
static uint8_t BMW_380_counter = 0;
static uint32_t BMW_328_counter = 0;
static bool battery_awake = false;
static bool battery2_awake = false;
static bool battery_info_available = false;
@ -205,6 +206,7 @@ static bool CRCCheckPassedPreviously = false;
static bool skipCRCCheck_battery2 = false;
static bool CRCCheckPassedPreviously_battery2 = false;
static uint16_t cellvoltage_temp_mV = 0;
static uint32_t battery_serial_number = 0;
static uint32_t battery_available_power_shortterm_charge = 0;
static uint32_t battery_available_power_shortterm_discharge = 0;
@ -272,6 +274,7 @@ static uint8_t battery_status_diagnosis_powertrain_immediate_multiplexer = 0;
static uint8_t battery_ID2 = 0;
static uint8_t battery_soh = 99;
static uint16_t cellvoltage2_temp_mV = 0;
static uint32_t battery2_serial_number = 0;
static uint32_t battery2_available_power_shortterm_charge = 0;
static uint32_t battery2_available_power_shortterm_discharge = 0;
@ -664,8 +667,14 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
switch (cmdState) {
case CELL_VOLTAGE_MINMAX:
if (next_data >= 4) {
datalayer.battery.status.cell_min_voltage_mV = (message_data[0] << 8 | message_data[1]);
datalayer.battery.status.cell_max_voltage_mV = (message_data[2] << 8 | message_data[3]);
cellvoltage_temp_mV = (message_data[0] << 8 | message_data[1]);
if (cellvoltage_temp_mV < 4500) { // Prevents garbage data from being read on bootup
datalayer.battery.status.cell_min_voltage_mV = cellvoltage_temp_mV;
}
cellvoltage_temp_mV = (message_data[2] << 8 | message_data[3]);
if (cellvoltage_temp_mV < 4500) { // Prevents garbage data from being read on bootup
datalayer.battery.status.cell_max_voltage_mV = cellvoltage_temp_mV;
}
}
break;
case SOH:
@ -847,8 +856,14 @@ void handle_incoming_can_frame_battery2(CAN_frame rx_frame) {
switch (cmdState) {
case CELL_VOLTAGE_MINMAX:
if (next_data >= 4) {
datalayer.battery2.status.cell_min_voltage_mV = (message_data[0] << 8 | message_data[1]);
datalayer.battery2.status.cell_max_voltage_mV = (message_data[2] << 8 | message_data[3]);
cellvoltage2_temp_mV = (message_data[0] << 8 | message_data[1]);
if (cellvoltage2_temp_mV < 4500) { // Prevents garbage data from being read on bootup
datalayer.battery2.status.cell_min_voltage_mV = cellvoltage2_temp_mV;
}
cellvoltage2_temp_mV = (message_data[2] << 8 | message_data[3]);
if (cellvoltage_temp_mV < 4500) { // Prevents garbage data from being read on bootup
datalayer.battery2.status.cell_max_voltage_mV = cellvoltage2_temp_mV;
}
}
break;
case SOH:
@ -1055,6 +1070,9 @@ void transmit_can_battery() {
current_cell_polled++;
if (current_cell_polled > 96) {
datalayer.battery.info.number_of_cells = 97;
#ifdef DOUBLE_BATTERY
datalayer.battery2.info.number_of_cells = 97;
#endif
cmdState = CELL_VOLTAGE_CELLNO_LAST;
} else {
cmdState = CELL_VOLTAGE_CELLNO;

View file

@ -1,6 +1,7 @@
#include "../include.h"
#ifdef KIA_HYUNDAI_64_BATTERY
#include "../datalayer/datalayer.h"
#include "../datalayer/datalayer_extended.h"
#include "../devboard/utils/events.h"
#include "KIA-HYUNDAI-64-BATTERY.h"
@ -25,6 +26,7 @@ static int16_t batteryAmps = 0;
static int16_t temperatureMax = 0;
static int16_t temperatureMin = 0;
static int16_t poll_data_pid = 0;
static bool holdPidCounter = false;
static uint8_t CellVmaxNo = 0;
static uint8_t CellVminNo = 0;
static uint8_t batteryManagementMode = 0;
@ -140,8 +142,17 @@ void update_values_battery() { //This function maps all the values fetched via
set_event(EVENT_12V_LOW, leadAcidBatteryVoltage);
}
/* Safeties verified. Perform USB serial printout if configured to do so */
// Update webserver datalayer
datalayer_extended.KiaHyundai64.total_cell_count = datalayer.battery.info.number_of_cells;
datalayer_extended.KiaHyundai64.battery_12V = leadAcidBatteryVoltage;
datalayer_extended.KiaHyundai64.waterleakageSensor = waterleakageSensor;
datalayer_extended.KiaHyundai64.temperature_water_inlet = temperature_water_inlet;
datalayer_extended.KiaHyundai64.powerRelayTemperature = powerRelayTemperature * 2;
datalayer_extended.KiaHyundai64.batteryManagementMode = batteryManagementMode;
datalayer_extended.KiaHyundai64.BMS_ign = BMS_ign;
datalayer_extended.KiaHyundai64.batteryRelay = batteryRelay;
//Perform logging if configured to do so
#ifdef DEBUG_LOG
logging.println(); //sepatator
logging.println("Values from battery: ");
@ -264,27 +275,28 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
case 0x5D8:
startedUp = true;
//PID data is polled after last message sent from battery:
if (poll_data_pid >= 10) { //polling one of ten PIDs at 100ms, resolution = 1s
poll_data_pid = 0;
}
poll_data_pid++;
if (poll_data_pid == 1) {
transmit_can_frame(&KIA64_7E4_id1, can_config.battery);
} else if (poll_data_pid == 2) {
transmit_can_frame(&KIA64_7E4_id2, can_config.battery);
} else if (poll_data_pid == 3) {
transmit_can_frame(&KIA64_7E4_id3, can_config.battery);
} else if (poll_data_pid == 4) {
transmit_can_frame(&KIA64_7E4_id4, can_config.battery);
} else if (poll_data_pid == 5) {
transmit_can_frame(&KIA64_7E4_id5, can_config.battery);
} else if (poll_data_pid == 6) {
transmit_can_frame(&KIA64_7E4_id6, can_config.battery);
} else if (poll_data_pid == 7) {
} else if (poll_data_pid == 8) {
} else if (poll_data_pid == 9) {
} else if (poll_data_pid == 10) {
//PID data is polled after last message sent from battery every other time:
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, can_config.battery);
} else if (poll_data_pid == 2) {
transmit_can_frame(&KIA64_7E4_id2, can_config.battery);
} else if (poll_data_pid == 3) {
transmit_can_frame(&KIA64_7E4_id3, can_config.battery);
} else if (poll_data_pid == 4) {
transmit_can_frame(&KIA64_7E4_id4, can_config.battery);
} else if (poll_data_pid == 5) {
transmit_can_frame(&KIA64_7E4_id5, can_config.battery);
} else if (poll_data_pid == 6) {
transmit_can_frame(&KIA64_7E4_id6, can_config.battery);
}
}
break;
case 0x7EC: //Data From polled PID group, BigEndian
@ -410,7 +422,9 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
cellvoltages_mv[87] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[88] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[89] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[90] = (rx_frame.data.u8[7] * 20);
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) {
batterySOH = ((rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3]);
}
@ -428,18 +442,38 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
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) {
cellvoltages_mv[91] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[92] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[93] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[94] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[95] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 5) {
cellvoltages_mv[96] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[97] = (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
cellvoltages_mv[91] = (rx_frame.data.u8[1] * 20);
}
if (rx_frame.data.u8[2] > 4) { // Perform extra checks
cellvoltages_mv[92] = (rx_frame.data.u8[2] * 20);
}
if (rx_frame.data.u8[3] > 4) { // Perform extra checks
cellvoltages_mv[93] = (rx_frame.data.u8[3] * 20);
}
if (rx_frame.data.u8[4] > 4) { // Perform extra checks
cellvoltages_mv[94] = (rx_frame.data.u8[4] * 20);
}
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
cellvoltages_mv[96] = (rx_frame.data.u8[4] * 20);
}
if (rx_frame.data.u8[5] > 4) { // Perform extra checks
cellvoltages_mv[97] = (rx_frame.data.u8[5] * 20);
}
}
break;
case 0x26: //Sixth datarow in PID group
//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
cellvoltages_mv[i] = 0; // Some packs incorrectly report the last unpopulated cells as 20-60mV
}
}
//Map all cell voltages to the global array
memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages_mv, 98 * sizeof(uint16_t));
//Update number of cells

View file

@ -8,7 +8,6 @@
https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3/blob/master/vehicle/OVMS.V3/components/vehicle_renaultzoe/src/vehicle_renaultzoe.cpp
The Zoe BMS apparently does not send total pack voltage, so we use the polled 96x cellvoltages summed up as total voltage
Still TODO:
- Fix the missing cell96 issue (Only cells 1-95 is shown on cellmonitor page)
- Automatically detect if we are on 22 or 41kWh battery (Nice to have, requires log file from 22kWh battery)
/*
@ -399,7 +398,7 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
case 0x29:
if (requested_poll == GROUP1_CELLVOLTAGES_1_POLL) {
cellvoltages[30] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2];
cellvoltages[21] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
cellvoltages[31] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
cellvoltages[32] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6];
highbyte_cell_next_frame = rx_frame.data.u8[7];
}

View file

@ -0,0 +1,183 @@
#include "../include.h"
#ifdef SONO_BATTERY
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
#include "SONO-BATTERY.h"
/* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
static unsigned long previousMillis1000 = 0; // will store last time a 1000ms CAN Message was send
static uint8_t seconds = 0;
static uint8_t functionalsafetybitmask = 0;
static uint16_t batteryVoltage = 3700;
static uint16_t allowedDischargePower = 0;
static uint16_t allowedChargePower = 0;
static uint16_t CellVoltMax_mV = 0;
static uint16_t CellVoltMin_mV = 0;
static int16_t batteryAmps = 0;
static int16_t temperatureMin = 0;
static int16_t temperatureMax = 0;
static uint8_t batterySOH = 99;
static uint8_t realSOC = 99;
CAN_frame SONO_400 = {.FD = false, //Message of Vehicle Command, 100ms
.ext_ID = false,
.DLC = 8,
.ID = 0x400,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SONO_401 = {.FD = false, //Message of Vehicle Date, 1000ms
.ext_ID = false,
.DLC = 8,
.ID = 0x400,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
datalayer.battery.status.real_soc = (realSOC * 100); //increase SOC range from 0-100 -> 100.00
datalayer.battery.status.soh_pptt = (batterySOH * 100); //Increase decimals from 100% -> 100.00%
datalayer.battery.status.voltage_dV = batteryVoltage;
datalayer.battery.status.current_dA = batteryAmps;
datalayer.battery.info.total_capacity_Wh = 54000;
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.max_discharge_power_W = allowedDischargePower * 100;
datalayer.battery.status.max_charge_power_W = allowedChargePower * 100;
datalayer.battery.status.cell_max_voltage_mV = CellVoltMax_mV;
datalayer.battery.status.cell_min_voltage_mV = CellVoltMin_mV;
datalayer.battery.status.temperature_min_dC = temperatureMin;
datalayer.battery.status.temperature_max_dC = temperatureMax;
}
void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x100:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x101:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x102:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
functionalsafetybitmask = rx_frame.data.u8[0]; //If any bits are high here, battery has a HSD fault active.
break;
case 0x200:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x220:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
allowedChargePower = (rx_frame.data.u8[0] << 8) + rx_frame.data.u8[1];
allowedDischargePower = (rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3];
break;
case 0x221:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x300:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
batteryVoltage = (rx_frame.data.u8[4] << 8) + rx_frame.data.u8[5];
break;
case 0x301:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
CellVoltMax_mV = (rx_frame.data.u8[1] << 8) + rx_frame.data.u8[2];
CellVoltMin_mV = (rx_frame.data.u8[4] << 8) + rx_frame.data.u8[5];
break;
case 0x310:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x311:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
batteryAmps = ((rx_frame.data.u8[4] << 8) + rx_frame.data.u8[5]) - 1000;
break;
case 0x320:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
temperatureMax = ((rx_frame.data.u8[1] << 8) + rx_frame.data.u8[2]) - 400;
temperatureMin = ((rx_frame.data.u8[4] << 8) + rx_frame.data.u8[5]) - 400;
break;
case 0x321:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
batterySOH = rx_frame.data.u8[4];
break;
case 0x330:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x331:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x601:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
realSOC = rx_frame.data.u8[0];
break;
case 0x610:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x611:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x613:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x614:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x615:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
default:
break;
}
}
void transmit_can_battery() {
unsigned long currentMillis = millis();
// Send 100ms CAN Message
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
previousMillis100 = currentMillis;
//VCU Command message
SONO_400.data.u8[0] = 0x15; //Charging enabled bit01, dischargign enabled bit23, dc charging bit45
if (datalayer.battery.status.bms_status == FAULT) {
SONO_400.data.u8[0] = 0x14; //Charging DISABLED
}
transmit_can_frame(&SONO_400, can_config.battery);
}
// Send 1000ms CAN Message
if (currentMillis - previousMillis1000 >= INTERVAL_1_S) {
previousMillis1000 = currentMillis;
//Time and date
//Let's see if the battery is happy with just getting seconds incrementing
SONO_401.data.u8[0] = 2025; //Year
SONO_401.data.u8[1] = 1; //Month
SONO_401.data.u8[2] = 1; //Day
SONO_401.data.u8[3] = 12; //Hour
SONO_401.data.u8[4] = 15; //Minute
SONO_401.data.u8[5] = seconds; //Second
seconds = (seconds + 1) % 61;
transmit_can_frame(&SONO_401, can_config.battery);
}
}
void setup_battery(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Sono Motors Sion 64kWh LFP ", 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 96;
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.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
datalayer.battery.info.chemistry = battery_chemistry_enum::LFP;
}
#endif

View file

@ -0,0 +1,17 @@
#ifndef SONO_BATTERY_H
#define SONO_BATTERY_H
#include <Arduino.h>
#include "../include.h"
#define BATTERY_SELECTED
#define MAX_PACK_VOLTAGE_DV 5000 //5000 = 500.0V
#define MIN_PACK_VOLTAGE_DV 2500
#define MAX_CELL_DEVIATION_MV 250
#define MAX_CELL_VOLTAGE_MV 3800 //Battery is put into emergency stop if one cell goes over this value
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
uint8_t CalculateCRC8(CAN_frame rx_frame);
void setup_battery(void);
void transmit_can_frame(CAN_frame* tx_frame, int interface);
#endif

View file

@ -8,7 +8,8 @@
/* Do not change code below unless you are sure what you are doing */
/* Credits: Most of the code comes from Per Carlen's bms_comms_tesla_model3.py (https://gitlab.com/pelle8/batt2gen24/) */
static unsigned long previousMillis50 = 0; // will store last time a 50ms CAN Message was send
static unsigned long previousMillis50 = 0; // will store last time a 50ms CAN Message was send
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
//0x221 545 VCFRONT_LVPowerState: "GenMsgCycleTime" 50ms
CAN_frame TESLA_221_1 = {
.FD = false,
@ -22,7 +23,12 @@ CAN_frame TESLA_221_2 = {
.DLC = 8,
.ID = 0x221,
.data = {0x61, 0x15, 0x01, 0x00, 0x00, 0x00, 0x20, 0xBA}}; //Contactor Frame 221 - hv_up_for_drive
CAN_frame TESLA_602 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x602,
.data = {0x02, 0x27, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Diagnostic request
static uint8_t stateMachineClearIsolationFault = 0xFF;
static uint16_t sendContactorClosingMessagesStill = 300;
static uint16_t battery_cell_max_v = 3300;
static uint16_t battery_cell_min_v = 3300;
@ -817,6 +823,10 @@ static const char* hvilStatusState[] = {"NOT OK",
"UNKNOWN(14)",
"UNKNOWN(15)"};
void clearIsolationFault() {
//CAN UDS messages to clear a latched isolation fault
}
void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
//After values are mapped, we perform some safety checks, and do some serial printouts
@ -901,8 +911,24 @@ void update_values_battery() { //This function maps all the values fetched via
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
}
// During forced balancing request via webserver, we allow the battery to exceed normal safety parameters
if (datalayer.battery.settings.user_requests_balancing) {
datalayer.battery.status.real_soc = 9900; //Force battery to show up as 99% when balancing
datalayer.battery.info.max_design_voltage_dV = datalayer.battery.settings.balancing_max_pack_voltage_dV;
datalayer.battery.info.max_cell_voltage_mV = datalayer.battery.settings.balancing_max_cell_voltage_mV;
datalayer.battery.info.max_cell_voltage_deviation_mV =
datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV;
datalayer.battery.status.max_charge_power_W = datalayer.battery.settings.balancing_float_power_W;
}
#endif // TESLA_MODEL_3Y_BATTERY
// Check if user requests some action
if (datalayer.battery.settings.user_requests_isolation_clear) {
stateMachineClearIsolationFault = 0; //Start the statemachine
datalayer.battery.settings.user_requests_isolation_clear = false;
}
// Update webserver datalayer
//0x20A
datalayer_extended.tesla.status_contactor = battery_contactor;
@ -2667,7 +2693,7 @@ the first, for a few cycles, then stop all messages which causes the contactor
}
#endif //defined(TESLA_MODEL_SX_BATTERY) || defined(EXP_TESLA_BMS_DIGITAL_HVIL)
//Send 30ms message
//Send 50ms message
if (currentMillis - previousMillis50 >= INTERVAL_50_MS) {
// Check if sending of CAN messages has been delayed too much.
if ((currentMillis - previousMillis50 >= INTERVAL_50_MS_DELAYED) && (currentMillis > BOOTUP_TIME)) {
@ -2699,6 +2725,54 @@ the first, for a few cycles, then stop all messages which causes the contactor
}
}
}
//Send 100ms message
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
previousMillis100 = currentMillis;
if (stateMachineClearIsolationFault != 0xFF) {
//This implementation should be rewritten to actually replying to the UDS replied sent by the BMS
//While this may work, it is not the correct way to implement this clearing logic
switch (stateMachineClearIsolationFault) {
case 0:
TESLA_602.data = {0x02, 0x27, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can_frame(&TESLA_602, can_config.battery);
stateMachineClearIsolationFault = 1;
break;
case 1:
TESLA_602.data = {0x30, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can_frame(&TESLA_602, can_config.battery);
// BMS should reply 02 50 C0 FF FF FF FF FF
stateMachineClearIsolationFault = 2;
break;
case 2:
TESLA_602.data = {0x10, 0x12, 0x27, 0x06, 0x35, 0x34, 0x37, 0x36};
transmit_can_frame(&TESLA_602, can_config.battery);
// BMS should reply 7E FF FF FF FF FF FF
stateMachineClearIsolationFault = 3;
break;
case 3:
TESLA_602.data = {0x21, 0x31, 0x30, 0x33, 0x32, 0x3D, 0x3C, 0x3F};
transmit_can_frame(&TESLA_602, can_config.battery);
stateMachineClearIsolationFault = 4;
break;
case 4:
TESLA_602.data = {0x22, 0x3E, 0x39, 0x38, 0x3B, 0x3A, 0x00, 0x00};
transmit_can_frame(&TESLA_602, can_config.battery);
stateMachineClearIsolationFault = 5;
break;
case 5:
TESLA_602.data = {0x04, 0x31, 0x01, 0x04, 0x0A, 0x00, 0x00, 0x00};
transmit_can_frame(&TESLA_602, can_config.battery);
stateMachineClearIsolationFault = 0xFF;
break;
default:
//Something went wrong. Reset all and cancel
stateMachineClearIsolationFault = 0xFF;
break;
}
}
}
}
void print_int_with_units(char* header, int value, char* units) {

View file

@ -10,10 +10,11 @@
#define MAXDISCHARGEPOWERALLOWED 60000 // 60000W we use a define since the value supplied by Tesla is always 0
/* Do not change the defines below */
#define RAMPDOWN_SOC 900 // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
#define RAMPDOWNPOWERALLOWED 15000 // What power we ramp down from towards top balancing
#define FLOAT_MAX_POWER_W 200 // W, what power to allow for top balancing battery
#define FLOAT_START_MV 20 // mV, how many mV under overvoltage to start float charging
#define RAMPDOWN_SOC 900 // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
#define RAMPDOWNPOWERALLOWED \
15000 // What power we ramp down from towards top balancing (usually same as MAXCHARGEPOWERALLOWED)
#define FLOAT_MAX_POWER_W 200 // W, what power to allow for top balancing battery
#define FLOAT_START_MV 20 // mV, how many mV under overvoltage to start float charging
#define MAX_PACK_VOLTAGE_SX_NCMA 4600 // V+1, if pack voltage goes over this, charge stops
#define MIN_PACK_VOLTAGE_SX_NCMA 3100 // V+1, if pack voltage goes over this, charge stops
@ -22,10 +23,10 @@
#define MAX_PACK_VOLTAGE_3Y_LFP 3880 // V+1, if pack voltage goes over this, charge stops
#define MIN_PACK_VOLTAGE_3Y_LFP 2968 // V+1, if pack voltage goes below this, discharge stops
#define MAX_CELL_DEVIATION_NCA_NCM 500 //LED turns yellow on the board if mv delta exceeds this value
#define MAX_CELL_DEVIATION_LFP 200 //LED turns yellow on the board if mv delta exceeds this value
#define MAX_CELL_DEVIATION_LFP 400 //LED turns yellow on the board if mv delta exceeds this value
#define MAX_CELL_VOLTAGE_NCA_NCM 4250 //Battery is put into emergency stop if one cell goes over this value
#define MIN_CELL_VOLTAGE_NCA_NCM 2950 //Battery is put into emergency stop if one cell goes below this value
#define MAX_CELL_VOLTAGE_LFP 3550 //Battery is put into emergency stop if one cell goes over this value
#define MAX_CELL_VOLTAGE_LFP 3650 //Battery is put into emergency stop if one cell goes over this value
#define MIN_CELL_VOLTAGE_LFP 2800 //Battery is put into emergency stop if one cell goes below this value
//#define EXP_TESLA_BMS_DIGITAL_HVIL // Experimental parameter. Forces the transmission of additional CAN frames for experimental purposes, to test potential HVIL issues in 3/Y packs with newer firmware.

View file

@ -1,11 +1,13 @@
#include "../include.h"
#ifdef VOLVO_SPA_BATTERY
#include "../datalayer/datalayer.h"
#include "../datalayer/datalayer_extended.h" //For "More battery info" webpage
#include "../devboard/utils/events.h"
#include "VOLVO-SPA-BATTERY.h"
/* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
static unsigned long previousMillis1s = 0; // will store last time a 1s CAN Message was send
static unsigned long previousMillis60s = 0; // will store last time a 60s CAN Message was send
static float BATT_U = 0; //0x3A
@ -19,23 +21,42 @@ static float BATT_T_MIN = 0; //0x413
static float BATT_T_AVG = 0; //0x413
static uint16_t SOC_BMS = 0; //0X37D
static uint16_t SOC_CALC = 0;
static uint16_t CELL_U_MAX = 3700; //0x37D
static uint16_t CELL_U_MIN = 3700; //0x37D
static uint8_t CELL_ID_U_MAX = 0; //0x37D
static uint16_t HvBattPwrLimDchaSoft = 0; //0x369
static uint8_t batteryModuleNumber = 0x10; // First battery module
static uint16_t CELL_U_MAX = 3700; //0x37D
static uint16_t CELL_U_MIN = 3700; //0x37D
static uint8_t CELL_ID_U_MAX = 0; //0x37D
static uint16_t HvBattPwrLimDchaSoft = 0; //0x369
static uint16_t HvBattPwrLimDcha1 = 0; //0x175
static uint16_t HvBattPwrLimDchaSlowAgi = 0; //0x177
static uint16_t HvBattPwrLimChrgSlowAgi = 0; //0x177
static uint8_t batteryModuleNumber = 0x10; // First battery module
static uint8_t battery_request_idx = 0;
static uint8_t rxConsecutiveFrames = 0;
static uint16_t min_max_voltage[2]; //contains cell min[0] and max[1] values in mV
static uint8_t cellcounter = 0;
static uint32_t remaining_capacity = 0;
static uint16_t cell_voltages[108]; //array with all the cellvoltages
static bool startedUp = false;
static uint8_t DTC_reset_counter = 0;
CAN_frame VOLVO_536 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x536,
.data = {0x00, 0x40, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Network manage frame
//.data = {0x00, 0x40, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Network manage frame
.data = {0x00, 0x40, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}}; //Network manage frame
CAN_frame VOLVO_140_CLOSE = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x140,
.data = {0x00, 0x02, 0x00, 0xB7, 0xFF, 0x03, 0xFF, 0x82}}; //Close contactors message
CAN_frame VOLVO_140_OPEN = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x140,
.data = {0x00, 0x02, 0x00, 0x9E, 0xFF, 0x03, 0xFF, 0x82}}; //Open contactor message
CAN_frame VOLVO_372 = {
.FD = false,
.ext_ID = false,
@ -57,10 +78,62 @@ CAN_frame VOLVO_SOH_Req = {.FD = false,
.DLC = 8,
.ID = 0x735,
.data = {0x03, 0x22, 0x49, 0x6D, 0x00, 0x00, 0x00, 0x00}}; //Battery SOH request frame
CAN_frame VOLVO_BECMsupplyVoltage_Req = {
.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x735,
.data = {0x03, 0x22, 0xF4, 0x42, 0x00, 0x00, 0x00, 0x00}}; //BECM supply voltage request frame
CAN_frame VOLVO_DTC_Erase = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7FF,
.data = {0x04, 0x14, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00}}; //Global DTC erase
CAN_frame VOLVO_BECM_ECUreset = {
.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x735,
.data = {0x02, 0x11, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00}}; //BECM ECU reset command (reboot/powercycle BECM)
CAN_frame VOLVO_DTCreadout = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7FF,
.data = {0x02, 0x19, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Global DTC readout
void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for the inverter
uint8_t cnt = 0;
// Update webserver datalayer
datalayer_extended.VolvoPolestar.soc_bms = SOC_BMS;
datalayer_extended.VolvoPolestar.soc_calc = SOC_CALC;
datalayer_extended.VolvoPolestar.soc_rescaled = datalayer.battery.status.reported_soc;
datalayer_extended.VolvoPolestar.soh_bms = datalayer.battery.status.soh_pptt;
datalayer_extended.VolvoPolestar.BECMBatteryVoltage = BATT_U;
datalayer_extended.VolvoPolestar.BECMBatteryCurrent = BATT_I;
datalayer_extended.VolvoPolestar.BECMUDynMaxLim = MAX_U;
datalayer_extended.VolvoPolestar.BECMUDynMinLim = MIN_U;
datalayer_extended.VolvoPolestar.HvBattPwrLimDcha1 = HvBattPwrLimDcha1;
datalayer_extended.VolvoPolestar.HvBattPwrLimDchaSoft = HvBattPwrLimDchaSoft;
datalayer_extended.VolvoPolestar.HvBattPwrLimDchaSlowAgi = HvBattPwrLimDchaSlowAgi;
datalayer_extended.VolvoPolestar.HvBattPwrLimChrgSlowAgi = HvBattPwrLimChrgSlowAgi;
// Update requests from webserver datalayer
if (datalayer_extended.VolvoPolestar.UserRequestDTCreset) {
transmit_can_frame(&VOLVO_DTC_Erase, can_config.battery); //Send global DTC erase command
datalayer_extended.VolvoPolestar.UserRequestDTCreset = false;
}
if (datalayer_extended.VolvoPolestar.UserRequestBECMecuReset) {
transmit_can_frame(&VOLVO_BECM_ECUreset, can_config.battery); //Send BECM ecu reset command
datalayer_extended.VolvoPolestar.UserRequestBECMecuReset = false;
}
if (datalayer_extended.VolvoPolestar.UserRequestDTCreadout) {
transmit_can_frame(&VOLVO_DTCreadout, can_config.battery); //Send DTC readout command
datalayer_extended.VolvoPolestar.UserRequestDTCreadout = false;
}
remaining_capacity = (78200 - CHARGE_ENERGY);
//datalayer.battery.status.real_soc = SOC_BMS; // Use BMS reported SOC, havent figured out how to get the BMS to calibrate empty/full yet
@ -80,9 +153,8 @@ void update_values_battery() { //This function maps all the values fetched via
datalayer.battery.status.current_dA = BATT_I * 10;
datalayer.battery.status.remaining_capacity_Wh = remaining_capacity;
//datalayer.battery.status.max_discharge_power_W = HvBattPwrLimDchaSoft * 1000; // Use power limit reported from BMS, not trusted ATM
datalayer.battery.status.max_discharge_power_W = 30000;
datalayer.battery.status.max_charge_power_W = 30000;
datalayer.battery.status.max_discharge_power_W = HvBattPwrLimDchaSlowAgi * 1000; //kW to W
datalayer.battery.status.max_charge_power_W = HvBattPwrLimChrgSlowAgi * 1000; //kW to W
datalayer.battery.status.temperature_min_dC = BATT_T_MIN;
datalayer.battery.status.temperature_max_dC = BATT_T_MAX;
@ -175,6 +247,25 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
logging.println("BATT_U not valid");
#endif
}
if ((rx_frame.data.u8[0] & 0x40) == 0x40)
datalayer_extended.VolvoPolestar.HVSysRlySts = ((rx_frame.data.u8[0] & 0x30) >> 4);
else
datalayer_extended.VolvoPolestar.HVSysRlySts = 0xFF;
if ((rx_frame.data.u8[2] & 0x40) == 0x40)
datalayer_extended.VolvoPolestar.HVSysDCRlySts1 = ((rx_frame.data.u8[2] & 0x30) >> 4);
else
datalayer_extended.VolvoPolestar.HVSysDCRlySts1 = 0xFF;
if ((rx_frame.data.u8[2] & 0x80) == 0x80)
datalayer_extended.VolvoPolestar.HVSysDCRlySts2 = ((rx_frame.data.u8[4] & 0x30) >> 4);
else
datalayer_extended.VolvoPolestar.HVSysDCRlySts2 = 0xFF;
if ((rx_frame.data.u8[0] & 0x80) == 0x80)
datalayer_extended.VolvoPolestar.HVSysIsoRMonrSts = ((rx_frame.data.u8[4] & 0xC0) >> 6);
else
datalayer_extended.VolvoPolestar.HVSysIsoRMonrSts = 0xFF;
break;
case 0x1A1:
if ((rx_frame.data.u8[4] & 0x10) == 0x10)
@ -216,6 +307,25 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
#endif
}
break;
case 0x175:
if ((rx_frame.data.u8[4] & 0x80) == 0x80) {
HvBattPwrLimDcha1 = (((rx_frame.data.u8[2] & 0x07) * 256 + rx_frame.data.u8[3]) >> 2);
} else {
HvBattPwrLimDcha1 = 0;
}
break;
case 0x177:
if ((rx_frame.data.u8[4] & 0x08) == 0x08) {
HvBattPwrLimDchaSlowAgi = (((rx_frame.data.u8[4] & 0x07) * 256 + rx_frame.data.u8[5]) >> 2);
} else {
HvBattPwrLimDchaSlowAgi = 0;
}
if ((rx_frame.data.u8[2] & 0x08) == 0x08) {
HvBattPwrLimChrgSlowAgi = (((rx_frame.data.u8[2] & 0x07) * 256 + rx_frame.data.u8[3]) >> 2);
} else {
HvBattPwrLimChrgSlowAgi = 0;
}
break;
case 0x37D:
if ((rx_frame.data.u8[0] & 0x40) == 0x40) {
SOC_BMS = ((rx_frame.data.u8[6] & 0x03) * 256 + rx_frame.data.u8[7]);
@ -260,6 +370,11 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
(rx_frame.data.u8[3] == 0x6D)) // SOH response frame
{
datalayer.battery.status.soh_pptt = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
transmit_can_frame(&VOLVO_BECMsupplyVoltage_Req, can_config.battery); //Send BECM supply voltage req
} else if ((rx_frame.data.u8[0] == 0x05) && (rx_frame.data.u8[1] == 0x62) && (rx_frame.data.u8[2] == 0xF4) &&
(rx_frame.data.u8[3] == 0x42)) // BECM module voltage supply
{
datalayer_extended.VolvoPolestar.BECMsupplyVoltage = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
} else if ((rx_frame.data.u8[0] == 0x10) && (rx_frame.data.u8[1] == 0x0B) && (rx_frame.data.u8[2] == 0x62) &&
(rx_frame.data.u8[3] == 0x4B)) // First response frame of cell voltages
{
@ -267,6 +382,10 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8);
transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control
rxConsecutiveFrames = 1;
} else if ((rx_frame.data.u8[0] == 0x10) && (rx_frame.data.u8[2] == 0x59) &&
(rx_frame.data.u8[3] == 0x03)) // First response frame for DTC with more than one code
{
transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control
} else if ((rx_frame.data.u8[0] == 0x21) && (rxConsecutiveFrames == 1)) {
cell_voltages[battery_request_idx++] = cell_voltages[battery_request_idx] | rx_frame.data.u8[1];
cell_voltages[battery_request_idx++] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
@ -285,7 +404,6 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
if (min_max_voltage[1] < cell_voltages[cellcounter])
min_max_voltage[1] = cell_voltages[cellcounter];
}
transmit_can_frame(&VOLVO_SOH_Req, can_config.battery); //Send SOH read request
}
rxConsecutiveFrames = 0;
@ -319,10 +437,23 @@ void transmit_can_battery() {
transmit_can_frame(&VOLVO_536, can_config.battery); //Send 0x536 Network managing frame to keep BMS alive
transmit_can_frame(&VOLVO_372, can_config.battery); //Send 0x372 ECMAmbientTempCalculated
if (datalayer.battery.status.bms_status == ACTIVE) {
if ((datalayer.battery.status.bms_status == ACTIVE) && startedUp) {
datalayer.system.status.battery_allows_contactor_closing = true;
} else { //datalayer.battery.status.bms_status == FAULT or inverter requested opening contactors
transmit_can_frame(&VOLVO_140_CLOSE, can_config.battery); //Send 0x140 Close contactors message
} else { //datalayer.battery.status.bms_status == FAULT , OR inverter requested opening contactors, OR system not started yet
datalayer.system.status.battery_allows_contactor_closing = false;
transmit_can_frame(&VOLVO_140_OPEN, can_config.battery); //Send 0x140 Open contactors message
}
}
if (currentMillis - previousMillis1s >= INTERVAL_1_S) {
previousMillis1s = currentMillis;
if (!startedUp) {
transmit_can_frame(&VOLVO_DTC_Erase, can_config.battery); //Erase any DTCs preventing startup
DTC_reset_counter++;
if (DTC_reset_counter > 1) { // Performed twice before starting
startedUp = true;
}
}
}
if (currentMillis - previousMillis60s >= INTERVAL_60_S) {

View file

@ -6,7 +6,7 @@
#include "../../datalayer/datalayer.h"
#include "../../devboard/utils/events.h"
#include "../../devboard/utils/value_mapping.h"
#include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../lib/mathieucarbou-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
#ifdef CAN_ADDON
#include "../../lib/pierremolinaro-acan2515/ACAN2515.h"

View file

@ -39,6 +39,11 @@ unsigned long negativeStartTime = 0;
unsigned long prechargeCompletedTime = 0;
unsigned long timeSpentInFaultedMode = 0;
#endif
unsigned long currentTime = 0;
unsigned long lastPowerRemovalTime = 0;
const unsigned long powerRemovalInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const unsigned long powerRemovalDuration = 30000; // 30 seconds in milliseconds
bool isBMSResetActive = false;
void set(uint8_t pin, bool direction, uint32_t pwm_freq = 0xFFFF) {
#ifdef PWM_CONTACTOR_CONTROL
@ -82,18 +87,41 @@ void init_contactors() {
set(SECOND_NEGATIVE_CONTACTOR_PIN, OFF);
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
// Init BMS contactor
#ifdef HW_STARK // TODO: Rewrite this so LilyGo can also handle this BMS contactor
#if defined HW_STARK || defined HW_3LB // This hardware has dedicated pin, always enable on start
pinMode(BMS_POWER, OUTPUT); //LilyGo is omitted from this, only enabled if user selects PERIODIC_BMS_RESET
digitalWrite(BMS_POWER, HIGH);
#ifdef BMS_2_POWER //Hardware supports 2x BMS
pinMode(BMS_2_POWER, OUTPUT);
digitalWrite(BMS_2_POWER, HIGH);
#endif BMS_2_POWER
#endif // HW with dedicated BMS pins
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) // User has enabled BMS reset, turn on output on start
pinMode(BMS_POWER, OUTPUT);
digitalWrite(BMS_POWER, HIGH);
#endif // HW_STARK
#ifdef BMS_2_POWER //Hardware supports 2x BMS
pinMode(BMS_2_POWER, OUTPUT);
digitalWrite(BMS_2_POWER, HIGH);
#endif //BMS_2_POWER
#endif //PERIODIC_BMS_RESET
}
// Main functions
static void dbg_contactors(const char* state) {
#ifdef DEBUG_LOG
logging.print("[");
logging.print(millis());
logging.print(" ms] contactors control: ");
logging.println(state);
#endif
}
// Main functions of the handle_contactors include checking if inverter allows for closing, checking battery 2, checking BMS power output, and actual contactor closing/precharge via GPIO
void handle_contactors() {
#if defined(SMA_BYD_H_CAN) || defined(SMA_BYD_HVS_CAN) || defined(SMA_TRIPOWER_CAN)
datalayer.system.status.inverter_allows_contactor_closing = digitalRead(INVERTER_CONTACTOR_ENABLE_PIN);
#endif
handle_BMSpower(); // Some batteries need to be periodically power cycled
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
handle_contactors_battery2();
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
@ -142,7 +170,7 @@ void handle_contactors() {
return;
}
unsigned long currentTime = millis();
currentTime = millis();
if (currentTime < INTERVAL_10_S) {
// Skip running the state machine before system has started up.
@ -154,6 +182,7 @@ void handle_contactors() {
switch (contactorStatus) {
case START_PRECHARGE:
set(NEGATIVE_CONTACTOR_PIN, ON, PWM_ON_DUTY);
dbg_contactors("NEGATIVE");
prechargeStartTime = currentTime;
contactorStatus = PRECHARGE;
break;
@ -161,6 +190,7 @@ void handle_contactors() {
case PRECHARGE:
if (currentTime - prechargeStartTime >= NEGATIVE_CONTACTOR_TIME_MS) {
set(PRECHARGE_PIN, ON);
dbg_contactors("PRECHARGE");
negativeStartTime = currentTime;
contactorStatus = POSITIVE;
}
@ -169,6 +199,7 @@ void handle_contactors() {
case POSITIVE:
if (currentTime - negativeStartTime >= PRECHARGE_TIME_MS) {
set(POSITIVE_CONTACTOR_PIN, ON, PWM_ON_DUTY);
dbg_contactors("POSITIVE");
prechargeCompletedTime = currentTime;
contactorStatus = PRECHARGE_OFF;
}
@ -179,6 +210,7 @@ void handle_contactors() {
set(PRECHARGE_PIN, OFF);
set(NEGATIVE_CONTACTOR_PIN, ON, PWM_HOLD_DUTY);
set(POSITIVE_CONTACTOR_PIN, ON, PWM_HOLD_DUTY);
dbg_contactors("PRECHARGE_OFF");
contactorStatus = COMPLETED;
datalayer.system.status.contactors_engaged = true;
}
@ -202,3 +234,69 @@ void handle_contactors_battery2() {
}
}
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
/* PERIODIC_BMS_RESET - Once every 24 hours we remove power from the BMS_power pin for 30 seconds.
REMOTE_BMS_RESET - Allows the user to remotely powercycle the BMS by sending a command to the emulator via MQTT.
This makes the BMS recalculate all SOC% and avoid memory leaks
During that time we also set the emulator state to paused in order to not try and send CAN messages towards the battery
Feature is only used if user has enabled PERIODIC_BMS_RESET in the USER_SETTINGS */
void handle_BMSpower() {
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
// Get current time
currentTime = millis();
#ifdef PERIODIC_BMS_RESET
// Check if 24 hours have passed since the last power removal
if (currentTime - lastPowerRemovalTime >= powerRemovalInterval) {
start_bms_reset();
}
#endif //PERIODIC_BMS_RESET
// If power has been removed for 30 seconds, restore the power and resume the emulator
if (isBMSResetActive && currentTime - lastPowerRemovalTime >= powerRemovalDuration) {
// Reapply power to the BMS
digitalWrite(BMS_POWER, HIGH);
//Resume the battery pause and CAN communication
setBatteryPause(false, false, false, false);
isBMSResetActive = false; // Reset the power removal flag
}
#endif //defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
}
void start_bms_reset() {
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
if (!isBMSResetActive) {
lastPowerRemovalTime = currentTime; // Record the time when BMS reset was started
// Set emulator state to paused (Max Charge/Discharge = 0 & CAN = stop)
// TODO: We try to keep contactors engaged during this pause, and just ramp power down to 0.
// If this turns out to not work properly, set also the third option to true to open contactors
setBatteryPause(true, true, false, false);
digitalWrite(BMS_POWER, LOW); // Remove power by setting the BMS power pin to LOW
#ifdef BMS_2_POWER
digitalWrite(BMS_2_POWER, LOW); // Same for battery 2
#endif
isBMSResetActive = true; // Set a flag to indicate power removal is active
}
// If power has been removed for 30 seconds, restore the power and resume the emulator
if (isBMSResetActive && currentTime - lastPowerRemovalTime >= powerRemovalDuration) {
// Reapply power to the BMS
digitalWrite(BMS_POWER, HIGH);
#ifdef BMS_2_POWER
digitalWrite(BMS_2_POWER, HIGH); // Same for battery 2
#endif
//Resume the battery pause and CAN communication
setBatteryPause(false, false, false, false);
isBMSResetActive = false; // Reset the power removal flag
}
#endif //PERIODIC_BMS_RESET
}

View file

@ -6,6 +6,24 @@
#include "../../datalayer/datalayer.h"
#include "../../devboard/utils/events.h"
/**
* @brief Handle BMS power output
*
* @param[in] void
*
* @return void
*/
void handle_BMSpower();
/**
* @brief Start BMS reset sequence
*
* @param[in] void
*
* @return void
*/
void start_bms_reset();
/**
* @brief Contactor initialization
*

View file

@ -121,6 +121,22 @@ 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;
/** Tesla specific settings that are edited on the fly when manually forcing a balance charge for LFP chemistry */
/* Bool for specifying if user has requested manual function */
bool user_requests_balancing = false;
bool user_requests_isolation_clear = false;
/* Forced balancing max time & start timestamp */
uint32_t balancing_time_ms = 3600000; //1h default, (60min*60sec*1000ms)
uint32_t balancing_start_time_ms = 0; //For keeping track when balancing started
/* Max cell voltage during forced balancing */
uint16_t balancing_max_cell_voltage_mV = 3650;
/* Max cell deviation allowed during forced balancing */
uint16_t balancing_max_deviation_cell_voltage_mV = 400;
/* Float max power during forced balancing */
uint16_t balancing_float_power_W = 1000;
/* Maximum voltage for entire battery pack during forced balancing */
uint16_t balancing_max_pack_voltage_dV = 3940;
} DATALAYER_BATTERY_SETTINGS_TYPE;
typedef struct {

View file

@ -194,6 +194,17 @@ typedef struct {
bool warning_Charger_not_responding = false;
} DATALAYER_INFO_CELLPOWER;
typedef struct {
uint8_t total_cell_count = 0;
int16_t battery_12V = 0;
uint8_t waterleakageSensor = 0;
int8_t temperature_water_inlet = 0;
int8_t powerRelayTemperature = 0;
uint8_t batteryManagementMode = 0;
uint8_t BMS_ign = 0;
uint8_t batteryRelay = 0;
} DATALAYER_INFO_KIAHYUNDAI64;
typedef struct {
/** uint8_t */
/** Contactor status */
@ -523,6 +534,36 @@ typedef struct {
int32_t BMS_voltage_dV = 0;
} DATALAYER_INFO_MEB;
typedef struct {
uint16_t soc_bms = 0;
uint16_t soc_calc = 0;
uint16_t soc_rescaled = 0;
uint16_t soh_bms = 0;
uint16_t BECMsupplyVoltage = 0;
uint16_t BECMBatteryVoltage = 0;
uint16_t BECMBatteryCurrent = 0;
uint16_t BECMUDynMaxLim = 0;
uint16_t BECMUDynMinLim = 0;
uint16_t HvBattPwrLimDcha1 = 0;
uint16_t HvBattPwrLimDchaSoft = 0;
uint16_t HvBattPwrLimDchaSlowAgi = 0;
uint16_t HvBattPwrLimChrgSlowAgi = 0;
uint8_t HVSysRlySts = 0;
uint8_t HVSysDCRlySts1 = 0;
uint8_t HVSysDCRlySts2 = 0;
uint8_t HVSysIsoRMonrSts = 0;
/** User requesting DTC reset via WebUI*/
bool UserRequestDTCreset = false;
/** User requesting DTC readout via WebUI*/
bool UserRequestDTCreadout = false;
/** User requesting BECM reset via WebUI*/
bool UserRequestBECMecuReset = false;
} DATALAYER_INFO_VOLVO_POLESTAR;
typedef struct {
/** uint16_t */
/** Values WIP*/
@ -576,9 +617,11 @@ class DataLayerExtended {
DATALAYER_INFO_BMWI3 bmwi3;
DATALAYER_INFO_BYDATTO3 bydAtto3;
DATALAYER_INFO_CELLPOWER cellpower;
DATALAYER_INFO_KIAHYUNDAI64 KiaHyundai64;
DATALAYER_INFO_TESLA tesla;
DATALAYER_INFO_NISSAN_LEAF nissanleaf;
DATALAYER_INFO_MEB meb;
DATALAYER_INFO_VOLVO_POLESTAR VolvoPolestar;
DATALAYER_INFO_ZOE_PH2 zoePH2;
};

View file

@ -51,10 +51,12 @@
#define POSITIVE_CONTACTOR_PIN 32
#define NEGATIVE_CONTACTOR_PIN 33
#define PRECHARGE_PIN 25
#define BMS_POWER 2
#define SECOND_POSITIVE_CONTACTOR_PIN 13
#define SECOND_NEGATIVE_CONTACTOR_PIN 16
#define SECOND_PRECHARGE_PIN 18
#define BMS_2_POWER 12
// SMA CAN contactor pins
#define INVERTER_CONTACTOR_ENABLE_PIN 36

View file

@ -51,6 +51,7 @@
#define POSITIVE_CONTACTOR_PIN 32
#define NEGATIVE_CONTACTOR_PIN 33
#define PRECHARGE_PIN 25
#define BMS_POWER 18 // Note, this pin collides with CAN add-ons and Chademo
// SMA CAN contactor pins
#define INVERTER_CONTACTOR_ENABLE_PIN 5

View file

@ -5,6 +5,7 @@
#include "../../../USER_SECRETS.h"
#include "../../../USER_SETTINGS.h"
#include "../../battery/BATTERIES.h"
#include "../../communication/contactorcontrol/comm_contactorcontrol.h"
#include "../../datalayer/datalayer.h"
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
#include "../../lib/knolleary-pubsubclient/PubSubClient.h"
@ -20,6 +21,7 @@ MyTimer check_global_timer(800); // check timmer - low-priority MQTT checks,
static String topic_name = "";
static String object_id_prefix = "";
static String device_name = "";
static String device_id = "";
// Tracking reconnection attempts and failures
static unsigned long lastReconnectAttempt = 0;
@ -90,6 +92,8 @@ SensorConfig sensorConfigs[] = {
#endif // DOUBLE_BATTERY
};
SensorConfig buttonConfigs[] = {{"BMSRESET", "Reset BMS", "", "", ""}};
static String generateCommonInfoAutoConfigTopic(const char* object_id) {
return "homeassistant/sensor/" + topic_name + "/" + String(object_id) + "/config";
}
@ -102,6 +106,14 @@ static String generateEventsAutoConfigTopic(const char* object_id) {
return "homeassistant/sensor/" + topic_name + "/" + String(object_id) + "/config";
}
static String generateButtonTopic(const char* subtype) {
return "homeassistant/button/" + topic_name + "/" + String(subtype);
}
static String generateButtonAutoConfigTopic(const char* subtype) {
return generateButtonTopic(subtype) + "/config";
}
#endif // HA_AUTODISCOVERY
static std::vector<EventData> order_events;
@ -130,7 +142,7 @@ static void publish_common_info(void) {
}
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["identifiers"][0] = ha_device_id;
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
@ -235,7 +247,7 @@ static void publish_cell_voltages(void) {
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}";
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["identifiers"][0] = ha_device_id;
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
@ -264,7 +276,7 @@ static void publish_cell_voltages(void) {
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}";
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["identifiers"][0] = ha_device_id;
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
@ -343,7 +355,7 @@ void publish_events() {
doc["json_attributes_topic"] = state_topic;
doc["json_attributes_template"] = "{{ value_json | tojson }}";
doc["enabled_by_default"] = true;
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["identifiers"][0] = ha_device_id;
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
@ -400,6 +412,66 @@ void publish_events() {
#endif // HA_AUTODISCOVERY
}
static void publish_buttons_discovery(void) {
#ifdef HA_AUTODISCOVERY
static bool mqtt_first_transmission = true;
if (mqtt_first_transmission == true) {
mqtt_first_transmission = false;
#ifdef DEBUG_LOG
logging.println("Publishing buttons discovery");
#endif // DEBUG_LOG
static JsonDocument doc;
for (int i = 0; i < sizeof(buttonConfigs) / sizeof(buttonConfigs[0]); i++) {
SensorConfig& config = buttonConfigs[i];
doc["name"] = config.name;
doc["unique_id"] = config.object_id;
doc["command_topic"] = generateButtonTopic(config.object_id);
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["device"]["identifiers"][0] = ha_device_id;
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
doc["origin"]["name"] = "BatteryEmulator";
doc["origin"]["sw"] = String(version_number) + "-mqtt";
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";
serializeJson(doc, mqtt_msg);
mqtt_publish(generateButtonAutoConfigTopic(config.object_id).c_str(), mqtt_msg, true);
doc.clear();
}
}
#endif // HA_AUTODISCOVERY
}
static void subscribe() {
for (int i = 0; i < sizeof(buttonConfigs) / sizeof(buttonConfigs[0]); i++) {
SensorConfig& config = buttonConfigs[i];
const char* topic = generateButtonTopic(config.object_id).c_str();
#ifdef DEBUG_LOG
logging.printf("Subscribing to topic: [%s]\n", topic);
#endif // DEBUG_LOG
client.subscribe(topic);
}
}
void mqtt_message_received(char* topic, byte* payload, unsigned int length) {
#ifdef DEBUG_LOG
logging.printf("MQTT message arrived: [%s]\n", topic);
#endif // DEBUG_LOG
#ifdef REMOTE_BMS_RESET
const char* bmsreset_topic = generateButtonTopic("BMSRESET").c_str();
if (strcmp(topic, bmsreset_topic) == 0) {
#ifdef DEBUG_LOG
logging.println("Triggering BMS reset");
#endif // DEBUG_LOG
start_bms_reset();
}
#endif // REMOTE_BMS_RESET
}
/* If we lose the connection, get it back */
static bool reconnect() {
// attempt one reconnection
@ -414,6 +486,10 @@ static bool reconnect() {
clear_event(EVENT_MQTT_DISCONNECT);
set_event(EVENT_MQTT_CONNECT, 0);
reconnectAttempts = 0; // Reset attempts on successful connection
#ifdef HA_AUTODISCOVERY
publish_buttons_discovery();
#endif
subscribe();
#ifdef DEBUG_LOG
logging.println("connected");
#endif // DEBUG_LOG
@ -440,16 +516,18 @@ void init_mqtt(void) {
topic_name = mqtt_topic_name;
object_id_prefix = mqtt_object_id_prefix;
device_name = mqtt_device_name;
device_id = ha_device_id;
#else
// Use default naming based on WiFi hostname for topic, object ID prefix, and device name
topic_name = "battery-emulator_" + String(WiFi.getHostname());
object_id_prefix = String(WiFi.getHostname()) + String("_");
device_name = "BatteryEmulator_" + String(WiFi.getHostname());
device_id = "battery-emulator";
#endif
#endif
client.setServer(MQTT_SERVER, MQTT_PORT);
client.setCallback(mqtt_message_received);
#ifdef DEBUG_LOG
logging.println("MQTT initialized");
#endif // DEBUG_LOG

View file

@ -47,6 +47,7 @@ extern const char* mqtt_password;
extern const char* mqtt_topic_name;
extern const char* mqtt_object_id_prefix;
extern const char* mqtt_device_name;
extern const char* ha_device_id;
extern char mqtt_msg[MQTT_MSG_BUFFER_SIZE];

View file

@ -8,6 +8,7 @@ static bool battery_full_event_fired = false;
static bool battery_empty_event_fired = false;
#define MAX_SOH_DEVIATION_PPTT 2500
#define CELL_CRITICAL_MV 100 // If cells go this much outside design voltage, shut battery down!
//battery pause status begin
bool emulator_pause_request_ON = false;
@ -54,13 +55,24 @@ void update_machineryprotection() {
clear_event(EVENT_BATTERY_UNDERVOLTAGE);
}
// Cell overvoltage, critical latching error without automatic reset. Requires user action.
// Cell overvoltage, further charging not possible. Battery might be imbalanced.
if (datalayer.battery.status.cell_max_voltage_mV >= datalayer.battery.info.max_cell_voltage_mV) {
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
datalayer.battery.status.max_charge_power_W = 0;
}
// Cell undervoltage, critical latching error without automatic reset. Requires user action.
// Cell CRITICAL overvoltage, critical latching error without automatic reset. Requires user action to inspect battery.
if (datalayer.battery.status.cell_max_voltage_mV >= (datalayer.battery.info.max_cell_voltage_mV + CELL_CRITICAL_MV)) {
set_event(EVENT_CELL_CRITICAL_OVER_VOLTAGE, 0);
}
// Cell undervoltage. Further discharge not possible. Battery might be imbalanced.
if (datalayer.battery.status.cell_min_voltage_mV <= datalayer.battery.info.min_cell_voltage_mV) {
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
datalayer.battery.status.max_discharge_power_W = 0;
}
//Cell CRITICAL undervoltage. critical latching error without automatic reset. Requires user action to inspect battery.
if (datalayer.battery.status.cell_min_voltage_mV <= (datalayer.battery.info.min_cell_voltage_mV - CELL_CRITICAL_MV)) {
set_event(EVENT_CELL_CRITICAL_UNDER_VOLTAGE, 0);
}
// Battery is fully charged. Dont allow any more power into it
@ -240,6 +252,26 @@ void update_machineryprotection() {
if (datalayer.battery.status.max_charge_power_W == 0) {
datalayer.battery.status.max_charge_current_dA = 0;
}
//Decrement the forced balancing timer incase user requested it
if (datalayer.battery.settings.user_requests_balancing) {
// If this is the start of the balancing period, capture the current time
if (datalayer.battery.settings.balancing_start_time_ms == 0) {
datalayer.battery.settings.balancing_start_time_ms = millis();
set_event(EVENT_BALANCING_START, 0);
} else {
clear_event(EVENT_BALANCING_START);
}
// Check if the elapsed time exceeds the balancing time
if (millis() - datalayer.battery.settings.balancing_start_time_ms >= datalayer.battery.settings.balancing_time_ms) {
datalayer.battery.settings.user_requests_balancing = false;
datalayer.battery.settings.balancing_start_time_ms = 0; // Reset the start time
set_event(EVENT_BALANCING_END, 0);
} else {
clear_event(EVENT_BALANCING_END);
}
}
}
//battery pause status begin

View file

@ -26,7 +26,7 @@ void delete_can_log() {
void resume_can_writing() {
can_logging_paused = false;
can_log_file = SD.open(CAN_LOG_FILE, FILE_APPEND);
can_log_file = SD_MMC.open(CAN_LOG_FILE, FILE_APPEND);
can_file_open = true;
}
@ -40,13 +40,13 @@ void delete_log() {
log_file.close();
log_file_open = false;
}
SD.remove(LOG_FILE);
SD_MMC.remove(LOG_FILE);
logging_paused = false;
}
void resume_log_writing() {
logging_paused = false;
log_file = SD.open(LOG_FILE, FILE_APPEND);
log_file = SD_MMC.open(LOG_FILE, FILE_APPEND);
log_file_open = true;
}
@ -59,11 +59,33 @@ void add_can_frame_to_buffer(CAN_frame frame, frameDirection msgDir) {
if (!sd_card_active)
return;
CAN_log_frame log_frame = {frame, msgDir};
if (xRingbufferSend(can_bufferHandle, &log_frame, sizeof(log_frame), 0) != pdTRUE) {
Serial.println("Failed to send CAN frame to ring buffer!");
unsigned long currentTime = millis();
static char messagestr_buffer[32];
size_t size =
snprintf(messagestr_buffer + size, sizeof(messagestr_buffer) - size, "(%lu.%03lu) %s %X [%u] ",
currentTime / 1000, currentTime % 1000, (msgDir == MSG_RX ? "RX0" : "TX1"), frame.ID, frame.DLC);
if (xRingbufferSend(can_bufferHandle, &messagestr_buffer, size, pdMS_TO_TICKS(2)) != pdTRUE) {
#ifdef DEBUG_VIA_USB
Serial.println("Failed to send message to can ring buffer!");
#endif // DEBUG_VIA_USB
return;
}
uint8_t i = 0;
for (i = 0; i < frame.DLC; i++) {
if (i < frame.DLC - 1)
size = snprintf(messagestr_buffer, sizeof(messagestr_buffer), "%02X ", frame.data.u8[i]);
else
size = snprintf(messagestr_buffer, sizeof(messagestr_buffer), "%02X\n", frame.data.u8[i]);
if (xRingbufferSend(can_bufferHandle, &messagestr_buffer, size, pdMS_TO_TICKS(2)) != pdTRUE) {
#ifdef DEBUG_VIA_USB
Serial.println("Failed to send message to can ring buffer!");
#endif // DEBUG_VIA_USB
return;
}
}
}
void write_can_frame_to_sdcard() {
@ -72,10 +94,9 @@ void write_can_frame_to_sdcard() {
return;
size_t receivedMessageSize;
CAN_log_frame* log_frame =
(CAN_log_frame*)xRingbufferReceive(can_bufferHandle, &receivedMessageSize, pdMS_TO_TICKS(10));
uint8_t* buffer = (uint8_t*)xRingbufferReceive(can_bufferHandle, &receivedMessageSize, pdMS_TO_TICKS(10));
if (log_frame != NULL) {
if (buffer != NULL) {
if (can_logging_paused) {
if (can_file_open) {
@ -83,37 +104,23 @@ void write_can_frame_to_sdcard() {
can_file_open = false;
}
if (delete_can_file) {
SD.remove(CAN_LOG_FILE);
SD_MMC.remove(CAN_LOG_FILE);
delete_can_file = false;
can_logging_paused = false;
}
vRingbufferReturnItem(can_bufferHandle, (void*)log_frame);
vRingbufferReturnItem(can_bufferHandle, (void*)buffer);
return;
}
if (can_file_open == false) {
can_log_file = SD.open(CAN_LOG_FILE, FILE_APPEND);
can_log_file = SD_MMC.open(CAN_LOG_FILE, FILE_APPEND);
can_file_open = true;
}
uint8_t i = 0;
can_log_file.print("(");
can_log_file.print(millis() / 1000.0);
(log_frame->direction == MSG_RX) ? can_log_file.print(") RX0 ") : can_log_file.print(") TX1 ");
can_log_file.print(log_frame->frame.ID, HEX);
can_log_file.print(" [");
can_log_file.print(log_frame->frame.DLC);
can_log_file.print("] ");
for (i = 0; i < log_frame->frame.DLC; i++) {
can_log_file.print(log_frame->frame.data.u8[i] < 16 ? "0" : "");
can_log_file.print(log_frame->frame.data.u8[i], HEX);
if (i < log_frame->frame.DLC - 1)
can_log_file.print(" ");
}
can_log_file.println("");
can_log_file.write(buffer, receivedMessageSize);
can_log_file.flush();
vRingbufferReturnItem(can_bufferHandle, (void*)log_frame);
vRingbufferReturnItem(can_bufferHandle, (void*)buffer);
}
}
@ -122,8 +129,10 @@ void add_log_to_buffer(const uint8_t* buffer, size_t size) {
if (!sd_card_active)
return;
if (xRingbufferSend(log_bufferHandle, buffer, size, 0) != pdTRUE) {
Serial.println("Failed to send log to ring buffer!");
if (xRingbufferSend(log_bufferHandle, buffer, size, pdMS_TO_TICKS(1)) != pdTRUE) {
#ifdef DEBUG_VIA_USB
Serial.println("Failed to send message to log ring buffer!");
#endif // DEBUG_VIA_USB
return;
}
}
@ -144,7 +153,7 @@ void write_log_to_sdcard() {
}
if (log_file_open == false) {
log_file = SD.open(LOG_FILE, FILE_APPEND);
log_file = SD_MMC.open(LOG_FILE, FILE_APPEND);
log_file_open = true;
}
@ -158,7 +167,9 @@ void init_logging_buffers() {
#if defined(LOG_CAN_TO_SD)
can_bufferHandle = xRingbufferCreate(32 * 1024, RINGBUF_TYPE_BYTEBUF);
if (can_bufferHandle == NULL) {
Serial.println("Failed to create CAN ring buffer!");
#ifdef DEBUG_LOG
logging.println("Failed to create CAN ring buffer!");
#endif // DEBUG_LOG
return;
}
#endif // defined(LOG_CAN_TO_SD)
@ -166,7 +177,9 @@ void init_logging_buffers() {
#if defined(LOG_TO_SD)
log_bufferHandle = xRingbufferCreate(1024, RINGBUF_TYPE_BYTEBUF);
if (log_bufferHandle == NULL) {
Serial.println("Failed to create log ring buffer!");
#ifdef DEBUG_LOG
logging.println("Failed to create log ring buffer!");
#endif // DEBUG_LOG
return;
}
#endif // defined(LOG_TO_SD)
@ -174,57 +187,60 @@ void init_logging_buffers() {
void init_sdcard() {
pinMode(SD_CS_PIN, OUTPUT);
digitalWrite(SD_CS_PIN, HIGH);
pinMode(SD_SCLK_PIN, OUTPUT);
pinMode(SD_MOSI_PIN, OUTPUT);
pinMode(SD_MISO_PIN, INPUT);
pinMode(SD_MISO_PIN, INPUT_PULLUP);
SPI.begin(SD_SCLK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
if (!SD.begin(SD_CS_PIN)) {
Serial.println("SD Card initialization failed!");
SD_MMC.setPins(SD_SCLK_PIN, SD_MOSI_PIN, SD_MISO_PIN);
if (!SD_MMC.begin("/root", true, true, SDMMC_FREQ_HIGHSPEED)) {
#ifdef DEBUG_LOG
logging.println("SD Card initialization failed!");
#endif // DEBUG_LOG
return;
}
Serial.println("SD Card initialization successful.");
#ifdef DEBUG_LOG
logging.println("SD Card initialization successful.");
#endif // DEBUG_LOG
sd_card_active = true;
print_sdcard_details();
#ifdef DEBUG_LOG
log_sdcard_details();
#endif // DEBUG_LOG
}
void print_sdcard_details() {
void log_sdcard_details() {
Serial.print("SD Card Type: ");
switch (SD.cardType()) {
logging.print("SD Card Type: ");
switch (SD_MMC.cardType()) {
case CARD_MMC:
Serial.println("MMC");
logging.println("MMC");
break;
case CARD_SD:
Serial.println("SD");
logging.println("SD");
break;
case CARD_SDHC:
Serial.println("SDHC");
logging.println("SDHC");
break;
case CARD_UNKNOWN:
Serial.println("UNKNOWN");
logging.println("UNKNOWN");
break;
case CARD_NONE:
Serial.println("No SD Card found");
logging.println("No SD Card found");
break;
}
if (SD.cardType() != CARD_NONE) {
Serial.print("SD Card Size: ");
Serial.print(SD.cardSize() / 1024 / 1024);
Serial.println(" MB");
if (SD_MMC.cardType() != CARD_NONE) {
logging.print("SD Card Size: ");
logging.print(SD_MMC.cardSize() / 1024 / 1024);
logging.println(" MB");
Serial.print("Total space: ");
Serial.print(SD.totalBytes() / 1024 / 1024);
Serial.println(" MB");
logging.print("Total space: ");
logging.print(SD_MMC.totalBytes() / 1024 / 1024);
logging.println(" MB");
Serial.print("Used space: ");
Serial.print(SD.usedBytes() / 1024 / 1024);
Serial.println(" MB");
logging.print("Used space: ");
logging.print(SD_MMC.usedBytes() / 1024 / 1024);
logging.println(" MB");
}
}
#endif // defined(SD_CS_PIN) && defined(SD_SCLK_PIN) && defined(SD_MOSI_PIN) && defined(SD_MISO_PIN)

View file

@ -1,8 +1,7 @@
#ifndef SDCARD_H
#define SDCARD_H
#include <SD.h>
#include <SPI.h>
#include <SD_MMC.h>
#include "../../communication/can/comm_can.h"
#include "../hal/hal.h"
@ -14,7 +13,7 @@
void init_logging_buffers();
void init_sdcard();
void print_sdcard_details();
void log_sdcard_details();
void add_can_frame_to_buffer(CAN_frame frame, frameDirection msgDir);
void write_can_frame_to_sdcard();

View file

@ -5,14 +5,8 @@
#endif
#include "../../../USER_SETTINGS.h"
#include "../../lib/YiannisBourkelis-Uptime-Library/src/uptime.h"
#include "timer.h"
// Time conversion macros
#define DAYS_TO_SECS 86400 // 24 * 60 * 60
#define HOURS_TO_SECS 3600 // 60 * 60
#define MINUTES_TO_SECS 60
#define EE_NOF_EVENT_ENTRIES 30
#define EE_EVENT_ENTRY_SIZE sizeof(EVENT_LOG_ENTRY_TYPE)
#define EE_WRITE_PERIOD_MINUTES 10
@ -70,10 +64,8 @@ static uint32_t lastMillis = millis();
static void set_event(EVENTS_ENUM_TYPE event, uint8_t data, bool latched);
static void update_event_level(void);
static void update_bms_status(void);
static void log_event(EVENTS_ENUM_TYPE event, uint8_t millisrolloverCount, uint32_t timestamp, uint8_t data);
static void print_event_log(void);
static void check_ee_write(void);
uint8_t millisrolloverCount = 0;
@ -87,8 +79,6 @@ void run_event_handling(void) {
}
lastMillis = currentMillis;
run_sequence_on_target();
//check_ee_write();
update_event_level();
}
@ -159,6 +149,8 @@ void init_events(void) {
events.entries[EVENT_SOC_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_SOC_UNAVAILABLE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_KWH_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_INFO;
events.entries[EVENT_BALANCING_START].level = EVENT_LEVEL_INFO;
events.entries[EVENT_BALANCING_END].level = EVENT_LEVEL_INFO;
events.entries[EVENT_BATTERY_EMPTY].level = EVENT_LEVEL_INFO;
events.entries[EVENT_BATTERY_FULL].level = EVENT_LEVEL_INFO;
events.entries[EVENT_BATTERY_FROZEN].level = EVENT_LEVEL_INFO;
@ -181,8 +173,10 @@ void init_events(void) {
events.entries[EVENT_INTERFACE_MISSING].level = EVENT_LEVEL_INFO;
events.entries[EVENT_MODBUS_INVERTER_MISSING].level = EVENT_LEVEL_INFO;
events.entries[EVENT_ERROR_OPEN_CONTACTOR].level = EVENT_LEVEL_INFO;
events.entries[EVENT_CELL_UNDER_VOLTAGE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CELL_OVER_VOLTAGE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CELL_CRITICAL_UNDER_VOLTAGE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CELL_CRITICAL_OVER_VOLTAGE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CELL_UNDER_VOLTAGE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CELL_OVER_VOLTAGE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CELL_DEVIATION_HIGH].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_UNKNOWN_EVENT_SET].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_OTA_UPDATE].level = EVENT_LEVEL_UPDATE;
@ -288,84 +282,92 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
case EVENT_CANFD_RX_FAILURE:
return "No CANFD communication detected for 60s. Shutting down battery control.";
case EVENT_CAN_RX_WARNING:
return "ERROR: High amount of corrupted CAN messages detected. Check CAN wire shielding!";
return "High amount of corrupted CAN messages detected. Check CAN wire shielding!";
case EVENT_CAN_TX_FAILURE:
return "ERROR: CAN messages failed to transmit, or no one on the bus to ACK the message!";
return "CAN messages failed to transmit, or no one on the bus to ACK the message!";
case EVENT_CAN_INVERTER_MISSING:
return "Warning: Inverter not sending messages on CAN bus. Check wiring!";
return "Inverter not sending messages on CAN bus. Check wiring!";
case EVENT_CONTACTOR_WELDED:
return "Warning: Contactors sticking/welded. Inspect battery with caution!";
return "Contactors sticking/welded. Inspect battery with caution!";
case EVENT_CHARGE_LIMIT_EXCEEDED:
return "Info: Inverter is charging faster than battery is allowing.";
return "Inverter is charging faster than battery is allowing.";
case EVENT_DISCHARGE_LIMIT_EXCEEDED:
return "Info: Inverter is discharging faster than battery is allowing.";
return "Inverter is discharging faster than battery is allowing.";
case EVENT_WATER_INGRESS:
return "Water leakage inside battery detected. Operation halted. Inspect battery!";
case EVENT_12V_LOW:
return "12V battery source below required voltage to safely close contactors. Inspect the supply/battery!";
case EVENT_SOC_PLAUSIBILITY_ERROR:
return "Warning: SOC reported by battery not plausible. Restart battery!";
return "SOC reported by battery not plausible. Restart battery!";
case EVENT_SOC_UNAVAILABLE:
return "Warning: SOC not sent by BMS. Calibrate BMS via app.";
return "SOC not sent by BMS. Calibrate BMS via app.";
case EVENT_KWH_PLAUSIBILITY_ERROR:
return "Info: kWh remaining reported by battery not plausible. Battery needs cycling.";
return "kWh remaining reported by battery not plausible. Battery needs cycling.";
case EVENT_BALANCING_START:
return "Balancing has started";
case EVENT_BALANCING_END:
return "Balancing has ended";
case EVENT_BATTERY_EMPTY:
return "Info: Battery is completely discharged";
return "Battery is completely discharged";
case EVENT_BATTERY_FULL:
return "Info: Battery is fully charged";
return "Battery is fully charged";
case EVENT_BATTERY_FROZEN:
return "Info: Battery is too cold to operate optimally. Consider warming it up!";
return "Battery is too cold to operate optimally. Consider warming it up!";
case EVENT_BATTERY_CAUTION:
return "Info: Battery has raised a general caution flag. Might want to inspect it closely.";
return "Battery has raised a general caution flag. Might want to inspect it closely.";
case EVENT_BATTERY_CHG_STOP_REQ:
return "ERROR: Battery raised caution indicator AND requested charge stop. Inspect battery status!";
return "Battery raised caution indicator AND requested charge stop. Inspect battery status!";
case EVENT_BATTERY_DISCHG_STOP_REQ:
return "ERROR: Battery raised caution indicator AND requested discharge stop. Inspect battery status!";
return "Battery raised caution indicator AND requested discharge stop. Inspect battery status!";
case EVENT_BATTERY_CHG_DISCHG_STOP_REQ:
return "ERROR: Battery raised caution indicator AND requested charge/discharge stop. Inspect battery status!";
return "Battery raised caution indicator AND requested charge/discharge stop. Inspect battery status!";
case EVENT_BATTERY_REQUESTS_HEAT:
return "Info: COLD BATTERY! Battery requesting heating pads to activate!";
return "COLD BATTERY! Battery requesting heating pads to activate!";
case EVENT_BATTERY_WARMED_UP:
return "Info: Battery requesting heating pads to stop. The battery is now warm enough.";
return "Battery requesting heating pads to stop. The battery is now warm enough.";
case EVENT_BATTERY_OVERHEAT:
return "ERROR: Battery overheated. Shutting down to prevent thermal runaway!";
return "Battery overheated. Shutting down to prevent thermal runaway!";
case EVENT_BATTERY_OVERVOLTAGE:
return "Warning: Battery exceeding maximum design voltage. Discharge battery to prevent damage!";
return "Battery exceeding maximum design voltage. Discharge battery to prevent damage!";
case EVENT_BATTERY_UNDERVOLTAGE:
return "Warning: Battery under minimum design voltage. Charge battery to prevent damage!";
return "Battery under minimum design voltage. Charge battery to prevent damage!";
case EVENT_BATTERY_VALUE_UNAVAILABLE:
return "Warning: Battery measurement unavailable. Check 12V power supply and battery wiring!";
return "Battery measurement unavailable. Check 12V power supply and battery wiring!";
case EVENT_BATTERY_ISOLATION:
return "Warning: Battery reports isolation error. High voltage might be leaking to ground. Check battery!";
return "Battery reports isolation error. High voltage might be leaking to ground. Check battery!";
case EVENT_VOLTAGE_DIFFERENCE:
return "Info: Too large voltage diff between the batteries. Second battery cannot join the DC-link";
return "Too large voltage diff between the batteries. Second battery cannot join the DC-link";
case EVENT_SOH_DIFFERENCE:
return "Warning: Large deviation in State of health between packs. Inspect battery.";
return "Large deviation in State of health between packs. Inspect battery.";
case EVENT_SOH_LOW:
return "ERROR: State of health critically low. Battery internal resistance too high to continue. Recycle "
return "State of health critically low. Battery internal resistance too high to continue. Recycle "
"battery.";
case EVENT_HVIL_FAILURE:
return "ERROR: Battery interlock loop broken. Check that high voltage / low voltage connectors are seated. "
return "Battery interlock loop broken. Check that high voltage / low voltage connectors are seated. "
"Battery will be disabled!";
case EVENT_PRECHARGE_FAILURE:
return "Info: Battery failed to precharge. Check that capacitor is seated on high voltage output.";
return "Battery failed to precharge. Check that capacitor is seated on high voltage output.";
case EVENT_INTERNAL_OPEN_FAULT:
return "ERROR: High voltage cable removed while battery running. Opening contactors!";
return "High voltage cable removed while battery running. Opening contactors!";
case EVENT_INVERTER_OPEN_CONTACTOR:
return "Info: Inverter side opened contactors. Normal operation.";
return "Inverter side opened contactors. Normal operation.";
case EVENT_INTERFACE_MISSING:
return "Info: Configuration trying to use CAN interface not baked into the software. Recompile software!";
return "Configuration trying to use CAN interface not baked into the software. Recompile software!";
case EVENT_ERROR_OPEN_CONTACTOR:
return "Info: Too much time spent in error state. Opening contactors, not safe to continue charging. "
return "Too much time spent in error state. Opening contactors, not safe to continue charging. "
"Check other error code for reason!";
case EVENT_MODBUS_INVERTER_MISSING:
return "Info: Modbus inverter has not sent any data. Inspect communication wiring!";
return "Modbus inverter has not sent any data. Inspect communication wiring!";
case EVENT_CELL_CRITICAL_UNDER_VOLTAGE:
return "CELL VOLTAGE CRITICALLY LOW! Not possible to continue. Inspect battery!";
case EVENT_CELL_UNDER_VOLTAGE:
return "ERROR: CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!";
return "Cell undervoltage. Further discharge not possible. Check balancing of cells";
case EVENT_CELL_OVER_VOLTAGE:
return "ERROR: CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!";
return "Cell overvoltage. Further charging not possible. Check balancing of cells";
case EVENT_CELL_CRITICAL_OVER_VOLTAGE:
return "CELL VOLTAGE CRITICALLY HIGH! Not possible to continue. Inspect battery!";
case EVENT_CELL_DEVIATION_HIGH:
return "ERROR: HIGH CELL DEVIATION!!! Inspect battery!";
return "Large cell voltage deviation! Check balancing of cells";
case EVENT_UNKNOWN_EVENT_SET:
return "An unknown event was set! Review your code!";
case EVENT_DUMMY_INFO:
@ -377,7 +379,7 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
case EVENT_DUMMY_ERROR:
return "The dummy error event was set!"; // Don't change this event message!
case EVENT_PERSISTENT_SAVE_INFO:
return "Info: Failed to save user settings. Namespace full?";
return "Failed to save user settings. Namespace full?";
case EVENT_SERIAL_RX_WARNING:
return "Error in serial function: No data received for some time, see data for minutes";
case EVENT_SERIAL_RX_FAILURE:
@ -391,54 +393,54 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
case EVENT_OTA_UPDATE_TIMEOUT:
return "OTA update timed out!";
case EVENT_EEPROM_WRITE:
return "Info: The EEPROM was written";
return "The EEPROM was written";
case EVENT_RESET_UNKNOWN:
return "Info: The board was reset unexpectedly, and reason can't be determined";
return "The board was reset unexpectedly, and reason can't be determined";
case EVENT_RESET_POWERON:
return "Info: The board was reset from a power-on event. Normal operation";
return "The board was reset from a power-on event. Normal operation";
case EVENT_RESET_EXT:
return "Info: The board was reset from an external pin";
return "The board was reset from an external pin";
case EVENT_RESET_SW:
return "Info: The board was reset via software, webserver or OTA. Normal operation";
return "The board was reset via software, webserver or OTA. Normal operation";
case EVENT_RESET_PANIC:
return "Warning: The board was reset due to an exception or panic. Inform developers!";
return "The board was reset due to an exception or panic. Inform developers!";
case EVENT_RESET_INT_WDT:
return "Warning: The board was reset due to an interrupt watchdog timeout. Inform developers!";
return "The board was reset due to an interrupt watchdog timeout. Inform developers!";
case EVENT_RESET_TASK_WDT:
return "Warning: The board was reset due to a task watchdog timeout. Inform developers!";
return "The board was reset due to a task watchdog timeout. Inform developers!";
case EVENT_RESET_WDT:
return "Warning: The board was reset due to other watchdog timeout. Inform developers!";
return "The board was reset due to other watchdog timeout. Inform developers!";
case EVENT_RESET_DEEPSLEEP:
return "Info: The board was reset after exiting deep sleep mode";
return "The board was reset after exiting deep sleep mode";
case EVENT_RESET_BROWNOUT:
return "Info: The board was reset due to a momentary low voltage condition. This is expected during certain "
return "The board was reset due to a momentary low voltage condition. This is expected during certain "
"operations like flashing via USB";
case EVENT_RESET_SDIO:
return "Info: The board was reset over SDIO";
return "The board was reset over SDIO";
case EVENT_RESET_USB:
return "Info: The board was reset by the USB peripheral";
return "The board was reset by the USB peripheral";
case EVENT_RESET_JTAG:
return "Info: The board was reset by JTAG";
return "The board was reset by JTAG";
case EVENT_RESET_EFUSE:
return "Info: The board was reset due to an efuse error";
return "The board was reset due to an efuse error";
case EVENT_RESET_PWR_GLITCH:
return "Info: The board was reset due to a detected power glitch";
return "The board was reset due to a detected power glitch";
case EVENT_RESET_CPU_LOCKUP:
return "Warning: The board was reset due to CPU lockup. Inform developers!";
return "The board was reset due to CPU lockup. Inform developers!";
case EVENT_PAUSE_BEGIN:
return "Warning: The emulator is trying to pause the battery.";
return "The emulator is trying to pause the battery.";
case EVENT_PAUSE_END:
return "Info: The emulator is attempting to resume battery operation from pause.";
return "The emulator is attempting to resume battery operation from pause.";
case EVENT_WIFI_CONNECT:
return "Info: Wifi connected.";
return "Wifi connected.";
case EVENT_WIFI_DISCONNECT:
return "Info: Wifi disconnected.";
return "Wifi disconnected.";
case EVENT_MQTT_CONNECT:
return "Info: MQTT connected.";
return "MQTT connected.";
case EVENT_MQTT_DISCONNECT:
return "Info: MQTT disconnected.";
return "MQTT disconnected.";
case EVENT_EQUIPMENT_STOP:
return "ERROR: EQUIPMENT STOP ACTIVATED!!!";
return "EQUIPMENT STOP ACTIVATED!!!";
default:
return "";
}
@ -609,19 +611,3 @@ static void print_event_log(void) {
}
}
}
static void check_ee_write(void) {
// Only actually write to flash emulated EEPROM every EE_WRITE_PERIOD_MINUTES minutes,
// and only if we've logged any events
if (events.ee_timer.elapsed() && (events.nof_logged_events > 0)) {
EEPROM.commit();
events.nof_eeprom_writes += (events.nof_eeprom_writes < 65535) ? 1 : 0;
events.nof_logged_events = 0;
// We want to know how many writes we have, and to increment the occurrence counter
// we need to clear it first. It's just the way life is. Using events is a smooth
// way to visualize it in the web UI
clear_event(EVENT_EEPROM_WRITE);
set_event(EVENT_EEPROM_WRITE, events.nof_eeprom_writes);
}
}

View file

@ -6,7 +6,7 @@
// #define INCLUDE_EVENTS_TEST // Enable to run an event test loop, see events_test_on_target.cpp
#define EE_MAGIC_HEADER_VALUE 0x0018 // 0x0000 to 0xFFFF
#define EE_MAGIC_HEADER_VALUE 0x0020 // 0x0000 to 0xFFFF
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
@ -45,6 +45,8 @@
XX(EVENT_SOC_PLAUSIBILITY_ERROR) \
XX(EVENT_SOC_UNAVAILABLE) \
XX(EVENT_KWH_PLAUSIBILITY_ERROR) \
XX(EVENT_BALANCING_START) \
XX(EVENT_BALANCING_END) \
XX(EVENT_BATTERY_EMPTY) \
XX(EVENT_BATTERY_FULL) \
XX(EVENT_BATTERY_FROZEN) \
@ -69,6 +71,8 @@
XX(EVENT_INTERFACE_MISSING) \
XX(EVENT_MODBUS_INVERTER_MISSING) \
XX(EVENT_ERROR_OPEN_CONTACTOR) \
XX(EVENT_CELL_CRITICAL_UNDER_VOLTAGE) \
XX(EVENT_CELL_CRITICAL_OVER_VOLTAGE) \
XX(EVENT_CELL_UNDER_VOLTAGE) \
XX(EVENT_CELL_OVER_VOLTAGE) \
XX(EVENT_CELL_DEVIATION_HIGH) \

View file

@ -1,142 +0,0 @@
#include "events.h"
#include "timer.h"
typedef enum {
ETOT_INIT,
ETOT_FIRST_WAIT,
ETOT_INFO,
ETOT_INFO_CLEAR,
ETOT_DEBUG,
ETOT_DEBUG_CLEAR,
ETOT_WARNING,
ETOT_WARNING_CLEAR,
ETOT_ERROR,
ETOT_ERROR_CLEAR,
ETOT_ERROR_LATCHED,
ETOT_DONE
} ETOT_TYPE;
MyTimer timer(5000);
ETOT_TYPE events_test_state = ETOT_INIT;
void run_sequence_on_target(void) {
#ifdef INCLUDE_EVENTS_TEST
switch (events_test_state) {
case ETOT_INIT:
timer.set_interval(10000);
events_test_state = ETOT_FIRST_WAIT;
logging.println("Events test: initialized");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
break;
case ETOT_FIRST_WAIT:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_INFO;
set_event(EVENT_DUMMY_INFO, 123);
set_event(EVENT_DUMMY_INFO, 234); // 234 should show, occurrence 1
logging.println("Events test: info event set, data: 234");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_INFO:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_INFO);
events_test_state = ETOT_INFO_CLEAR;
logging.println("Events test : info event cleared");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_INFO_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_DEBUG;
set_event(EVENT_DUMMY_DEBUG, 111);
set_event(EVENT_DUMMY_DEBUG, 222); // 222 should show, occurrence 1
logging.println("Events test : debug event set, data: 222");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_DEBUG:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_DEBUG);
events_test_state = ETOT_DEBUG_CLEAR;
logging.println("Events test : info event cleared");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_DEBUG_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_WARNING;
set_event(EVENT_DUMMY_WARNING, 234);
set_event(EVENT_DUMMY_WARNING, 121); // 121 should show, occurrence 1
logging.println("Events test : warning event set, data: 121");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_WARNING:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_WARNING);
events_test_state = ETOT_WARNING_CLEAR;
logging.println("Events test : warning event cleared");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_WARNING_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_ERROR;
set_event(EVENT_DUMMY_ERROR, 221);
set_event(EVENT_DUMMY_ERROR, 133); // 133 should show, occurrence 1
logging.println("Events test : error event set, data: 133");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_ERROR:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_ERROR);
events_test_state = ETOT_ERROR_CLEAR;
logging.println("Events test : error event cleared");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_ERROR_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_ERROR_LATCHED;
set_event_latched(EVENT_DUMMY_ERROR, 221);
set_event_latched(EVENT_DUMMY_ERROR, 133); // 133 should show, occurrence 1
logging.println("Events test : latched error event set, data: 133");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_ERROR_LATCHED:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_ERROR);
events_test_state = ETOT_DONE;
logging.println("Events test : latched error event cleared?");
logging.print("datalayer.battery.status.bms_status: ");
logging.println(datalayer.battery.status.bms_status);
}
break;
case ETOT_DONE:
default:
break;
}
#endif
}

View file

@ -2,7 +2,6 @@
#include "../../datalayer/datalayer.h"
#include "../../include.h"
#include "events.h"
#include "timer.h"
#include "value_mapping.h"
#define COLOR_GREEN(x) (((uint32_t)0 << 16) | ((uint32_t)x << 8) | 0)
@ -30,58 +29,43 @@ led_color led_get_color() {
}
void LED::exe(void) {
// Don't run too often
if (!timer.elapsed()) {
return;
}
switch (state) {
// Update brightness
switch (mode) {
case led_mode::FLOW:
flow_run();
break;
case led_mode::HEARTBEAT:
heartbeat_run();
break;
case led_mode::CLASSIC:
default:
case LED_NORMAL:
// Update brightness
switch (mode) {
case led_mode::FLOW:
flow_run();
break;
case led_mode::HEARTBEAT:
heartbeat_run();
break;
case led_mode::CLASSIC:
default:
classic_run();
break;
}
// Set color
switch (get_event_level()) {
case EVENT_LEVEL_INFO:
color = led_color::GREEN;
pixels.setPixelColor(0, COLOR_GREEN(brightness)); // Green pulsing LED
break;
case EVENT_LEVEL_WARNING:
color = led_color::YELLOW;
pixels.setPixelColor(0, COLOR_YELLOW(brightness)); // Yellow pulsing LED
break;
case EVENT_LEVEL_DEBUG:
case EVENT_LEVEL_UPDATE:
color = led_color::BLUE;
pixels.setPixelColor(0, COLOR_BLUE(brightness)); // Blue pulsing LED
break;
case EVENT_LEVEL_ERROR:
color = led_color::RED;
pixels.setPixelColor(0, COLOR_RED(LED_MAX_BRIGHTNESS)); // Red LED full brightness
break;
default:
break;
}
break;
case LED_COMMAND:
break;
case LED_RGB:
rainbow_run();
classic_run();
break;
}
// Set color
switch (get_event_level()) {
case EVENT_LEVEL_INFO:
color = led_color::GREEN;
pixels.setPixelColor(0, COLOR_GREEN(brightness)); // Green pulsing LED
break;
case EVENT_LEVEL_WARNING:
color = led_color::YELLOW;
pixels.setPixelColor(0, COLOR_YELLOW(brightness)); // Yellow pulsing LED
break;
case EVENT_LEVEL_DEBUG:
case EVENT_LEVEL_UPDATE:
color = led_color::BLUE;
pixels.setPixelColor(0, COLOR_BLUE(brightness)); // Blue pulsing LED
break;
case EVENT_LEVEL_ERROR:
color = led_color::RED;
pixels.setPixelColor(0, COLOR_RED(LED_MAX_BRIGHTNESS)); // Red LED full brightness
break;
default:
break;
}
pixels.show(); // This sends the updated pixel color to the hardware.
}
@ -92,11 +76,10 @@ void LED::classic_run(void) {
void LED::flow_run(void) {
// Determine how bright the LED should be
int16_t power_W = datalayer.battery.status.active_power_W;
if (power_W < -50) {
if (datalayer.battery.status.active_power_W < -50) {
// Discharging
brightness = max_brightness - up_down(0.95);
} else if (power_W > 50) {
} else if (datalayer.battery.status.active_power_W > 50) {
// Charging
brightness = up_down(0.95);
} else {
@ -146,39 +129,6 @@ void LED::heartbeat_run(void) {
brightness = (uint8_t)(brightness_f * LED_MAX_BRIGHTNESS);
}
void LED::rainbow_run(void) {
brightness = LED_MAX_BRIGHTNESS / 2;
uint16_t ms = (uint16_t)(millis() % LED_PERIOD_MS);
float value = ((float)ms) / LED_PERIOD_MS;
// Clamp the input value to the range [0.0, 1.0]
value = value < 0.0f ? 0.0f : value;
value = value > 1.0f ? 1.0f : value;
uint8_t r = 0, g = 0, b = 0;
// Scale the value to the range [0, 3), which will be used to transition through the colors
float scaledValue = value * 3.0f;
if (scaledValue < 1.0f) {
// From red to green
r = static_cast<uint8_t>((1.0f - scaledValue) * brightness);
g = static_cast<uint8_t>((scaledValue - 0.0f) * brightness);
} else if (scaledValue < 2.0f) {
// From green to blue
g = static_cast<uint8_t>((2.0f - scaledValue) * brightness);
b = static_cast<uint8_t>((scaledValue - 1.0f) * brightness);
} else {
// From blue back to red
b = static_cast<uint8_t>((3.0f - scaledValue) * brightness);
r = static_cast<uint8_t>((scaledValue - 2.0f) * brightness);
}
// Assemble the color
pixels.setPixelColor(0, pixels.Color(r, g, b)); // RGB
}
uint8_t LED::up_down(float middle_point_f) {
// Determine how bright the LED should be
middle_point_f = CONSTRAIN(middle_point_f, 0.0f, 1.0f);

View file

@ -3,10 +3,8 @@
#include "../../include.h"
#include "../../lib/adafruit-Adafruit_NeoPixel/Adafruit_NeoPixel.h"
#include "timer.h"
enum led_mode { CLASSIC, FLOW, HEARTBEAT };
enum led_state { LED_NORMAL, LED_COMMAND, LED_RGB };
class LED {
public:
@ -16,17 +14,13 @@ class LED {
: pixels(1, LED_PIN, NEO_GRB + NEO_KHZ800),
max_brightness(LED_MAX_BRIGHTNESS),
brightness(LED_MAX_BRIGHTNESS),
mode(led_mode::CLASSIC),
state(LED_NORMAL),
timer(LED_EXECUTION_FREQUENCY) {}
mode(led_mode::CLASSIC) {}
LED(led_mode mode)
: pixels(1, LED_PIN, NEO_GRB + NEO_KHZ800),
max_brightness(LED_MAX_BRIGHTNESS),
brightness(LED_MAX_BRIGHTNESS),
mode(mode),
state(LED_NORMAL),
timer(LED_EXECUTION_FREQUENCY) {}
mode(mode) {}
void exe(void);
void init(void) { pixels.begin(); }
@ -36,12 +30,9 @@ class LED {
uint8_t max_brightness;
uint8_t brightness;
led_mode mode;
led_state state = LED_NORMAL;
MyTimer timer;
void classic_run(void);
void flow_run(void);
void rainbow_run(void);
void heartbeat_run(void);
uint8_t up_down(float middle_point_f);

View file

@ -5,7 +5,7 @@
enum bms_status_enum { STANDBY = 0, INACTIVE = 1, DARKSTART = 2, ACTIVE = 3, FAULT = 4, UPDATING = 5 };
enum battery_chemistry_enum { NCA, NMC, LFP };
enum led_color { GREEN, YELLOW, RED, BLUE, RGB };
enum led_color { GREEN, YELLOW, RED, BLUE };
#define DISCHARGING 1
#define CHARGING 2

View file

@ -1,36 +0,0 @@
# Battery Emulator Webserver
This webserver creates a WiFi access point. It also connects ot an existing network.
The webserver intends to display useful information to the user of the battery emulator
development board, without the need to physically connect to the board via USB.
The webserver implementation also provides the option to update the firmware of the board over the air.
To use the webserver, follow the following steps:
- Connect to the board via Serial, and boot up the board.
- The IP address of the WiFi access point is printed to Serial when the board boots up. Note this down.
- Connect to the access point created by board via a WiFi-capable device
- On that device, open a webbrowser and type the IP address of the WiFi access point.
- If the ssid and password of an existing WiFi network are provided, the board will also connect to this network. The IP address obtained on the existing network is shown in the webserver. Note this down.
- From this point onwards, any device connected to the existing WiFi network can access the webserver via a webbrowser. To do this:
- Connect your WiFi-capable device to the existing nwetork.
- Open a webbrowser and type the IP address obtained on the existing WiFi network.
To update the software over the air:
- In Arduino, go to `Sketch` > `Export Compiled Binary`. This will create the `.bin` file that you need to update the firmware. It is found in the folder `Software/build/`
- In your webbrowser, go to the url consisting of the IP address, followed by `/update`, for instance `http://192.168.0.224/update`.
- In the webbrowser, follow the steps to select the `.bin` file and to upload the file to the board.
Security Concerns
(https://randomnerdtutorials.com/esp32-esp8266-web-server-http-authentication/)
Authentication implemented here is meant to be used in your local network to protect from anyone just typing the ESP IP address and accessing the web server (like unauthorized family member or friend).
## Future work
This section lists a number of features that can be implemented as part of the webserver in the future.
- TODO: list all available ssids: scan WiFi Networks https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/
- TODO: add option to add/change ssid and password and save, connect to the new ssid (Option: save ssid and password using Preferences.h library https://randomnerdtutorials.com/esp32-save-data-permanently-preferences/)
- TODO: add functionality to turn WiFi AP off
- TODO: fix IP address on home network (https://randomnerdtutorials.com/esp32-static-fixed-ip-address-arduino-ide/)
- TODO: set hostname (https://randomnerdtutorials.com/esp32-set-custom-hostname-arduino/)
# References
The code for this webserver is based on code provided by Rui Santos at https://randomnerdtutorials.com.

View file

@ -316,6 +316,19 @@ String advanced_battery_processor(const String& var) {
String(falseTrue[datalayer_extended.cellpower.warning_Charger_not_responding]) + "</h4>";
#endif //CELLPOWER_BMS
#ifdef KIA_HYUNDAI_64_BATTERY
content += "<h4>Cells: " + String(datalayer_extended.KiaHyundai64.total_cell_count) + "S</h4>";
content += "<h4>12V voltage: " + String(datalayer_extended.KiaHyundai64.battery_12V / 10.0, 1) + "</h4>";
content += "<h4>Waterleakage: " + String(datalayer_extended.KiaHyundai64.waterleakageSensor) + "</h4>";
content +=
"<h4>Temperature, water inlet: " + String(datalayer_extended.KiaHyundai64.temperature_water_inlet) + "</h4>";
content +=
"<h4>Temperature, power relay: " + String(datalayer_extended.KiaHyundai64.powerRelayTemperature) + "</h4>";
content += "<h4>Batterymanagement mode: " + String(datalayer_extended.KiaHyundai64.batteryManagementMode) + "</h4>";
content += "<h4>BMS ignition: " + String(datalayer_extended.KiaHyundai64.BMS_ign) + "</h4>";
content += "<h4>Battery relay: " + String(datalayer_extended.KiaHyundai64.batteryRelay) + "</h4>";
#endif //KIA_HYUNDAI_64_BATTERY
#ifdef BYD_ATTO_3_BATTERY
static const char* SOCmethod[2] = {"Estimated from voltage", "Measured by BMS"};
content += "<h4>SOC method used: " + String(SOCmethod[datalayer_extended.bydAtto3.SOC_method]) + "</h4>";
@ -496,6 +509,8 @@ String advanced_battery_processor(const String& var) {
static const char* falseTrue[] = {"False", "True"};
static const char* noYes[] = {"No", "Yes"};
static const char* Fault[] = {"NOT_ACTIVE", "ACTIVE"};
//Buttons for user action
content += "<button onclick='askClearIsolation()'>Clear isolation fault</button>";
//0x20A 522 HVP_contatorState
content += "<h4>Contactor Status: " + String(contactorText[datalayer_extended.tesla.status_contactor]) + "</h4>";
content += "<h4>HVIL: " + String(hvilStatusState[datalayer_extended.tesla.hvil_status]) + "</h4>";
@ -1033,15 +1048,123 @@ String advanced_battery_processor(const String& var) {
content += "<h4>soc max: " + String(datalayer_extended.zoePH2.battery_soc_max) + "</h4>";
#endif //RENAULT_ZOE_GEN2_BATTERY
#ifdef VOLVO_SPA_BATTERY
content += "<h4>BECM reported SOC: " + String(datalayer_extended.VolvoPolestar.soc_bms) + "</h4>";
content += "<h4>Calculated SOC: " + String(datalayer_extended.VolvoPolestar.soc_calc) + "</h4>";
content += "<h4>Rescaled SOC: " + String(datalayer_extended.VolvoPolestar.soc_rescaled / 10) + "</h4>";
content += "<h4>BECM reported SOH: " + String(datalayer_extended.VolvoPolestar.soh_bms) + "</h4>";
content += "<h4>BECM supply voltage: " + String(datalayer_extended.VolvoPolestar.BECMsupplyVoltage) + " mV</h4>";
content += "<h4>HV voltage: " + String(datalayer_extended.VolvoPolestar.BECMBatteryVoltage) + " V</h4>";
content += "<h4>HV current: " + String(datalayer_extended.VolvoPolestar.BECMBatteryCurrent) + " A</h4>";
content += "<h4>Dynamic max voltage: " + String(datalayer_extended.VolvoPolestar.BECMUDynMaxLim) + " V</h4>";
content += "<h4>Dynamic min voltage: " + String(datalayer_extended.VolvoPolestar.BECMUDynMinLim) + " V</h4>";
content +=
"<h4>Discharge power limit 1: " + String(datalayer_extended.VolvoPolestar.HvBattPwrLimDcha1) + " kW</h4>";
content +=
"<h4>Discharge soft power limit: " + String(datalayer_extended.VolvoPolestar.HvBattPwrLimDchaSoft) + " kW</h4>";
content +=
"<h4>Discharge power limit slow aging: " + String(datalayer_extended.VolvoPolestar.HvBattPwrLimDchaSlowAgi) +
" kW</h4>";
content +=
"<h4>Charge power limit slow aging: " + String(datalayer_extended.VolvoPolestar.HvBattPwrLimChrgSlowAgi) +
" kW</h4>";
content += "<h4>HV system relay status: ";
switch (datalayer_extended.VolvoPolestar.HVSysRlySts) {
case 0:
content += String("Open");
break;
case 1:
content += String("Closed");
break;
case 2:
content += String("KeepStatus");
break;
case 3:
content += String("OpenAndRequestActiveDischarge");
break;
default:
content += String("Not valid");
}
content += "</h4><h4>HV system relay status 1: ";
switch (datalayer_extended.VolvoPolestar.HVSysDCRlySts1) {
case 0:
content += String("Open");
break;
case 1:
content += String("Closed");
break;
case 2:
content += String("KeepStatus");
break;
case 3:
content += String("Fault");
break;
default:
content += String("Not valid");
}
content += "</h4><h4>HV system relay status 2: ";
switch (datalayer_extended.VolvoPolestar.HVSysDCRlySts2) {
case 0:
content += String("Open");
break;
case 1:
content += String("Closed");
break;
case 2:
content += String("KeepStatus");
break;
case 3:
content += String("Fault");
break;
default:
content += String("Not valid");
}
content += "</h4><h4>HV system isolation resistance monitoring status: ";
switch (datalayer_extended.VolvoPolestar.HVSysIsoRMonrSts) {
case 0:
content += String("Not valid 1");
break;
case 1:
content += String("False");
break;
case 2:
content += String("True");
break;
case 3:
content += String("Not valid 2");
break;
default:
content += String("Not valid");
}
content += "<br><br><button onclick='Volvo_askEraseDTC()'>Erase DTC</button><br>";
content += "<button onclick='Volvo_askReadDTC()'>Read DTC (result must be checked in CANlog)</button><br>";
content += "<button onclick='Volvo_BECMecuReset()'>Restart BECM module</button>";
#endif // VOLVO_SPA_BATTERY
#if !defined(BMW_IX_BATTERY) && !defined(BOLT_AMPERA_BATTERY) && !defined(TESLA_BATTERY) && \
!defined(NISSAN_LEAF_BATTERY) && !defined(BMW_I3_BATTERY) && !defined(BYD_ATTO_3_BATTERY) && \
!defined(RENAULT_ZOE_GEN2_BATTERY) && !defined(CELLPOWER_BMS) && \
!defined(MEB_BATTERY) // Only the listed types have extra info
!defined(RENAULT_ZOE_GEN2_BATTERY) && !defined(CELLPOWER_BMS) && !defined(MEB_BATTERY) && \
!defined(VOLVO_SPA_BATTERY) && !defined(KIA_HYUNDAI_64_BATTERY) //Only the listed types have extra info
content += "No extra information available for this battery type";
#endif
content += "</div>";
content += "<script>";
content +=
"function askClearIsolation() { if (window.confirm('Are you sure you want to clear any active isolation "
"fault?')) { "
"clearIsolation(); } }";
content += "function clearIsolation() {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/clearIsolation', true);";
content += " xhr.send();";
content += "}";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
content += "<script>";
content +=
"function askResetSOH() { if (window.confirm('Are you sure you want to reset degradation data? "
@ -1054,6 +1177,40 @@ String advanced_battery_processor(const String& var) {
content += "}";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
content += "<script>";
content +=
"function Volvo_askEraseDTC() { if (window.confirm('Are you sure you want to erase DTCs?')) { "
"volvoEraseDTC(); } }";
content += "function volvoEraseDTC() {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/volvoEraseDTC', true);";
content += " xhr.send();";
content += "}";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
content += "<script>";
content += "function Volvo_askReadDTC() { volvoReadDTC(); } ";
content += "function volvoReadDTC() {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/volvoReadDTC', true);";
content += " xhr.send();";
content += "}";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
content += "<script>";
content +=
"function Volvo_BECMecuReset() { if (window.confirm('Are you sure you want to restart BECM?')) { "
"volvoBECMecuReset(); } }";
content += "function volvoBECMecuReset() {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/volvoBECMecuReset', true);";
content += " xhr.send();";
content += "}";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
return content;
}
return String();

View file

@ -102,6 +102,42 @@ String settings_processor(const String& var) {
content += "</div>";
#endif
#ifdef TESLA_MODEL_3Y_BATTERY
// Start a new block with grey background color
content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
content +=
"<h4 style='color: white;'>Manual LFP balancing: <span id='TSL_BAL_ACT'>" +
String(datalayer.battery.settings.user_requests_balancing ? "<span>&#10003;</span>"
: "<span style='color: red;'>&#10005;</span>") +
"</span> <button onclick='editTeslaBalAct()'>Edit</button></h4>";
content +=
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
";'>Balancing max time: " + String(datalayer.battery.settings.balancing_time_ms / 60000.0, 1) +
" Minutes </span> <button onclick='editBalTime()'>Edit</button></h4>";
content +=
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
";'>Balancing float power: " + String(datalayer.battery.settings.balancing_float_power_W / 1.0, 0) +
" W </span> <button onclick='editBalFloatPower()'>Edit</button></h4>";
content +=
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
";'>Max battery voltage: " + String(datalayer.battery.settings.balancing_max_pack_voltage_dV / 10.0, 0) +
" V </span> <button onclick='editBalMaxPackV()'>Edit</button></h4>";
content +=
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
";'>Max cell voltage: " + String(datalayer.battery.settings.balancing_max_cell_voltage_mV / 1.0, 0) +
" mV </span> <button onclick='editBalMaxCellV()'>Edit</button></h4>";
content +=
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
";'>Max cell voltage deviation: " +
String(datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV / 1.0, 0) +
" mV </span> <button onclick='editBalMaxDevCellV()'>Edit</button></h4>";
// Close the block
content += "</div>";
#endif
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
// Start a new block with orange background color
@ -211,6 +247,47 @@ String settings_processor(const String& var) {
"between 0 "
"and 1000.0');}}}";
#ifdef TESLA_MODEL_3Y_BATTERY
content +=
"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');}}";
content +=
"function editBalTime(){var value=prompt('Enter new max balancing time in "
"minutes');if(value!==null){if(value>=1&&value<=300){var xhr=new "
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
"BalTime?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
"between 1 and 300');}}}";
content +=
"function editBalFloatPower(){var value=prompt('Power level in Watt to float charge during forced "
"balancing');if(value!==null){if(value>=100&&value<=2000){var xhr=new "
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
"BalFloatPower?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
"between 100 and 2000');}}}";
content +=
"function editBalMaxPackV(){var value=prompt('Battery pack max voltage temporarily raised to this value during "
"forced balancing. Value in V');if(value!==null){if(value>=380&&value<=410){var xhr=new "
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
"BalMaxPackV?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
"between 380 and 410');}}}";
content +=
"function editBalMaxCellV(){var value=prompt('Cellvoltage max temporarily raised to this value during forced "
"balancing. Value in mV');if(value!==null){if(value>=3400&&value<=3750){var xhr=new "
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
"BalMaxCellV?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
"between 3400 and 3750');}}}";
content +=
"function editBalMaxDevCellV(){var value=prompt('Cellvoltage max deviation temporarily raised to this value "
"during forced balancing. Value in mV');if(value!==null){if(value>=300&&value<=600){var xhr=new "
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
"BalMaxDevCellV?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
"between 300 and 600');}}}";
#endif
#ifdef TEST_FAKE_BATTERY
content +=
"function editFakeBatteryVoltage(){var value=prompt('Enter new fake battery "

View file

@ -112,7 +112,7 @@ void init_webserver() {
// Define the handler to export can log
server.on("/export_can_log", HTTP_GET, [](AsyncWebServerRequest* request) {
pause_can_writing();
request->send(SD, CAN_LOG_FILE, String(), true);
request->send(SD_MMC, CAN_LOG_FILE, String(), true);
resume_can_writing();
});
@ -133,7 +133,7 @@ void init_webserver() {
// Define the handler to export debug log
server.on("/export_log", HTTP_GET, [](AsyncWebServerRequest* request) {
pause_log_writing();
request->send(SD, LOG_FILE, String(), true);
request->send(SD_MMC, LOG_FILE, String(), true);
resume_log_writing();
});
#endif
@ -385,6 +385,15 @@ void init_webserver() {
}
});
// Route for clearing isolation faults on Tesla
server.on("/clearIsolation", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer.battery.settings.user_requests_isolation_clear = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for resetting SOH on Nissan LEAF batteries
server.on("/resetSOH", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
@ -394,6 +403,33 @@ void init_webserver() {
request->send(200, "text/plain", "Updated successfully");
});
// Route for erasing DTC on Volvo/Polestar batteries
server.on("/volvoEraseDTC", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.VolvoPolestar.UserRequestDTCreset = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for reading DTC on Volvo/Polestar batteries
server.on("/volvoReadDTC", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.VolvoPolestar.UserRequestDTCreadout = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for performing ECU reset on Volvo/Polestar batteries
server.on("/volvoBECMecuReset", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.VolvoPolestar.UserRequestBECMecuReset = true;
request->send(200, "text/plain", "Updated successfully");
});
#ifdef TEST_FAKE_BATTERY
// Route for editing FakeBatteryVoltage
server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) {
@ -412,6 +448,93 @@ void init_webserver() {
});
#endif // TEST_FAKE_BATTERY
#ifdef TESLA_MODEL_3Y_BATTERY
// Route for editing balancing enabled
server.on("/TeslaBalAct", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.user_requests_balancing = value.toInt();
store_settings();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing balancing max time
server.on("/BalTime", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.balancing_time_ms = static_cast<uint32_t>(value.toFloat() * 60000);
store_settings();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing balancing max power
server.on("/BalFloatPower", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.balancing_float_power_W = static_cast<uint16_t>(value.toFloat());
store_settings();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing balancing max pack voltage
server.on("/BalMaxPackV", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.balancing_max_pack_voltage_dV = static_cast<uint16_t>(value.toFloat() * 10);
store_settings();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing balancing max cell voltage
server.on("/BalMaxCellV", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.balancing_max_cell_voltage_mV = static_cast<uint16_t>(value.toFloat());
store_settings();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing balancing max cell voltage deviation
server.on("/BalMaxDevCellV", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV = static_cast<uint16_t>(value.toFloat());
store_settings();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
#endif
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
// Route for editing ChargerTargetV
server.on("/updateChargeSetpointV", HTTP_GET, [](AsyncWebServerRequest* request) {
@ -709,7 +832,6 @@ String processor(const String& var) {
content += "#F5CC00;";
break;
case led_color::BLUE:
case led_color::RGB:
content += "#2B35AF;"; // Blue in test mode
break;
case led_color::RED:

View file

@ -6,8 +6,8 @@
#include "../../include.h"
#include "../../lib/YiannisBourkelis-Uptime-Library/src/uptime_formatter.h"
#include "../../lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h"
#include "../../lib/me-no-dev-AsyncTCP/src/AsyncTCP.h"
#include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include "../../lib/mathieucarbou-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
extern const char* version_number; // The current software version, shown on webserver

View file

@ -44,6 +44,15 @@
#endif
#endif
#ifdef HW_LILYGO
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
#if defined(CAN_ADDON) || defined(CANFD_ADDON) || defined(CHADEMO_BATTERY)
//Check that BMS reset is not used at the same time as Chademo and CAN addons
#error BMS RESET CANNOT BE USED AT SAME TIME AS CAN-ADDONS / CHADMEO! NOT ENOUGH GPIO!
#endif
#endif
#endif
#ifndef BATTERY_SELECTED
#error No battery selected! Choose one from the USER_SETTINGS.h file
#endif

View file

@ -83,7 +83,8 @@ static int16_t inverter_temperature = 0;
static uint16_t remaining_capacity_ah = 0;
static uint16_t fully_charged_capacity_ah = 0;
static long inverter_timestamp = 0;
static bool initialDataSent = 0;
static bool initialDataSent = false;
static bool inverterStartedUp = false;
void update_values_can_inverter() { //This function maps all the values fetched from battery CAN to the correct CAN messages
@ -167,6 +168,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
void map_can_frame_to_variable_inverter(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x151: //Message originating from BYD HVS compatible inverter. Reply with CAN identifier!
inverterStartedUp = true;
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
if (rx_frame.data.u8[0] & 0x01) { //Battery requests identification
send_intial_data();
@ -177,16 +179,19 @@ void map_can_frame_to_variable_inverter(CAN_frame rx_frame) {
}
break;
case 0x091:
inverterStartedUp = true;
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
inverter_voltage = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]) * 0.1;
inverter_current = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]) * 0.1;
inverter_temperature = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]) * 0.1;
break;
case 0x0D1:
inverterStartedUp = true;
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
inverter_SOC = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]) * 0.1;
break;
case 0x111:
inverterStartedUp = true;
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
inverter_timestamp = ((rx_frame.data.u8[0] << 24) | (rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[2] << 8) |
rx_frame.data.u8[3]);
@ -198,10 +203,16 @@ void map_can_frame_to_variable_inverter(CAN_frame rx_frame) {
void transmit_can_inverter() {
unsigned long currentMillis = millis();
if (!inverterStartedUp) {
//Avoid sending messages towards inverter, unless it has woken up and sent something to us first
return;
}
// Send initial CAN data once on bootup
if (!initialDataSent) {
send_intial_data();
initialDataSent = 1;
initialDataSent = true;
}
// Send 2s CAN Message

View file

@ -0,0 +1,548 @@
#include "../include.h"
#ifdef GROWATT_HV_CAN
#include "../datalayer/datalayer.h"
#include "GROWATT-HV-CAN.h"
/* TODO:
This protocol has not been tested with any inverter. Proceed with extreme caution.
Search the file for "TODO" to see all the places that might require work /*
/* Growatt BMS CAN-Bus-protocol High Voltage V1.10 2023-11-06
29-bit identifier
500kBit/sec
Big-endian
Terms and abbreviations:
PCS - Power conversion system (the Storage Inverter)
Cell - A single battery cell
Module - A battery module composed of 16 strings of cells
Pack - A battery pack composed of the BMS and battery modules connected in parallel and series, which can work independently
FCC - Full charge capacity
RM - Remaining capacity
BMS - Battery Information Collector*/
//Total number of Cells (1-512)
//(Total number of Cells = number of Packs in parallel * number of Modules in series * number of Cells in the module)
#define TOTAL_NUMBER_OF_CELLS 300
// Number of Modules in series (1-32)
#define NUMBER_OF_MODULES_IN_SERIES 20
// Number of packs in parallel (1-65536)
#define NUMBER_OF_PACKS_IN_PARALLEL 1
//Manufacturer abbreviation, part 1
#define MANUFACTURER_ASCII_0 0x47 //G
#define MANUFACTURER_ASCII_1 0x54 //T
/* Do not change code below unless you are sure what you are doing */
//Actual content messages
CAN_frame GROWATT_3110 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3110,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3120 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3120,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3130 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3130,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3140 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3140,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3150 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3150,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3160 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3160,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3170 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3170,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3180 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3180,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3190 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3190,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3200 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3200,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3210 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3210,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3220 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3220,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3230 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3230,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3240 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3240,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3250 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3250,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3260 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3260,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3270 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3270,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3280 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3280,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3290 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3290,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_3F00 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x3F00,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
static unsigned long previousMillis1s = 0; // will store last time a 1s CAN Message was send
static uint32_t unix_time = 0;
static uint16_t ampere_hours_remaining = 0;
static uint16_t ampere_hours_full = 0;
static uint16_t send_times = 0; // Overflows every 18hours. Cumulative number, plus 1 for each transmission
static uint8_t safety_specification = 0;
static uint8_t charging_command = 0;
static uint8_t discharging_command = 0;
static uint8_t shielding_external_communication_failure = 0;
static uint8_t clearing_battery_fault =
0; //When PCS receives the forced charge Mark 1 and Cell under- voltage protection fault, it will send 0XAA
static uint8_t ISO_detection_command = 0;
static uint8_t sleep_wakeup_control = 0;
static uint8_t PCS_working_status = 0; //00 standby, 01 operating
static uint8_t serial_number_counter = 0; //0-1-2-0-1-2...
static bool inverter_alive = false;
void update_values_can_inverter() { //This function maps all the values fetched from battery CAN to the correct CAN messages
if (datalayer.battery.status.voltage_dV > 10) { // Only update value when we have voltage available to avoid div0
ampere_hours_remaining =
((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) *
100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah)
ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) *
100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah)
}
//Map values to CAN messages
//Battery operating parameters and status information
//Recommended charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_3110.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV >> 8);
GROWATT_3110.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
//Charge limited current, 125 =12.5A (0.1, A) (Min 0, Max 300A)
GROWATT_3110.data.u8[2] = (datalayer.battery.status.max_charge_current_dA >> 8);
GROWATT_3110.data.u8[3] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
//Discharge limited current, 500 = 50A, (0.1, A)
GROWATT_3110.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA >> 8);
GROWATT_3110.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
//Status bits (see documentation for all bits, most important are mapped
if (datalayer.battery.status.active_power_W < -1) { // Discharging
GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000011);
} else if (datalayer.battery.status.active_power_W > 1) { // Charging
GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000010);
} else { //Idle
GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000001);
}
if ((datalayer.battery.status.max_charge_current_dA == 0) || (datalayer.battery.status.reported_soc == 10000) ||
(datalayer.battery.status.bms_status == FAULT)) {
GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b01000000); // No Charge
} else { //continue using battery
GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000000); // Charge allowed
}
if ((datalayer.battery.status.max_discharge_current_dA == 0) || (datalayer.battery.status.reported_soc == 0) ||
(datalayer.battery.status.bms_status == FAULT)) {
GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00100000); // No Discharge
} else { //continue using battery
GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000000); // Discharge allowed
}
GROWATT_3110.data.u8[6] = (GROWATT_3110.data.u8[6] | 0b00100000); // ISO Detection status: Detected
GROWATT_3110.data.u8[6] = (GROWATT_3110.data.u8[6] | 0b00010000); // Battery status: Normal
//Battery protection and alarm information
//Fault and warning status bits. TODO, map these according to documentation.
//GROWATT_3120.data.u8[0] =
//GROWATT_3120.data.u8[1] =
//GROWATT_3120.data.u8[2] =
//GROWATT_3120.data.u8[3] =
//GROWATT_3120.data.u8[4] =
//GROWATT_3120.data.u8[5] =
//GROWATT_3120.data.u8[6] =
//GROWATT_3120.data.u8[7] =
//Battery operation information
//Voltage of the pack (0.1V) [0-1000V]
GROWATT_3130.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8);
GROWATT_3130.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Total current (0.1A -300 to 300A)
GROWATT_3130.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
GROWATT_3130.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
//Cell max temperature (0.1C) [-40 to 120*C]
GROWATT_3130.data.u8[4] = (datalayer.battery.status.temperature_max_dC >> 8);
GROWATT_3130.data.u8[5] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//SOC (%) [0-100]
GROWATT_3130.data.u8[6] = (datalayer.battery.status.reported_soc / 100);
//SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 SOH flag (Indicates that battery is in unsafe use)
GROWATT_3130.data.u8[7] = (datalayer.battery.status.soh_pptt / 100);
//Battery capacity information
//Remaining capacity (10 mAh) [0.0 ~ 500000.0 mAH]
GROWATT_3140.data.u8[0] = ((ampere_hours_remaining * 100) >> 8);
GROWATT_3140.data.u8[1] = ((ampere_hours_remaining * 100) & 0x00FF);
//Fully charged capacity (10 mAh) [0.0 ~ 500000.0 mAH]
GROWATT_3140.data.u8[2] = ((ampere_hours_full * 100) >> 8);
GROWATT_3140.data.u8[3] = ((ampere_hours_full * 100) & 0x00FF);
//Manufacturer code
GROWATT_3140.data.u8[4] = MANUFACTURER_ASCII_0;
GROWATT_3140.data.u8[5] = MANUFACTURER_ASCII_1;
//Cycle count (h)
GROWATT_3140.data.u8[6] = 0;
GROWATT_3140.data.u8[7] = 0;
//Battery working parameters and module number information
//Discharge cutoff voltage (0.1V) [0-1000V]
GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8);
GROWATT_3150.data.u8[1] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
//Main control unit temperature (0.1C) [-40 to 120*C]
GROWATT_3150.data.u8[2] = (datalayer.battery.status.temperature_max_dC >> 8);
GROWATT_3150.data.u8[3] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Total number of cells
GROWATT_3150.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8);
GROWATT_3150.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF);
//Number of modules in series
GROWATT_3150.data.u8[6] = (NUMBER_OF_MODULES_IN_SERIES >> 8);
GROWATT_3150.data.u8[7] = (NUMBER_OF_MODULES_IN_SERIES & 0x00FF);
//Battery fault and voltage number information
//Fault flag bit
GROWATT_3160.data.u8[0] = 0; //TODO: Map according to documentation
//Fault extension flag bit
GROWATT_3160.data.u8[1] = 0; //TODO: Map according to documentation
//Number of module with the maximum cell voltage (1-32)
GROWATT_3160.data.u8[2] = 1;
//Number of cell with the maximum cell voltage (1-128)
GROWATT_3160.data.u8[3] = 1;
//Number of module with the minimum cell voltage (1-32)
GROWATT_3160.data.u8[4] = 1;
//Number of cell with the minimum cell voltage (1-128)
GROWATT_3160.data.u8[5] = 2;
//Minimum cell temperature (0.1C) [-40 to 120*C]
GROWATT_3160.data.u8[6] = (datalayer.battery.status.temperature_min_dC >> 8);
GROWATT_3160.data.u8[7] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Software version and temperature number information
//Number of Module with the maximum cell temperature (1-32)
GROWATT_3170.data.u8[0] = 1;
//Number of cell with the maximum cell temperature (1-128)
GROWATT_3170.data.u8[1] = 1;
//Number of module with the minimum cell temperature (1-32)
GROWATT_3170.data.u8[2] = 1;
//Number of cell with the minimum cell temperature (1-128)
GROWATT_3170.data.u8[3] = 2;
//Battery actial capacity (0-100) TODO, what is unit?
GROWATT_3170.data.u8[4] = 50;
//Battery correction status display value (0-255)
GROWATT_3170.data.u8[5] = 0;
// Remaining balancing time (0-255)
GROWATT_3170.data.u8[6] = 0;
//Balancing state, bit0-3(range 0-15) , Internal short circuit state, bit4-7 (range 0-15)
GROWATT_3170.data.u8[7] = 0;
//Battery Code and quantity information
//Manufacturer code
GROWATT_3180.data.u8[0] = MANUFACTURER_ASCII_0;
GROWATT_3180.data.u8[1] = MANUFACTURER_ASCII_1;
//Number of Packs in parallel (1-65536)
GROWATT_3180.data.u8[2] = (NUMBER_OF_PACKS_IN_PARALLEL >> 8);
GROWATT_3180.data.u8[3] = (NUMBER_OF_PACKS_IN_PARALLEL & 0x00FF);
//Total number of cells (1-65536)
GROWATT_3180.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8);
GROWATT_3180.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF);
//Pack number + BIC forward/reverse encoding number
// Bits 0-3: Pack number
// Bits 4-9: Max. number of BIC in forward BIC encoding in daisy- chain communication
// Bits 10-15: Max. number of BIC in reverse BIC encoding in daisy- chain communication
GROWATT_3180.data.u8[6] = 0; //TODO, this OK?
GROWATT_3180.data.u8[7] = 0; //TODO, this OK?
//Cell voltage and status information
//Battery status
GROWATT_3190.data.u8[0] = 0; //LFP, no forced charge
//Maximum cell voltage (mV)
GROWATT_3190.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
GROWATT_3190.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
// Min cell voltage (mV)
GROWATT_3190.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
GROWATT_3190.data.u8[4] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
//Reserved
GROWATT_3190.data.u8[5] = 0;
// Faulty battery pack number (1-16)
GROWATT_3190.data.u8[6] = 0;
//Faulty battery module number (1-16)
GROWATT_3190.data.u8[7] = 0;
//Manufacturer name and version information
// Manufacturer name (ASCII) Battery manufacturer abbreviation in capital letters
GROWATT_3200.data.u8[0] = MANUFACTURER_ASCII_0;
GROWATT_3200.data.u8[1] = MANUFACTURER_ASCII_1;
// Hardware revision (0 null, 1 verA, 2verB)
GROWATT_3200.data.u8[2] = 0x01;
// Reserved
GROWATT_3200.data.u8[3] = 0; //Reserved
//Circulating current value (0.1A), Range 0-20A
GROWATT_3200.data.u8[4] = 0;
GROWATT_3200.data.u8[5] = 0;
//Cell charge cutoff voltage
GROWATT_3200.data.u8[6] = (datalayer.battery.info.max_cell_voltage_mV >> 8);
GROWATT_3200.data.u8[7] = (datalayer.battery.info.max_cell_voltage_mV & 0x00FF);
//Upgrade information
//Message 0x3210 is update status. All blank is OK
//GROWATT_3210.data.u8[0] = 0;
//De-rating and fault information (reserved)
//Power reduction sign
GROWATT_3220.data.u8[0] = 0; //Bits set to high incase we need to derate
GROWATT_3220.data.u8[1] = 0; //Bits set to high incase we need to derate
//System fault status
GROWATT_3220.data.u8[2] = 0; //All normal
GROWATT_3220.data.u8[3] = 0; //All normal
//Forced discharge mark
GROWATT_3220.data.u8[4] = 0; //When you want to force charge battery, send 0xAA here
//Battery rated energy information (Unit 0.1 kWh ) 30kWh = 300 , so 30000Wh needs to be div by 100
GROWATT_3220.data.u8[5] = ((datalayer.battery.info.total_capacity_Wh / 100) >> 8);
GROWATT_3220.data.u8[6] = ((datalayer.battery.info.total_capacity_Wh / 100) & 0x00FF);
//Software subversion number
GROWATT_3220.data.u8[7] = 0;
//Serial number
//Frame number
GROWATT_3230.data.u8[0] = serial_number_counter;
//Serial number content
//The serial number includes the PACK number (1byte: range [1, 11]) and serial number (16bytes).
//(Reserved and filled with 0x00) Explanation: Byte 1 (Battery ID) = 0:Invalid.
//When Byte 1 (Battery ID) = 1, it represents the SN (Serial Number) of the
//high-voltage controller. When BYTE1 (Battery ID) = 2~11, it represents the SN of PACK 1~10.
switch (serial_number_counter) {
case 0:
GROWATT_3230.data.u8[1] = 0; // BATTERY ID
GROWATT_3230.data.u8[2] = 0; // SN0 //TODO, is this needed?
GROWATT_3230.data.u8[3] = 0; // SN1
GROWATT_3230.data.u8[4] = 0; // SN2
GROWATT_3230.data.u8[5] = 0; // SN3
GROWATT_3230.data.u8[6] = 0; // SN4
GROWATT_3230.data.u8[7] = 0; // SN5
break;
case 1:
GROWATT_3230.data.u8[1] = 0; // SN6
GROWATT_3230.data.u8[2] = 0; // SN7
GROWATT_3230.data.u8[3] = 0; // SN8
GROWATT_3230.data.u8[4] = 0; // SN9
GROWATT_3230.data.u8[5] = 0; // SN10
GROWATT_3230.data.u8[6] = 0; // SN11
GROWATT_3230.data.u8[7] = 0; // SN12
break;
case 2:
GROWATT_3230.data.u8[1] = 0; // SN13
GROWATT_3230.data.u8[2] = 0; // SN14
GROWATT_3230.data.u8[3] = 0; // SN15
GROWATT_3230.data.u8[4] = 0; // RESERVED
GROWATT_3230.data.u8[5] = 0; // RESERVED
GROWATT_3230.data.u8[6] = 0; // RESERVED
GROWATT_3230.data.u8[7] = 0; // RESERVED
break;
default:
break;
}
serial_number_counter = (serial_number_counter + 1) % 3; // cycles between 0-1-2-0-1...
//Total charge/discharge energy
//Pack number (1-16)
GROWATT_3240.data.u8[0] = 1;
//Total lifetime discharge energy Unit: 0.1kWh Range [0.0~10000000.0kWh]
GROWATT_3240.data.u8[1] = 0;
GROWATT_3240.data.u8[2] = 0;
GROWATT_3240.data.u8[3] = 0;
//Pack number (1-16)
GROWATT_3240.data.u8[4] = 1;
//Total lifetime charge energy Unit: 0.1kWh Range [0.0~10000000.0kWh]
GROWATT_3240.data.u8[5] = 0;
GROWATT_3240.data.u8[6] = 0;
GROWATT_3240.data.u8[7] = 0;
//Fault history
// Not applicable for our use. All values at 0 indicates no fault
GROWATT_3250.data.u8[0] = 0;
GROWATT_3250.data.u8[1] = 0;
GROWATT_3250.data.u8[2] = 0;
GROWATT_3250.data.u8[3] = 0;
GROWATT_3250.data.u8[4] = 0;
GROWATT_3250.data.u8[5] = 0;
GROWATT_3250.data.u8[6] = 0;
GROWATT_3250.data.u8[7] = 0;
//Battery internal debugging fault message
// Not applicable for our use. All values at 0 indicates no fault
GROWATT_3260.data.u8[0] = 0;
GROWATT_3260.data.u8[1] = 0;
GROWATT_3260.data.u8[2] = 0;
GROWATT_3260.data.u8[3] = 0;
GROWATT_3260.data.u8[4] = 0;
GROWATT_3260.data.u8[5] = 0;
GROWATT_3260.data.u8[6] = 0;
GROWATT_3260.data.u8[7] = 0;
//Battery internal debugging fault message
// Not applicable for our use. All values at 0 indicates no fault
GROWATT_3270.data.u8[0] = 0;
GROWATT_3270.data.u8[1] = 0;
GROWATT_3270.data.u8[2] = 0;
GROWATT_3270.data.u8[3] = 0;
GROWATT_3270.data.u8[4] = 0;
GROWATT_3270.data.u8[5] = 0;
GROWATT_3270.data.u8[6] = 0;
GROWATT_3270.data.u8[7] = 0;
//Product Version Information
GROWATT_3280.data.u8[0] = 0; //Reserved
//Product version number (1-5)
GROWATT_3280.data.u8[1] = 0; //No software version (1 indicates PRODUCT, 2 indicates COMMUNICATION version)
//For example, the version code for the main control unit (product) of a high-voltage battery is QBAA,
// and the code formonitoring (communication)is ZEAA
//TODO, is this needed?
GROWATT_3280.data.u8[2] = 0; //ASCII
GROWATT_3280.data.u8[3] = 0; //ASCII
GROWATT_3280.data.u8[4] = 0; //ASCII
GROWATT_3280.data.u8[5] = 0; //ASCII
GROWATT_3280.data.u8[6] = 0; //Software version information
//Battery series information
GROWATT_3290.data.u8[0] = 0; // DTC, range Range [0,65536], Default 12041 (TODO, shall we send that?)
GROWATT_3290.data.u8[1] = 0; // RESERVED
GROWATT_3290.data.u8[2] = 0; // RESERVED
GROWATT_3290.data.u8[3] = 0; // RESERVED
GROWATT_3290.data.u8[4] = 0; // RESERVED
GROWATT_3290.data.u8[5] = 0; // RESERVED
GROWATT_3290.data.u8[6] = 0; // RESERVED
GROWATT_3290.data.u8[7] = 0; // RESERVED
//Internal alarm information
//Battery internal debugging fault message
GROWATT_3F00.data.u8[0] = 0; // RESERVED
GROWATT_3F00.data.u8[1] = 0; // RESERVED
GROWATT_3F00.data.u8[2] = 0; // RESERVED
GROWATT_3F00.data.u8[3] = 0; // RESERVED
GROWATT_3F00.data.u8[4] = 0; // RESERVED
GROWATT_3F00.data.u8[5] = 0; // RESERVED
GROWATT_3F00.data.u8[6] = 0; // RESERVED
GROWATT_3F00.data.u8[7] = 0; // RESERVED
}
void map_can_frame_to_variable_inverter(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x3010: // Heartbeat command, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
inverter_alive = true;
send_times = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
safety_specification = rx_frame.data.u8[2];
break;
case 0x3020: // Control command, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
inverter_alive = true;
charging_command = rx_frame.data.u8[0];
discharging_command = rx_frame.data.u8[1];
shielding_external_communication_failure = rx_frame.data.u8[2];
clearing_battery_fault = rx_frame.data.u8[3];
ISO_detection_command = rx_frame.data.u8[4];
sleep_wakeup_control = rx_frame.data.u8[7];
break;
case 0x3030: // Time command, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
inverter_alive = true;
unix_time = ((rx_frame.data.u8[0] << 24) | (rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[2] << 8) |
rx_frame.data.u8[3]);
PCS_working_status = rx_frame.data.u8[7];
break;
default:
break;
}
}
void transmit_can_inverter() {
if (!inverter_alive) {
return; //Dont send messages towards inverter until it has started
}
unsigned long currentMillis = millis();
//Send 1s periodic CAN messages
if (currentMillis - previousMillis1s >= INTERVAL_1_S) {
previousMillis1s = currentMillis;
transmit_can_frame(&GROWATT_3110, can_config.inverter);
transmit_can_frame(&GROWATT_3120, can_config.inverter);
transmit_can_frame(&GROWATT_3130, can_config.inverter);
transmit_can_frame(&GROWATT_3140, can_config.inverter);
transmit_can_frame(&GROWATT_3150, can_config.inverter);
transmit_can_frame(&GROWATT_3160, can_config.inverter);
transmit_can_frame(&GROWATT_3170, can_config.inverter);
transmit_can_frame(&GROWATT_3180, can_config.inverter);
transmit_can_frame(&GROWATT_3190, can_config.inverter);
transmit_can_frame(&GROWATT_3200, can_config.inverter);
transmit_can_frame(&GROWATT_3210, can_config.inverter);
transmit_can_frame(&GROWATT_3220, can_config.inverter);
transmit_can_frame(&GROWATT_3230, can_config.inverter);
transmit_can_frame(&GROWATT_3240, can_config.inverter);
transmit_can_frame(&GROWATT_3250, can_config.inverter);
transmit_can_frame(&GROWATT_3260, can_config.inverter);
transmit_can_frame(&GROWATT_3270, can_config.inverter);
transmit_can_frame(&GROWATT_3280, can_config.inverter);
transmit_can_frame(&GROWATT_3290, can_config.inverter);
transmit_can_frame(&GROWATT_3F00, can_config.inverter);
}
}
void setup_inverter(void) { // Performs one time setup at startup over CAN bus
strncpy(datalayer.system.info.inverter_protocol, "Growatt High Voltage protocol via CAN", 63);
datalayer.system.info.inverter_protocol[63] = '\0';
}
#endif

View file

@ -0,0 +1,10 @@
#ifndef GROWATT_HV_CAN_H
#define GROWATT_HV_CAN_H
#include "../include.h"
#define CAN_INVERTER_SELECTED
void transmit_can_frame(CAN_frame* tx_frame, int interface);
void setup_inverter(void);
#endif

View file

@ -0,0 +1,278 @@
#include "../include.h"
#ifdef GROWATT_LV_CAN
#include "../datalayer/datalayer.h"
#include "GROWATT-LV-CAN.h"
/* Growatt BMS CAN-Bus-protocol Low Voltage Rev_04
CAN 2.0A
500kBit/sec
Big-endian
The inverter replies data every second (standard frame/decimal)0x301:*/
/* Do not change code below unless you are sure what you are doing */
//Actual content messages
CAN_frame GROWATT_311 = {.FD = false, //Voltage and charge limits and status
.ext_ID = false,
.DLC = 8,
.ID = 0x311,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_312 = {.FD = false, //status bits , pack number, total cell number
.ext_ID = false,
.DLC = 8,
.ID = 0x312,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_313 = {.FD = false, //voltage, current, temp, soc, soh
.ext_ID = false,
.DLC = 8,
.ID = 0x313,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_314 = {.FD = false, //capacity, delta V, cycle count
.ext_ID = false,
.DLC = 8,
.ID = 0x314,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_319 = {.FD = false, //max/min cell voltage, num of cell max/min, protect pack ID
.ext_ID = false,
.DLC = 8,
.ID = 0x319,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_320 = {.FD = false, //manufacturer name, hw ver, sw ver, date and time
.ext_ID = false,
.DLC = 8,
.ID = 0x320,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_321 = {.FD = false, //Update status, ID
.ext_ID = false,
.DLC = 8,
.ID = 0x321,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
//Cellvoltages
CAN_frame GROWATT_315 = {.FD = false, //Cells 1-4
.ext_ID = false,
.DLC = 8,
.ID = 0x315,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_316 = {.FD = false, //Cells 5-8
.ext_ID = false,
.DLC = 8,
.ID = 0x316,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_317 = {.FD = false, //Cells 9-12
.ext_ID = false,
.DLC = 8,
.ID = 0x317,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_318 = {.FD = false, //Cells 13-16
.ext_ID = false,
.DLC = 8,
.ID = 0x318,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
#define VOLTAGE_OFFSET_DV 40 //Offset in deciVolt from max charge voltage and min discharge voltage
#define MAX_VOLTAGE_DV 630
#define MIN_VOLTAGE_DV 410
static uint16_t cell_delta_mV = 0;
static uint16_t ampere_hours_remaining = 0;
static uint16_t ampere_hours_full = 0;
void update_values_can_inverter() { //This function maps all the values fetched from battery CAN to the correct CAN messages
cell_delta_mV = datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV;
if (datalayer.battery.status.voltage_dV > 10) { // Only update value when we have voltage available to avoid div0
ampere_hours_remaining =
((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) *
100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah)
ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) *
100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah)
}
//Map values to CAN messages
//Battery charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 41V, MAX 63V, default 54V)
GROWATT_311.data.u8[0] = ((datalayer.battery.info.max_design_voltage_dV - VOLTAGE_OFFSET_DV) >> 8);
GROWATT_311.data.u8[1] = ((datalayer.battery.info.max_design_voltage_dV - VOLTAGE_OFFSET_DV) & 0x00FF);
//Charge limited current, 125 =12.5A (0.1, A)
GROWATT_311.data.u8[2] = (datalayer.battery.status.max_charge_current_dA >> 8);
GROWATT_311.data.u8[3] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
//Discharge limited current, 500 = 50A, (0.1, A)
GROWATT_311.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA >> 8);
GROWATT_311.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
//Status bits (see documentation for all bits, most important are bit0-1 (Status), and bit 10-11 (SP status))
if (datalayer.battery.status.active_power_W < -1) { // Discharging
GROWATT_311.data.u8[6] = 0x0C; //0b11 discharging on bit10-11
GROWATT_311.data.u8[7] = 0x03; //0b11 discharging on bit0-1
} else if (datalayer.battery.status.active_power_W > 1) { // Charging
GROWATT_311.data.u8[6] = 0x08; //0b10 charging on bit10-11
GROWATT_311.data.u8[7] = 0x02; //0b10 charging on bit0-1
} else { //Idle
GROWATT_311.data.u8[6] = 0x04; //0b01 charging on bit10-11
GROWATT_311.data.u8[7] = 0x01; //0b01 charging on bit0-1
}
//Fault status bits. TODO, map these according to docmentation.
//GROWATT_312.data.u8[0] =
//GROWATT_312.data.u8[1] =
//GROWATT_312.data.u8[2] =
//GROWATT_312.data.u8[3] =
GROWATT_312.data.u8[4] = 0x01; // Pack number
GROWATT_312.data.u8[5] = 0xAA; // Manufacturer code
GROWATT_312.data.u8[6] = 0xBB; // Manufacturer code
GROWATT_312.data.u8[7] = datalayer.battery.info.number_of_cells; // Total cell number (1-254)
//Voltage of single module or Average module voltage of system (0.01V)
GROWATT_313.data.u8[0] = ((datalayer.battery.status.voltage_dV * 10) >> 8);
GROWATT_313.data.u8[1] = ((datalayer.battery.status.voltage_dV * 10) & 0x00FF);
//Module or system total current (0.1A Sint16)
GROWATT_313.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
GROWATT_313.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
//Cell max temperature (0.1C)
GROWATT_313.data.u8[4] = (datalayer.battery.status.temperature_max_dC >> 8);
GROWATT_313.data.u8[5] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//SOC of single module or average value of system (%)
GROWATT_313.data.u8[6] = (datalayer.battery.status.reported_soc / 100);
//SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 SOH flag (Indicates that battery is in unsafe use)
GROWATT_313.data.u8[7] = (datalayer.battery.status.soh_pptt / 100);
//Remaining capacity (10 mAh)
GROWATT_314.data.u8[0] = ((ampere_hours_remaining * 100) >> 8);
GROWATT_314.data.u8[1] = ((ampere_hours_remaining * 100) & 0x00FF);
//Fully charged capacity (10 mAh)
GROWATT_314.data.u8[2] = ((ampere_hours_full * 100) >> 8);
GROWATT_314.data.u8[3] = ((ampere_hours_full * 100) & 0x00FF);
//Delta V (mV)
GROWATT_314.data.u8[4] = (cell_delta_mV >> 8);
GROWATT_314.data.u8[5] = (cell_delta_mV & 0x00FF);
//Cycle count (h)
GROWATT_314.data.u8[6] = 0;
GROWATT_314.data.u8[7] = 0;
//Request charge/discharge
if (datalayer.battery.status.bms_status == ACTIVE) {
GROWATT_319.data.u8[0] =
0xC0; //Bit7 charge enabled, Bit 6 discharge enabled (bit5 req force charge, bit 4 req force charge 2)
} else {
GROWATT_319.data.u8[0] = 0x00;
}
//TODO: if battery falls below SOC 5% during long idle time, we should set bit 5
//Maximum cell voltage (mV)
GROWATT_319.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
GROWATT_319.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
// Min cell voltage (mV)
GROWATT_319.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
GROWATT_319.data.u8[4] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
//Maximum cell voltage number
GROWATT_319.data.u8[5] = 1; //Fake
// Min cell voltage number
GROWATT_319.data.u8[6] = 2; //Fake
//Protect pack ID
GROWATT_319.data.u8[7] = 0; //?
// Manufacturer name (ASCII) Battery manufacturer abbreviation in capital letters
GROWATT_320.data.u8[0] = 0x42; //B
GROWATT_320.data.u8[1] = 0x45; //E
// Hardware revision (1-9)
GROWATT_320.data.u8[2] = 0x01;
// Software version (1-9)
GROWATT_320.data.u8[3] = 0x01;
//Date and Time
//Bit 0~5 Second0~59
//Bit 6~11 Minute0~59
//Bit 12~16 Hour0~23
//Bit 17~21Day1~31
//Bit 22~25 Month 1-12
//Bit 26~31 Year (2000-2063)
GROWATT_320.data.u8[4] = 0; //TODO
GROWATT_320.data.u8[5] = 0;
GROWATT_320.data.u8[6] = 0;
GROWATT_320.data.u8[7] = 0;
//Message 0x321 is update status. All blank is OK
//Cellvoltage #1
GROWATT_315.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[0] >> 8);
GROWATT_315.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[0] & 0x00FF);
//Cellvoltage #2
GROWATT_315.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[1] >> 8);
GROWATT_315.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[1] & 0x00FF);
//Cellvoltage #3
GROWATT_315.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[2] >> 8);
GROWATT_315.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[2] & 0x00FF);
//Cellvoltage #4
GROWATT_315.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[3] >> 8);
GROWATT_315.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[3] & 0x00FF);
//Cellvoltage #5
GROWATT_316.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[4] >> 8);
GROWATT_316.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[4] & 0x00FF);
//Cellvoltage #6
GROWATT_316.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[5] >> 8);
GROWATT_316.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[5] & 0x00FF);
//Cellvoltage #7
GROWATT_316.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[6] >> 8);
GROWATT_316.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[6] & 0x00FF);
//Cellvoltage #8
GROWATT_316.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[7] >> 8);
GROWATT_316.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[7] & 0x00FF);
//Cellvoltage #9
GROWATT_317.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[8] >> 8);
GROWATT_317.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[8] & 0x00FF);
//Cellvoltage #10
GROWATT_317.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[9] >> 8);
GROWATT_317.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[9] & 0x00FF);
//Cellvoltage #11
GROWATT_317.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[10] >> 8);
GROWATT_317.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[10] & 0x00FF);
//Cellvoltage #12
GROWATT_317.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[11] >> 8);
GROWATT_317.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[11] & 0x00FF);
//Cellvoltage #13
GROWATT_318.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[12] >> 8);
GROWATT_318.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[12] & 0x00FF);
//Cellvoltage #14
GROWATT_318.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[13] >> 8);
GROWATT_318.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[13] & 0x00FF);
//Cellvoltage #15
GROWATT_318.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[14] >> 8);
GROWATT_318.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[14] & 0x00FF);
//Cellvoltage #16
GROWATT_318.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[15] >> 8);
GROWATT_318.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[15] & 0x00FF);
}
void map_can_frame_to_variable_inverter(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x301:
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
transmit_can_frame(&GROWATT_311, can_config.inverter);
transmit_can_frame(&GROWATT_312, can_config.inverter);
transmit_can_frame(&GROWATT_313, can_config.inverter);
transmit_can_frame(&GROWATT_314, can_config.inverter);
transmit_can_frame(&GROWATT_315, can_config.inverter);
transmit_can_frame(&GROWATT_316, can_config.inverter);
transmit_can_frame(&GROWATT_317, can_config.inverter);
transmit_can_frame(&GROWATT_318, can_config.inverter);
transmit_can_frame(&GROWATT_319, can_config.inverter);
transmit_can_frame(&GROWATT_320, can_config.inverter);
transmit_can_frame(&GROWATT_321, can_config.inverter);
break;
default:
break;
}
}
void transmit_can_inverter() {
// No periodic sending for this battery type. Data is sent when inverter requests it
}
void setup_inverter(void) { // Performs one time setup at startup over CAN bus
strncpy(datalayer.system.info.inverter_protocol, "Growatt Low Voltage (48V) protocol via CAN", 63);
datalayer.system.info.inverter_protocol[63] = '\0';
}
#endif

View file

@ -0,0 +1,10 @@
#ifndef GROWATT_LV_CAN_H
#define GROWATT_LV_CAN_H
#include "../include.h"
#define CAN_INVERTER_SELECTED
void transmit_can_frame(CAN_frame* tx_frame, int interface);
void setup_inverter(void);
#endif

View file

@ -23,6 +23,14 @@
#include "FOXESS-CAN.h"
#endif
#ifdef GROWATT_HV_CAN
#include "GROWATT-HV-CAN.h"
#endif
#ifdef GROWATT_LV_CAN
#include "GROWATT-LV-CAN.h"
#endif
#ifdef PYLON_CAN
#include "PYLON-CAN.h"
#endif
@ -59,6 +67,10 @@
#include "SOLAX-CAN.h"
#endif
#ifdef SUNGROW_CAN
#include "SUNGROW-CAN.h"
#endif
#ifdef SERIAL_LINK_TRANSMITTER
#include "SERIAL-LINK-TRANSMITTER-INVERTER.h"
#endif

View file

@ -117,19 +117,40 @@ void float2frameMSB(byte* arr, float value, byte framepointer) {
arr[framepointer + 1] = g.b[3];
}
void send_kostal(byte* arr, int alen) {
static void dbg_timestamp(void) {
#ifdef DEBUG_KOSTAL_RS485_DATA
logging.print("TX: ");
for (int i = 0; i < alen; i++) {
if (arr[i] < 0x10) {
logging.print("[");
logging.print(millis());
logging.print(" ms] ");
#endif
}
static void dbg_frame(byte* frame, int len, const char* prefix) {
dbg_timestamp();
#ifdef DEBUG_KOSTAL_RS485_DATA
logging.print(prefix);
logging.print(": ");
for (uint8_t i = 0; i < len; i++) {
if (frame[i] < 0x10) {
logging.print("0");
}
logging.print(arr[i], HEX);
logging.print(frame[i], HEX);
logging.print(" ");
}
logging.println("\n");
logging.println("");
#endif
Serial2.write(arr, alen);
}
static void dbg_message(const char* msg) {
dbg_timestamp();
#ifdef DEBUG_KOSTAL_RS485_DATA
logging.println(msg);
#endif
}
static void send_kostal(byte* frame, int len) {
dbg_frame(frame, len, "TX");
Serial2.write(frame, len);
}
byte calculate_longframe_crc(byte* lfc, int lastbyte) {
@ -247,6 +268,7 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream
}
if (currentMillis - contactorMillis >= INTERVAL_2_S & !RX_allow) {
RX_allow = true;
dbg_message("RX_allow -> true");
}
if (startupMillis) {
@ -255,10 +277,12 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream
// Disconnect allowed only, when curren zero
if (datalayer.battery.status.current_dA == 0) {
datalayer.system.status.inverter_allows_contactor_closing = false;
dbg_message("inverter_allows_contactor_closing -> false");
}
} else if (((currentMillis - startupMillis) >= 7000) &
datalayer.system.status.inverter_allows_contactor_closing == false) {
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
}
}
@ -266,6 +290,7 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream
if ((currentMillis - B1_last_millis) > INTERVAL_1_S) {
send_kostal(frameB1b, 8);
B1_delay = false;
dbg_message("B1_delay -> false");
}
} else if (Serial2.available()) {
RS485_RXFRAME[rx_index] = Serial2.read();
@ -273,14 +298,7 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream
rx_index++;
if (RS485_RXFRAME[rx_index - 1] == 0x00) {
if ((rx_index == 10) && (RS485_RXFRAME[0] == 0x09) && register_content_ok) {
#ifdef DEBUG_KOSTAL_RS485_DATA
logging.print("RX: ");
for (uint8_t i = 0; i < 10; i++) {
logging.print(RS485_RXFRAME[i], HEX);
logging.print(" ");
}
logging.println("");
#endif
dbg_frame(RS485_RXFRAME, 10, "RX");
rx_index = 0;
if (check_kostal_frame_crc()) {
incoming_message_counter = RS485_HEALTHY;
@ -299,6 +317,7 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream
if (headerB && (RS485_RXFRAME[6] == 0x5E) && (RS485_RXFRAME[7] == 0xFF)) {
send_kostal(frameB1, 10);
B1_delay = true;
dbg_message("B1_delay -> true");
B1_last_millis = currentMillis;
}
@ -334,11 +353,14 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream
send_kostal(frame3, 9);
}
}
} else {
dbg_frame(RS485_RXFRAME, 10, "RX (dropped)");
}
rx_index = 0;
}
}
if (rx_index >= 10) {
dbg_frame(RS485_RXFRAME, 10, "RX (!RX_allow)");
rx_index = 0;
}
}

View file

@ -4,6 +4,10 @@
#include "../include.h"
#define RS485_INVERTER_SELECTED
//#define DEBUG_KOSTAL_RS485_DATA // Enable this line to get TX / RX printed out via serial
//#define DEBUG_KOSTAL_RS485_DATA // Enable this line to get TX / RX printed out via logging
#if defined(DEBUG_KOSTAL_RS485_DATA) && !defined(DEBUG_LOG)
#error "enable LOG_TO_SD, DEBUG_VIA_USB or DEBUG_VIA_WEB in order to use DEBUG_KOSTAL_RS485_DATA"
#endif
#endif

View file

@ -134,7 +134,21 @@ CAN_frame PYLON_4291 = {.FD = false,
.ID = 0x4291,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
static uint16_t discharge_cutoff_voltage_dV = 0;
static uint16_t charge_cutoff_voltage_dV = 0;
#define VOLTAGE_OFFSET_DV 20 // Small offset voltage to avoid generating voltage events
void update_values_can_inverter() { //This function maps all the values fetched from battery CAN to the correct CAN messages
//Check what discharge and charge cutoff voltages to send
if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage
discharge_cutoff_voltage_dV = datalayer.battery.settings.max_user_set_discharge_voltage_dV;
charge_cutoff_voltage_dV = datalayer.battery.settings.max_user_set_charge_voltage_dV;
} else {
discharge_cutoff_voltage_dV = (datalayer.battery.info.min_design_voltage_dV + VOLTAGE_OFFSET_DV);
charge_cutoff_voltage_dV = (datalayer.battery.info.max_design_voltage_dV - VOLTAGE_OFFSET_DV);
}
//There are more mappings that could be added, but this should be enough to use as a starting point
// Note we map both 0 and 1 messages
@ -166,12 +180,12 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4210.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
PYLON_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
PYLON_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
#else
#else // Not INVERT_LOW_HIGH_BYTES
PYLON_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
PYLON_4210.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
PYLON_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
PYLON_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
#endif
#endif // INVERT_LOW_HIGH_BYTES
//SOC (100.00%)
PYLON_4210.data.u8[6] = (datalayer.battery.status.reported_soc / 100); //Remove decimals
PYLON_4211.data.u8[6] = (datalayer.battery.status.reported_soc / 100); //Remove decimals
@ -204,12 +218,12 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4210.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) >> 8);
PYLON_4211.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF);
PYLON_4211.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) >> 8);
#else
#else // Not SET_30K_OFFSET
PYLON_4210.data.u8[2] = (datalayer.battery.status.current_dA & 0x00FF);
PYLON_4210.data.u8[3] = (datalayer.battery.status.current_dA >> 8);
PYLON_4211.data.u8[2] = (datalayer.battery.status.current_dA & 0x00FF);
PYLON_4211.data.u8[3] = (datalayer.battery.status.current_dA >> 8);
#endif
#endif //SET_30K_OFFSET
// BMS Temperature (We dont have BMS temp, send max cell voltage instead)
PYLON_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
@ -217,17 +231,17 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
PYLON_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
//Maxvoltage (eg 400.0V = 4000 , 16bits long) Discharge Cutoff Voltage
PYLON_4220.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
PYLON_4220.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV >> 8);
PYLON_4221.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
PYLON_4221.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV >> 8);
//Maxvoltage (eg 400.0V = 4000 , 16bits long) Charge Cutoff Voltage
PYLON_4220.data.u8[0] = (charge_cutoff_voltage_dV & 0x00FF);
PYLON_4220.data.u8[1] = (charge_cutoff_voltage_dV >> 8);
PYLON_4221.data.u8[0] = (charge_cutoff_voltage_dV & 0x00FF);
PYLON_4221.data.u8[1] = (charge_cutoff_voltage_dV >> 8);
//Minvoltage (eg 300.0V = 3000 , 16bits long) Charge Cutoff Voltage
PYLON_4220.data.u8[2] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
PYLON_4220.data.u8[3] = (datalayer.battery.info.min_design_voltage_dV >> 8);
PYLON_4221.data.u8[2] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
PYLON_4221.data.u8[3] = (datalayer.battery.info.min_design_voltage_dV >> 8);
//Minvoltage (eg 300.0V = 3000 , 16bits long) Discharge Cutoff Voltage
PYLON_4220.data.u8[2] = (discharge_cutoff_voltage_dV & 0x00FF);
PYLON_4220.data.u8[3] = (discharge_cutoff_voltage_dV >> 8);
PYLON_4221.data.u8[2] = (discharge_cutoff_voltage_dV & 0x00FF);
PYLON_4221.data.u8[3] = (discharge_cutoff_voltage_dV >> 8);
#ifdef SET_30K_OFFSET
//Max ChargeCurrent
@ -241,7 +255,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4220.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8);
PYLON_4221.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF);
PYLON_4221.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8);
#else
#else // Not SET_30K_OFFSET
//Max ChargeCurrent
PYLON_4220.data.u8[4] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
PYLON_4220.data.u8[5] = (datalayer.battery.status.max_charge_current_dA >> 8);
@ -253,7 +267,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4220.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA >> 8);
PYLON_4221.data.u8[6] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
PYLON_4221.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA >> 8);
#endif
#endif // SET_30K_OFFSET
//Max cell voltage
PYLON_4230.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
@ -276,8 +290,8 @@ void update_values_can_inverter() { //This function maps all the values fetched
//Max/Min temperature per cell
PYLON_4240.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
PYLON_4240.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4240.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
PYLON_4240.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4241.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
PYLON_4241.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
//Max temperature per module
PYLON_4270.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
@ -290,7 +304,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4270.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4271.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
PYLON_4271.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
#else
#else // Not INVERT_LOW_HIGH_BYTES
//Voltage (370.0)
PYLON_4210.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8;
PYLON_4210.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF);
@ -303,12 +317,12 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4210.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF);
PYLON_4211.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) >> 8);
PYLON_4211.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF);
#else
#else // Not SET_30K_OFFSET
PYLON_4210.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
PYLON_4210.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
PYLON_4211.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
PYLON_4211.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
#endif
#endif //SET_30K_OFFSET
// BMS Temperature (We dont have BMS temp, send max cell voltage instead)
PYLON_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
@ -316,17 +330,17 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
PYLON_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
//Maxvoltage (eg 400.0V = 4000 , 16bits long) Discharge Cutoff Voltage
PYLON_4220.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV >> 8);
PYLON_4220.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
PYLON_4221.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV >> 8);
PYLON_4221.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
//Maxvoltage (eg 400.0V = 4000 , 16bits long) Charge Cutoff Voltage
PYLON_4220.data.u8[0] = (charge_cutoff_voltage_dV >> 8);
PYLON_4220.data.u8[1] = (charge_cutoff_voltage_dV & 0x00FF);
PYLON_4221.data.u8[0] = (charge_cutoff_voltage_dV >> 8);
PYLON_4221.data.u8[1] = (charge_cutoff_voltage_dV & 0x00FF);
//Minvoltage (eg 300.0V = 3000 , 16bits long) Charge Cutoff Voltage
PYLON_4220.data.u8[2] = (datalayer.battery.info.min_design_voltage_dV >> 8);
PYLON_4220.data.u8[3] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
PYLON_4221.data.u8[2] = (datalayer.battery.info.min_design_voltage_dV >> 8);
PYLON_4221.data.u8[3] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
//Minvoltage (eg 300.0V = 3000 , 16bits long) Discharge Cutoff Voltage
PYLON_4220.data.u8[2] = (discharge_cutoff_voltage_dV >> 8);
PYLON_4220.data.u8[3] = (discharge_cutoff_voltage_dV & 0x00FF);
PYLON_4221.data.u8[2] = (discharge_cutoff_voltage_dV >> 8);
PYLON_4221.data.u8[3] = (discharge_cutoff_voltage_dV & 0x00FF);
#ifdef SET_30K_OFFSET
//Max ChargeCurrent
@ -340,7 +354,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4220.data.u8[7] = ((30000 - max_discharge_current) & 0x00FF);
PYLON_4221.data.u8[6] = ((30000 - max_discharge_current) >> 8);
PYLON_4221.data.u8[7] = ((30000 - max_discharge_current) & 0x00FF);
#else
#else // Not SET_30K_OFFSET
//Max ChargeCurrent
PYLON_4220.data.u8[4] = (max_charge_current >> 8);
PYLON_4220.data.u8[5] = (max_charge_current & 0x00FF);
@ -352,7 +366,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4220.data.u8[7] = (max_discharge_current & 0x00FF);
PYLON_4221.data.u8[6] = (max_discharge_current >> 8);
PYLON_4221.data.u8[7] = (max_discharge_current & 0x00FF);
#endif
#endif //SET_30K_OFFSET
//Max cell voltage
PYLON_4230.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
@ -375,8 +389,8 @@ void update_values_can_inverter() { //This function maps all the values fetched
//Max/Min temperature per cell
PYLON_4240.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4240.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
PYLON_4240.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4240.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
PYLON_4241.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4241.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Max temperature per module
PYLON_4270.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
@ -389,25 +403,7 @@ void update_values_can_inverter() { //This function maps all the values fetched
PYLON_4270.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
PYLON_4271.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4271.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
#endif
//Max/Min cell voltage
PYLON_4230.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
PYLON_4230.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
PYLON_4230.data.u8[2] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
PYLON_4230.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
//Max/Min temperature per cell
PYLON_4240.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
PYLON_4240.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
PYLON_4240.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4240.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Max/Min temperature per module
PYLON_4270.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
PYLON_4270.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
PYLON_4270.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
PYLON_4270.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
#endif // Not INVERT_LOW_HIGH_BYTES
//In case we run into any errors/faults, we can set charge / discharge forbidden
if (datalayer.battery.status.bms_status == FAULT) {

View file

@ -0,0 +1,606 @@
#include "../include.h"
#ifdef SUNGROW_CAN
#include "../datalayer/datalayer.h"
#include "SUNGROW-CAN.h"
/* TODO:
This protocol is still under development. It can not be used yet for Sungrow inverters,
see the Wiki for more info on how to use your Sungrow inverter */
/* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis500ms = 0;
static bool alternate = false;
static uint8_t mux = 0;
static uint8_t version_char[14] = {0};
static uint8_t manufacturer_char[14] = {0};
static uint8_t model_char[14] = {0};
static bool inverter_sends_000 = false;
//Actual content messages
CAN_frame SUNGROW_000 = {.FD = false, // Sent by inv or BMS?
.ext_ID = false,
.DLC = 8,
.ID = 0x000,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_001 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x001,
.data = {0xF0, 0x05, 0x20, 0x03, 0x2C, 0x01, 0x2C, 0x01}};
CAN_frame SUNGROW_002 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x002,
.data = {0xA2, 0x05, 0x10, 0x27, 0x9B, 0x03, 0x00, 0x19}};
CAN_frame SUNGROW_003 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x003,
.data = {0x2A, 0x1D, 0x00, 0x00, 0xBF, 0x18, 0x00, 0x00}};
CAN_frame SUNGROW_004 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x004,
.data = {0x27, 0x05, 0x00, 0x00, 0x24, 0x05, 0x08, 0x01}};
CAN_frame SUNGROW_005 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x005,
.data = {0x02, 0x00, 0x01, 0xE6, 0x20, 0x24, 0x05, 0x00}};
CAN_frame SUNGROW_006 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x006,
.data = {0x0E, 0x01, 0x01, 0x01, 0xDE, 0x0C, 0xD5, 0x0C}};
CAN_frame SUNGROW_013 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x013,
.data = {0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x0E, 0x01}};
CAN_frame SUNGROW_014 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x014,
.data = {0x05, 0x01, 0xAC, 0x80, 0x10, 0x02, 0x57, 0x80}};
CAN_frame SUNGROW_015 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x015,
.data = {0x93, 0x80, 0xAC, 0x80, 0x57, 0x80, 0x93, 0x80}};
CAN_frame SUNGROW_016 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x016,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_017 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x017,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_018 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x018,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_019 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x019,
.data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_01A = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x01A,
.data = {0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_01B = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x01B,
.data = {0xBE, 0x8F, 0x61, 0x01, 0xBE, 0x8F, 0x61, 0x01}};
CAN_frame SUNGROW_01C = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x01C,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_01D = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x01D,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_01E = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x01E,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_400 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x400,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_500 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x500,
.data = {0x01, 0x01, 0x00, 0xFF, 0x00, 0x01, 0x00, 0x32}};
CAN_frame SUNGROW_501 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x501,
.data = {0xF0, 0x05, 0x20, 0x03, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_502 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x502,
.data = {0xA2, 0x05, 0x00, 0x00, 0x9B, 0x03, 0x00, 0x19}};
CAN_frame SUNGROW_503 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x503,
.data = {0x2A, 0x1D, 0x00, 0x00, 0xBF, 0x18, 0x00, 0x00}};
CAN_frame SUNGROW_504 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x504,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_505 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x505,
.data = {0x00, 0x02, 0x01, 0xE6, 0x20, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_506 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x506,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_512 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x512,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_700 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x700,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_701 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x701,
.data = {0xF0, 0x05, 0x20, 0x03, 0x2C, 0x01, 0x2C, 0x01}};
CAN_frame SUNGROW_702 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x702,
.data = {0xA2, 0x05, 0x10, 0x27, 0x9B, 0x03, 0x00, 0x19}};
CAN_frame SUNGROW_703 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x703,
.data = {0x2A, 0x1D, 0x00, 0x00, 0xBF, 0x18, 0x00, 0x00}};
CAN_frame SUNGROW_704 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x704,
.data = {0x27, 0x05, 0x00, 0x00, 0x24, 0x05, 0x08, 0x01}};
CAN_frame SUNGROW_705 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x705,
.data = {0x02, 0x00, 0x01, 0xE6, 0x20, 0x24, 0x05, 0x00}};
CAN_frame SUNGROW_706 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x706,
.data = {0x0E, 0x01, 0x01, 0x01, 0xDE, 0x0C, 0xD5, 0x0C}};
CAN_frame SUNGROW_713 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x713,
.data = {0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x0E, 0x01}};
CAN_frame SUNGROW_714 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x714,
.data = {0x05, 0x01, 0xAC, 0x80, 0x10, 0x02, 0x57, 0x80}};
CAN_frame SUNGROW_715 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x715,
.data = {0x93, 0x80, 0xAC, 0x80, 0x57, 0x80, 0x93, 0x80}};
CAN_frame SUNGROW_716 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x716,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_717 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x717,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_718 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x718,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_719 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x719,
.data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_71A = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x71A,
.data = {0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_71B = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x71B,
.data = {0xBE, 0x8F, 0x61, 0x01, 0xBE, 0x8F, 0x61, 0x01}};
CAN_frame SUNGROW_71C = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x71C,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_71D = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x71D,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SUNGROW_71E = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x71E,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
void update_values_can_inverter() { //This function maps all the values fetched from battery CAN to the inverter CAN messages
//Maxvoltage (eg 400.0V = 4000 , 16bits long)
SUNGROW_701.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
SUNGROW_701.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV >> 8);
//Minvoltage (eg 300.0V = 3000 , 16bits long)
SUNGROW_701.data.u8[2] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
SUNGROW_701.data.u8[3] = (datalayer.battery.info.min_design_voltage_dV >> 8);
//Vcharge request (Maxvoltage-X)
SUNGROW_702.data.u8[0] = ((datalayer.battery.info.max_design_voltage_dV - 20) & 0x00FF);
SUNGROW_702.data.u8[1] = ((datalayer.battery.info.max_design_voltage_dV - 20) >> 8);
//SOH (100.00%)
SUNGROW_702.data.u8[2] = (datalayer.battery.status.soh_pptt & 0x00FF);
SUNGROW_702.data.u8[3] = (datalayer.battery.status.soh_pptt >> 8);
//SOC (100.0%)
SUNGROW_702.data.u8[4] = ((datalayer.battery.status.reported_soc / 10) & 0x00FF);
SUNGROW_702.data.u8[5] = ((datalayer.battery.status.reported_soc / 10) >> 8);
//Capacity max (Wh) TODO: Will overflow if larger than 32kWh
SUNGROW_702.data.u8[6] = (datalayer.battery.info.total_capacity_Wh & 0x00FF);
SUNGROW_702.data.u8[7] = (datalayer.battery.info.total_capacity_Wh >> 8);
// Energy total charged (Wh)
//SUNGROW_703.data.u8[0] =
//SUNGROW_703.data.u8[1] =
//SUNGROW_703.data.u8[2] =
//SUNGROW_703.data.u8[3] =
// Energy total discharged (Wh)
//SUNGROW_703.data.u8[4] =
//SUNGROW_703.data.u8[5] =
//SUNGROW_703.data.u8[6] =
//SUNGROW_703.data.u8[7] =
//Vbat (eg 400.0V = 4000 , 16bits long)
SUNGROW_704.data.u8[0] = (datalayer.battery.status.voltage_dV & 0x00FF);
SUNGROW_704.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8);
//Temperature //TODO: Signed correctly? Also should be put AVG here?
SUNGROW_704.data.u8[6] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SUNGROW_704.data.u8[7] = (datalayer.battery.status.temperature_max_dC >> 8);
//Status bytes?
//SUNGROW_705.data.u8[0] =
//SUNGROW_705.data.u8[1] =
//SUNGROW_705.data.u8[2] =
//SUNGROW_705.data.u8[3] =
//Vbat, again (eg 400.0V = 4000 , 16bits long)
SUNGROW_705.data.u8[5] = (datalayer.battery.status.voltage_dV & 0x00FF);
SUNGROW_705.data.u8[6] = (datalayer.battery.status.voltage_dV >> 8);
//Temperature Max //TODO: Signed correctly?
SUNGROW_706.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SUNGROW_706.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
//Temperature Min //TODO: Signed correctly?
SUNGROW_706.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
SUNGROW_706.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
//Cell voltage max
SUNGROW_706.data.u8[4] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SUNGROW_706.data.u8[5] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//Cell voltage min
SUNGROW_706.data.u8[6] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
SUNGROW_706.data.u8[7] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
//Temperature TODO: Signed correctly?
SUNGROW_713.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SUNGROW_713.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
//Temperature TODO: Signed correctly?
SUNGROW_713.data.u8[2] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SUNGROW_713.data.u8[3] = (datalayer.battery.status.temperature_max_dC >> 8);
//Current module mA (Is whole current OK, or should it be divided/2?) Also signed OK? Scaling?
SUNGROW_713.data.u8[4] = (datalayer.battery.status.current_dA * 10 & 0x00FF);
SUNGROW_713.data.u8[5] = (datalayer.battery.status.current_dA * 10 >> 8);
//Temperature TODO: Signed correctly?
SUNGROW_713.data.u8[6] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SUNGROW_713.data.u8[7] = (datalayer.battery.status.temperature_max_dC >> 8);
//Temperature TODO: Signed correctly?
SUNGROW_714.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SUNGROW_714.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
//Cell voltage
SUNGROW_714.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SUNGROW_714.data.u8[3] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//Current module mA (Is whole current OK, or should it be divided/2?) Also signed OK? Scaling?
SUNGROW_714.data.u8[4] = (datalayer.battery.status.current_dA * 10 & 0x00FF);
SUNGROW_714.data.u8[5] = (datalayer.battery.status.current_dA * 10 >> 8);
//Cell voltage
SUNGROW_714.data.u8[6] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SUNGROW_714.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//Cell voltage
SUNGROW_715.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SUNGROW_715.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//Cell voltage
SUNGROW_715.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SUNGROW_715.data.u8[3] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//Cell voltage
SUNGROW_715.data.u8[4] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SUNGROW_715.data.u8[5] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//Cell voltage
SUNGROW_715.data.u8[6] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SUNGROW_715.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//716-71A, reserved for 8 more modules
//Copy 7## content to 0## messages
for (int i = 0; i < 8; i++) {
SUNGROW_001.data.u8[i] = SUNGROW_701.data.u8[i];
SUNGROW_002.data.u8[i] = SUNGROW_702.data.u8[i];
SUNGROW_003.data.u8[i] = SUNGROW_703.data.u8[i];
SUNGROW_004.data.u8[i] = SUNGROW_704.data.u8[i];
SUNGROW_005.data.u8[i] = SUNGROW_705.data.u8[i];
SUNGROW_006.data.u8[i] = SUNGROW_706.data.u8[i];
SUNGROW_013.data.u8[i] = SUNGROW_713.data.u8[i];
SUNGROW_014.data.u8[i] = SUNGROW_714.data.u8[i];
SUNGROW_015.data.u8[i] = SUNGROW_715.data.u8[i];
SUNGROW_016.data.u8[i] = SUNGROW_716.data.u8[i];
SUNGROW_017.data.u8[i] = SUNGROW_717.data.u8[i];
SUNGROW_018.data.u8[i] = SUNGROW_718.data.u8[i];
SUNGROW_019.data.u8[i] = SUNGROW_719.data.u8[i];
SUNGROW_01A.data.u8[i] = SUNGROW_71A.data.u8[i];
SUNGROW_01B.data.u8[i] = SUNGROW_71B.data.u8[i];
SUNGROW_01C.data.u8[i] = SUNGROW_71C.data.u8[i];
SUNGROW_01D.data.u8[i] = SUNGROW_71D.data.u8[i];
SUNGROW_01E.data.u8[i] = SUNGROW_71E.data.u8[i];
}
//Copy 7## content to 5## messages
for (int i = 0; i < 8; i++) {
SUNGROW_501.data.u8[i] = SUNGROW_701.data.u8[i];
SUNGROW_502.data.u8[i] = SUNGROW_702.data.u8[i];
SUNGROW_503.data.u8[i] = SUNGROW_703.data.u8[i];
SUNGROW_504.data.u8[i] = SUNGROW_704.data.u8[i];
SUNGROW_505.data.u8[i] = SUNGROW_705.data.u8[i];
SUNGROW_506.data.u8[i] = SUNGROW_706.data.u8[i];
}
//Status bytes (TODO: Unknown)
//SUNGROW_100.data.u8[4] =
//SUNGROW_100.data.u8[5] =
//SUNGROW_100.data.u8[6] =
//SUNGROW_100.data.u8[7] =
//SUNGROW_500.data.u8[4] =
//SUNGROW_500.data.u8[5] =
//SUNGROW_500.data.u8[6] =
//SUNGROW_500.data.u8[7] =
//SUNGROW_400.data.u8[4] =
//SUNGROW_400.data.u8[5] =
//SUNGROW_400.data.u8[6] =
//SUNGROW_400.data.u8[7] =
#ifdef DEBUG_VIA_USB
if (inverter_sends_000) {
Serial.println("Inverter sends 0x000");
}
#endif
}
void map_can_frame_to_variable_inverter(CAN_frame rx_frame) {
switch (rx_frame.ID) { //In here we need to respond to the inverter
case 0x000:
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
inverter_sends_000 = true;
transmit_can_frame(&SUNGROW_001, can_config.inverter);
transmit_can_frame(&SUNGROW_002, can_config.inverter);
transmit_can_frame(&SUNGROW_003, can_config.inverter);
transmit_can_frame(&SUNGROW_004, can_config.inverter);
transmit_can_frame(&SUNGROW_005, can_config.inverter);
transmit_can_frame(&SUNGROW_006, can_config.inverter);
transmit_can_frame(&SUNGROW_013, can_config.inverter);
transmit_can_frame(&SUNGROW_014, can_config.inverter);
transmit_can_frame(&SUNGROW_015, can_config.inverter);
transmit_can_frame(&SUNGROW_016, can_config.inverter);
transmit_can_frame(&SUNGROW_017, can_config.inverter);
transmit_can_frame(&SUNGROW_018, can_config.inverter);
transmit_can_frame(&SUNGROW_019, can_config.inverter);
transmit_can_frame(&SUNGROW_01A, can_config.inverter);
transmit_can_frame(&SUNGROW_01B, can_config.inverter);
transmit_can_frame(&SUNGROW_01C, can_config.inverter);
transmit_can_frame(&SUNGROW_01D, can_config.inverter);
transmit_can_frame(&SUNGROW_01E, can_config.inverter);
break;
case 0x100: // SH10RS RUN
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x101: // Both SH10RS / SH15T
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x102: // 250ms - SH10RS init
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x103: // 250ms - SH10RS init
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
mux = rx_frame.data.u8[0];
if (mux == 0) {
// Version number byte1-7 (e.g. @23A229)
version_char[0] = rx_frame.data.u8[1];
version_char[1] = rx_frame.data.u8[2];
version_char[2] = rx_frame.data.u8[3];
version_char[3] = rx_frame.data.u8[4];
version_char[4] = rx_frame.data.u8[5];
version_char[5] = rx_frame.data.u8[6];
version_char[6] = rx_frame.data.u8[7];
}
if (mux == 1) {
// Version number byte1-7 continued (e.g 2795)
version_char[7] = rx_frame.data.u8[1];
version_char[8] = rx_frame.data.u8[2];
version_char[9] = rx_frame.data.u8[3];
version_char[10] = rx_frame.data.u8[4];
version_char[11] = rx_frame.data.u8[5];
version_char[12] = rx_frame.data.u8[6];
version_char[13] = rx_frame.data.u8[7];
}
break;
case 0x104: // 250ms - SH10RS init
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
mux = rx_frame.data.u8[0];
if (mux == 0) {
// Manufacturer byte1-7 (e.g. SUNGROW)
manufacturer_char[0] = rx_frame.data.u8[1];
manufacturer_char[1] = rx_frame.data.u8[2];
manufacturer_char[2] = rx_frame.data.u8[3];
manufacturer_char[3] = rx_frame.data.u8[4];
manufacturer_char[4] = rx_frame.data.u8[5];
manufacturer_char[5] = rx_frame.data.u8[6];
manufacturer_char[6] = rx_frame.data.u8[7];
}
if (mux == 1) {
// Manufacturer byte1-7 continued (e.g )
manufacturer_char[7] = rx_frame.data.u8[1];
manufacturer_char[8] = rx_frame.data.u8[2];
manufacturer_char[9] = rx_frame.data.u8[3];
manufacturer_char[10] = rx_frame.data.u8[4];
manufacturer_char[11] = rx_frame.data.u8[5];
manufacturer_char[12] = rx_frame.data.u8[6];
manufacturer_char[13] = rx_frame.data.u8[7];
}
break;
case 0x105: // 250ms - SH10RS init
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
mux = rx_frame.data.u8[0];
if (mux == 0) {
// Model byte1-7 (e.g. SH10RT)
model_char[0] = rx_frame.data.u8[1];
model_char[1] = rx_frame.data.u8[2];
model_char[2] = rx_frame.data.u8[3];
model_char[3] = rx_frame.data.u8[4];
model_char[4] = rx_frame.data.u8[5];
model_char[5] = rx_frame.data.u8[6];
model_char[6] = rx_frame.data.u8[7];
}
if (mux == 1) {
// Model byte1-7 continued (e.g )
model_char[7] = rx_frame.data.u8[1];
model_char[8] = rx_frame.data.u8[2];
model_char[9] = rx_frame.data.u8[3];
model_char[10] = rx_frame.data.u8[4];
model_char[11] = rx_frame.data.u8[5];
model_char[12] = rx_frame.data.u8[6];
model_char[13] = rx_frame.data.u8[7];
}
break;
case 0x106: // 250ms - SH10RS RUN
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x151: //Only sent by SH15T (Inverter trying to use BYD CAN)
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
mux = rx_frame.data.u8[0];
if (mux == 0) {
// Manufacturer byte1-7 (e.g. SUNGROW)
manufacturer_char[0] = rx_frame.data.u8[1];
manufacturer_char[1] = rx_frame.data.u8[2];
manufacturer_char[2] = rx_frame.data.u8[3];
manufacturer_char[3] = rx_frame.data.u8[4];
manufacturer_char[4] = rx_frame.data.u8[5];
manufacturer_char[5] = rx_frame.data.u8[6];
manufacturer_char[6] = rx_frame.data.u8[7];
}
if (mux == 1) {
// Manufacturer byte1-7 continued (e.g )
manufacturer_char[7] = rx_frame.data.u8[1];
manufacturer_char[8] = rx_frame.data.u8[2];
manufacturer_char[9] = rx_frame.data.u8[3];
manufacturer_char[10] = rx_frame.data.u8[4];
manufacturer_char[11] = rx_frame.data.u8[5];
manufacturer_char[12] = rx_frame.data.u8[6];
manufacturer_char[13] = rx_frame.data.u8[7];
}
break;
case 0x191: //Only sent by SH15T (Inverter trying to use BYD CAN)
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x00004200: //Only sent by SH15T (Inverter trying to use Pylon CAN)
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x02007F00: //Only sent by SH15T (Inverter trying to use Pylon CAN)
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
default:
break;
}
}
void transmit_can_inverter() {
unsigned long currentMillis = millis();
// Send 1s CAN Message
if (currentMillis - previousMillis500ms >= INTERVAL_500_MS) {
previousMillis500ms = currentMillis;
//Flip flop between two sets, end result is 1s periodic rate
if (alternate) {
transmit_can_frame(&SUNGROW_512, can_config.inverter);
transmit_can_frame(&SUNGROW_501, can_config.inverter);
transmit_can_frame(&SUNGROW_502, can_config.inverter);
transmit_can_frame(&SUNGROW_503, can_config.inverter);
transmit_can_frame(&SUNGROW_504, can_config.inverter);
transmit_can_frame(&SUNGROW_505, can_config.inverter);
transmit_can_frame(&SUNGROW_506, can_config.inverter);
transmit_can_frame(&SUNGROW_500, can_config.inverter);
transmit_can_frame(&SUNGROW_400, can_config.inverter);
alternate = false;
} else {
transmit_can_frame(&SUNGROW_700, can_config.inverter);
transmit_can_frame(&SUNGROW_701, can_config.inverter);
transmit_can_frame(&SUNGROW_702, can_config.inverter);
transmit_can_frame(&SUNGROW_703, can_config.inverter);
transmit_can_frame(&SUNGROW_704, can_config.inverter);
transmit_can_frame(&SUNGROW_705, can_config.inverter);
transmit_can_frame(&SUNGROW_706, can_config.inverter);
transmit_can_frame(&SUNGROW_713, can_config.inverter);
transmit_can_frame(&SUNGROW_714, can_config.inverter);
transmit_can_frame(&SUNGROW_715, can_config.inverter);
transmit_can_frame(&SUNGROW_716, can_config.inverter);
transmit_can_frame(&SUNGROW_717, can_config.inverter);
transmit_can_frame(&SUNGROW_718, can_config.inverter);
transmit_can_frame(&SUNGROW_719, can_config.inverter);
transmit_can_frame(&SUNGROW_71A, can_config.inverter);
transmit_can_frame(&SUNGROW_71B, can_config.inverter);
transmit_can_frame(&SUNGROW_71C, can_config.inverter);
transmit_can_frame(&SUNGROW_71D, can_config.inverter);
transmit_can_frame(&SUNGROW_71E, can_config.inverter);
alternate = true;
}
}
}
void setup_inverter(void) { // Performs one time setup at startup over CAN bus
strncpy(datalayer.system.info.inverter_protocol, "Sungrow SBR064 battery over CAN bus", 63);
datalayer.system.info.inverter_protocol[63] = '\0';
}
#endif

View file

@ -0,0 +1,10 @@
#ifndef SUNGROW_CAN_H
#define SUNGROW_CAN_H
#include "../include.h"
#define CAN_INVERTER_SELECTED
void transmit_can_frame(CAN_frame* tx_frame, int interface);
void setup_inverter(void);
#endif

View file

@ -1,35 +0,0 @@
/* ***********************************************************************
* Uptime library for Arduino boards and compatible systems
* (C) 2019 by Yiannis Bourkelis (https://github.com/YiannisBourkelis/)
*
* This file is part of Uptime library for Arduino boards and compatible systems
*
* Uptime library for Arduino boards and compatible systems is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Uptime library for Arduino boards and compatible systems is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Uptime library for Arduino boards and compatible systems. If not, see <http://www.gnu.org/licenses/>.
* ***********************************************************************/
#include "uptime_formatter.h"
void setup() {
// connect at 115200 so we can read the uptime fast enough
Serial.begin(115200);
}
void loop() {
//uptime_formatter::get_uptime() returns a string
//containing the total device uptime since startup in days, hours, minutes and seconds
Serial.println("up " + uptime_formatter::getUptime());
//wait 1 second
delay(1000);
}

View file

@ -1,56 +0,0 @@
/* ***********************************************************************
* Uptime library for Arduino boards and compatible systems
* (C) 2019 by Yiannis Bourkelis (https://github.com/YiannisBourkelis/)
*
* This file is part of Uptime library for Arduino boards and compatible systems
*
* Uptime library for Arduino boards and compatible systems is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Uptime library for Arduino boards and compatible systems is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Uptime library for Arduino boards and compatible systems. If not, see <http://www.gnu.org/licenses/>.
* ***********************************************************************/
#include "uptime.h"
void setup() {
// connect at 115200 so we can read the uptime fast enough
Serial.begin(115200);
}
void loop() {
//If you do not want to use the string library
//you can get the total uptime variables
//and format the output the way you want.
//First call calculate_uptime() to calculate the uptime
//and then read the uptime variables.
uptime::calculateUptime();
Serial.print("days: ");
Serial.println(uptime::getDays());
Serial.print("hours: ");
Serial.println(uptime::getHours());
Serial.print("minutes: ");
Serial.println(uptime::getMinutes());
Serial.print("seconds: ");
Serial.println(uptime::getSeconds());
Serial.print("milliseconds: ");
Serial.println(uptime::getMilliseconds());
Serial.print("\n");
//wait 1 second
delay(1000);
}

View file

@ -59,19 +59,15 @@ unsigned long uptime::m_remaining_days = 0;
//private variables that in combination hold the actual time passed
//Use the coresponding uptime::get_.... to read these private variables
unsigned long uptime::m_mod_milliseconds;
unsigned long uptime::m_mod_seconds;
unsigned long uptime::m_mod_minutes;
unsigned long uptime::m_mod_hours;
uint8_t uptime::m_mod_seconds;
uint8_t uptime::m_mod_minutes;
uint8_t uptime::m_mod_hours;
uptime::uptime()
{
}
/**** get the actual time passed from device boot time ****/
unsigned long uptime::getMilliseconds()
{
return uptime::m_mod_milliseconds;
}
unsigned long uptime::getSeconds()
{
return uptime::m_mod_seconds;

View file

@ -47,7 +47,6 @@ class uptime
static void calculateUptime();
static unsigned long getMilliseconds();
static unsigned long getSeconds();
static unsigned long getMinutes();
static unsigned long getHours();
@ -61,9 +60,9 @@ class uptime
static unsigned long m_days;
static unsigned long m_mod_milliseconds;
static unsigned long m_mod_seconds;
static unsigned long m_mod_minutes;
static unsigned long m_mod_hours;
static uint8_t m_mod_seconds;
static uint8_t m_mod_minutes;
static uint8_t m_mod_hours;
static unsigned long m_last_milliseconds;
static unsigned long m_remaining_seconds;

View file

@ -36,11 +36,3 @@ String uptime_formatter::getUptime()
(String)(uptime::getMinutes()) + " minutes, " +
(String)(uptime::getSeconds()) + " seconds";
}
//returns the actual time passed since device boot
//in the format: x days, y hours, z minutes, s seconds, n milliseconds
String uptime_formatter::getUptimeWithMillis()
{
return uptime_formatter::getUptime() + ", " +
(String)(uptime::getMilliseconds()) + " milliseconds";
}

View file

@ -31,6 +31,5 @@ class uptime_formatter
uptime_formatter();
static String getUptime();
static String getUptimeWithMillis();
};
#endif

View file

@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

View file

@ -1,40 +0,0 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
.docusaurus
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
node_modules
.DS_Store
.vscode
/build
/portal
.pio

View file

@ -1,30 +0,0 @@
Modifying HTML for the ElegantOTA Library
This guide provides the necessary steps to update and modify the HTML used in the ElegantOTA library.
Follow these steps carefully to ensure that your changes are properly integrated.
Steps to Modify the HTML:
1 - Edit the CurrentPlainHTML.txt:
Locate the file CurrentPlainHTML.txt in your project directory.
Modify the HTML content as needed. This file contains the plain HTML code that will be served through the OTA interface.
Convert the HTML to GZIP and Decimal Format:
Copy the content of the updated CurrentPlainHTML.txt.
Navigate to the CyberChef tool for encoding and compression.
Apply the following recipe:
Gzip with the "Dynamic Huffman Coding" option enabled.
Convert to Decimal with a comma separator.
Use this link for the process: https://gchq.github.io/CyberChef/#recipe=Gzip('Dynamic%20Huffman%20Coding','','',false)To_Decimal('Comma',false)
2 - Update the ELEGANT_HTML Array:
Copy the resulting decimal output from CyberChef.
Replace the existing content of the ELEGANT_HTML array in elop.cpp with the new decimal data from CyberChef.
3 - Adjust the ELEGANT_HTML Array Size:
After updating the ELEGANT_HTML array in both elop.h and elop.cpp, update the array size to match the length of the new output from CyberChef.
Ensure that the array size reflects the new length of the compressed HTML to avoid errors during compilation.

View file

@ -64,8 +64,8 @@ _____ _ _ ___ _____ _
#include "Update.h"
#include "StreamString.h"
#if ELEGANTOTA_USE_ASYNC_WEBSERVER == 1
#include "../../me-no-dev-AsyncTCP/src/AsyncTCP.h"
#include "../../me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include "../../mathieucarbou-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#define ELEGANTOTA_WEBSERVER AsyncWebServer
#else
#include "WiFi.h"

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_ETHERNET_H
#define _MODBUS_BRIDGE_ETHERNET_H
#include "options.h"
#if HAS_ETHERNET == 1
#include <Ethernet.h>
#include <SPI.h>
#undef SERVER_END
#define SERVER_END // NIL for Ethernet
#include "ModbusServerTCPtemp.h"
#include "ModbusBridgeTemp.h"
using ModbusBridgeEthernet = ModbusBridge<ModbusServerTCP<EthernetServer, EthernetClient>>;
#endif
#endif

View file

@ -1,14 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_RTU_H
#define _MODBUS_BRIDGE_RTU_H
#include "options.h"
#include "ModbusServerRTU.h"
#include "ModbusBridgeTemp.h"
#include "RTUutils.h"
using ModbusBridgeRTU = ModbusBridge<ModbusServerRTU>;
#endif

View file

@ -1,199 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_TEMP_H
#define _MODBUS_BRIDGE_TEMP_H
#include <map>
#include <functional>
#include "ModbusClient.h"
#include "ModbusClientTCP.h" // Needed for client.setTarget()
#include "RTUutils.h" // Needed for RTScallback
#undef LOCAL_LOG_LEVEL
#define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
using std::bind;
using std::placeholders::_1;
// Known server types: TCP (client, host/port) and RTU (client)
enum ServerType : uint8_t { TCP_SERVER, RTU_SERVER };
// Bridge class template, takes one of ModbusServerRTU, ModbusServerWiFi, ModbusServerEthernet or ModbusServerTCPasync as parameter
template<typename SERVERCLASS>
class ModbusBridge : public SERVERCLASS {
public:
// Constructor for TCP server variants.
ModbusBridge();
// Constructors for the RTU variant. Parameters as are for ModbusServerRTU
ModbusBridge(uint32_t timeout, int rtsPin = -1);
ModbusBridge(uint32_t timeout, RTScallback rts);
// Destructor
~ModbusBridge();
// Method to link external servers to the bridge
bool attachServer(uint8_t aliasID, uint8_t serverID, uint8_t functionCode, ModbusClient *client, IPAddress host = IPAddress(0, 0, 0, 0), uint16_t port = 0);
// Link another function code to the server
bool addFunctionCode(uint8_t aliasID, uint8_t functionCode);
// Block a function code (respond with ILLEGAL_FUNCTION error)
bool denyFunctionCode(uint8_t aliasID, uint8_t functionCode);
protected:
// ServerData holds all data necessary to address a single server
struct ServerData {
uint8_t serverID; // External server id
ModbusClient *client; // client to be used to request the server
ServerType serverType; // TCP_SERVER or RTU_SERVER
IPAddress host; // TCP: host IP address, else 0.0.0.0
uint16_t port; // TCP: host port number, else 0
// RTU constructor
ServerData(uint8_t sid, ModbusClient *c) :
serverID(sid),
client(c),
serverType(RTU_SERVER),
host(IPAddress(0, 0, 0, 0)),
port(0) {}
// TCP constructor
ServerData(uint8_t sid, ModbusClient *c, IPAddress h, uint16_t p) :
serverID(sid),
client(c),
serverType(TCP_SERVER),
host(h),
port(p) {}
};
// Default worker functions
ModbusMessage bridgeWorker(ModbusMessage msg);
ModbusMessage bridgeDenyWorker(ModbusMessage msg);
// Map of servers attached
std::map<uint8_t, ServerData *> servers;
};
// Constructor for TCP variants
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::ModbusBridge() :
SERVERCLASS() { }
// Constructors for RTU variant
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::ModbusBridge(uint32_t timeout, int rtsPin) :
SERVERCLASS(timeout, rtsPin) { }
// Alternate constructors for RTU variant
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::ModbusBridge(uint32_t timeout, RTScallback rts) :
SERVERCLASS(timeout, rts) { }
// Destructor
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::~ModbusBridge() {
// Release ServerData storage in servers array
for (auto itr = servers.begin(); itr != servers.end(); itr++) {
delete (itr->second);
}
servers.clear();
}
// attachServer: memorize the access data for an external server with ID serverID under bridge ID aliasID
template<typename SERVERCLASS>
bool ModbusBridge<SERVERCLASS>::attachServer(uint8_t aliasID, uint8_t serverID, uint8_t functionCode, ModbusClient *client, IPAddress host, uint16_t port) {
// Is there already an entry for the aliasID?
if (servers.find(aliasID) == servers.end()) {
// No. Store server data in map.
// Do we have a port number?
if (port != 0) {
// Yes. Must be a TCP client
servers[aliasID] = new ServerData(serverID, static_cast<ModbusClient *>(client), host, port);
LOG_D("(TCP): %02X->%02X %d.%d.%d.%d:%d\n", aliasID, serverID, host[0], host[1], host[2], host[3], port);
} else {
// No - RTU client required
servers[aliasID] = new ServerData(serverID, static_cast<ModbusClient *>(client));
LOG_D("(RTU): %02X->%02X\n", aliasID, serverID);
}
}
// Register the server/FC combination for the bridgeWorker
addFunctionCode(aliasID, functionCode);
return true;
}
template<typename SERVERCLASS>
bool ModbusBridge<SERVERCLASS>::addFunctionCode(uint8_t aliasID, uint8_t functionCode) {
// Is there already an entry for the aliasID?
if (servers.find(aliasID) != servers.end()) {
// Yes. Link server to own worker function
this->registerWorker(aliasID, functionCode, std::bind(&ModbusBridge<SERVERCLASS>::bridgeWorker, this, std::placeholders::_1));
LOG_D("FC %02X added for server %02X\n", functionCode, aliasID);
} else {
LOG_E("Server %d not attached to bridge!\n", aliasID);
return false;
}
return true;
}
template<typename SERVERCLASS>
bool ModbusBridge<SERVERCLASS>::denyFunctionCode(uint8_t aliasID, uint8_t functionCode) {
// Is there already an entry for the aliasID?
if (servers.find(aliasID) != servers.end()) {
// Yes. Link server to own worker function
this->registerWorker(aliasID, functionCode, std::bind(&ModbusBridge<SERVERCLASS>::bridgeDenyWorker, this, std::placeholders::_1));
LOG_D("FC %02X blocked for server %02X\n", functionCode, aliasID);
} else {
LOG_E("Server %d not attached to bridge!\n", aliasID);
return false;
}
return true;
}
// bridgeWorker: default worker function to process bridge requests
template<typename SERVERCLASS>
ModbusMessage ModbusBridge<SERVERCLASS>::bridgeWorker(ModbusMessage msg) {
uint8_t aliasID = msg.getServerID();
uint8_t functionCode = msg.getFunctionCode();
ModbusMessage response;
// Find the (alias) serverID
if (servers.find(aliasID) != servers.end()) {
// Found it. We may use servers[aliasID] now without allocating a new map slot
// Set real target server ID
msg.setServerID(servers[aliasID]->serverID);
// Issue the request
LOG_D("Request (%02X/%02X) sent\n", servers[aliasID]->serverID, functionCode);
// TCP servers have a target host/port that needs to be set in the client
if (servers[aliasID]->serverType == TCP_SERVER) {
response = reinterpret_cast<ModbusClientTCP *>(servers[aliasID]->client)->syncRequestMT(msg, (uint32_t)millis(), servers[aliasID]->host, servers[aliasID]->port);
} else {
response = servers[aliasID]->client->syncRequestM(msg, (uint32_t)millis());
}
// Re-set the requested server ID
response.setServerID(aliasID);
} else {
// If we get here, something has gone wrong internally. We send back an error response anyway.
response.setError(aliasID, functionCode, INVALID_SERVER);
}
return response;
}
// bridgeDenyWorker: worker function to block function codes
template<typename SERVERCLASS>
ModbusMessage ModbusBridge<SERVERCLASS>::bridgeDenyWorker(ModbusMessage msg) {
ModbusMessage response;
response.setError(msg.getServerID(), msg.getFunctionCode(), ILLEGAL_FUNCTION);
return response;
}
#endif

View file

@ -1,18 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_WIFI_H
#define _MODBUS_BRIDGE_WIFI_H
#include "options.h"
#include <WiFi.h>
#undef SERVER_END
#define SERVER_END server.end();
#include "ModbusServerTCPtemp.h"
#include "ModbusBridgeTemp.h"
using ModbusBridgeWiFi = ModbusBridge<ModbusServerTCP<WiFiServer, WiFiClient>>;
#endif

View file

@ -1,103 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusClient.h"
#undef LOCAL_LOG_LEVEL
#include "Logging.h"
uint16_t ModbusClient::instanceCounter = 0;
// Default constructor: set the default timeout to 2000ms, zero out all other
ModbusClient::ModbusClient() :
messageCount(0),
errorCount(0),
#if HAS_FREERTOS
worker(NULL),
#elif IS_LINUX
worker(0),
#endif
onData(nullptr),
onError(nullptr),
onResponse(nullptr) { instanceCounter++; }
// onDataHandler: register callback for data responses
bool ModbusClient::onDataHandler(MBOnData handler) {
if (onData) {
LOG_W("onData handler was already claimed\n");
} else if (onResponse) {
LOG_E("onData handler is unavailable with an onResponse handler\n");
return false;
}
onData = handler;
return true;
}
// onErrorHandler: register callback for error responses
bool ModbusClient::onErrorHandler(MBOnError handler) {
if (onError) {
LOG_W("onError handler was already claimed\n");
} else if (onResponse) {
LOG_E("onError handler is unavailable with an onResponse handler\n");
return false;
}
onError = handler;
return true;
}
// onResponseHandler: register callback for error responses
bool ModbusClient::onResponseHandler(MBOnResponse handler) {
if (onError || onData) {
LOG_E("onResponse handler is unavailable with an onData or onError handler\n");
return false;
}
onResponse = handler;
return true;
}
// getMessageCount: return message counter value
uint32_t ModbusClient::getMessageCount() {
return messageCount;
}
// getErrorCount: return error counter value
uint32_t ModbusClient::getErrorCount() {
return errorCount;
}
// resetCounts: Set both message and error counts to zero
void ModbusClient::resetCounts() {
{
LOCK_GUARD(cntLock, countAccessM);
messageCount = 0;
errorCount = 0;
}
}
// waitSync: wait for response on syncRequest to arrive
ModbusMessage ModbusClient::waitSync(uint8_t serverID, uint8_t functionCode, uint32_t token) {
ModbusMessage response;
unsigned long lostPatience = millis();
// Default response is TIMEOUT
response.setError(serverID, functionCode, TIMEOUT);
// Loop 60 seconds, if unlucky
while (millis() - lostPatience < 60000) {
{
LOCK_GUARD(lg, syncRespM);
// Look for the token
auto sR = syncResponse.find(token);
// Is it there?
if (sR != syncResponse.end()) {
// Yes. get the response, delete it from the map and return
response = sR->second;
syncResponse.erase(sR);
break;
}
}
// Give the watchdog time to act
delay(10);
}
return response;
}

View file

@ -1,119 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_CLIENT_H
#define _MODBUS_CLIENT_H
#include <functional>
#include <map>
#include "options.h"
#include "ModbusMessage.h"
#if HAS_FREERTOS
extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
}
#elif IS_LINUX
#include <pthread.h>
#endif
#if USE_MUTEX
#include <mutex> // NOLINT
using std::mutex;
using std::lock_guard;
#endif
typedef std::function<void(ModbusMessage msg, uint32_t token)> MBOnData;
typedef std::function<void(Modbus::Error errorCode, uint32_t token)> MBOnError;
typedef std::function<void(ModbusMessage msg, uint32_t token)> MBOnResponse;
class ModbusClient {
public:
bool onDataHandler(MBOnData handler); // Accept onData handler
bool onErrorHandler(MBOnError handler); // Accept onError handler
bool onResponseHandler(MBOnResponse handler); // Accept onResponse handler
uint32_t getMessageCount(); // Informative: return number of messages created
uint32_t getErrorCount(); // Informative: return number of errors received
void resetCounts(); // Set both message and error counts to zero
inline Error addRequest(ModbusMessage m, uint32_t token) { return addRequestM(m, token); }
inline ModbusMessage syncRequest(ModbusMessage m, uint32_t token) { return syncRequestM(m, token); }
// Template function to generate syncRequest functions as long as there is a
// matching ModbusMessage::setMessage() call
template <typename... Args>
ModbusMessage syncRequest(uint32_t token, Args&&... args) {
Error rc = SUCCESS;
// Create request, if valid
ModbusMessage m;
rc = m.setMessage(std::forward<Args>(args) ...);
// Add it to the queue and wait for a response, if valid
if (rc == SUCCESS) {
return syncRequestM(m, token);
}
// Else return the error as a message
return buildErrorMsg(rc, std::forward<Args>(args) ...);
}
// Template function to create an error response message from a variadic pattern
template <typename... Args>
ModbusMessage buildErrorMsg(Error e, uint8_t serverID, uint8_t functionCode, Args&&... args) {
ModbusMessage m;
m.setError(serverID, functionCode, e);
return m;
}
// Template function to generate addRequest functions as long as there is a
// matching ModbusMessage::setMessage() call
template <typename... Args>
Error addRequest(uint32_t token, Args&&... args) {
Error rc = SUCCESS; // Return value
// Create request, if valid
ModbusMessage m;
rc = m.setMessage(std::forward<Args>(args) ...);
// Add it to the queue, if valid
if (rc == SUCCESS) {
return addRequestM(m, token);
}
// Else return the error
return rc;
}
protected:
ModbusClient(); // Default constructor
virtual void isInstance() = 0; // Make class abstract
ModbusMessage waitSync(uint8_t serverID, uint8_t functionCode, uint32_t token); // wait for syncRequest response to arrive
// Virtual addRequest variant needed internally. All others done by template!
virtual Error addRequestM(ModbusMessage msg, uint32_t token) = 0;
// Virtual syncRequest variant following the same pattern
virtual ModbusMessage syncRequestM(ModbusMessage msg, uint32_t token) = 0;
// Prevent copy construction or assignment
ModbusClient(ModbusClient& other) = delete;
ModbusClient& operator=(ModbusClient& other) = delete;
uint32_t messageCount; // Number of requests generated. Used for transactionID in TCPhead
uint32_t errorCount; // Number of errors received
#if HAS_FREERTOS
TaskHandle_t worker; // Interface instance worker task
#elif IS_LINUX
pthread_t worker;
#endif
MBOnData onData; // Data response handler
MBOnError onError; // Error response handler
MBOnResponse onResponse; // Uniform response handler
static uint16_t instanceCounter; // Number of ModbusClients created
std::map<uint32_t, ModbusMessage> syncResponse; // Map to hold response messages on synchronous requests
#if USE_MUTEX
std::mutex syncRespM; // Mutex protecting syncResponse map against race conditions
std::mutex countAccessM; // Mutex protecting access to the message and error counts
#endif
// Let any ModbusBridge class use protected members
template<typename SERVERCLASS> friend class ModbusBridge;
};
#endif

View file

@ -1,354 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusClientRTU.h"
#if HAS_FREERTOS
#undef LOCAL_LOG_LEVEL
// #define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
// Constructor takes an optional DE/RE pin and queue size
ModbusClientRTU::ModbusClientRTU(int8_t rtsPin, uint16_t queueLimit) :
ModbusClient(),
MR_serial(nullptr),
MR_lastMicros(micros()),
MR_interval(2000),
MR_rtsPin(rtsPin),
MR_qLimit(queueLimit),
MR_timeoutValue(DEFAULTTIMEOUT),
MR_useASCII(false),
MR_skipLeadingZeroByte(false) {
if (MR_rtsPin >= 0) {
pinMode(MR_rtsPin, OUTPUT);
MTRSrts = [this](bool level) {
digitalWrite(MR_rtsPin, level);
};
MTRSrts(LOW);
} else {
MTRSrts = RTUutils::RTSauto;
}
}
// Alternative constructor takes an RTS callback function
ModbusClientRTU::ModbusClientRTU(RTScallback rts, uint16_t queueLimit) :
ModbusClient(),
MR_serial(nullptr),
MR_lastMicros(micros()),
MR_interval(2000),
MTRSrts(rts),
MR_qLimit(queueLimit),
MR_timeoutValue(DEFAULTTIMEOUT),
MR_useASCII(false),
MR_skipLeadingZeroByte(false) {
MR_rtsPin = -1;
MTRSrts(LOW);
}
// Destructor: clean up queue, task etc.
ModbusClientRTU::~ModbusClientRTU() {
// Kill worker task and clean up request queue
end();
}
// begin: start worker task - general version
void ModbusClientRTU::begin(Stream& serial, uint32_t baudRate, int coreID, uint32_t userInterval) {
MR_serial = &serial;
doBegin(baudRate, coreID, userInterval);
}
// begin: start worker task - HardwareSerial version
void ModbusClientRTU::begin(HardwareSerial& serial, int coreID, uint32_t userInterval) {
MR_serial = &serial;
uint32_t baudRate = serial.baudRate();
serial.setRxFIFOFull(1);
doBegin(baudRate, coreID, userInterval);
}
void ModbusClientRTU::doBegin(uint32_t baudRate, int coreID, uint32_t userInterval) {
// Task already running? End it in case
end();
// Pull down RTS toggle, if necessary
MTRSrts(LOW);
// Set minimum interval time
MR_interval = RTUutils::calculateInterval(baudRate);
// If user defined interval is longer, use that
if (MR_interval < userInterval) {
MR_interval = userInterval;
}
// Create unique task name
char taskName[18];
snprintf(taskName, 18, "Modbus%02XRTU", instanceCounter);
// Start task to handle the queue
xTaskCreatePinnedToCore((TaskFunction_t)&handleConnection, taskName, CLIENT_TASK_STACK, this, 6, &worker, coreID >= 0 ? coreID : NULL);
LOG_D("Client task %d started. Interval=%d\n", (uint32_t)worker, MR_interval);
}
// end: stop worker task
void ModbusClientRTU::end() {
if (worker) {
// Clean up queue
{
// Safely lock access
LOCK_GUARD(lockGuard, qLock);
// Get all queue entries one by one
while (!requests.empty()) {
// Remove front entry
requests.pop();
}
}
// Kill task
vTaskDelete(worker);
LOG_D("Client task %d killed.\n", (uint32_t)worker);
worker = nullptr;
}
}
// setTimeOut: set/change the default interface timeout
void ModbusClientRTU::setTimeout(uint32_t TOV) {
MR_timeoutValue = TOV;
LOG_D("Timeout set to %d\n", TOV);
}
// Toggle protocol to ModbusASCII
void ModbusClientRTU::useModbusASCII(unsigned long timeout) {
MR_useASCII = true;
MR_timeoutValue = timeout; // Switch timeout to ASCII's value
LOG_D("Protocol mode: ASCII\n");
}
// Toggle protocol to ModbusRTU
void ModbusClientRTU::useModbusRTU() {
MR_useASCII = false;
LOG_D("Protocol mode: RTU\n");
}
// Inquire protocol mode
bool ModbusClientRTU::isModbusASCII() {
return MR_useASCII;
}
// Toggle skipping of leading 0x00 byte
void ModbusClientRTU::skipLeading0x00(bool onOff) {
MR_skipLeadingZeroByte = onOff;
LOG_D("Skip leading 0x00 mode = %s\n", onOff ? "ON" : "OFF");
}
// Return number of unprocessed requests in queue
uint32_t ModbusClientRTU::pendingRequests() {
return requests.size();
}
// Remove all pending request from queue
void ModbusClientRTU::clearQueue()
{
std::queue<RequestEntry> empty;
LOCK_GUARD(lockGuard, qLock);
std::swap(requests, empty);
}
// Base addRequest taking a preformatted data buffer and length as parameters
Error ModbusClientRTU::addRequestM(ModbusMessage msg, uint32_t token) {
Error rc = SUCCESS; // Return value
LOG_D("request for %02X/%02X\n", msg.getServerID(), msg.getFunctionCode());
// Add it to the queue, if valid
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg)) {
// No. Return error after deleting the allocated request.
rc = REQUEST_QUEUE_FULL;
}
}
LOG_D("RC=%02X\n", rc);
return rc;
}
// Base syncRequest follows the same pattern
ModbusMessage ModbusClientRTU::syncRequestM(ModbusMessage msg, uint32_t token) {
ModbusMessage response;
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg, true)) {
// No. Return error after deleting the allocated request.
response.setError(msg.getServerID(), msg.getFunctionCode(), REQUEST_QUEUE_FULL);
} else {
// Request is queued - wait for the result.
response = waitSync(msg.getServerID(), msg.getFunctionCode(), token);
}
} else {
response.setError(msg.getServerID(), msg.getFunctionCode(), EMPTY_MESSAGE);
}
return response;
}
// addBroadcastMessage: create a fire-and-forget message to all servers on the RTU bus
Error ModbusClientRTU::addBroadcastMessage(const uint8_t *data, uint8_t len) {
Error rc = SUCCESS; // Return value
LOG_D("Broadcast request of length %d\n", len);
// We do only accept requests with data, 0 byte, data and CRC must fit into 256 bytes.
if (len && len < 254) {
// Create a "broadcast token"
uint32_t token = (millis() & 0xFFFFFF) | 0xBC000000;
ModbusMessage msg;
// Server ID is 0x00 for broadcast
msg.add((uint8_t)0x00);
// Append data
msg.add(data, len);
// Queue add successful?
if (!addToQueue(token, msg)) {
// No. Return error after deleting the allocated request.
rc = REQUEST_QUEUE_FULL;
}
} else {
rc = BROADCAST_ERROR;
}
LOG_D("RC=%02X\n", rc);
return rc;
}
// addToQueue: send freshly created request to queue
bool ModbusClientRTU::addToQueue(uint32_t token, ModbusMessage request, bool syncReq) {
bool rc = false;
// Did we get one?
if (request) {
RequestEntry re(token, request, syncReq);
if (requests.size()<MR_qLimit) {
// Yes. Safely lock queue and push request to queue
rc = true;
LOCK_GUARD(lockGuard, qLock);
requests.push(re);
}
{
LOCK_GUARD(cntLock, countAccessM);
messageCount++;
}
}
LOG_D("RC=%02X\n", rc);
return rc;
}
// handleConnection: worker task
// This was created in begin() to handle the queue entries
void ModbusClientRTU::handleConnection(ModbusClientRTU *instance) {
// initially clean the serial buffer
while (instance->MR_serial->available()) instance->MR_serial->read();
delay(100);
// Loop forever - or until task is killed
while (1) {
// Do we have a reuest in queue?
if (!instance->requests.empty()) {
// Yes. pull it.
RequestEntry request = instance->requests.front();
LOG_D("Pulled request from queue\n");
// Send it via Serial
RTUutils::send(*(instance->MR_serial), instance->MR_lastMicros, instance->MR_interval, instance->MTRSrts, request.msg, instance->MR_useASCII);
LOG_D("Request sent.\n");
// HEXDUMP_V("Data", request.msg.data(), request.msg.size());
// For a broadcast, we will not wait for a response
if (request.msg.getServerID() != 0 || ((request.token & 0xFF000000) != 0xBC000000)) {
// This is a regular request, Get the response - if any
ModbusMessage response = RTUutils::receive(
'C',
*(instance->MR_serial),
instance->MR_timeoutValue,
instance->MR_lastMicros,
instance->MR_interval,
instance->MR_useASCII,
instance->MR_skipLeadingZeroByte);
LOG_D("%s response (%d bytes) received.\n", response.size()>1 ? "Data" : "Error", response.size());
HEXDUMP_V("Data", response.data(), response.size());
// No error in receive()?
if (response.size() > 1) {
// No. Check message contents
// Does the serverID match the requested?
if (request.msg.getServerID() != response.getServerID()) {
// No. Return error response
response.setError(request.msg.getServerID(), request.msg.getFunctionCode(), SERVER_ID_MISMATCH);
// ServerID ok, but does the FC match as well?
} else if (request.msg.getFunctionCode() != (response.getFunctionCode() & 0x7F)) {
// No. Return error response
response.setError(request.msg.getServerID(), request.msg.getFunctionCode(), FC_MISMATCH);
}
} else {
// No, we got an error code from receive()
// Return it as error response
response.setError(request.msg.getServerID(), request.msg.getFunctionCode(), static_cast<Error>(response[0]));
}
LOG_D("Response generated.\n");
HEXDUMP_V("Response packet", response.data(), response.size());
// If we got an error, count it
if (response.getError() != SUCCESS) {
instance->errorCount++;
}
// Was it a synchronous request?
if (request.isSyncRequest) {
// Yes. Put it into the response map
{
LOCK_GUARD(sL, instance->syncRespM);
instance->syncResponse[request.token] = response;
}
// No, an async request. Do we have an onResponse handler?
} else if (instance->onResponse) {
// Yes. Call it
instance->onResponse(response, request.token);
} else {
// No, but we may have onData or onError handlers
// Did we get a normal response?
if (response.getError()==SUCCESS) {
// Yes. Do we have an onData handler registered?
if (instance->onData) {
// Yes. call it
instance->onData(response, request.token);
}
} else {
// No, something went wrong. All we have is an error
// Do we have an onError handler?
if (instance->onError) {
// Yes. Forward the error code to it
instance->onError(response.getError(), request.token);
}
}
}
}
// Clean-up time.
{
// Safely lock the queue
LOCK_GUARD(lockGuard, instance->qLock);
// Remove the front queue entry
instance->requests.pop();
}
} else {
delay(1);
}
}
}
#endif // HAS_FREERTOS

View file

@ -1,111 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_CLIENT_RTU_H
#define _MODBUS_CLIENT_RTU_H
#include "options.h"
#if HAS_FREERTOS
#include "ModbusClient.h"
#include "Stream.h"
#include "RTUutils.h"
#include <queue>
#include <vector>
using std::queue;
#define DEFAULTTIMEOUT 2000
class ModbusClientRTU : public ModbusClient {
public:
// Constructor takes an optional DE/RE pin and queue limit
explicit ModbusClientRTU(int8_t rtsPin = -1, uint16_t queueLimit = 100);
// Alternative Constructor takes an RTS line toggle callback
explicit ModbusClientRTU(RTScallback rts, uint16_t queueLimit = 100);
// Destructor: clean up queue, task etc.
~ModbusClientRTU();
// begin: start worker task
void begin(Stream& serial, uint32_t baudrate, int coreID = -1, uint32_t userInterval = 0);
// Special variant for HardwareSerial
void begin(HardwareSerial& serial, int coreID = -1, uint32_t userInterval = 0);
// end: stop the worker
void end();
// Set default timeout value for interface
void setTimeout(uint32_t TOV);
// Toggle protocol to ModbusASCII
void useModbusASCII(unsigned long timeout = 1000);
// Toggle protocol to ModbusRTU
void useModbusRTU();
// Inquire protocol mode
bool isModbusASCII();
// Toggle skipping of leading 0x00 byte
void skipLeading0x00(bool onOff = true);
// Return number of unprocessed requests in queue
uint32_t pendingRequests();
// Remove all pending request from queue
void clearQueue();
// addBroadcastMessage: create a fire-and-forget message to all servers on the RTU bus
Error addBroadcastMessage(const uint8_t *data, uint8_t len);
protected:
struct RequestEntry {
uint32_t token;
ModbusMessage msg;
bool isSyncRequest;
RequestEntry(uint32_t t, ModbusMessage m, bool syncReq = false) :
token(t),
msg(m),
isSyncRequest(syncReq) {}
};
// Base addRequest and syncRequest must be present
Error addRequestM(ModbusMessage msg, uint32_t token);
ModbusMessage syncRequestM(ModbusMessage msg, uint32_t token);
// addToQueue: send freshly created request to queue
bool addToQueue(uint32_t token, ModbusMessage msg, bool syncReq = false);
// handleConnection: worker task method
static void handleConnection(ModbusClientRTU *instance);
// receive: get response via Serial
ModbusMessage receive(const ModbusMessage request);
// start background task
void doBegin(uint32_t baudRate, int coreID, uint32_t userInterval);
void isInstance() { return; } // make class instantiable
queue<RequestEntry> requests; // Queue to hold requests to be processed
#if USE_MUTEX
mutex qLock; // Mutex to protect queue
#endif
Stream *MR_serial; // Ptr to the serial interface used
unsigned long MR_lastMicros; // Microseconds since last bus activity
uint32_t MR_interval; // Modbus RTU bus quiet time
int8_t MR_rtsPin; // GPIO pin to toggle RS485 DE/RE line. -1 if none.
RTScallback MTRSrts; // RTS line callback function
uint16_t MR_qLimit; // Maximum number of requests to hold in the queue
uint32_t MR_timeoutValue; // Interface default timeout
bool MR_useASCII; // true=ModbusASCII, false=ModbusRTU
bool MR_skipLeadingZeroByte; // true=skip the first byte if it is 0x00, false=accept all bytes
};
#endif // HAS_FREERTOS
#endif // INCLUDE GUARD

View file

@ -1,16 +0,0 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_SERVER_WIFI_H
#define _MODBUS_SERVER_WIFI_H
#include "options.h"
#include <WiFi.h>
#undef SERVER_END
#define SERVER_END server.end();
#include "ModbusServerTCPtemp.h"
using ModbusServerWiFi = ModbusServerTCP<WiFiServer, WiFiClient>;
#endif

View file

@ -0,0 +1,22 @@
{
"name":"AsyncTCPSock",
"description":"Reimplementation of an Asynchronous TCP Library for ESP32, using BSD Sockets",
"keywords":"async,tcp",
"authors":
{
"name": "Alex Villacís Lasso",
"maintainer": true
},
"repository":
{
"type": "git",
"url": "https://github.com/yubox-node-org/AsyncTCPSock.git"
},
"version": "1.0.2-dev",
"license": "LGPL-3.0",
"frameworks": "arduino",
"platforms": "espressif32",
"build": {
"libCompatMode": 2
}
}

View file

@ -0,0 +1,9 @@
name=AsyncTCPSock
version=1.0.2-dev
author=avillacis
maintainer=avillacis
sentence=Reimplemented Async TCP Library for ESP32 using BSD Sockets
paragraph=This is a reimplementation of AsyncTCP (Async TCP Library for ESP32) by Me No Dev, using high-level BSD Sockets instead of the low-level packet API and a message queue.
category=Other
url=https://github.com/yubox-node-org/AsyncTCPSock
architectures=*

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,321 @@
/*
Reimplementation of an asynchronous TCP library for Espressif MCUs, using
BSD sockets.
Copyright (c) 2020 Alex Villacís Lasso.
Original AsyncTCP API Copyright (c) 2016 Hristo Gochkov. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef ASYNCTCP_H_
#define ASYNCTCP_H_
#include "../../../system_settings.h"
#include "../../../devboard/hal/hal.h"
#include "IPAddress.h"
#include "sdkconfig.h"
#include <functional>
#include <deque>
#include <list>
#if ASYNC_TCP_SSL_ENABLED
#include <ssl_client.h>
#include "AsyncTCP_TLS_Context.h"
#endif
extern "C" {
#include "lwip/err.h"
#include "lwip/sockets.h"
}
#define ASYNCTCP_VERSION "1.0.2-dev"
#define ASYNCTCP_VERSION_MAJOR 1
#define ASYNCTCP_VERSION_MINOR 2
#define ASYNCTCP_VERSION_REVISION 2
#define ASYNCTCP_FORK_mathieucarbou
//If core is not defined, then we are running in Arduino or PIO
#ifndef CONFIG_ASYNC_TCP_RUNNING_CORE
#define CONFIG_ASYNC_TCP_RUNNING_CORE WIFI_CORE
#define CONFIG_ASYNC_TCP_USE_WDT 0 //if enabled, adds between 33us and 200us per event
#endif
#ifndef CONFIG_ASYNC_TCP_STACK_SIZE
#define CONFIG_ASYNC_TCP_STACK_SIZE 16384 // 8192 * 2
#endif
#ifndef CONFIG_ASYNC_TCP_STACK
#define CONFIG_ASYNC_TCP_STACK CONFIG_ASYNC_TCP_STACK_SIZE
#endif
#ifndef CONFIG_ASYNC_TCP_PRIORITY
#define CONFIG_ASYNC_TCP_PRIORITY 3
#endif
#ifndef CONFIG_ASYNC_TCP_TASK_PRIORITY
#define CONFIG_ASYNC_TCP_TASK_PRIORITY TASK_CONNECTIVITY_PRIO
#endif
#ifndef CONFIG_ASYNC_TCP_TASK_NAME
#define CONFIG_ASYNC_TCP_TASK_NAME "asyncTcpSock"
#endif
class AsyncClient;
#ifndef CONFIG_ASYNC_TCP_MAX_ACK_TIME
#define CONFIG_ASYNC_TCP_MAX_ACK_TIME 5000
#endif
#ifndef ASYNC_MAX_ACK_TIME
#define ASYNC_MAX_ACK_TIME CONFIG_ASYNC_TCP_MAX_ACK_TIME
#endif
#define ASYNC_WRITE_FLAG_COPY 0x01 //will allocate new buffer to hold the data while sending (else will hold reference to the data given)
#define ASYNC_WRITE_FLAG_MORE 0x02 //will not send PSH flag, meaning that there should be more data to be sent before the application should react.
#define SSL_HANDSHAKE_TIMEOUT 5000 // timeout to complete SSL handshake
typedef std::function<void(void*, AsyncClient*)> AcConnectHandler;
typedef std::function<void(void*, AsyncClient*, size_t len, uint32_t time)> AcAckHandler;
typedef std::function<void(void*, AsyncClient*, int8_t error)> AcErrorHandler;
typedef std::function<void(void*, AsyncClient*, void *data, size_t len)> AcDataHandler;
//typedef std::function<void(void*, AsyncClient*, struct pbuf *pb)> AcPacketHandler;
typedef std::function<void(void*, AsyncClient*, uint32_t time)> AcTimeoutHandler;
class AsyncSocketBase
{
private:
static std::list<AsyncSocketBase *> & _getSocketBaseList(void);
protected:
int _socket = -1;
bool _selected = false;
bool _isdnsfinished = false;
uint32_t _sock_lastactivity = 0;
virtual void _sockIsReadable(void) {} // Action to take on readable socket
virtual bool _sockIsWriteable(void) { return false; } // Action to take on writable socket
virtual void _sockPoll(void) {} // Action to take on idle socket activity poll
virtual void _sockDelayedConnect(void) {} // Action to take on DNS-resolve finished
virtual bool _pendingWrite(void) { return false; } // Test if there is data pending to be written
virtual bool _isServer(void) { return false; } // Will a read from this socket result in one more client?
public:
AsyncSocketBase(void);
virtual ~AsyncSocketBase();
friend void _asynctcpsock_task(void *);
};
class AsyncClient : public AsyncSocketBase
{
public:
AsyncClient(int sockfd = -1);
~AsyncClient();
#if ASYNC_TCP_SSL_ENABLED
bool connect(IPAddress ip, uint16_t port, bool secure = false);
bool connect(const char* host, uint16_t port, bool secure = false);
void setRootCa(const char* rootca, const size_t len);
void setClientCert(const char* cli_cert, const size_t len);
void setClientKey(const char* cli_key, const size_t len);
void setPsk(const char* psk_ident, const char* psk);
#else
bool connect(IPAddress ip, uint16_t port);
bool connect(const char* host, uint16_t port);
#endif // ASYNC_TCP_SSL_ENABLED
void close(bool now = false);
int8_t abort();
bool free();
bool canSend() { return space() > 0; }
size_t space();
size_t add(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY);//add for sending
bool send();
//write equals add()+send()
size_t write(const char* data);
size_t write(const char* data, size_t size, uint8_t apiflags=ASYNC_WRITE_FLAG_COPY); //only when canSend() == true
uint8_t state() { return _conn_state; }
bool connected();
bool freeable();//disconnected or disconnecting
uint32_t getAckTimeout();
void setAckTimeout(uint32_t timeout);//no ACK timeout for the last sent packet in milliseconds
uint32_t getRxTimeout();
void setRxTimeout(uint32_t timeout);//no RX data timeout for the connection in seconds
void setNoDelay(bool nodelay);
bool getNoDelay();
uint32_t getRemoteAddress();
uint16_t getRemotePort();
uint32_t getLocalAddress();
uint16_t getLocalPort();
//compatibility
IPAddress remoteIP();
uint16_t remotePort();
IPAddress localIP();
uint16_t localPort();
void onConnect(AcConnectHandler cb, void* arg = 0); //on successful connect
void onDisconnect(AcConnectHandler cb, void* arg = 0); //disconnected
void onAck(AcAckHandler cb, void* arg = 0); //ack received
void onError(AcErrorHandler cb, void* arg = 0); //unsuccessful connect or error
void onData(AcDataHandler cb, void* arg = 0); //data received
void onTimeout(AcTimeoutHandler cb, void* arg = 0); //ack timeout
void onPoll(AcConnectHandler cb, void* arg = 0); //every 125ms when connected
// The following functions are just for API compatibility and do nothing
size_t ack(size_t len) { return len; }
void ackLater() {}
const char * errorToString(int8_t error);
// const char * stateToString();
protected:
bool _sockIsWriteable(void);
void _sockIsReadable(void);
void _sockPoll(void);
void _sockDelayedConnect(void);
bool _pendingWrite(void);
private:
AcConnectHandler _connect_cb;
void* _connect_cb_arg;
AcConnectHandler _discard_cb;
void* _discard_cb_arg;
AcAckHandler _sent_cb;
void* _sent_cb_arg;
AcErrorHandler _error_cb;
void* _error_cb_arg;
AcDataHandler _recv_cb;
void* _recv_cb_arg;
AcTimeoutHandler _timeout_cb;
void* _timeout_cb_arg;
AcConnectHandler _poll_cb;
void* _poll_cb_arg;
uint32_t _rx_last_packet;
uint32_t _rx_since_timeout;
uint32_t _ack_timeout;
// Used on asynchronous DNS resolving scenario - I do not want to connect()
// from the LWIP thread itself.
struct ip_addr _connect_addr;
uint16_t _connect_port = 0;
//const char * _connect_dnsname = NULL;
#if ASYNC_TCP_SSL_ENABLED
size_t _root_ca_len;
char* _root_ca;
size_t _cli_cert_len;
char* _cli_cert;
size_t _cli_key_len;
char* _cli_key;
bool _secure;
bool _handshake_done;
const char* _psk_ident;
const char* _psk;
String _hostname;
AsyncTCP_TLS_Context * _sslctx;
#endif // ASYNC_TCP_SSL_ENABLED
// The following private struct represents a buffer enqueued with the add()
// method. Each of these buffers are flushed whenever the socket becomes
// writable
typedef struct {
uint8_t * data; // Pointer to data queued for write
uint32_t length; // Length of data queued for write
uint32_t written; // Length of data written to socket so far
uint32_t queued_at;// Timestamp at which this data buffer was queued
uint32_t written_at; // Timestamp at which this data buffer was completely written
int write_errno; // If != 0, errno value while writing this buffer
bool owned; // If true, we malloc'ed the data and should be freed after completely written.
// If false, app owns the memory and should ensure it remains valid until acked
} queued_writebuf;
// Internal struct used to implement sent buffer notification
typedef struct {
uint32_t length;
uint32_t delay;
} notify_writebuf;
// Queue of buffers to write to socket
SemaphoreHandle_t _write_mutex;
std::deque<queued_writebuf> _writeQueue;
bool _ack_timeout_signaled = false;
// Remaining space willing to queue for writing
uint32_t _writeSpaceRemaining;
// Simulation of connection state
uint8_t _conn_state;
void _error(int8_t err);
void _close(void);
void _removeAllCallbacks(void);
bool _flushWriteQueue(void);
void _clearWriteQueue(void);
void _collectNotifyWrittenBuffers(std::deque<notify_writebuf> &, int &);
void _notifyWrittenBuffers(std::deque<notify_writebuf> &, int);
#if ASYNC_TCP_SSL_ENABLED
int _runSSLHandshakeLoop(void);
#endif
friend void _tcpsock_dns_found(const char * name, struct ip_addr * ipaddr, void * arg);
};
#if ASYNC_TCP_SSL_ENABLED
typedef std::function<int(void* arg, const char *filename, uint8_t **buf)> AcSSlFileHandler;
#endif
class AsyncServer : public AsyncSocketBase
{
public:
AsyncServer(IPAddress addr, uint16_t port);
AsyncServer(uint16_t port);
~AsyncServer();
void onClient(AcConnectHandler cb, void* arg);
#if ASYNC_TCP_SSL_ENABLED
// Dummy, so it compiles with ESP Async WebServer library enabled.
void onSslFileRequest(AcSSlFileHandler cb, void* arg) {};
void beginSecure(const char *cert, const char *private_key_file, const char *password) {};
#endif
void begin();
void end();
void setNoDelay(bool nodelay) { _noDelay = nodelay; }
bool getNoDelay() { return _noDelay; }
uint8_t status();
protected:
uint16_t _port;
IPAddress _addr;
bool _noDelay;
AcConnectHandler _connect_cb;
void* _connect_cb_arg;
// Listening socket is readable on incoming connection
void _sockIsReadable(void);
// Mark this class as a server
bool _isServer(void) { return true; }
};
#endif /* ASYNCTCP_H_ */

View file

@ -0,0 +1,6 @@
#ifndef ASYNCTCP_SSL_H_
#define ASYNCTCP_SSL_H_
#include "AsyncTCP_SSL.hpp"
#endif /* ASYNCTCP_SSL_H_ */

View file

@ -0,0 +1,17 @@
#ifndef ASYNCTCP_SSL_HPP
#define ASYNCTCP_SSL_HPP
#ifdef ASYNC_TCP_SSL_ENABLED
#include <AsyncTCP.h>
#define AsyncSSLClient AsyncClient
#define AsyncSSLServer AsyncServer
#define ASYNC_TCP_SSL_VERSION "AsyncTCPSock SSL shim v0.0.1"
#else
#error Compatibility shim requires ASYNC_TCP_SSL_ENABLED to be defined!
#endif
#endif

View file

@ -0,0 +1,346 @@
#include <Arduino.h>
#include <esp32-hal-log.h>
#include <lwip/err.h>
#include <lwip/sockets.h>
#include <lwip/sys.h>
#include <lwip/netdb.h>
#include <mbedtls/sha256.h>
#include <mbedtls/oid.h>
#include "AsyncTCP_TLS_Context.h"
#if ASYNC_TCP_SSL_ENABLED
#if !defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) && !defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED)
# warning "Please configure IDF framework to include mbedTLS -> Enable pre-shared-key ciphersuites and activate at least one cipher"
#else
static const char *pers = "esp32-tls";
static int _handle_error(int err, const char * function, int line)
{
if(err == -30848){
return err;
}
#ifdef MBEDTLS_ERROR_C
char error_buf[100];
mbedtls_strerror(err, error_buf, 100);
log_e("[%s():%d]: (%d) %s", function, line, err, error_buf);
#else
log_e("[%s():%d]: code %d", function, line, err);
#endif
return err;
}
#define handle_error(e) _handle_error(e, __FUNCTION__, __LINE__)
AsyncTCP_TLS_Context::AsyncTCP_TLS_Context(void)
{
mbedtls_ssl_init(&ssl_ctx);
mbedtls_ssl_config_init(&ssl_conf);
mbedtls_ctr_drbg_init(&drbg_ctx);
_socket = -1;
_have_ca_cert = false;
_have_client_cert = false;
_have_client_key = false;
handshake_timeout = 120000;
}
int AsyncTCP_TLS_Context::startSSLClientInsecure(int sck, const char * host_or_ip)
{
return _startSSLClient(sck, host_or_ip,
NULL, 0,
NULL, 0,
NULL, 0,
NULL, NULL,
true);
}
int AsyncTCP_TLS_Context::startSSLClient(int sck, const char * host_or_ip,
const char *pskIdent, const char *psKey)
{
return _startSSLClient(sck, host_or_ip,
NULL, 0,
NULL, 0,
NULL, 0,
pskIdent, psKey,
false);
}
int AsyncTCP_TLS_Context::startSSLClient(int sck, const char * host_or_ip,
const char *rootCABuff,
const char *cli_cert,
const char *cli_key)
{
return startSSLClient(sck, host_or_ip,
(const unsigned char *)rootCABuff, (rootCABuff != NULL) ? strlen(rootCABuff) + 1 : 0,
(const unsigned char *)cli_cert, (cli_cert != NULL) ? strlen(cli_cert) + 1 : 0,
(const unsigned char *)cli_key, (cli_key != NULL) ? strlen(cli_key) + 1 : 0);
}
int AsyncTCP_TLS_Context::startSSLClient(int sck, const char * host_or_ip,
const unsigned char *rootCABuff, const size_t rootCABuff_len,
const unsigned char *cli_cert, const size_t cli_cert_len,
const unsigned char *cli_key, const size_t cli_key_len)
{
return _startSSLClient(sck, host_or_ip,
rootCABuff, rootCABuff_len,
cli_cert, cli_cert_len,
cli_key, cli_key_len,
NULL, NULL,
false);
}
int AsyncTCP_TLS_Context::_startSSLClient(int sck, const char * host_or_ip,
const unsigned char *rootCABuff, const size_t rootCABuff_len,
const unsigned char *cli_cert, const size_t cli_cert_len,
const unsigned char *cli_key, const size_t cli_key_len,
const char *pskIdent, const char *psKey,
bool insecure)
{
int ret;
int enable = 1;
// The insecure flag will skip server certificate validation. Otherwise some
// certificate is required.
if (rootCABuff == NULL && pskIdent == NULL && psKey == NULL && !insecure) {
return -1;
}
#define ROE(x,msg) { if (((x)<0)) { log_e("LWIP Socket config of " msg " failed."); return -1; }}
// ROE(lwip_setsockopt(sck, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)),"SO_RCVTIMEO");
// ROE(lwip_setsockopt(sck, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)),"SO_SNDTIMEO");
ROE(lwip_setsockopt(sck, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(enable)),"TCP_NODELAY");
ROE(lwip_setsockopt(sck, SOL_SOCKET, SO_KEEPALIVE, &enable, sizeof(enable)),"SO_KEEPALIVE");
log_v("Seeding the random number generator");
mbedtls_entropy_init(&entropy_ctx);
ret = mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func,
&entropy_ctx, (const unsigned char *) pers, strlen(pers));
if (ret < 0) {
return handle_error(ret);
}
log_v("Setting up the SSL/TLS structure...");
if ((ret = mbedtls_ssl_config_defaults(&ssl_conf,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
return handle_error(ret);
}
if (insecure) {
mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE);
log_i("WARNING: Skipping SSL Verification. INSECURE!");
} else if (rootCABuff != NULL) {
log_v("Loading CA cert");
mbedtls_x509_crt_init(&ca_cert);
mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED);
ret = mbedtls_x509_crt_parse(&ca_cert, rootCABuff, rootCABuff_len);
_have_ca_cert = true;
mbedtls_ssl_conf_ca_chain(&ssl_conf, &ca_cert, NULL);
if (ret < 0) {
// free the ca_cert in the case parse failed, otherwise, the old ca_cert still in the heap memory, that lead to "out of memory" crash.
_deleteHandshakeCerts();
return handle_error(ret);
}
} else if (pskIdent != NULL && psKey != NULL) {
log_v("Setting up PSK");
// convert PSK from hex to binary
if ((strlen(psKey) & 1) != 0 || strlen(psKey) > 2*MBEDTLS_PSK_MAX_LEN) {
log_e("pre-shared key not valid hex or too long");
return -1;
}
unsigned char psk[MBEDTLS_PSK_MAX_LEN];
size_t psk_len = strlen(psKey)/2;
for (int j=0; j<strlen(psKey); j+= 2) {
char c = psKey[j];
if (c >= '0' && c <= '9') c -= '0';
else if (c >= 'A' && c <= 'F') c -= 'A' - 10;
else if (c >= 'a' && c <= 'f') c -= 'a' - 10;
else return -1;
psk[j/2] = c<<4;
c = psKey[j+1];
if (c >= '0' && c <= '9') c -= '0';
else if (c >= 'A' && c <= 'F') c -= 'A' - 10;
else if (c >= 'a' && c <= 'f') c -= 'a' - 10;
else return -1;
psk[j/2] |= c;
}
// set mbedtls config
ret = mbedtls_ssl_conf_psk(&ssl_conf, psk, psk_len,
(const unsigned char *)pskIdent, strlen(pskIdent));
if (ret != 0) {
log_e("mbedtls_ssl_conf_psk returned %d", ret);
return handle_error(ret);
}
} else {
return -1;
}
if (!insecure && cli_cert != NULL && cli_key != NULL) {
mbedtls_x509_crt_init(&client_cert);
mbedtls_pk_init(&client_key);
log_v("Loading CRT cert");
ret = mbedtls_x509_crt_parse(&client_cert, cli_cert, cli_cert_len);
_have_client_cert = true;
if (ret < 0) {
// free the client_cert in the case parse failed, otherwise, the old client_cert still in the heap memory, that lead to "out of memory" crash.
_deleteHandshakeCerts();
return handle_error(ret);
}
log_v("Loading private key");
#if MBEDTLS_VERSION_NUMBER < 0x03000000
ret = mbedtls_pk_parse_key(&client_key, cli_key, cli_key_len, NULL, 0);
#else
ret = mbedtls_pk_parse_key(&client_key, cli_key, cli_key_len, NULL, 0, mbedtls_ctr_drbg_random, &drbg_ctx);
#endif
_have_client_key = true;
if (ret != 0) {
_deleteHandshakeCerts();
return handle_error(ret);
}
mbedtls_ssl_conf_own_cert(&ssl_conf, &client_cert, &client_key);
}
log_v("Setting hostname for TLS session...");
// Hostname set here should match CN in server certificate
if ((ret = mbedtls_ssl_set_hostname(&ssl_ctx, host_or_ip)) != 0){
_deleteHandshakeCerts();
return handle_error(ret);
}
mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &drbg_ctx);
if ((ret = mbedtls_ssl_setup(&ssl_ctx, &ssl_conf)) != 0) {
_deleteHandshakeCerts();
return handle_error(ret);
}
_socket = sck;
mbedtls_ssl_set_bio(&ssl_ctx, &_socket, mbedtls_net_send, mbedtls_net_recv, NULL );
handshake_start_time = 0;
return 0;
}
int AsyncTCP_TLS_Context::runSSLHandshake(void)
{
int ret, flags;
if (_socket < 0) return -1;
if (handshake_start_time == 0) handshake_start_time = millis();
ret = mbedtls_ssl_handshake(&ssl_ctx);
if (ret != 0) {
// Something happened before SSL handshake could be completed
// Negotiation error, other than socket not readable/writable when required
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
return handle_error(ret);
}
// Handshake is taking too long
if ((millis()-handshake_start_time) > handshake_timeout)
return -1;
// Either MBEDTLS_ERR_SSL_WANT_READ or MBEDTLS_ERR_SSL_WANT_WRITE
return ret;
}
// Handshake completed, validate remote side if required...
if (_have_client_cert && _have_client_key) {
log_d("Protocol is %s Ciphersuite is %s", mbedtls_ssl_get_version(&ssl_ctx), mbedtls_ssl_get_ciphersuite(&ssl_ctx));
if ((ret = mbedtls_ssl_get_record_expansion(&ssl_ctx)) >= 0) {
log_d("Record expansion is %d", ret);
} else {
log_w("Record expansion is unknown (compression)");
}
}
log_v("Verifying peer X.509 certificate...");
if ((flags = mbedtls_ssl_get_verify_result(&ssl_ctx)) != 0) {
char buf[512];
memset(buf, 0, sizeof(buf));
mbedtls_x509_crt_verify_info(buf, sizeof(buf), " ! ", flags);
log_e("Failed to verify peer certificate! verification info: %s", buf);
_deleteHandshakeCerts();
return handle_error(ret);
} else {
log_v("Certificate verified.");
}
_deleteHandshakeCerts();
log_v("Free internal heap after TLS %u", ESP.getFreeHeap());
return 0;
}
int AsyncTCP_TLS_Context::write(const uint8_t *data, size_t len)
{
if (_socket < 0) return -1;
log_v("Writing packet, %d bytes unencrypted...", len);
int ret = mbedtls_ssl_write(&ssl_ctx, data, len);
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret < 0) {
log_v("Handling error %d", ret); //for low level debug
return handle_error(ret);
}
return ret;
}
int AsyncTCP_TLS_Context::read(uint8_t * data, size_t len)
{
int ret = mbedtls_ssl_read(&ssl_ctx, data, len);
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret < 0) {
log_v("Handling error %d", ret); //for low level debug
return handle_error(ret);
}
if (ret > 0) log_v("Read packet, %d out of %d requested bytes...", ret, len);
return ret;
}
void AsyncTCP_TLS_Context::_deleteHandshakeCerts(void)
{
if (_have_ca_cert) {
log_v("Cleaning CA certificate.");
mbedtls_x509_crt_free(&ca_cert);
_have_ca_cert = false;
}
if (_have_client_cert) {
log_v("Cleaning client certificate.");
mbedtls_x509_crt_free(&client_cert);
_have_client_cert = false;
}
if (_have_client_key) {
log_v("Cleaning client certificate key.");
mbedtls_pk_free(&client_key);
_have_client_key = false;
}
}
AsyncTCP_TLS_Context::~AsyncTCP_TLS_Context()
{
_deleteHandshakeCerts();
log_v("Cleaning SSL connection.");
mbedtls_ssl_free(&ssl_ctx);
mbedtls_ssl_config_free(&ssl_conf);
mbedtls_ctr_drbg_free(&drbg_ctx);
mbedtls_entropy_free(&entropy_ctx); // <-- Is this OK to free if mbedtls_entropy_init() has not been called on it?
}
#endif
#endif // ASYNC_TCP_SSL_ENABLED

View file

@ -0,0 +1,79 @@
#pragma once
#if ASYNC_TCP_SSL_ENABLED
#include "mbedtls/version.h"
#include "mbedtls/platform.h"
#if MBEDTLS_VERSION_NUMBER < 0x03000000
#include "mbedtls/net.h"
#else
#include "mbedtls/net_sockets.h"
#endif
#include "mbedtls/debug.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/error.h"
#define ASYNCTCP_TLS_CAN_RETRY(r) (((r) == MBEDTLS_ERR_SSL_WANT_READ) || ((r) == MBEDTLS_ERR_SSL_WANT_WRITE))
#define ASYNCTCP_TLS_EOF(r) (((r) == MBEDTLS_ERR_SSL_CONN_EOF) || ((r) == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY))
class AsyncTCP_TLS_Context
{
private:
// These fields must persist for the life of the encrypted connection, destroyed on
// object destructor.
mbedtls_ssl_context ssl_ctx;
mbedtls_ssl_config ssl_conf;
mbedtls_ctr_drbg_context drbg_ctx;
mbedtls_entropy_context entropy_ctx;
// These allocate memory during handshake but must be freed on either success or failure
mbedtls_x509_crt ca_cert;
mbedtls_x509_crt client_cert;
mbedtls_pk_context client_key;
bool _have_ca_cert;
bool _have_client_cert;
bool _have_client_key;
unsigned long handshake_timeout;
unsigned long handshake_start_time;
int _socket;
int _startSSLClient(int sck, const char * host_or_ip,
const unsigned char *rootCABuff, const size_t rootCABuff_len,
const unsigned char *cli_cert, const size_t cli_cert_len,
const unsigned char *cli_key, const size_t cli_key_len,
const char *pskIdent, const char *psKey,
bool insecure);
// Delete certificates used in handshake
void _deleteHandshakeCerts(void);
public:
AsyncTCP_TLS_Context(void);
virtual ~AsyncTCP_TLS_Context();
int startSSLClientInsecure(int sck, const char * host_or_ip);
int startSSLClient(int sck, const char * host_or_ip,
const char *pskIdent, const char *psKey);
int startSSLClient(int sck, const char * host_or_ip,
const char *rootCABuff,
const char *cli_cert,
const char *cli_key);
int startSSLClient(int sck, const char * host_or_ip,
const unsigned char *rootCABuff, const size_t rootCABuff_len,
const unsigned char *cli_cert, const size_t cli_cert_len,
const unsigned char *cli_key, const size_t cli_key_len);
int runSSLHandshake(void);
int write(const uint8_t *data, size_t len);
int read(uint8_t * data, size_t len);
};
#endif // ASYNC_TCP_SSL_ENABLED

View file

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -0,0 +1,65 @@
{
"name": "ESPAsyncWebServer",
"version": "3.6.0",
"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/mathieucarbou/ESPAsyncWebServer",
"repository": {
"type": "git",
"url": "https://github.com/mathieucarbou/ESPAsyncWebServer.git"
},
"authors": [
{
"name": "Hristo Gochkov"
},
{
"name": "Mathieu Carbou",
"maintainer": true
},
{
"name": "Emil Muratov",
"maintainer": true
}
],
"license": "LGPL-3.0",
"frameworks": "arduino",
"platforms": [
"espressif32",
"espressif8266",
"raspberrypi"
],
"dependencies": [
{
"owner": "mathieucarbou",
"name": "AsyncTCP",
"version": "^3.3.2",
"platforms": "espressif32"
},
{
"owner": "esphome",
"name": "ESPAsyncTCP-esphome",
"version": "^2.0.0",
"platforms": "espressif8266"
},
{
"name": "Hash",
"platforms": "espressif8266"
},
{
"owner": "khoih-prog",
"name": "AsyncTCP_RP2040W",
"version": "^1.2.0",
"platforms": "raspberrypi"
}
],
"export": {
"include": [
"examples",
"src",
"library.json",
"library.properties",
"LICENSE",
"README.md"
]
}
}

View file

@ -0,0 +1,11 @@
name=ESP Async WebServer
includes=ESPAsyncWebServer.h
version=3.6.0
author=Me-No-Dev
maintainer=Mathieu Carbou <mathieu.carbou@gmail.com>
sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040
paragraph=Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc
category=Other
url=https://github.com/mathieucarbou/ESPAsyncWebServer
architectures=*
license=LGPL-3.0

View file

@ -0,0 +1,435 @@
/*
Asynchronous WebServer library for Espressif MCUs
Copyright (c) 2016 Hristo Gochkov. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "Arduino.h"
#include <rom/ets_sys.h>
#include "AsyncEventSource.h"
#define ASYNC_SSE_NEW_LINE_CHAR (char)0xa
using namespace asyncsrv;
static String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect) {
String str;
size_t len{0};
if (message)
len += strlen(message);
if (event)
len += strlen(event);
len += 42; // give it some overhead
str.reserve(len);
if (reconnect) {
str += T_retry_;
str += reconnect;
str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
}
if (id) {
str += T_id__;
str += id;
str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
}
if (event != NULL) {
str += T_event_;
str += event;
str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
}
if (!message)
return str;
size_t messageLen = strlen(message);
char* lineStart = (char*)message;
char* lineEnd;
do {
char* nextN = strchr(lineStart, '\n');
char* nextR = strchr(lineStart, '\r');
if (nextN == NULL && nextR == NULL) {
// a message is a single-line string
str += T_data_;
str += message;
str += T_nn;
return str;
}
// a message is a multi-line string
char* nextLine = NULL;
if (nextN != NULL && nextR != NULL) { // windows line-ending \r\n
if (nextR + 1 == nextN) {
// normal \r\n sequense
lineEnd = nextR;
nextLine = nextN + 1;
} else {
// some abnormal \n \r mixed sequence
lineEnd = std::min(nextR, nextN);
nextLine = lineEnd + 1;
}
} else if (nextN != NULL) { // Unix/Mac OS X LF
lineEnd = nextN;
nextLine = nextN + 1;
} else { // some ancient garbage
lineEnd = nextR;
nextLine = nextR + 1;
}
str += T_data_;
str.concat(lineStart, lineEnd - lineStart);
str += ASYNC_SSE_NEW_LINE_CHAR; // \n
lineStart = nextLine;
} while (lineStart < ((char*)message + messageLen));
// append another \n to terminate message
str += ASYNC_SSE_NEW_LINE_CHAR; // '\n'
return str;
}
// Message
size_t AsyncEventSourceMessage::ack(size_t len, __attribute__((unused)) uint32_t time) {
// If the whole message is now acked...
if (_acked + len > _data->length()) {
// Return the number of extra bytes acked (they will be carried on to the next message)
const size_t extra = _acked + len - _data->length();
_acked = _data->length();
return extra;
}
// Return that no extra bytes left.
_acked += len;
return 0;
}
size_t AsyncEventSourceMessage::write(AsyncClient* client) {
if (!client)
return 0;
if (_sent >= _data->length() || !client->canSend()) {
return 0;
}
size_t len = std::min(_data->length() - _sent, client->space());
/*
add() would call lwip's tcp_write() under the AsyncTCP hood with apiflags argument.
By default apiflags=ASYNC_WRITE_FLAG_COPY
we could have used apiflags with this flag unset to pass data by reference and avoid copy to socket buffer,
but looks like it does not work for Arduino's lwip in ESP32/IDF
it is enforced in https://github.com/espressif/esp-lwip/blob/0606eed9d8b98a797514fdf6eabb4daf1c8c8cd9/src/core/tcp_out.c#L422C5-L422C30
if LWIP_NETIF_TX_SINGLE_PBUF is set, and it is set indeed in IDF
https://github.com/espressif/esp-idf/blob/a0f798cfc4bbd624aab52b2c194d219e242d80c1/components/lwip/port/include/lwipopts.h#L744
So let's just keep it enforced ASYNC_WRITE_FLAG_COPY and keep in mind that there is no zero-copy
*/
size_t written = client->add(_data->c_str() + _sent, len, ASYNC_WRITE_FLAG_COPY); // ASYNC_WRITE_FLAG_MORE
_sent += written;
return written;
}
size_t AsyncEventSourceMessage::send(AsyncClient* client) {
size_t sent = write(client);
return sent && client->send() ? sent : 0;
}
// Client
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server)
: _client(request->client()), _server(server) {
if (request->hasHeader(T_Last_Event_ID))
_lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str());
_client->setRxTimeout(0);
_client->onError(NULL, NULL);
_client->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; static_cast<AsyncEventSourceClient*>(r)->_onAck(len, time); }, this);
_client->onPoll([](void* r, AsyncClient* c) { (void)c; static_cast<AsyncEventSourceClient*>(r)->_onPoll(); }, this);
_client->onData(NULL, NULL);
_client->onTimeout([this](void* r, AsyncClient* c __attribute__((unused)), uint32_t time) { static_cast<AsyncEventSourceClient*>(r)->_onTimeout(time); }, this);
_client->onDisconnect([this](void* r, AsyncClient* c) { static_cast<AsyncEventSourceClient*>(r)->_onDisconnect(); delete c; }, this);
_server->_addClient(this);
delete request;
_client->setNoDelay(true);
}
AsyncEventSourceClient::~AsyncEventSourceClient() {
std::lock_guard<std::mutex> lock(_lockmq);
_messageQueue.clear();
close();
}
bool AsyncEventSourceClient::_queueMessage(const char* message, size_t len) {
if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
log_e("Event message queue overflow: discard message");
return false;
}
// length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq);
_messageQueue.emplace_back(message, len);
/*
throttle queue run
if Q is filled for >25% then network/CPU is congested, since there is no zero-copy mode for socket buff
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
*/
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
_runQueue();
}
return true;
}
bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t&& msg) {
if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
log_e("Event message queue overflow: discard message");
return false;
}
// length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq);
_messageQueue.emplace_back(std::move(msg));
/*
throttle queue run
if Q is filled for >25% then network/CPU is congested, since there is no zero-copy mode for socket buff
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
*/
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
_runQueue();
}
return true;
}
void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) {
// Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq);
// adjust in-flight len
if (len < _inflight)
_inflight -= len;
else
_inflight = 0;
// acknowledge as much messages's data as we got confirmed len from a AsyncTCP
while (len && _messageQueue.size()) {
len = _messageQueue.front().ack(len);
if (_messageQueue.front().finished()) {
// now we could release full ack'ed messages, we were keeping it unless send confirmed from AsyncTCP
_messageQueue.pop_front();
}
}
// try to send another batch of data
if (_messageQueue.size())
_runQueue();
}
void AsyncEventSourceClient::_onPoll() {
if (_messageQueue.size()) {
// Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq);
_runQueue();
}
}
void AsyncEventSourceClient::_onTimeout(uint32_t time __attribute__((unused))) {
if (_client)
_client->close(true);
}
void AsyncEventSourceClient::_onDisconnect() {
if (!_client)
return;
_client = nullptr;
_server->_handleDisconnect(this);
}
void AsyncEventSourceClient::close() {
if (_client)
_client->close();
}
bool AsyncEventSourceClient::send(const char* message, const char* event, uint32_t id, uint32_t reconnect) {
if (!connected())
return false;
return _queueMessage(std::make_shared<String>(generateEventMessage(message, event, id, reconnect)));
}
void AsyncEventSourceClient::_runQueue() {
if (!_client)
return;
// there is no need to lock the mutex here, 'cause all the calls to this method must be already lock'ed
size_t total_bytes_written = 0;
for (auto i = _messageQueue.begin(); i != _messageQueue.end(); ++i) {
if (!i->sent()) {
const size_t bytes_written = i->write(_client);
total_bytes_written += bytes_written;
_inflight += bytes_written;
if (bytes_written == 0 || _inflight > _max_inflight) {
// Serial.print("_");
break;
}
}
}
// flush socket
if (total_bytes_written)
_client->send();
}
void AsyncEventSourceClient::set_max_inflight_bytes(size_t value) {
if (value >= SSE_MIN_INFLIGH && value <= SSE_MAX_INFLIGH)
_max_inflight = value;
}
/* AsyncEventSource */
void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb) {
AsyncAuthorizationMiddleware* m = new AsyncAuthorizationMiddleware(401, cb);
m->_freeOnRemoval = true;
addMiddleware(m);
}
void AsyncEventSource::_addClient(AsyncEventSourceClient* client) {
if (!client)
return;
std::lock_guard<std::mutex> lock(_client_queue_lock);
_clients.emplace_back(client);
if (_connectcb)
_connectcb(client);
_adjust_inflight_window();
}
void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient* client) {
if (_disconnectcb)
_disconnectcb(client);
std::lock_guard<std::mutex> lock(_client_queue_lock);
for (auto i = _clients.begin(); i != _clients.end(); ++i) {
if (i->get() == client)
_clients.erase(i);
}
_adjust_inflight_window();
}
void AsyncEventSource::close() {
// While the whole loop is not done, the linked list is locked and so the
// iterator should remain valid even when AsyncEventSource::_handleDisconnect()
// is called very early
std::lock_guard<std::mutex> lock(_client_queue_lock);
for (const auto& c : _clients) {
if (c->connected())
c->close();
}
}
// pmb fix
size_t AsyncEventSource::avgPacketsWaiting() const {
size_t aql = 0;
uint32_t nConnectedClients = 0;
std::lock_guard<std::mutex> lock(_client_queue_lock);
if (!_clients.size())
return 0;
for (const auto& c : _clients) {
if (c->connected()) {
aql += c->packetsWaiting();
++nConnectedClients;
}
}
return ((aql) + (nConnectedClients / 2)) / (nConnectedClients); // round up
}
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));
std::lock_guard<std::mutex> lock(_client_queue_lock);
size_t hits = 0;
size_t miss = 0;
for (const auto& c : _clients) {
if (c->write(shared_msg))
++hits;
else
++miss;
}
return hits == 0 ? DISCARDED : (miss == 0 ? ENQUEUED : PARTIALLY_ENQUEUED);
}
size_t AsyncEventSource::count() const {
std::lock_guard<std::mutex> lock(_client_queue_lock);
size_t n_clients{0};
for (const auto& i : _clients)
if (i->connected())
++n_clients;
return n_clients;
}
bool AsyncEventSource::canHandle(AsyncWebServerRequest* request) const {
return request->isSSE() && request->url().equals(_url);
}
void AsyncEventSource::handleRequest(AsyncWebServerRequest* request) {
request->send(new AsyncEventSourceResponse(this));
}
void AsyncEventSource::_adjust_inflight_window() {
if (_clients.size()) {
size_t inflight = SSE_MAX_INFLIGH / _clients.size();
for (const auto& c : _clients)
c->set_max_inflight_bytes(inflight);
// Serial.printf("adjusted inflight to: %u\n", inflight);
}
}
/* Response */
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource* server) {
_server = server;
_code = 200;
_contentType = T_text_event_stream;
_sendContentLength = false;
addHeader(T_Cache_Control, T_no_cache);
addHeader(T_Connection, T_keep_alive);
}
void AsyncEventSourceResponse::_respond(AsyncWebServerRequest* request) {
String out;
_assembleHead(out, request->version());
request->client()->write(out.c_str(), _headLength);
_state = RESPONSE_WAIT_ACK;
}
size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest* request, size_t len, uint32_t time __attribute__((unused))) {
if (len) {
new AsyncEventSourceClient(request, _server);
}
return 0;
}

View file

@ -0,0 +1,261 @@
/*
Asynchronous WebServer library for Espressif MCUs
Copyright (c) 2016 Hristo Gochkov. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef ASYNCEVENTSOURCE_H_
#define ASYNCEVENTSOURCE_H_
#include <Arduino.h>
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include <mutex>
#ifndef SSE_MAX_QUEUED_MESSAGES
#define SSE_MAX_QUEUED_MESSAGES 32
#endif
#define SSE_MIN_INFLIGH 2 * 1460 // allow 2 MSS packets
#define SSE_MAX_INFLIGH 16 * 1024 // but no more than 16k, no need to blow it, since same data is kept in local Q
#include "ESPAsyncWebServer.h"
class AsyncEventSource;
class AsyncEventSourceResponse;
class AsyncEventSourceClient;
using ArEventHandlerFunction = std::function<void(AsyncEventSourceClient* client)>;
using ArAuthorizeConnectHandler = ArAuthorizeFunction;
// shared message object container
using AsyncEvent_SharedData_t = std::shared_ptr<String>;
/**
* @brief Async Event Message container with shared message content data
*
*/
class AsyncEventSourceMessage {
private:
const AsyncEvent_SharedData_t _data;
size_t _sent{0}; // num of bytes already sent
size_t _acked{0}; // num of bytes acked
public:
AsyncEventSourceMessage(AsyncEvent_SharedData_t data) : _data(data) {};
AsyncEventSourceMessage(const char* data, size_t len) : _data(std::make_shared<String>(data, len)) {};
/**
* @brief acknowledge sending len bytes of data
* @note if num of bytes to ack is larger then the unacknowledged message length the number of carried over bytes are returned
*
* @param len bytes to acknowlegde
* @param time
* @return size_t number of extra bytes carried over
*/
size_t ack(size_t len, uint32_t time = 0);
/**
* @brief write message data to client's buffer
* @note this method does NOT call client's send
*
* @param client
* @return size_t number of bytes written
*/
size_t write(AsyncClient* client);
/**
* @brief writes message data to client's buffer and calls client's send method
*
* @param client
* @return size_t returns num of bytes the clien was able to send()
*/
size_t send(AsyncClient* client);
// returns true if full message's length were acked
bool finished() { return _acked == _data->length(); }
/**
* @brief returns true if all data has been sent already
*
*/
bool sent() { return _sent == _data->length(); }
};
/**
* @brief class holds a sse messages queue for a particular client's connection
*
*/
class AsyncEventSourceClient {
private:
AsyncClient* _client;
AsyncEventSource* _server;
uint32_t _lastId{0};
size_t _inflight{0}; // num of unacknowledged bytes that has been written to socket buffer
size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer
std::list<AsyncEventSourceMessage> _messageQueue;
mutable std::mutex _lockmq;
bool _queueMessage(const char* message, size_t len);
bool _queueMessage(AsyncEvent_SharedData_t&& msg);
void _runQueue();
public:
AsyncEventSourceClient(AsyncWebServerRequest* request, AsyncEventSource* server);
~AsyncEventSourceClient();
/**
* @brief Send an SSE message to client
* it will craft an SSE message and place it to client's message queue
*
* @param message body string, could be single or multi-line string sepprated by \n, \r, \r\n
* @param event body string, a sinle line string
* @param id sequence id
* @param reconnect client's reconnect timeout
* @return true if message was placed in a queue
* @return false if queue is full
*/
bool send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
bool send(const String& message, const String& event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); }
bool send(const String& message, const char* event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event, id, reconnect); }
/**
* @brief place supplied preformatted SSE message to the message queue
* @note message must a properly formatted SSE string according to https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
*
* @param message data
* @return true on success
* @return false on queue overflow or no client connected
*/
bool write(AsyncEvent_SharedData_t message) { return connected() && _queueMessage(std::move(message)); };
[[deprecated("Use _write(AsyncEvent_SharedData_t message) instead to share same data with multiple SSE clients")]]
bool write(const char* message, size_t len) { return connected() && _queueMessage(message, len); };
// close client's connection
void close();
// getters
AsyncClient* client() { return _client; }
bool connected() const { return _client && _client->connected(); }
uint32_t lastId() const { return _lastId; }
size_t packetsWaiting() const { return _messageQueue.size(); };
/**
* @brief Sets max amount of bytes that could be written to client's socket while awaiting delivery acknowledge
* used to throttle message delivery length to tradeoff memory consumption
* @note actual amount of data written could possible be a bit larger but no more than available socket buff space
*
* @param value
*/
void set_max_inflight_bytes(size_t value);
/**
* @brief Get current max inflight bytes value
*
* @return size_t
*/
size_t get_max_inflight_bytes() const { return _max_inflight; }
// system callbacks (do not call if from user code!)
void _onAck(size_t len, uint32_t time);
void _onPoll();
void _onTimeout(uint32_t time);
void _onDisconnect();
};
/**
* @brief a class that maintains all connected HTTP clients subscribed to SSE delivery
* dispatches supplied messages to the client's queues
*
*/
class AsyncEventSource : public AsyncWebHandler {
private:
String _url;
std::list<std::unique_ptr<AsyncEventSourceClient>> _clients;
// Same as for individual messages, protect mutations of _clients list
// since simultaneous access from different tasks is possible
mutable std::mutex _client_queue_lock;
ArEventHandlerFunction _connectcb = nullptr;
ArEventHandlerFunction _disconnectcb = nullptr;
// this method manipulates in-fligh data size for connected client depending on number of active connections
void _adjust_inflight_window();
public:
typedef enum {
DISCARDED = 0,
ENQUEUED = 1,
PARTIALLY_ENQUEUED = 2,
} SendStatus;
AsyncEventSource(const char* url) : _url(url) {};
AsyncEventSource(const String& url) : _url(url) {};
~AsyncEventSource() { close(); };
const char* url() const { return _url.c_str(); }
// close all connected clients
void close();
/**
* @brief set on-connect callback for the client
* used to deliver messages to client on first connect
*
* @param cb
*/
void onConnect(ArEventHandlerFunction cb) { _connectcb = cb; }
/**
* @brief Send an SSE message to client
* it will craft an SSE message and place it to all connected client's message queues
*
* @param message body string, could be single or multi-line string sepprated by \n, \r, \r\n
* @param event body string, a sinle line string
* @param id sequence id
* @param reconnect client's reconnect timeout
* @return SendStatus if message was placed in any/all/part of the client's queues
*/
SendStatus send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
SendStatus send(const String& message, const String& event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); }
SendStatus send(const String& message, const char* event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event, id, reconnect); }
// The client pointer sent to the callback is only for reference purposes. DO NOT CALL ANY METHOD ON IT !
void onDisconnect(ArEventHandlerFunction cb) { _disconnectcb = cb; }
void authorizeConnect(ArAuthorizeConnectHandler cb);
// returns number of connected clients
size_t count() const;
// returns average number of messages pending in all client's queues
size_t avgPacketsWaiting() const;
// system callbacks (do not call from user code!)
void _addClient(AsyncEventSourceClient* client);
void _handleDisconnect(AsyncEventSourceClient* client);
bool canHandle(AsyncWebServerRequest* request) const override final;
void handleRequest(AsyncWebServerRequest* request) override final;
};
class AsyncEventSourceResponse : public AsyncWebServerResponse {
private:
AsyncEventSource* _server;
public:
AsyncEventSourceResponse(AsyncEventSource* server);
void _respond(AsyncWebServerRequest* request);
size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time);
bool _sourceValid() const { return true; }
};
#endif /* ASYNCEVENTSOURCE_H_ */

View file

@ -0,0 +1,151 @@
#include "AsyncJson.h"
#if ASYNC_JSON_SUPPORT == 1
#if ARDUINOJSON_VERSION_MAJOR == 5
AsyncJsonResponse::AsyncJsonResponse(bool isArray) : _isValid{false} {
_code = 200;
_contentType = asyncsrv::T_application_json;
if (isArray)
_root = _jsonBuffer.createArray();
else
_root = _jsonBuffer.createObject();
}
#elif ARDUINOJSON_VERSION_MAJOR == 6
AsyncJsonResponse::AsyncJsonResponse(bool isArray, size_t maxJsonBufferSize) : _jsonBuffer(maxJsonBufferSize), _isValid{false} {
_code = 200;
_contentType = asyncsrv::T_application_json;
if (isArray)
_root = _jsonBuffer.createNestedArray();
else
_root = _jsonBuffer.createNestedObject();
}
#else
AsyncJsonResponse::AsyncJsonResponse(bool isArray) : _isValid{false} {
_code = 200;
_contentType = asyncsrv::T_application_json;
if (isArray)
_root = _jsonBuffer.add<JsonArray>();
else
_root = _jsonBuffer.add<JsonObject>();
}
#endif
size_t AsyncJsonResponse::setLength() {
#if ARDUINOJSON_VERSION_MAJOR == 5
_contentLength = _root.measureLength();
#else
_contentLength = measureJson(_root);
#endif
if (_contentLength) {
_isValid = true;
}
return _contentLength;
}
size_t AsyncJsonResponse::_fillBuffer(uint8_t* data, size_t len) {
ChunkPrint dest(data, _sentLength, len);
#if ARDUINOJSON_VERSION_MAJOR == 5
_root.printTo(dest);
#else
serializeJson(_root, dest);
#endif
return len;
}
#if ARDUINOJSON_VERSION_MAJOR == 6
PrettyAsyncJsonResponse::PrettyAsyncJsonResponse(bool isArray, size_t maxJsonBufferSize) : AsyncJsonResponse{isArray, maxJsonBufferSize} {}
#else
PrettyAsyncJsonResponse::PrettyAsyncJsonResponse(bool isArray) : AsyncJsonResponse{isArray} {}
#endif
size_t PrettyAsyncJsonResponse::setLength() {
#if ARDUINOJSON_VERSION_MAJOR == 5
_contentLength = _root.measurePrettyLength();
#else
_contentLength = measureJsonPretty(_root);
#endif
if (_contentLength) {
_isValid = true;
}
return _contentLength;
}
size_t PrettyAsyncJsonResponse::_fillBuffer(uint8_t* data, size_t len) {
ChunkPrint dest(data, _sentLength, len);
#if ARDUINOJSON_VERSION_MAJOR == 5
_root.prettyPrintTo(dest);
#else
serializeJsonPretty(_root, dest);
#endif
return len;
}
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
#else
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
#endif
bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest* request) const {
if (!_onRequest || !request->isHTTP() || !(_method & request->method()))
return false;
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/")))
return false;
if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_json))
return false;
return true;
}
void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest* request) {
if (_onRequest) {
if (request->method() == HTTP_GET) {
JsonVariant json;
_onRequest(request, json);
return;
} else if (request->_tempObject != NULL) {
#if ARDUINOJSON_VERSION_MAJOR == 5
DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((uint8_t*)(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>();
#else
JsonDocument jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif
_onRequest(request, json);
return;
}
}
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);
}
if (request->_tempObject != NULL) {
memcpy((uint8_t*)(request->_tempObject) + index, data, len);
}
}
}
#endif // ASYNC_JSON_SUPPORT

View file

@ -0,0 +1,131 @@
// AsyncJson.h
/*
Async Response to use with ArduinoJson and AsyncWebServer
Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon.
Example of callback in use
server.on("/json", HTTP_ANY, [](AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse();
JsonObject& root = response->getRoot();
root["key1"] = "key number one";
JsonObject& nested = root.createNestedObject("nested");
nested["key1"] = "key number one";
response->setLength();
request->send(response);
});
--------------------
Async Request to use with ArduinoJson and AsyncWebServer
Written by Arsène von Wyss (avonwyss)
Example
AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler("/rest/endpoint");
handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) {
JsonObject jsonObj = json.as<JsonObject>();
// ...
});
server.addHandler(handler);
*/
#ifndef ASYNC_JSON_H_
#define ASYNC_JSON_H_
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#if ARDUINOJSON_VERSION_MAJOR >= 5
#define ASYNC_JSON_SUPPORT 1
#else
#define ASYNC_JSON_SUPPORT 0
#endif // ARDUINOJSON_VERSION_MAJOR >= 5
#endif // __has_include("ArduinoJson.h")
#if ASYNC_JSON_SUPPORT == 1
#include <ESPAsyncWebServer.h>
#include "ChunkPrint.h"
#if ARDUINOJSON_VERSION_MAJOR == 6
#ifndef DYNAMIC_JSON_DOCUMENT_SIZE
#define DYNAMIC_JSON_DOCUMENT_SIZE 1024
#endif
#endif
class AsyncJsonResponse : public AsyncAbstractResponse {
protected:
#if ARDUINOJSON_VERSION_MAJOR == 5
DynamicJsonBuffer _jsonBuffer;
#elif ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument _jsonBuffer;
#else
JsonDocument _jsonBuffer;
#endif
JsonVariant _root;
bool _isValid;
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else
AsyncJsonResponse(bool isArray = false);
#endif
JsonVariant& getRoot() { return _root; }
bool _sourceValid() const { return _isValid; }
size_t setLength();
size_t getSize() const { return _jsonBuffer.size(); }
size_t _fillBuffer(uint8_t* data, size_t len);
#if ARDUINOJSON_VERSION_MAJOR >= 6
bool overflowed() const { return _jsonBuffer.overflowed(); }
#endif
};
class PrettyAsyncJsonResponse : public AsyncJsonResponse {
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
PrettyAsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else
PrettyAsyncJsonResponse(bool isArray = false);
#endif
size_t setLength();
size_t _fillBuffer(uint8_t* data, size_t len);
};
typedef std::function<void(AsyncWebServerRequest* request, JsonVariant& json)> ArJsonRequestHandlerFunction;
class AsyncCallbackJsonWebHandler : public AsyncWebHandler {
protected:
String _uri;
WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest;
size_t _contentLength;
#if ARDUINOJSON_VERSION_MAJOR == 6
size_t maxJsonBufferSize;
#endif
size_t _maxContentLength;
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else
AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr);
#endif
void setMethod(WebRequestMethodComposite method) { _method = method; }
void setMaxContentLength(int maxContentLength) { _maxContentLength = maxContentLength; }
void onRequest(ArJsonRequestHandlerFunction fn) { _onRequest = fn; }
bool canHandle(AsyncWebServerRequest* request) const override final;
void handleRequest(AsyncWebServerRequest* request) override final;
void handleUpload(__unused AsyncWebServerRequest* request, __unused const String& filename, __unused size_t index, __unused uint8_t* data, __unused size_t len, __unused bool final) override final {}
void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final;
bool isRequestHandlerTrivial() const override final { return !_onRequest; }
};
#endif // ASYNC_JSON_SUPPORT == 1
#endif // ASYNC_JSON_H_

View file

@ -0,0 +1,102 @@
#include "AsyncMessagePack.h"
#if ASYNC_MSG_PACK_SUPPORT == 1
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray, size_t maxJsonBufferSize) : _jsonBuffer(maxJsonBufferSize), _isValid{false} {
_code = 200;
_contentType = asyncsrv::T_application_msgpack;
if (isArray)
_root = _jsonBuffer.createNestedArray();
else
_root = _jsonBuffer.createNestedObject();
}
#else
AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray) : _isValid{false} {
_code = 200;
_contentType = asyncsrv::T_application_msgpack;
if (isArray)
_root = _jsonBuffer.add<JsonArray>();
else
_root = _jsonBuffer.add<JsonObject>();
}
#endif
size_t AsyncMessagePackResponse::setLength() {
_contentLength = measureMsgPack(_root);
if (_contentLength) {
_isValid = true;
}
return _contentLength;
}
size_t AsyncMessagePackResponse::_fillBuffer(uint8_t* data, size_t len) {
ChunkPrint dest(data, _sentLength, len);
serializeMsgPack(_root, dest);
return len;
}
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler(const String& uri, ArMessagePackRequestHandlerFunction onRequest, size_t maxJsonBufferSize)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
#else
AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler(const String& uri, ArMessagePackRequestHandlerFunction onRequest)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
#endif
bool AsyncCallbackMessagePackWebHandler::canHandle(AsyncWebServerRequest* request) const {
if (!_onRequest || !request->isHTTP() || !(_method & request->method()))
return false;
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/")))
return false;
if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack))
return false;
return true;
}
void AsyncCallbackMessagePackWebHandler::handleRequest(AsyncWebServerRequest* request) {
if (_onRequest) {
if (request->method() == HTTP_GET) {
JsonVariant json;
_onRequest(request, json);
return;
} else if (request->_tempObject != NULL) {
#if ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t*)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#else
JsonDocument jsonBuffer;
DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t*)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif
_onRequest(request, json);
return;
}
}
request->send(_contentLength > _maxContentLength ? 413 : 400);
} else {
request->send(500);
}
}
void AsyncCallbackMessagePackWebHandler::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);
}
if (request->_tempObject != NULL) {
memcpy((uint8_t*)(request->_tempObject) + index, data, len);
}
}
}
#endif // ASYNC_MSG_PACK_SUPPORT

View file

@ -0,0 +1,81 @@
#pragma once
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#if ARDUINOJSON_VERSION_MAJOR >= 6
#define ASYNC_MSG_PACK_SUPPORT 1
#else
#define ASYNC_MSG_PACK_SUPPORT 0
#endif // ARDUINOJSON_VERSION_MAJOR >= 6
#endif // __has_include("ArduinoJson.h")
#if ASYNC_MSG_PACK_SUPPORT == 1
#include <ESPAsyncWebServer.h>
#include "ChunkPrint.h"
#if ARDUINOJSON_VERSION_MAJOR == 6
#ifndef DYNAMIC_JSON_DOCUMENT_SIZE
#define DYNAMIC_JSON_DOCUMENT_SIZE 1024
#endif
#endif
class AsyncMessagePackResponse : public AsyncAbstractResponse {
protected:
#if ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument _jsonBuffer;
#else
JsonDocument _jsonBuffer;
#endif
JsonVariant _root;
bool _isValid;
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncMessagePackResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else
AsyncMessagePackResponse(bool isArray = false);
#endif
JsonVariant& getRoot() { return _root; }
bool _sourceValid() const { return _isValid; }
size_t setLength();
size_t getSize() const { return _jsonBuffer.size(); }
size_t _fillBuffer(uint8_t* data, size_t len);
#if ARDUINOJSON_VERSION_MAJOR >= 6
bool overflowed() const { return _jsonBuffer.overflowed(); }
#endif
};
typedef std::function<void(AsyncWebServerRequest* request, JsonVariant& json)> ArMessagePackRequestHandlerFunction;
class AsyncCallbackMessagePackWebHandler : public AsyncWebHandler {
protected:
String _uri;
WebRequestMethodComposite _method;
ArMessagePackRequestHandlerFunction _onRequest;
size_t _contentLength;
#if ARDUINOJSON_VERSION_MAJOR == 6
size_t maxJsonBufferSize;
#endif
size_t _maxContentLength;
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackMessagePackWebHandler(const String& uri, ArMessagePackRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else
AsyncCallbackMessagePackWebHandler(const String& uri, ArMessagePackRequestHandlerFunction onRequest = nullptr);
#endif
void setMethod(WebRequestMethodComposite method) { _method = method; }
void setMaxContentLength(int maxContentLength) { _maxContentLength = maxContentLength; }
void onRequest(ArMessagePackRequestHandlerFunction fn) { _onRequest = fn; }
bool canHandle(AsyncWebServerRequest* request) const override final;
void handleRequest(AsyncWebServerRequest* request) override final;
void handleUpload(__unused AsyncWebServerRequest* request, __unused const String& filename, __unused size_t index, __unused uint8_t* data, __unused size_t len, __unused bool final) override final {}
void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final;
bool isRequestHandlerTrivial() const override final { return !_onRequest; }
};
#endif // ASYNC_MSG_PACK_SUPPORT == 1

View file

@ -0,0 +1,22 @@
#include "ESPAsyncWebServer.h"
AsyncWebHeader::AsyncWebHeader(const String& data) {
if (!data)
return;
int index = data.indexOf(':');
if (index < 0)
return;
_name = data.substring(0, index);
_value = data.substring(index + 2);
}
String AsyncWebHeader::toString() const {
String str;
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);
return str;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,337 @@
/*
Asynchronous WebServer library for Espressif MCUs
Copyright (c) 2016 Hristo Gochkov. All rights reserved.
This file is part of the esp8266 core for Arduino environment.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef ASYNCWEBSOCKET_H_
#define ASYNCWEBSOCKET_H_
#include <Arduino.h>
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include <mutex>
#ifndef WS_MAX_QUEUED_MESSAGES
#define WS_MAX_QUEUED_MESSAGES 32
#endif
#include "ESPAsyncWebServer.h"
#include <memory>
#ifndef DEFAULT_MAX_WS_CLIENTS
#define DEFAULT_MAX_WS_CLIENTS 8
#endif
using AsyncWebSocketSharedBuffer = std::shared_ptr<std::vector<uint8_t>>;
class AsyncWebSocket;
class AsyncWebSocketResponse;
class AsyncWebSocketClient;
class AsyncWebSocketControl;
typedef struct {
/** Message type as defined by enum AwsFrameType.
* Note: Applications will only see WS_TEXT and WS_BINARY.
* All other types are handled by the library. */
uint8_t message_opcode;
/** Frame number of a fragmented message. */
uint32_t num;
/** Is this the last frame in a fragmented message ?*/
uint8_t final;
/** Is this frame masked? */
uint8_t masked;
/** Message type as defined by enum AwsFrameType.
* This value is the same as message_opcode for non-fragmented
* messages, but may also be WS_CONTINUATION in a fragmented message. */
uint8_t opcode;
/** Length of the current frame.
* This equals the total length of the message if num == 0 && final == true */
uint64_t len;
/** Mask key */
uint8_t mask[4];
/** Offset of the data inside the current frame. */
uint64_t index;
} AwsFrameInfo;
typedef enum { WS_DISCONNECTED,
WS_CONNECTED,
WS_DISCONNECTING } AwsClientStatus;
typedef enum { WS_CONTINUATION,
WS_TEXT,
WS_BINARY,
WS_DISCONNECT = 0x08,
WS_PING,
WS_PONG } AwsFrameType;
typedef enum { WS_MSG_SENDING,
WS_MSG_SENT,
WS_MSG_ERROR } AwsMessageStatus;
typedef enum { WS_EVT_CONNECT,
WS_EVT_DISCONNECT,
WS_EVT_PING,
WS_EVT_PONG,
WS_EVT_ERROR,
WS_EVT_DATA } AwsEventType;
class AsyncWebSocketMessageBuffer {
friend AsyncWebSocket;
friend AsyncWebSocketClient;
private:
AsyncWebSocketSharedBuffer _buffer;
public:
AsyncWebSocketMessageBuffer() {}
explicit AsyncWebSocketMessageBuffer(size_t size);
AsyncWebSocketMessageBuffer(const uint8_t* data, size_t size);
//~AsyncWebSocketMessageBuffer();
bool reserve(size_t size);
uint8_t* get() { return _buffer->data(); }
size_t length() const { return _buffer->size(); }
};
class AsyncWebSocketMessage {
private:
AsyncWebSocketSharedBuffer _WSbuffer;
uint8_t _opcode{WS_TEXT};
bool _mask{false};
AwsMessageStatus _status{WS_MSG_ERROR};
size_t _sent{};
size_t _ack{};
size_t _acked{};
public:
AsyncWebSocketMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false);
bool finished() const { return _status != WS_MSG_SENDING; }
bool betweenFrames() const { return _acked == _ack; }
void ack(size_t len, uint32_t time);
size_t send(AsyncClient* client);
};
class AsyncWebSocketClient {
private:
AsyncClient* _client;
AsyncWebSocket* _server;
uint32_t _clientId;
AwsClientStatus _status;
mutable std::mutex _lock;
std::deque<AsyncWebSocketControl> _controlQueue;
std::deque<AsyncWebSocketMessage> _messageQueue;
bool closeWhenFull = true;
uint8_t _pstate;
AwsFrameInfo _pinfo;
uint32_t _lastMessageTime;
uint32_t _keepAlivePeriod;
bool _queueControl(uint8_t opcode, const uint8_t* data = NULL, size_t len = 0, bool mask = false);
bool _queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false);
void _runQueue();
void _clearQueue();
public:
void* _tempObject;
AsyncWebSocketClient(AsyncWebServerRequest* request, AsyncWebSocket* server);
~AsyncWebSocketClient();
// client id increments for the given server
uint32_t id() const { return _clientId; }
AwsClientStatus status() const { return _status; }
AsyncClient* client() { return _client; }
const AsyncClient* client() const { return _client; }
AsyncWebSocket* server() { return _server; }
const AsyncWebSocket* server() const { return _server; }
AwsFrameInfo const& pinfo() const { return _pinfo; }
// - If "true" (default), the connection will be closed if the message queue is full.
// This is the default behavior in yubox-node-org, which is not silently discarding messages but instead closes the connection.
// The big issue with this behavior is that is can cause the UI to automatically re-create a new WS connection, which can be filled again,
// and so on, causing a resource exhaustion.
//
// - If "false", the incoming message will be discarded if the queue is full.
// This is the default behavior in the original ESPAsyncWebServer library from me-no-dev.
// This behavior allows the best performance at the expense of unreliable message delivery in case the queue is full (some messages may be lost).
//
// - In any case, when the queue is full, a message is logged.
// - IT is recommended to use the methods queueIsFull(), availableForWriteAll(), availableForWrite(clientId) to check if the queue is full before sending a message.
//
// Usage:
// - can be set in the onEvent listener when connecting (event type is: WS_EVT_CONNECT)
//
// Use cases:,
// - if using websocket to send logging messages, maybe some loss is acceptable.
// - But if using websocket to send UI update messages, maybe the connection should be closed and the UI redrawn.
void setCloseClientOnQueueFull(bool close) { closeWhenFull = close; }
bool willCloseClientOnQueueFull() const { return closeWhenFull; }
IPAddress remoteIP() const;
uint16_t remotePort() const;
bool shouldBeDeleted() const { return !_client; }
// control frames
void close(uint16_t code = 0, const char* message = NULL);
bool ping(const uint8_t* data = NULL, size_t len = 0);
// set auto-ping period in seconds. disabled if zero (default)
void keepAlivePeriod(uint16_t seconds) {
_keepAlivePeriod = seconds * 1000;
}
uint16_t keepAlivePeriod() {
return (uint16_t)(_keepAlivePeriod / 1000);
}
// data packets
void message(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false) { _queueMessage(buffer, opcode, mask); }
bool queueIsFull() const;
size_t queueLen() const;
size_t printf(const char* format, ...) __attribute__((format(printf, 2, 3)));
bool text(AsyncWebSocketSharedBuffer buffer);
bool text(const uint8_t* message, size_t len);
bool text(const char* message, size_t len);
bool text(const char* message);
bool text(const String& message);
bool text(AsyncWebSocketMessageBuffer* buffer);
bool binary(AsyncWebSocketSharedBuffer buffer);
bool binary(const uint8_t* message, size_t len);
bool binary(const char* message, size_t len);
bool binary(const char* message);
bool binary(const String& message);
bool binary(AsyncWebSocketMessageBuffer* buffer);
bool canSend() const;
// system callbacks (do not call)
void _onAck(size_t len, uint32_t time);
void _onError(int8_t);
void _onPoll();
void _onTimeout(uint32_t time);
void _onDisconnect();
void _onData(void* pbuf, size_t plen);
};
using AwsHandshakeHandler = std::function<bool(AsyncWebServerRequest* request)>;
using AwsEventHandler = std::function<void(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)>;
// WebServer Handler implementation that plays the role of a socket server
class AsyncWebSocket : public AsyncWebHandler {
private:
String _url;
std::list<AsyncWebSocketClient> _clients;
uint32_t _cNextId;
AwsEventHandler _eventHandler{nullptr};
AwsHandshakeHandler _handshakeHandler;
bool _enabled;
mutable std::mutex _lock;
public:
typedef enum {
DISCARDED = 0,
ENQUEUED = 1,
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) {}
~AsyncWebSocket() {};
const char* url() const { return _url.c_str(); }
void enable(bool e) { _enabled = e; }
bool enabled() const { return _enabled; }
bool availableForWriteAll();
bool availableForWrite(uint32_t id);
size_t count() const;
AsyncWebSocketClient* client(uint32_t id);
bool hasClient(uint32_t id) { return client(id) != nullptr; }
void close(uint32_t id, uint16_t code = 0, const char* message = NULL);
void closeAll(uint16_t code = 0, const char* message = NULL);
void cleanupClients(uint16_t maxClients = DEFAULT_MAX_WS_CLIENTS);
bool ping(uint32_t id, const uint8_t* data = NULL, size_t len = 0);
SendStatus pingAll(const uint8_t* data = NULL, size_t len = 0); // done
bool text(uint32_t id, const uint8_t* message, size_t len);
bool text(uint32_t id, const char* message, size_t len);
bool text(uint32_t id, const char* message);
bool text(uint32_t id, const String& message);
bool text(uint32_t id, AsyncWebSocketMessageBuffer* buffer);
bool text(uint32_t id, AsyncWebSocketSharedBuffer buffer);
SendStatus textAll(const uint8_t* message, size_t len);
SendStatus textAll(const char* message, size_t len);
SendStatus textAll(const char* message);
SendStatus textAll(const String& message);
SendStatus textAll(AsyncWebSocketMessageBuffer* buffer);
SendStatus textAll(AsyncWebSocketSharedBuffer buffer);
bool binary(uint32_t id, const uint8_t* message, size_t len);
bool binary(uint32_t id, const char* message, size_t len);
bool binary(uint32_t id, const char* message);
bool binary(uint32_t id, const String& message);
bool binary(uint32_t id, AsyncWebSocketMessageBuffer* buffer);
bool binary(uint32_t id, AsyncWebSocketSharedBuffer buffer);
SendStatus binaryAll(const uint8_t* message, size_t len);
SendStatus binaryAll(const char* message, size_t len);
SendStatus binaryAll(const char* message);
SendStatus binaryAll(const String& message);
SendStatus binaryAll(AsyncWebSocketMessageBuffer* buffer);
SendStatus binaryAll(AsyncWebSocketSharedBuffer buffer);
size_t printf(uint32_t id, const char* format, ...) __attribute__((format(printf, 3, 4)));
size_t printfAll(const char* format, ...) __attribute__((format(printf, 2, 3)));
void onEvent(AwsEventHandler handler) { _eventHandler = handler; }
void handleHandshake(AwsHandshakeHandler handler) { _handshakeHandler = handler; }
// system callbacks (do not call)
uint32_t _getNextId() { return _cNextId++; }
AsyncWebSocketClient* _newClient(AsyncWebServerRequest* request);
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;
// messagebuffer functions/objects.
AsyncWebSocketMessageBuffer* makeBuffer(size_t size = 0);
AsyncWebSocketMessageBuffer* makeBuffer(const uint8_t* data, size_t size);
std::list<AsyncWebSocketClient>& getClients() { return _clients; }
};
// WebServer response to authenticate the socket and detach the tcp client from the web server request
class AsyncWebSocketResponse : public AsyncWebServerResponse {
private:
String _content;
AsyncWebSocket* _server;
public:
AsyncWebSocketResponse(const String& key, AsyncWebSocket* server);
void _respond(AsyncWebServerRequest* request);
size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time);
bool _sourceValid() const { return true; }
};
#endif /* ASYNCWEBSOCKET_H_ */

View file

@ -0,0 +1,284 @@
/*
* FIPS-180-1 compliant SHA-1 implementation
*
* Copyright (C) 2006-2015, ARM Limited, All Rights Reserved
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is part of mbed TLS (https://tls.mbed.org)
* Modified for esp32 by Lucas Saavedra Vaz on 11 Jan 2024
*/
#include <Arduino.h>
#if ESP_IDF_VERSION_MAJOR < 5
#include "BackPort_SHA1Builder.h"
// 32-bit integer manipulation macros (big endian)
#ifndef GET_UINT32_BE
#define GET_UINT32_BE(n, b, i) \
{ (n) = ((uint32_t)(b)[(i)] << 24) | ((uint32_t)(b)[(i) + 1] << 16) | ((uint32_t)(b)[(i) + 2] << 8) | ((uint32_t)(b)[(i) + 3]); }
#endif
#ifndef PUT_UINT32_BE
#define PUT_UINT32_BE(n, b, i) \
{ \
(b)[(i)] = (uint8_t)((n) >> 24); \
(b)[(i) + 1] = (uint8_t)((n) >> 16); \
(b)[(i) + 2] = (uint8_t)((n) >> 8); \
(b)[(i) + 3] = (uint8_t)((n)); \
}
#endif
// Constants
static const uint8_t sha1_padding[64] = {0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Private methods
void SHA1Builder::process(const uint8_t *data) {
uint32_t temp, W[16], A, B, C, D, E;
GET_UINT32_BE(W[0], data, 0);
GET_UINT32_BE(W[1], data, 4);
GET_UINT32_BE(W[2], data, 8);
GET_UINT32_BE(W[3], data, 12);
GET_UINT32_BE(W[4], data, 16);
GET_UINT32_BE(W[5], data, 20);
GET_UINT32_BE(W[6], data, 24);
GET_UINT32_BE(W[7], data, 28);
GET_UINT32_BE(W[8], data, 32);
GET_UINT32_BE(W[9], data, 36);
GET_UINT32_BE(W[10], data, 40);
GET_UINT32_BE(W[11], data, 44);
GET_UINT32_BE(W[12], data, 48);
GET_UINT32_BE(W[13], data, 52);
GET_UINT32_BE(W[14], data, 56);
GET_UINT32_BE(W[15], data, 60);
#define sha1_S(x, n) ((x << n) | ((x & 0xFFFFFFFF) >> (32 - n)))
#define sha1_R(t) (temp = W[(t - 3) & 0x0F] ^ W[(t - 8) & 0x0F] ^ W[(t - 14) & 0x0F] ^ W[t & 0x0F], (W[t & 0x0F] = sha1_S(temp, 1)))
#define sha1_P(a, b, c, d, e, x) \
{ \
e += sha1_S(a, 5) + sha1_F(b, c, d) + sha1_K + x; \
b = sha1_S(b, 30); \
}
A = state[0];
B = state[1];
C = state[2];
D = state[3];
E = state[4];
#define sha1_F(x, y, z) (z ^ (x & (y ^ z)))
#define sha1_K 0x5A827999
sha1_P(A, B, C, D, E, W[0]);
sha1_P(E, A, B, C, D, W[1]);
sha1_P(D, E, A, B, C, W[2]);
sha1_P(C, D, E, A, B, W[3]);
sha1_P(B, C, D, E, A, W[4]);
sha1_P(A, B, C, D, E, W[5]);
sha1_P(E, A, B, C, D, W[6]);
sha1_P(D, E, A, B, C, W[7]);
sha1_P(C, D, E, A, B, W[8]);
sha1_P(B, C, D, E, A, W[9]);
sha1_P(A, B, C, D, E, W[10]);
sha1_P(E, A, B, C, D, W[11]);
sha1_P(D, E, A, B, C, W[12]);
sha1_P(C, D, E, A, B, W[13]);
sha1_P(B, C, D, E, A, W[14]);
sha1_P(A, B, C, D, E, W[15]);
sha1_P(E, A, B, C, D, sha1_R(16));
sha1_P(D, E, A, B, C, sha1_R(17));
sha1_P(C, D, E, A, B, sha1_R(18));
sha1_P(B, C, D, E, A, sha1_R(19));
#undef sha1_K
#undef sha1_F
#define sha1_F(x, y, z) (x ^ y ^ z)
#define sha1_K 0x6ED9EBA1
sha1_P(A, B, C, D, E, sha1_R(20));
sha1_P(E, A, B, C, D, sha1_R(21));
sha1_P(D, E, A, B, C, sha1_R(22));
sha1_P(C, D, E, A, B, sha1_R(23));
sha1_P(B, C, D, E, A, sha1_R(24));
sha1_P(A, B, C, D, E, sha1_R(25));
sha1_P(E, A, B, C, D, sha1_R(26));
sha1_P(D, E, A, B, C, sha1_R(27));
sha1_P(C, D, E, A, B, sha1_R(28));
sha1_P(B, C, D, E, A, sha1_R(29));
sha1_P(A, B, C, D, E, sha1_R(30));
sha1_P(E, A, B, C, D, sha1_R(31));
sha1_P(D, E, A, B, C, sha1_R(32));
sha1_P(C, D, E, A, B, sha1_R(33));
sha1_P(B, C, D, E, A, sha1_R(34));
sha1_P(A, B, C, D, E, sha1_R(35));
sha1_P(E, A, B, C, D, sha1_R(36));
sha1_P(D, E, A, B, C, sha1_R(37));
sha1_P(C, D, E, A, B, sha1_R(38));
sha1_P(B, C, D, E, A, sha1_R(39));
#undef sha1_K
#undef sha1_F
#define sha1_F(x, y, z) ((x & y) | (z & (x | y)))
#define sha1_K 0x8F1BBCDC
sha1_P(A, B, C, D, E, sha1_R(40));
sha1_P(E, A, B, C, D, sha1_R(41));
sha1_P(D, E, A, B, C, sha1_R(42));
sha1_P(C, D, E, A, B, sha1_R(43));
sha1_P(B, C, D, E, A, sha1_R(44));
sha1_P(A, B, C, D, E, sha1_R(45));
sha1_P(E, A, B, C, D, sha1_R(46));
sha1_P(D, E, A, B, C, sha1_R(47));
sha1_P(C, D, E, A, B, sha1_R(48));
sha1_P(B, C, D, E, A, sha1_R(49));
sha1_P(A, B, C, D, E, sha1_R(50));
sha1_P(E, A, B, C, D, sha1_R(51));
sha1_P(D, E, A, B, C, sha1_R(52));
sha1_P(C, D, E, A, B, sha1_R(53));
sha1_P(B, C, D, E, A, sha1_R(54));
sha1_P(A, B, C, D, E, sha1_R(55));
sha1_P(E, A, B, C, D, sha1_R(56));
sha1_P(D, E, A, B, C, sha1_R(57));
sha1_P(C, D, E, A, B, sha1_R(58));
sha1_P(B, C, D, E, A, sha1_R(59));
#undef sha1_K
#undef sha1_F
#define sha1_F(x, y, z) (x ^ y ^ z)
#define sha1_K 0xCA62C1D6
sha1_P(A, B, C, D, E, sha1_R(60));
sha1_P(E, A, B, C, D, sha1_R(61));
sha1_P(D, E, A, B, C, sha1_R(62));
sha1_P(C, D, E, A, B, sha1_R(63));
sha1_P(B, C, D, E, A, sha1_R(64));
sha1_P(A, B, C, D, E, sha1_R(65));
sha1_P(E, A, B, C, D, sha1_R(66));
sha1_P(D, E, A, B, C, sha1_R(67));
sha1_P(C, D, E, A, B, sha1_R(68));
sha1_P(B, C, D, E, A, sha1_R(69));
sha1_P(A, B, C, D, E, sha1_R(70));
sha1_P(E, A, B, C, D, sha1_R(71));
sha1_P(D, E, A, B, C, sha1_R(72));
sha1_P(C, D, E, A, B, sha1_R(73));
sha1_P(B, C, D, E, A, sha1_R(74));
sha1_P(A, B, C, D, E, sha1_R(75));
sha1_P(E, A, B, C, D, sha1_R(76));
sha1_P(D, E, A, B, C, sha1_R(77));
sha1_P(C, D, E, A, B, sha1_R(78));
sha1_P(B, C, D, E, A, sha1_R(79));
#undef sha1_K
#undef sha1_F
state[0] += A;
state[1] += B;
state[2] += C;
state[3] += D;
state[4] += E;
}
// Public methods
void SHA1Builder::begin() {
total[0] = 0;
total[1] = 0;
state[0] = 0x67452301;
state[1] = 0xEFCDAB89;
state[2] = 0x98BADCFE;
state[3] = 0x10325476;
state[4] = 0xC3D2E1F0;
memset(buffer, 0x00, sizeof(buffer));
memset(hash, 0x00, sizeof(hash));
}
void SHA1Builder::add(const uint8_t *data, size_t len) {
size_t fill;
uint32_t left;
if (len == 0) {
return;
}
left = total[0] & 0x3F;
fill = 64 - left;
total[0] += (uint32_t)len;
total[0] &= 0xFFFFFFFF;
if (total[0] < (uint32_t)len) {
total[1]++;
}
if (left && len >= fill) {
memcpy((void *)(buffer + left), data, fill);
process(buffer);
data += fill;
len -= fill;
left = 0;
}
while (len >= 64) {
process(data);
data += 64;
len -= 64;
}
if (len > 0) {
memcpy((void *)(buffer + left), data, len);
}
}
void SHA1Builder::calculate(void) {
uint32_t last, padn;
uint32_t high, low;
uint8_t msglen[8];
high = (total[0] >> 29) | (total[1] << 3);
low = (total[0] << 3);
PUT_UINT32_BE(high, msglen, 0);
PUT_UINT32_BE(low, msglen, 4);
last = total[0] & 0x3F;
padn = (last < 56) ? (56 - last) : (120 - last);
add((uint8_t *)sha1_padding, padn);
add(msglen, 8);
PUT_UINT32_BE(state[0], hash, 0);
PUT_UINT32_BE(state[1], hash, 4);
PUT_UINT32_BE(state[2], hash, 8);
PUT_UINT32_BE(state[3], hash, 12);
PUT_UINT32_BE(state[4], hash, 16);
}
void SHA1Builder::getBytes(uint8_t *output) {
memcpy(output, hash, SHA1_HASH_SIZE);
}
#endif // ESP_IDF_VERSION_MAJOR < 5

View file

@ -0,0 +1,44 @@
// Copyright 2024 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <Arduino.h>
#if ESP_IDF_VERSION_MAJOR < 5
#ifndef SHA1Builder_h
#define SHA1Builder_h
#include <Stream.h>
#include <WString.h>
#define SHA1_HASH_SIZE 20
class SHA1Builder {
private:
uint32_t total[2]; /* number of bytes processed */
uint32_t state[5]; /* intermediate digest state */
unsigned char buffer[64]; /* data block being processed */
uint8_t hash[SHA1_HASH_SIZE]; /* SHA-1 result */
void process(const uint8_t* data);
public:
void begin();
void add(const uint8_t* data, size_t len);
void calculate();
void getBytes(uint8_t* output);
};
#endif // SHA1Builder_h
#endif // ESP_IDF_VERSION_MAJOR < 5

View file

@ -0,0 +1,16 @@
#include "ChunkPrint.h"
ChunkPrint::ChunkPrint(uint8_t* destination, size_t from, size_t len)
: _destination(destination), _to_skip(from), _to_write(len), _pos{0} {}
size_t ChunkPrint::write(uint8_t c) {
if (_to_skip > 0) {
_to_skip--;
return 1;
} else if (_to_write > 0) {
_to_write--;
_destination[_pos++] = c;
return 1;
}
return 0;
}

View file

@ -0,0 +1,18 @@
#ifndef CHUNKPRINT_H
#define CHUNKPRINT_H
#include <Print.h>
class ChunkPrint : public Print {
private:
uint8_t* _destination;
size_t _to_skip;
size_t _to_write;
size_t _pos;
public:
ChunkPrint(uint8_t* destination, size_t from, size_t len);
size_t write(uint8_t c);
size_t write(const uint8_t* buffer, size_t size) { return this->Print::write(buffer, size); }
};
#endif

View file

@ -0,0 +1,901 @@
/*
Asynchronous WebServer library for Espressif MCUs
Copyright (c) 2016 Hristo Gochkov. All rights reserved.
This file is part of the esp8266 core for Arduino environment.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _ESPAsyncWebServer_H_
#define _ESPAsyncWebServer_H_
#include "Arduino.h"
#include "FS.h"
#include <algorithm>
#include <deque>
#include <functional>
#include <list>
#include <unordered_map>
#include <vector>
#ifdef ESP32
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040)
#include <AsyncTCP_RP2040W.h>
#include <HTTP_Method.h>
#include <WiFi.h>
#include <http_parser.h>
#else
#error Platform not supported
#endif
#include "literals.h"
#define ASYNCWEBSERVER_VERSION "3.6.0"
#define ASYNCWEBSERVER_VERSION_MAJOR 3
#define ASYNCWEBSERVER_VERSION_MINOR 6
#define ASYNCWEBSERVER_VERSION_REVISION 0
#define ASYNCWEBSERVER_FORK_mathieucarbou
#ifdef ASYNCWEBSERVER_REGEX
#define ASYNCWEBSERVER_REGEX_ATTRIBUTE
#else
#define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined")))
#endif
class AsyncWebServer;
class AsyncWebServerRequest;
class AsyncWebServerResponse;
class AsyncWebHeader;
class AsyncWebParameter;
class AsyncWebRewrite;
class AsyncWebHandler;
class AsyncStaticWebHandler;
class AsyncCallbackWebHandler;
class AsyncResponseStream;
class AsyncMiddlewareChain;
typedef enum {
HTTP_GET = 0b00000001,
HTTP_POST = 0b00000010,
HTTP_DELETE = 0b00000100,
HTTP_PUT = 0b00001000,
HTTP_PATCH = 0b00010000,
HTTP_HEAD = 0b00100000,
HTTP_OPTIONS = 0b01000000,
HTTP_ANY = 0b01111111,
} WebRequestMethod;
#ifndef HAVE_FS_FILE_OPEN_MODE
namespace fs {
class FileOpenMode {
public:
static const char* read;
static const char* write;
static const char* append;
};
};
#else
#include "FileOpenMode.h"
#endif
// if this value is returned when asked for data, packet will not be sent and you will be asked for data again
#define RESPONSE_TRY_AGAIN 0xFFFFFFFF
#define RESPONSE_STREAM_BUFFER_SIZE 1460
typedef uint8_t WebRequestMethodComposite;
typedef std::function<void(void)> ArDisconnectHandler;
/*
* PARAMETER :: Chainable object to hold GET/POST and FILE parameters
* */
class AsyncWebParameter {
private:
String _name;
String _value;
size_t _size;
bool _isForm;
bool _isFile;
public:
AsyncWebParameter(const String& name, const String& value, bool form = false, bool file = false, size_t size = 0) : _name(name), _value(value), _size(size), _isForm(form), _isFile(file) {}
const String& name() const { return _name; }
const String& value() const { return _value; }
size_t size() const { return _size; }
bool isPost() const { return _isForm; }
bool isFile() const { return _isFile; }
};
/*
* HEADER :: Chainable object to hold the headers
* */
class AsyncWebHeader {
private:
String _name;
String _value;
public:
AsyncWebHeader(const 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);
AsyncWebHeader& operator=(const AsyncWebHeader&) = default;
const String& name() const { return _name; }
const String& value() const { return _value; }
String toString() const;
};
/*
* REQUEST :: Each incoming Client is wrapped inside a Request and both live together until disconnect
* */
typedef enum { RCT_NOT_USED = -1,
RCT_DEFAULT = 0,
RCT_HTTP,
RCT_WS,
RCT_EVENT,
RCT_MAX } RequestedConnectionType;
// this enum is similar to Arduino WebServer's AsyncAuthType and PsychicHttp
typedef enum {
AUTH_NONE = 0, // always allow
AUTH_BASIC = 1,
AUTH_DIGEST = 2,
AUTH_BEARER = 3,
AUTH_OTHER = 4,
AUTH_DENIED = 255, // always returns 401
} AsyncAuthType;
typedef std::function<size_t(uint8_t*, size_t, size_t)> AwsResponseFiller;
typedef std::function<String(const String&)> AwsTemplateProcessor;
class AsyncWebServerRequest {
using File = fs::File;
using FS = fs::FS;
friend class AsyncWebServer;
friend class AsyncCallbackWebHandler;
private:
AsyncClient* _client;
AsyncWebServer* _server;
AsyncWebHandler* _handler;
AsyncWebServerResponse* _response;
ArDisconnectHandler _onDisconnectfn;
// response is sent
bool _sent = false;
String _temp;
uint8_t _parseState;
uint8_t _version;
WebRequestMethodComposite _method;
String _url;
String _host;
String _contentType;
String _boundary;
String _authorization;
RequestedConnectionType _reqconntype;
AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE;
bool _isMultipart;
bool _isPlainPost;
bool _expectingContinue;
size_t _contentLength;
size_t _parsedLength;
std::list<AsyncWebHeader> _headers;
std::list<AsyncWebParameter> _params;
std::vector<String> _pathParams;
std::unordered_map<const char*, String, std::hash<const char*>, std::equal_to<const char*>> _attributes;
uint8_t _multiParseState;
uint8_t _boundaryPosition;
size_t _itemStartIndex;
size_t _itemSize;
String _itemName;
String _itemFilename;
String _itemType;
String _itemValue;
uint8_t* _itemBuffer;
size_t _itemBufferIndex;
bool _itemIsFile;
void _onPoll();
void _onAck(size_t len, uint32_t time);
void _onError(int8_t error);
void _onTimeout(uint32_t time);
void _onDisconnect();
void _onData(void* buf, size_t len);
void _addPathParam(const char* param);
bool _parseReqHead();
bool _parseReqHeader();
void _parseLine();
void _parsePlainPostChar(uint8_t data);
void _parseMultipartPostByte(uint8_t data, bool last);
void _addGetParams(const String& params);
void _handleUploadStart();
void _handleUploadByte(uint8_t data, bool last);
void _handleUploadEnd();
public:
File _tempFile;
void* _tempObject;
AsyncWebServerRequest(AsyncWebServer*, AsyncClient*);
~AsyncWebServerRequest();
AsyncClient* client() { return _client; }
uint8_t version() const { return _version; }
WebRequestMethodComposite method() const { return _method; }
const String& url() const { return _url; }
const String& host() const { return _host; }
const String& contentType() const { return _contentType; }
size_t contentLength() const { return _contentLength; }
bool multipart() const { return _isMultipart; }
const char* methodToString() const;
const char* requestedConnTypeToString() const;
RequestedConnectionType requestedConnType() const { return _reqconntype; }
bool isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2 = RCT_NOT_USED, RequestedConnectionType erct3 = RCT_NOT_USED) const;
bool isWebSocketUpgrade() const { return _method == HTTP_GET && isExpectedRequestedConnType(RCT_WS); }
bool isSSE() const { return _method == HTTP_GET && isExpectedRequestedConnType(RCT_EVENT); }
bool isHTTP() const { return isExpectedRequestedConnType(RCT_DEFAULT, RCT_HTTP); }
void onDisconnect(ArDisconnectHandler fn);
// hash is the string representation of:
// base64(user:pass) for basic or
// user:realm:md5(user:realm:pass) for digest
bool authenticate(const char* hash) const;
bool authenticate(const char* username, const char* credentials, const char* realm = NULL, bool isHash = false) const;
void requestAuthentication(const char* realm = nullptr, bool isDigest = true) { requestAuthentication(isDigest ? AsyncAuthType::AUTH_DIGEST : AsyncAuthType::AUTH_BASIC, realm); }
void requestAuthentication(AsyncAuthType method, const char* realm = nullptr, const char* _authFailMsg = nullptr);
void setHandler(AsyncWebHandler* handler) { _handler = handler; }
#ifndef ESP8266
[[deprecated("All headers are now collected. Use removeHeader(name) or AsyncHeaderFreeMiddleware if you really need to free some headers.")]]
#endif
void addInterestingHeader(__unused const char* name) {
}
#ifndef ESP8266
[[deprecated("All headers are now collected. Use removeHeader(name) or AsyncHeaderFreeMiddleware if you really need to free some headers.")]]
#endif
void addInterestingHeader(__unused const String& name) {
}
/**
* @brief issue HTTP redirect responce with Location header
*
* @param url - url to redirect to
* @param code - responce code, default is 302 : temporary redirect
*/
void redirect(const char* url, int code = 302);
void redirect(const String& url, int code = 302) { return redirect(url.c_str(), code); };
void send(AsyncWebServerResponse* response);
AsyncWebServerResponse* getResponse() const { return _response; }
void send(int code, const char* contentType = asyncsrv::empty, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, callback)); }
void send(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType.c_str(), content, callback)); }
void send(int code, const String& contentType, const String& content, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType.c_str(), content.c_str(), callback)); }
void send(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, len, callback)); }
void send(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { 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 String& contentType, bool download = false, AwsTemplateProcessor callback = nullptr) { send(fs, path, contentType.c_str(), download, callback); }
void send(File content, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) {
if (content) {
send(beginResponse(content, path, contentType, download, callback));
} else
send(404);
}
void send(File content, const String& path, const String& contentType, bool download = false, AwsTemplateProcessor callback = nullptr) { send(content, path, contentType.c_str(), download, callback); }
void send(Stream& stream, const char* contentType, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(stream, contentType, len, callback)); }
void send(Stream& stream, const String& contentType, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(stream, contentType, len, callback)); }
void send(const char* contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginResponse(contentType, len, callback, templateCallback)); }
void send(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginResponse(contentType, len, callback, templateCallback)); }
void sendChunked(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginChunkedResponse(contentType, callback, templateCallback)); }
void sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginChunkedResponse(contentType, callback, templateCallback)); }
#ifndef ESP8266
[[deprecated("Replaced by send(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr)")]]
#endif
void send_P(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) {
send(code, contentType, content, len, callback);
}
#ifndef ESP8266
[[deprecated("Replaced by send(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr)")]]
void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) {
send(code, contentType, content, callback);
}
#else
void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) {
send(beginResponse_P(code, contentType, content, callback));
}
#endif
AsyncWebServerResponse* beginResponse(int code, const char* contentType = asyncsrv::empty, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr);
AsyncWebServerResponse* beginResponse(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content, callback); }
AsyncWebServerResponse* beginResponse(int code, const String& contentType, const String& content, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content.c_str(), callback); }
AsyncWebServerResponse* beginResponse(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr);
AsyncWebServerResponse* beginResponse(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content, len, callback); }
AsyncWebServerResponse* beginResponse(FS& fs, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr);
AsyncWebServerResponse* beginResponse(FS& fs, const String& path, const String& contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { return beginResponse(fs, path, contentType.c_str(), download, callback); }
AsyncWebServerResponse* beginResponse(File content, const String& path, const char* contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr);
AsyncWebServerResponse* beginResponse(File content, const String& path, const String& contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { return beginResponse(content, path, contentType.c_str(), download, callback); }
AsyncWebServerResponse* beginResponse(Stream& stream, const char* contentType, size_t len, AwsTemplateProcessor callback = nullptr);
AsyncWebServerResponse* beginResponse(Stream& stream, const String& contentType, size_t len, AwsTemplateProcessor callback = nullptr) { return beginResponse(stream, contentType.c_str(), len, callback); }
AsyncWebServerResponse* beginResponse(const char* contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncWebServerResponse* beginResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { return beginResponse(contentType.c_str(), len, callback, templateCallback); }
AsyncWebServerResponse* beginChunkedResponse(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncWebServerResponse* beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncResponseStream* beginResponseStream(const char* contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE);
AsyncResponseStream* beginResponseStream(const String& contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) { return beginResponseStream(contentType.c_str(), bufferSize); }
#ifndef ESP8266
[[deprecated("Replaced by beginResponse(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr)")]]
#endif
AsyncWebServerResponse* beginResponse_P(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) {
return beginResponse(code, contentType.c_str(), content, len, callback);
}
#ifndef ESP8266
[[deprecated("Replaced by beginResponse(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr)")]]
#endif
AsyncWebServerResponse* beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr);
/**
* @brief Get the Request parameter by name
*
* @param name
* @param post
* @param file
* @return const AsyncWebParameter*
*/
const AsyncWebParameter* getParam(const char* name, bool post = false, bool file = false) const;
const AsyncWebParameter* getParam(const String& name, bool post = false, bool file = false) const { return getParam(name.c_str(), post, file); };
/**
* @brief Get request parameter by number
* i.e., n-th parameter
* @param num
* @return const AsyncWebParameter*
*/
const AsyncWebParameter* getParam(size_t num) const;
size_t args() const { return params(); } // get arguments count
// get request argument value by name
const String& arg(const char* name) const;
// get request argument value by name
const String& arg(const String& name) const { return arg(name.c_str()); };
const String& arg(size_t i) const; // get request argument value by number
const String& argName(size_t i) const; // get request argument name by number
bool hasArg(const char* name) const; // check if argument exists
bool hasArg(const String& name) const { return hasArg(name.c_str()); };
const String& ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const;
// get request header value by name
const String& header(const char* name) const;
const String& header(const String& name) const { return header(name.c_str()); };
const String& header(size_t i) const; // get request header value by number
const String& headerName(size_t i) const; // get request header name by number
size_t headers() const; // get header count
// check if header exists
bool hasHeader(const char* name) const;
bool hasHeader(const String& name) const { return hasHeader(name.c_str()); };
const AsyncWebHeader* getHeader(const char* name) const;
const AsyncWebHeader* getHeader(const String& name) const { return getHeader(name.c_str()); };
const AsyncWebHeader* getHeader(size_t num) const;
const std::list<AsyncWebHeader>& getHeaders() const { return _headers; }
size_t getHeaderNames(std::vector<const char*>& names) const;
// Remove a header from the request.
// It will free the memory and prevent the header to be seen during request processing.
bool removeHeader(const char* name);
// Remove all request headers.
void removeHeaders() { _headers.clear(); }
size_t params() const; // get arguments count
bool hasParam(const char* name, bool post = false, bool file = false) const;
bool hasParam(const String& name, bool post = false, bool file = false) const { return hasParam(name.c_str(), post, file); };
// REQUEST ATTRIBUTES
void setAttribute(const char* name, const char* value) { _attributes[name] = value; }
void setAttribute(const char* name, bool value) { _attributes[name] = value ? "1" : emptyString; }
void setAttribute(const char* name, long value) { _attributes[name] = String(value); }
void setAttribute(const char* name, float value, unsigned int decimalPlaces = 2) { _attributes[name] = String(value, decimalPlaces); }
void setAttribute(const char* name, double value, unsigned int decimalPlaces = 2) { _attributes[name] = String(value, decimalPlaces); }
bool hasAttribute(const char* name) const { return _attributes.find(name) != _attributes.end(); }
const String& getAttribute(const char* name, const String& defaultValue = emptyString) const;
bool getAttribute(const char* name, bool defaultValue) const;
long getAttribute(const char* name, long defaultValue) const;
float getAttribute(const char* name, float defaultValue) const;
double getAttribute(const char* name, double defaultValue) const;
String urlDecode(const String& text) const;
};
/*
* FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server)
* */
using ArRequestFilterFunction = std::function<bool(AsyncWebServerRequest* request)>;
bool ON_STA_FILTER(AsyncWebServerRequest* request);
bool ON_AP_FILTER(AsyncWebServerRequest* request);
/*
* MIDDLEWARE :: Request interceptor, assigned to a AsyncWebHandler (or the server), which can be used:
* 1. to run some code before the final handler is executed (e.g. check authentication)
* 2. decide whether to proceed or not with the next handler
* */
using ArMiddlewareNext = std::function<void(void)>;
using ArMiddlewareCallback = std::function<void(AsyncWebServerRequest* request, ArMiddlewareNext next)>;
// Middleware is a base class for all middleware
class AsyncMiddleware {
public:
virtual ~AsyncMiddleware() {}
virtual void run(__unused AsyncWebServerRequest* request, __unused ArMiddlewareNext next) { return next(); };
private:
friend class AsyncWebHandler;
friend class AsyncEventSource;
friend class AsyncMiddlewareChain;
bool _freeOnRemoval = false;
};
// Create a custom middleware by providing an anonymous callback function
class AsyncMiddlewareFunction : public AsyncMiddleware {
public:
AsyncMiddlewareFunction(ArMiddlewareCallback fn) : _fn(fn) {}
void run(AsyncWebServerRequest* request, ArMiddlewareNext next) override { return _fn(request, next); };
private:
ArMiddlewareCallback _fn;
};
// For internal use only: super class to add/remove middleware to server or handlers
class AsyncMiddlewareChain {
public:
~AsyncMiddlewareChain();
void addMiddleware(ArMiddlewareCallback fn);
void addMiddleware(AsyncMiddleware* middleware);
void addMiddlewares(std::vector<AsyncMiddleware*> middlewares);
bool removeMiddleware(AsyncMiddleware* middleware);
// For internal use only
void _runChain(AsyncWebServerRequest* request, ArMiddlewareNext finalizer);
protected:
std::list<AsyncMiddleware*> _middlewares;
};
// AsyncAuthenticationMiddleware is a middleware that checks if the request is authenticated
class AsyncAuthenticationMiddleware : public AsyncMiddleware {
public:
void setUsername(const char* username);
void setPassword(const char* password);
void setPasswordHash(const char* hash);
void setRealm(const char* realm) { _realm = realm; }
void setAuthFailureMessage(const char* message) { _authFailMsg = message; }
// set the authentication method to use
// default is AUTH_NONE: no authentication required
// AUTH_BASIC: basic authentication
// AUTH_DIGEST: digest authentication
// AUTH_BEARER: bearer token authentication
// AUTH_OTHER: other authentication method
// AUTH_DENIED: always return 401 Unauthorized
// if a method is set but no username or password is set, authentication will be ignored
void setAuthType(AsyncAuthType authMethod) { _authMethod = authMethod; }
// precompute and store the hash value based on the username, password, realm.
// can be used for DIGEST and BASIC to avoid recomputing the hash for each request.
// returns true if the hash was successfully generated and replaced
bool generateHash();
// returns true if the username and password (or hash) are set
bool hasCredentials() const { return _hasCreds; }
bool allowed(AsyncWebServerRequest* request) const;
void run(AsyncWebServerRequest* request, ArMiddlewareNext next);
private:
String _username;
String _credentials;
bool _hash = false;
String _realm = asyncsrv::T_LOGIN_REQ;
AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE;
String _authFailMsg;
bool _hasCreds = false;
};
using ArAuthorizeFunction = std::function<bool(AsyncWebServerRequest* request)>;
// AsyncAuthorizationMiddleware is a middleware that checks if the request is authorized
class AsyncAuthorizationMiddleware : public AsyncMiddleware {
public:
AsyncAuthorizationMiddleware(ArAuthorizeFunction authorizeConnectHandler) : _code(403), _authz(authorizeConnectHandler) {}
AsyncAuthorizationMiddleware(int code, ArAuthorizeFunction authorizeConnectHandler) : _code(code), _authz(authorizeConnectHandler) {}
void run(AsyncWebServerRequest* request, ArMiddlewareNext next) { return _authz && !_authz(request) ? request->send(_code) : next(); }
private:
int _code;
ArAuthorizeFunction _authz;
};
// remove all headers from the incoming request except the ones provided in the constructor
class AsyncHeaderFreeMiddleware : public AsyncMiddleware {
public:
void keep(const char* name) { _toKeep.push_back(name); }
void unKeep(const char* name) { _toKeep.erase(std::remove(_toKeep.begin(), _toKeep.end(), name), _toKeep.end()); }
void run(AsyncWebServerRequest* request, ArMiddlewareNext next);
private:
std::vector<const char*> _toKeep;
};
// filter out specific headers from the incoming request
class AsyncHeaderFilterMiddleware : public AsyncMiddleware {
public:
void filter(const char* name) { _toRemove.push_back(name); }
void unFilter(const char* name) { _toRemove.erase(std::remove(_toRemove.begin(), _toRemove.end(), name), _toRemove.end()); }
void run(AsyncWebServerRequest* request, ArMiddlewareNext next);
private:
std::vector<const char*> _toRemove;
};
// curl-like logging of incoming requests
class AsyncLoggingMiddleware : public AsyncMiddleware {
public:
void setOutput(Print& output) { _out = &output; }
void setEnabled(bool enabled) { _enabled = enabled; }
bool isEnabled() const { return _enabled && _out; }
void run(AsyncWebServerRequest* request, ArMiddlewareNext next);
private:
Print* _out = nullptr;
bool _enabled = true;
};
// CORS Middleware
class AsyncCorsMiddleware : public AsyncMiddleware {
public:
void setOrigin(const char* origin) { _origin = origin; }
void setMethods(const char* methods) { _methods = methods; }
void setHeaders(const char* headers) { _headers = headers; }
void setAllowCredentials(bool credentials) { _credentials = credentials; }
void setMaxAge(uint32_t seconds) { _maxAge = seconds; }
void addCORSHeaders(AsyncWebServerResponse* response);
void run(AsyncWebServerRequest* request, ArMiddlewareNext next);
private:
String _origin = "*";
String _methods = "*";
String _headers = "*";
bool _credentials = true;
uint32_t _maxAge = 86400;
};
// Rate limit Middleware
class AsyncRateLimitMiddleware : public AsyncMiddleware {
public:
void setMaxRequests(size_t maxRequests) { _maxRequests = maxRequests; }
void setWindowSize(uint32_t seconds) { _windowSizeMillis = seconds * 1000; }
bool isRequestAllowed(uint32_t& retryAfterSeconds);
void run(AsyncWebServerRequest* request, ArMiddlewareNext next);
private:
size_t _maxRequests = 0;
uint32_t _windowSizeMillis = 0;
std::list<uint32_t> _requestTimes;
};
/*
* REWRITE :: One instance can be handle any Request (done by the Server)
* */
class AsyncWebRewrite {
protected:
String _from;
String _toUrl;
String _params;
ArRequestFilterFunction _filter{nullptr};
public:
AsyncWebRewrite(const char* from, const char* to) : _from(from), _toUrl(to) {
int index = _toUrl.indexOf('?');
if (index > 0) {
_params = _toUrl.substring(index + 1);
_toUrl = _toUrl.substring(0, index);
}
}
virtual ~AsyncWebRewrite() {}
AsyncWebRewrite& setFilter(ArRequestFilterFunction fn) {
_filter = fn;
return *this;
}
bool filter(AsyncWebServerRequest* request) const { return _filter == NULL || _filter(request); }
const String& from(void) const { return _from; }
const String& toUrl(void) const { return _toUrl; }
const String& params(void) const { return _params; }
virtual bool match(AsyncWebServerRequest* request) { return from() == request->url() && filter(request); }
};
/*
* HANDLER :: One instance can be attached to any Request (done by the Server)
* */
class AsyncWebHandler : public AsyncMiddlewareChain {
protected:
ArRequestFilterFunction _filter = nullptr;
AsyncAuthenticationMiddleware* _authMiddleware = nullptr;
public:
AsyncWebHandler() {}
virtual ~AsyncWebHandler() {}
AsyncWebHandler& setFilter(ArRequestFilterFunction fn);
AsyncWebHandler& setAuthentication(const char* username, const char* password, AsyncAuthType authMethod = AsyncAuthType::AUTH_DIGEST);
AsyncWebHandler& setAuthentication(const String& username, const String& password, AsyncAuthType authMethod = AsyncAuthType::AUTH_DIGEST) { return setAuthentication(username.c_str(), password.c_str(), authMethod); };
bool filter(AsyncWebServerRequest* request) { return _filter == NULL || _filter(request); }
virtual bool canHandle(AsyncWebServerRequest* request __attribute__((unused))) const { return false; }
virtual void handleRequest(__unused AsyncWebServerRequest* request) {}
virtual void handleUpload(__unused AsyncWebServerRequest* request, __unused const String& filename, __unused size_t index, __unused uint8_t* data, __unused size_t len, __unused bool final) {}
virtual void handleBody(__unused AsyncWebServerRequest* request, __unused uint8_t* data, __unused size_t len, __unused size_t index, __unused size_t total) {}
virtual bool isRequestHandlerTrivial() const { return true; }
};
/*
* RESPONSE :: One instance is created for each Request (attached by the Handler)
* */
typedef enum {
RESPONSE_SETUP,
RESPONSE_HEADERS,
RESPONSE_CONTENT,
RESPONSE_WAIT_ACK,
RESPONSE_END,
RESPONSE_FAILED
} WebResponseState;
class AsyncWebServerResponse {
protected:
int _code;
std::list<AsyncWebHeader> _headers;
String _contentType;
size_t _contentLength;
bool _sendContentLength;
bool _chunked;
size_t _headLength;
size_t _sentLength;
size_t _ackedLength;
size_t _writtenLength;
WebResponseState _state;
public:
static const char* responseCodeToString(int code);
public:
AsyncWebServerResponse();
virtual ~AsyncWebServerResponse() {}
void setCode(int code);
int code() const { return _code; }
void setContentLength(size_t len);
void setContentType(const String& type) { setContentType(type.c_str()); }
void setContentType(const char* type);
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); }
bool addHeader(const char* name, long value, bool replaceExisting = true) { return addHeader(name, String(value), replaceExisting); }
bool addHeader(const String& name, long value, bool replaceExisting = true) { return addHeader(name.c_str(), value, replaceExisting); }
bool removeHeader(const char* name);
const AsyncWebHeader* getHeader(const char* name) const;
const std::list<AsyncWebHeader>& getHeaders() const { return _headers; }
#ifndef ESP8266
[[deprecated("Use instead: _assembleHead(String& buffer, uint8_t version)")]]
#endif
String _assembleHead(uint8_t version) {
String buffer;
_assembleHead(buffer, version);
return buffer;
}
void _assembleHead(String& buffer, uint8_t version);
virtual bool _started() const;
virtual bool _finished() const;
virtual bool _failed() const;
virtual bool _sourceValid() const;
virtual void _respond(AsyncWebServerRequest* request);
virtual size_t _ack(AsyncWebServerRequest* request, size_t len, uint32_t time);
};
/*
* SERVER :: One instance
* */
typedef std::function<void(AsyncWebServerRequest* request)> ArRequestHandlerFunction;
typedef std::function<void(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final)> ArUploadHandlerFunction;
typedef std::function<void(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total)> ArBodyHandlerFunction;
class AsyncWebServer : public AsyncMiddlewareChain {
protected:
AsyncServer _server;
std::list<std::shared_ptr<AsyncWebRewrite>> _rewrites;
std::list<std::unique_ptr<AsyncWebHandler>> _handlers;
AsyncCallbackWebHandler* _catchAllHandler;
public:
AsyncWebServer(uint16_t port);
~AsyncWebServer();
void begin();
void end();
#if ASYNC_TCP_SSL_ENABLED
void onSslFileRequest(AcSSlFileHandler cb, void* arg);
void beginSecure(const char* cert, const char* private_key_file, const char* password);
#endif
AsyncWebRewrite& addRewrite(AsyncWebRewrite* rewrite);
/**
* @brief (compat) Add url rewrite rule by pointer
* a deep copy of the pointer object will be created,
* it is up to user to manage further lifetime of the object in argument
*
* @param rewrite pointer to rewrite object to copy setting from
* @return AsyncWebRewrite& reference to a newly created rewrite rule
*/
AsyncWebRewrite& addRewrite(std::shared_ptr<AsyncWebRewrite> rewrite);
/**
* @brief add url rewrite rule
*
* @param from
* @param to
* @return AsyncWebRewrite&
*/
AsyncWebRewrite& rewrite(const char* from, const char* to);
/**
* @brief (compat) remove rewrite rule via referenced object
* this will NOT deallocate pointed object itself, internal rule with same from/to urls will be removed if any
* it's a compat method, better use `removeRewrite(const char* from, const char* to)`
* @param rewrite
* @return true
* @return false
*/
bool removeRewrite(AsyncWebRewrite* rewrite);
/**
* @brief remove rewrite rule
*
* @param from
* @param to
* @return true
* @return false
*/
bool removeRewrite(const char* from, const char* to);
AsyncWebHandler& addHandler(AsyncWebHandler* handler);
bool removeHandler(AsyncWebHandler* handler);
AsyncCallbackWebHandler& on(const char* uri, ArRequestHandlerFunction onRequest) { return on(uri, HTTP_ANY, onRequest); }
AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr, ArBodyHandlerFunction onBody = nullptr);
AsyncStaticWebHandler& serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL);
void onNotFound(ArRequestHandlerFunction fn); // called when handler is not assigned
void onFileUpload(ArUploadHandlerFunction fn); // handle file uploads
void onRequestBody(ArBodyHandlerFunction fn); // handle posts with plain body content (JSON often transmitted this way as a request)
void reset(); // remove all writers and handlers, with onNotFound/onFileUpload/onRequestBody
void _handleDisconnect(AsyncWebServerRequest* request);
void _attachHandler(AsyncWebServerRequest* request);
void _rewriteRequest(AsyncWebServerRequest* request);
};
class DefaultHeaders {
using headers_t = std::list<AsyncWebHeader>;
headers_t _headers;
public:
DefaultHeaders() = default;
using ConstIterator = headers_t::const_iterator;
void addHeader(const String& name, const String& value) {
_headers.emplace_back(name, value);
}
ConstIterator begin() const { return _headers.begin(); }
ConstIterator end() const { return _headers.end(); }
DefaultHeaders(DefaultHeaders const&) = delete;
DefaultHeaders& operator=(DefaultHeaders const&) = delete;
static DefaultHeaders& Instance() {
static DefaultHeaders instance;
return instance;
}
};
#include "AsyncEventSource.h"
#include "AsyncWebSocket.h"
#include "WebHandlerImpl.h"
#include "WebResponseImpl.h"
#endif /* _AsyncWebServer_H_ */

View file

@ -0,0 +1,256 @@
#include "WebAuthentication.h"
#include "ESPAsyncWebServer.h"
AsyncMiddlewareChain::~AsyncMiddlewareChain() {
for (AsyncMiddleware* m : _middlewares)
if (m->_freeOnRemoval)
delete m;
}
void AsyncMiddlewareChain::addMiddleware(ArMiddlewareCallback fn) {
AsyncMiddlewareFunction* m = new AsyncMiddlewareFunction(fn);
m->_freeOnRemoval = true;
_middlewares.emplace_back(m);
}
void AsyncMiddlewareChain::addMiddleware(AsyncMiddleware* middleware) {
if (middleware)
_middlewares.emplace_back(middleware);
}
void AsyncMiddlewareChain::addMiddlewares(std::vector<AsyncMiddleware*> middlewares) {
for (AsyncMiddleware* m : middlewares)
addMiddleware(m);
}
bool AsyncMiddlewareChain::removeMiddleware(AsyncMiddleware* middleware) {
// remove all middlewares from _middlewares vector being equal to middleware, delete them having _freeOnRemoval flag to true and resize the vector.
const size_t size = _middlewares.size();
_middlewares.erase(std::remove_if(_middlewares.begin(), _middlewares.end(), [middleware](AsyncMiddleware* m) {
if (m == middleware) {
if (m->_freeOnRemoval)
delete m;
return true;
}
return false;
}),
_middlewares.end());
return size != _middlewares.size();
}
void AsyncMiddlewareChain::_runChain(AsyncWebServerRequest* request, ArMiddlewareNext finalizer) {
if (!_middlewares.size())
return finalizer();
ArMiddlewareNext next;
std::list<AsyncMiddleware*>::iterator it = _middlewares.begin();
next = [this, &next, &it, request, finalizer]() {
if (it == _middlewares.end())
return finalizer();
AsyncMiddleware* m = *it;
it++;
return m->run(request, next);
};
return next();
}
void AsyncAuthenticationMiddleware::setUsername(const char* username) {
_username = username;
_hasCreds = _username.length() && _credentials.length();
}
void AsyncAuthenticationMiddleware::setPassword(const char* password) {
_credentials = password;
_hash = false;
_hasCreds = _username.length() && _credentials.length();
}
void AsyncAuthenticationMiddleware::setPasswordHash(const char* hash) {
_credentials = hash;
_hash = _credentials.length();
_hasCreds = _username.length() && _credentials.length();
}
bool AsyncAuthenticationMiddleware::generateHash() {
// ensure we have all the necessary data
if (!_hasCreds)
return false;
// if we already have a hash, do nothing
if (_hash)
return false;
switch (_authMethod) {
case AsyncAuthType::AUTH_DIGEST:
_credentials = generateDigestHash(_username.c_str(), _credentials.c_str(), _realm.c_str());
_hash = true;
return true;
case AsyncAuthType::AUTH_BASIC:
_credentials = generateBasicHash(_username.c_str(), _credentials.c_str());
_hash = true;
return true;
default:
return false;
}
}
bool AsyncAuthenticationMiddleware::allowed(AsyncWebServerRequest* request) const {
if (_authMethod == AsyncAuthType::AUTH_NONE)
return true;
if (_authMethod == AsyncAuthType::AUTH_DENIED)
return false;
if (!_hasCreds)
return true;
return request->authenticate(_username.c_str(), _credentials.c_str(), _realm.c_str(), _hash);
}
void AsyncAuthenticationMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) {
return allowed(request) ? next() : request->requestAuthentication(_authMethod, _realm.c_str(), _authFailMsg.c_str());
}
void AsyncHeaderFreeMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) {
std::vector<const char*> reqHeaders;
request->getHeaderNames(reqHeaders);
for (const char* h : reqHeaders) {
bool keep = false;
for (const char* k : _toKeep) {
if (strcasecmp(h, k) == 0) {
keep = true;
break;
}
}
if (!keep) {
request->removeHeader(h);
}
}
next();
}
void AsyncHeaderFilterMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) {
for (auto it = _toRemove.begin(); it != _toRemove.end(); ++it)
request->removeHeader(*it);
next();
}
void AsyncLoggingMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) {
if (!isEnabled()) {
next();
return;
}
_out->print(F("* Connection from "));
_out->print(request->client()->remoteIP().toString());
_out->print(':');
_out->println(request->client()->remotePort());
_out->print('>');
_out->print(' ');
_out->print(request->methodToString());
_out->print(' ');
_out->print(request->url().c_str());
_out->print(F(" HTTP/1."));
_out->println(request->version());
for (auto& h : request->getHeaders()) {
if (h.value().length()) {
_out->print('>');
_out->print(' ');
_out->print(h.name());
_out->print(':');
_out->print(' ');
_out->println(h.value());
}
}
_out->println(F(">"));
uint32_t elapsed = millis();
next();
elapsed = millis() - elapsed;
AsyncWebServerResponse* response = request->getResponse();
if (response) {
_out->print(F("* Processed in "));
_out->print(elapsed);
_out->println(F(" ms"));
_out->print('<');
_out->print(F(" HTTP/1."));
_out->print(request->version());
_out->print(' ');
_out->print(response->code());
_out->print(' ');
_out->println(AsyncWebServerResponse::responseCodeToString(response->code()));
for (auto& h : response->getHeaders()) {
if (h.value().length()) {
_out->print('<');
_out->print(' ');
_out->print(h.name());
_out->print(':');
_out->print(' ');
_out->println(h.value());
}
}
_out->println('<');
} else {
_out->println(F("* Connection closed!"));
}
}
void AsyncCorsMiddleware::addCORSHeaders(AsyncWebServerResponse* response) {
response->addHeader(asyncsrv::T_CORS_ACAO, _origin.c_str());
response->addHeader(asyncsrv::T_CORS_ACAM, _methods.c_str());
response->addHeader(asyncsrv::T_CORS_ACAH, _headers.c_str());
response->addHeader(asyncsrv::T_CORS_ACAC, _credentials ? asyncsrv::T_TRUE : asyncsrv::T_FALSE);
response->addHeader(asyncsrv::T_CORS_ACMA, String(_maxAge).c_str());
}
void AsyncCorsMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) {
// Origin header ? => CORS handling
if (request->hasHeader(asyncsrv::T_CORS_O)) {
// check if this is a preflight request => handle it and return
if (request->method() == HTTP_OPTIONS) {
AsyncWebServerResponse* response = request->beginResponse(200);
addCORSHeaders(response);
request->send(response);
return;
}
// CORS request, no options => let the request pass and add CORS headers after
next();
AsyncWebServerResponse* response = request->getResponse();
if (response) {
addCORSHeaders(response);
}
} else {
// NO Origin header => no CORS handling
next();
}
}
bool AsyncRateLimitMiddleware::isRequestAllowed(uint32_t& retryAfterSeconds) {
uint32_t now = millis();
while (!_requestTimes.empty() && _requestTimes.front() <= now - _windowSizeMillis)
_requestTimes.pop_front();
_requestTimes.push_back(now);
if (_requestTimes.size() > _maxRequests) {
_requestTimes.pop_front();
retryAfterSeconds = (_windowSizeMillis - (now - _requestTimes.front())) / 1000 + 1;
return false;
}
retryAfterSeconds = 0;
return true;
}
void AsyncRateLimitMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) {
uint32_t retryAfterSeconds;
if (isRequestAllowed(retryAfterSeconds)) {
next();
} else {
AsyncWebServerResponse* response = request->beginResponse(429);
response->addHeader(asyncsrv::T_retry_after, retryAfterSeconds);
request->send(response);
}
}

Some files were not shown because too many files have changed in this diff Show more