mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 17:59:27 +02:00
Merge remote-tracking branch 'origin/main' into feature/automatic-precharge
This commit is contained in:
commit
fbd9fe6900
147 changed files with 15677 additions and 12777 deletions
|
@ -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
|
||||
|
|
|
@ -75,6 +75,7 @@ jobs:
|
|||
- BYD_KOSTAL_RS485
|
||||
- BYD_MODBUS
|
||||
- FOXESS_CAN
|
||||
- GROWATT_LV_CAN
|
||||
- PYLON_CAN
|
||||
- PYLON_LV_CAN
|
||||
- SCHNEIDER_CAN
|
||||
|
|
3
.github/workflows/compile-all-inverters.yml
vendored
3
.github/workflows/compile-all-inverters.yml
vendored
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
183
Software/src/battery/SONO-BATTERY.cpp
Normal file
183
Software/src/battery/SONO-BATTERY.cpp
Normal 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
|
17
Software/src/battery/SONO-BATTERY.h
Normal file
17
Software/src/battery/SONO-BATTERY.h
Normal 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
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) \
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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();
|
||||
|
|
|
@ -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>✓</span>"
|
||||
: "<span style='color: red;'>✕</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 "
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
548
Software/src/inverter/GROWATT-HV-CAN.cpp
Normal file
548
Software/src/inverter/GROWATT-HV-CAN.cpp
Normal 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
|
10
Software/src/inverter/GROWATT-HV-CAN.h
Normal file
10
Software/src/inverter/GROWATT-HV-CAN.h
Normal 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
|
278
Software/src/inverter/GROWATT-LV-CAN.cpp
Normal file
278
Software/src/inverter/GROWATT-LV-CAN.cpp
Normal 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
|
10
Software/src/inverter/GROWATT-LV-CAN.h
Normal file
10
Software/src/inverter/GROWATT-LV-CAN.h
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
606
Software/src/inverter/SUNGROW-CAN.cpp
Normal file
606
Software/src/inverter/SUNGROW-CAN.cpp
Normal 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
|
10
Software/src/inverter/SUNGROW-CAN.h
Normal file
10
Software/src/inverter/SUNGROW-CAN.h
Normal 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
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -31,6 +31,5 @@ class uptime_formatter
|
|||
uptime_formatter();
|
||||
|
||||
static String getUptime();
|
||||
static String getUptimeWithMillis();
|
||||
};
|
||||
#endif
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
22
Software/src/lib/mathieucarbou-AsyncTCPSock/library.json
Normal file
22
Software/src/lib/mathieucarbou-AsyncTCPSock/library.json
Normal 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
|
||||
}
|
||||
}
|
|
@ -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=*
|
1301
Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.cpp
Normal file
1301
Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.cpp
Normal file
File diff suppressed because it is too large
Load diff
321
Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h
Normal file
321
Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h
Normal 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_ */
|
|
@ -0,0 +1,6 @@
|
|||
#ifndef ASYNCTCP_SSL_H_
|
||||
#define ASYNCTCP_SSL_H_
|
||||
|
||||
#include "AsyncTCP_SSL.hpp"
|
||||
|
||||
#endif /* ASYNCTCP_SSL_H_ */
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
165
Software/src/lib/mathieucarbou-ESPAsyncWebServer/LICENSE
Normal file
165
Software/src/lib/mathieucarbou-ESPAsyncWebServer/LICENSE
Normal 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.
|
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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_ */
|
|
@ -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
|
131
Software/src/lib/mathieucarbou-ESPAsyncWebServer/src/AsyncJson.h
Normal file
131
Software/src/lib/mathieucarbou-ESPAsyncWebServer/src/AsyncJson.h
Normal 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_
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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_ */
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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_ */
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue