diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..2d7bc4b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: Triage +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the encountered bug is + +**To Reproduce** +Please try to explain the steps required to reproduce the issue + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Version and settings (please complete the following information):** +- Please attach your USER_SETTINGS files for easier debugging +- Software version: X.Y.Z +- Battery used: [NISSAN_LEAF] +- Inverter communication protocol: [BYD_MODBUS] +- Hardware used for Battery-Emulator: [HW_LILYGO/HW_STARK/HW_DEVKIT] +- CONTACTOR_CONTROL: [yes/no] +- MQTT: [yes/no] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..8562fc03 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Discord + url: https://www.patreon.com/dala + about: Get direct support and hang out with us! Join via Patreon + - name: Wiki + url: https://github.com/dalathegreat/Battery-Emulator/wiki + about: Highly recommended to read before asking questions! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..ee4f0b44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature request] " +labels: Feature request +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/compile-all-batteries.yml b/.github/workflows/compile-all-batteries.yml index 7a60ec63..aa553e24 100644 --- a/.github/workflows/compile-all-batteries.yml +++ b/.github/workflows/compile-all-batteries.yml @@ -64,6 +64,7 @@ jobs: - MEB_BATTERY - MG_5_BATTERY - NISSAN_LEAF_BATTERY + - ORION_BMS - PYLON_BATTERY - RJXZS_BMS - RANGE_ROVER_PHEV_BATTERY diff --git a/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml b/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml index 8a4f76e2..a522b186 100644 --- a/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml +++ b/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml @@ -55,6 +55,7 @@ jobs: # These are the batteries for which the code will be compiled. battery: - NISSAN_LEAF_BATTERY + - ORION_BMS - PYLON_BATTERY - RJXZS_BMS - RANGE_ROVER_PHEV_BATTERY diff --git a/.github/workflows/compile-all-inverters.yml b/.github/workflows/compile-all-inverters.yml index ad46efc8..e01bb44d 100644 --- a/.github/workflows/compile-all-inverters.yml +++ b/.github/workflows/compile-all-inverters.yml @@ -58,6 +58,7 @@ jobs: - BYD_CAN - BYD_KOSTAL_RS485 - BYD_MODBUS + - FERROAMP_CAN - FOXESS_CAN - GROWATT_HV_CAN - GROWATT_LV_CAN diff --git a/README.md b/README.md index 487c0935..5ff2741a 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ This code uses the following excellent libraries: - [bblanchon/ArduinoJson](https://github.com/bblanchon/ArduinoJson) MIT-License - [eModbus/eModbus](https://github.com/eModbus/eModbus) MIT-License - [mackelec/SerialDataLink](https://github.com/mackelec/SerialDataLink) -- [mathieucarbou/AsyncTCPsock](https://github.com/mathieucarbou/AsyncTCPSock) LGPL-3.0 license +- [ESP32Async/AsyncTCP](https://github.com/ESP32Async/AsyncTCP) LGPL-3.0 license - [ESP32Async/ESPAsyncWebServer](https://github.com/ESP32Async/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 diff --git a/Software/Software.ino b/Software/Software.ino index 619a0153..19890bc0 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -46,7 +46,7 @@ #endif // WIFI // The current software version, shown on webserver -const char* version_number = "8.5.dev"; +const char* version_number = "8.7.dev"; // Interval settings uint16_t intervalUpdateValues = INTERVAL_1_S; // Interval at which to update inverter values / Modbus registers @@ -73,6 +73,8 @@ TaskHandle_t logging_loop_task; Logging logging; +#define WDT_TIMEOUT_SECONDS 5 // If code hangs for longer than this, it will be rebooted by the watchdog + // Initialization void setup() { init_serial(); @@ -80,6 +82,8 @@ void setup() { // We print this after setting up serial, such that is also printed to serial with DEBUG_VIA_USB set. logging.printf("Battery emulator %s build " __DATE__ " " __TIME__ "\n", version_number); + init_events(); + init_stored_settings(); #ifdef WIFI @@ -92,8 +96,6 @@ void setup() { TASK_CONNECTIVITY_PRIO, &logging_loop_task, WIFI_CORE); #endif - init_events(); - init_CAN(); init_contactors(); @@ -118,10 +120,19 @@ void setup() { // BOOT button at runtime is used as an input for various things pinMode(0, INPUT_PULLUP); - esp_task_wdt_deinit(); // Disable watchdog - check_reset_reason(); + // Initialize Task Watchdog for subscribed tasks + esp_task_wdt_config_t wdt_config = { + .timeout_ms = WDT_TIMEOUT_SECONDS * 1000, // Convert seconds to milliseconds + .idle_core_mask = (1 << CORE_FUNCTION_CORE) | (1 << WIFI_CORE), // Watch both cores + .trigger_panic = true // Enable panic reset on timeout + }; + + // Initialize Task Watchdog + esp_task_wdt_init(&wdt_config); + + // Start tasks xTaskCreatePinnedToCore((TaskFunction_t)&core_loop, "core_loop", 4096, &core_task_time_us, TASK_CORE_PRIO, &main_loop_task, CORE_FUNCTION_CORE); } @@ -157,7 +168,7 @@ void logging_loop(void* task_time_us) { #ifdef WIFI void connectivity_loop(void* task_time_us) { - + esp_task_wdt_add(NULL); // Register this task with WDT // Init wifi init_WiFi(); @@ -191,12 +202,14 @@ void connectivity_loop(void* task_time_us) { datalayer.system.status.wifi_task_10s_max_us = 0; } #endif + esp_task_wdt_reset(); // Reset watchdog delay(1); } } #endif void core_loop(void* task_time_us) { + esp_task_wdt_add(NULL); // Register this task with WDT TickType_t xLastWakeTime = xTaskGetTickCount(); const TickType_t xFrequency = pdMS_TO_TICKS(1); // Convert 1ms to ticks led_init(); @@ -210,7 +223,7 @@ void core_loop(void* task_time_us) { // Input, Runs as fast as possible receive_can(); // Receive CAN messages -#ifdef RS485_INVERTER_SELECTED +#if defined(RS485_INVERTER_SELECTED) || defined(RS485_BATTERY_SELECTED) receive_RS485(); // Process serial2 RS485 interface #endif // RS485_INVERTER_SELECTED #if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER) @@ -255,6 +268,10 @@ void core_loop(void* task_time_us) { // Output transmit_can(); // Send CAN messages to all components +#ifdef RS485_BATTERY_SELECTED + transmit_rs485(); +#endif // RS485_BATTERY_SELECTED + END_TIME_MEASUREMENT_MAX(cantx, datalayer.system.status.time_cantx_us); END_TIME_MEASUREMENT_MAX(all, datalayer.system.status.core_task_10s_max_us); #ifdef FUNCTION_TIME_MEASUREMENT @@ -286,7 +303,7 @@ void core_loop(void* task_time_us) { #ifdef DEBUG_LOG logging.log_bms_status(datalayer.battery.status.real_bms_status, 1); #endif - + esp_task_wdt_reset(); // Reset watchdog to prevent reset vTaskDelayUntil(&xLastWakeTime, xFrequency); } } @@ -335,13 +352,48 @@ void update_calculated_values() { /* Restrict values from user settings if needed*/ if (datalayer.battery.status.max_charge_current_dA > datalayer.battery.settings.max_user_set_charge_dA) { datalayer.battery.status.max_charge_current_dA = datalayer.battery.settings.max_user_set_charge_dA; + datalayer.battery.settings.user_settings_limit_charge = true; + } else { + datalayer.battery.settings.user_settings_limit_charge = false; } if (datalayer.battery.status.max_discharge_current_dA > datalayer.battery.settings.max_user_set_discharge_dA) { datalayer.battery.status.max_discharge_current_dA = datalayer.battery.settings.max_user_set_discharge_dA; + datalayer.battery.settings.user_settings_limit_discharge = true; + } else { + datalayer.battery.settings.user_settings_limit_discharge = false; } /* Calculate active power based on voltage and current*/ datalayer.battery.status.active_power_W = (datalayer.battery.status.current_dA * (datalayer.battery.status.voltage_dV / 100)); + /* Calculate if battery or inverter is limiting factor*/ + + if (datalayer.battery.status.current_dA == 0) { //Battery idle + if (datalayer.battery.status.max_discharge_current_dA > 0) { + //We allow discharge, but inverter does nothing. Inverter is limiting + datalayer.battery.settings.inverter_limits_discharge = true; + } else { + datalayer.battery.settings.inverter_limits_discharge = false; + } + if (datalayer.battery.status.max_charge_current_dA > 0) { + //We allow charge, but inverter does nothing. Inverter is limiting + datalayer.battery.settings.inverter_limits_charge = true; + } else { + datalayer.battery.settings.inverter_limits_charge = false; + } + } else if (datalayer.battery.status.current_dA < 0) { //Battery discharging + if (-datalayer.battery.status.current_dA < datalayer.battery.status.max_discharge_current_dA) { + datalayer.battery.settings.inverter_limits_discharge = true; + } else { + datalayer.battery.settings.inverter_limits_discharge = false; + } + } else { // > 0 Battery charging + //If actual current is smaller than max we allow, inverter is limiting factor + if (datalayer.battery.status.current_dA < datalayer.battery.status.max_charge_current_dA) { + datalayer.battery.settings.inverter_limits_charge = true; + } else { + datalayer.battery.settings.inverter_limits_charge = false; + } + } #ifdef DOUBLE_BATTERY /* Calculate active power based on voltage and current for battery 2*/ diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index c50b0c36..c75e96c9 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -25,7 +25,9 @@ //#define MEB_BATTERY //#define MG_5_BATTERY //#define NISSAN_LEAF_BATTERY +//#define ORION_BMS //#define PYLON_BATTERY +//#define DALY_BMS //#define RJXZS_BMS //#define RANGE_ROVER_PHEV_BATTERY //#define RENAULT_KANGOO_BATTERY @@ -39,15 +41,17 @@ //#define TESLA_MODEL_3Y_BATTERY //#define TESLA_MODEL_SX_BATTERY //#define VOLVO_SPA_BATTERY +//#define VOLVO_SPA_HYBRID_BATTERY //#define TEST_FAKE_BATTERY //#define DOUBLE_BATTERY //Enable this line if you use two identical batteries at the same time (requires CAN_ADDON setup) //#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery) /* Select inverter communication protocol. See Wiki for which to use with your inverter: https://github.com/dalathegreat/BYD-Battery-Emulator-For-Gen24/wiki */ //#define AFORE_CAN //Enable this line to emulate an "Afore battery" over CAN bus -//#define BYD_CAN //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus +//#define BYD_CAN //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus //#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 FERROAMP_CAN //Enable this line to emulate a "Pylon 4x96V Force H2" over CAN Bus //#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 @@ -92,8 +96,8 @@ //#define EQUIPMENT_STOP_BUTTON // Enable this to allow an equipment stop button connected to the Battery-Emulator to disengage the battery //#define LFP_CHEMISTRY //Tesla specific setting, enable this line to startup in LFP mode //#define INTERLOCK_REQUIRED //Nissan LEAF specific setting, if enabled requires both high voltage conenctors to be seated before starting -//#define LOG_TO_SD //Enable this line to log diagnostic data to SD card -//#define LOG_CAN_TO_SD //Enable this line to log incoming/outgoing CAN & CAN-FD messages to SD card +//#define LOG_TO_SD //Enable this line to log diagnostic data to SD card (WARNING, raises CPU load, do not use for production) +//#define LOG_CAN_TO_SD //Enable this line to log incoming/outgoing CAN & CAN-FD messages to SD card (WARNING, raises CPU load, do not use for production) //#define DEBUG_VIA_USB //Enable this line to have the USB port output serial diagnostic data while program runs (WARNING, raises CPU load, do not use for production) //#define DEBUG_VIA_WEB //Enable this line to log diagnostic data while program runs, which can be viewed via webpage (WARNING, slightly raises CPU load, do not use for production) //#define DEBUG_CAN_DATA //Enable this line to print incoming/outgoing CAN & CAN-FD messages to USB serial (WARNING, raises CPU load, do not use for production) @@ -140,6 +144,8 @@ #define BATTERY_MAXTEMPERATURE 500 // -250 = -25.0 °C , Min temperature (Will produce a battery frozen event if below) #define BATTERY_MINTEMPERATURE -250 +// 150 = 15.0 °C , Max difference between min and max temperature (Will produce a battery temperature deviation event if greater) +#define BATTERY_MAX_TEMPERATURE_DEVIATION 150 // 300 = 30.0A , Max charge in Amp (Some inverters needs to be limited) #define BATTERY_MAX_CHARGE_AMP 300 // 300 = 30.0A , Max discharge in Amp (Some inverters needs to be limited) diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index 78de9ae7..149b9965 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -42,6 +42,10 @@ void setup_can_shunt(); #include "FOXESS-BATTERY.h" #endif +#ifdef ORION_BMS +#include "ORION-BMS.h" +#endif + #ifdef SONO_BATTERY #include "SONO-BATTERY.h" #endif @@ -86,6 +90,10 @@ void setup_can_shunt(); #include "PYLON-BATTERY.h" #endif +#ifdef DALY_BMS +#include "DALY-BMS.h" +#endif + #ifdef RJXZS_BMS #include "RJXZS-BMS.h" #endif @@ -131,14 +139,24 @@ void setup_can_shunt(); #include "VOLVO-SPA-BATTERY.h" #endif +#ifdef VOLVO_SPA_HYBRID_BATTERY +#include "VOLVO-SPA-HYBRID-BATTERY.h" +#endif + #ifdef SERIAL_LINK_RECEIVER #include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h" #endif -void handle_incoming_can_frame_battery(CAN_frame rx_frame); -void update_values_battery(); -void transmit_can_battery(); void setup_battery(void); +void update_values_battery(); + +#ifdef RS485_BATTERY_SELECTED +void transmit_rs485(); +void receive_RS485(); +#else +void handle_incoming_can_frame_battery(CAN_frame rx_frame); +void transmit_can_battery(); +#endif #ifdef DOUBLE_BATTERY void update_values_battery2(); diff --git a/Software/src/battery/BMW-IX-BATTERY.cpp b/Software/src/battery/BMW-IX-BATTERY.cpp index 9e1eacd1..300ddb27 100644 --- a/Software/src/battery/BMW-IX-BATTERY.cpp +++ b/Software/src/battery/BMW-IX-BATTERY.cpp @@ -790,6 +790,9 @@ void setup_battery(void) { // Performs one time setup at startup strncpy(datalayer.system.info.battery_protocol, "BMW iX and i4-7 platform", 63); datalayer.system.info.battery_protocol[63] = '\0'; + //Reset Battery at bootup + transmit_can_frame(&BMWiX_6F4_REQUEST_HARD_RESET, can_config.battery); + //Before we have started up and detected which battery is in use, use 108S values datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV; datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; diff --git a/Software/src/battery/BMW-IX-BATTERY.h b/Software/src/battery/BMW-IX-BATTERY.h index e04a0d87..aa97b03b 100644 --- a/Software/src/battery/BMW-IX-BATTERY.h +++ b/Software/src/battery/BMW-IX-BATTERY.h @@ -15,7 +15,7 @@ #define MAX_CHARGE_POWER_WHEN_TOPBALANCING_W 500 #define RAMPDOWN_SOC 9000 // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00% #define STALE_PERIOD_CONFIG \ - 300000; //Number of milliseconds before critical values are classed as stale/stuck 300000 = 300 seconds + 400000; //Number of milliseconds before critical values are classed as stale/stuck 300000 = 400 seconds void setup_battery(void); void transmit_can_frame(CAN_frame* tx_frame, int interface); diff --git a/Software/src/battery/BOLT-AMPERA-BATTERY.h b/Software/src/battery/BOLT-AMPERA-BATTERY.h index 3c900e87..1f075530 100644 --- a/Software/src/battery/BOLT-AMPERA-BATTERY.h +++ b/Software/src/battery/BOLT-AMPERA-BATTERY.h @@ -7,7 +7,7 @@ #define MAX_PACK_VOLTAGE_DV 4150 //5000 = 500.0V #define MIN_PACK_VOLTAGE_DV 2500 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 150 #define MAX_CELL_VOLTAGE_MV 4250 //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 diff --git a/Software/src/battery/BYD-ATTO-3-BATTERY.cpp b/Software/src/battery/BYD-ATTO-3-BATTERY.cpp index 5c37a74f..e78d08c7 100644 --- a/Software/src/battery/BYD-ATTO-3-BATTERY.cpp +++ b/Software/src/battery/BYD-ATTO-3-BATTERY.cpp @@ -36,6 +36,9 @@ static int16_t BMS_highest_cell_temperature = 0; static int16_t BMS_average_cell_temperature = 0; static uint16_t BMS_lowest_cell_voltage_mV = 3300; static uint16_t BMS_highest_cell_voltage_mV = 3300; +static uint8_t battery_frame_index = 0; +#define NOF_CELLS 126 +static uint16_t battery_cellvoltages[NOF_CELLS] = {0}; #ifdef DOUBLE_BATTERY static int16_t battery2_temperature_ambient = 0; static int16_t battery2_daughterboard_temperatures[10]; @@ -52,6 +55,8 @@ static int16_t BMS2_highest_cell_temperature = 0; static int16_t BMS2_average_cell_temperature = 0; static uint16_t BMS2_lowest_cell_voltage_mV = 3300; static uint16_t BMS2_highest_cell_voltage_mV = 3300; +static uint8_t battery2_frame_index = 0; +static uint16_t battery2_cellvoltages[NOF_CELLS] = {0}; #endif //DOUBLE_BATTERY #define POLL_FOR_BATTERY_SOC 0x05 #define POLL_FOR_BATTERY_VOLTAGE 0x08 @@ -135,9 +140,38 @@ void update_values_battery() { //This function maps all the values fetched via datalayer.battery.status.cell_min_voltage_mV = BMS_lowest_cell_voltage_mV; - datalayer.battery.status.temperature_min_dC = BMS_lowest_cell_temperature * 10; // Add decimals + //Map all cell voltages to the global array + memcpy(datalayer.battery.status.cell_voltages_mV, battery_cellvoltages, NOF_CELLS * sizeof(uint16_t)); +#ifdef SKIP_TEMPERATURE_SENSOR_NUMBER + // Initialize min and max variables for temperature calculation + battery_calc_min_temperature = battery_daughterboard_temperatures[0]; + battery_calc_max_temperature = battery_daughterboard_temperatures[0]; + + // Loop through the array of 10x daughterboard temps to find the smallest and largest values + // Note, it is possible for user to skip using a faulty sensor in the .h file + if (SKIP_TEMPERATURE_SENSOR_NUMBER == 1) { //If sensor 1 is skipped, init minmax to sensor 2 + battery_calc_min_temperature = battery_daughterboard_temperatures[1]; + battery_calc_max_temperature = battery_daughterboard_temperatures[1]; + } + for (int i = 1; i < 10; i++) { + if (i == (SKIP_TEMPERATURE_SENSOR_NUMBER - 1)) { + i++; + } + if (battery_daughterboard_temperatures[i] < battery_calc_min_temperature) { + battery_calc_min_temperature = battery_daughterboard_temperatures[i]; + } + if (battery_daughterboard_temperatures[i] > battery_calc_max_temperature) { + battery_calc_max_temperature = battery_daughterboard_temperatures[i]; + } + } + //Write the result to datalayer + datalayer.battery.status.temperature_min_dC = battery_calc_min_temperature * 10; + datalayer.battery.status.temperature_max_dC = battery_calc_max_temperature * 10; +#else //User does not need filtering out a broken sensor, just use the min-max the BMS sends + datalayer.battery.status.temperature_min_dC = BMS_lowest_cell_temperature * 10; datalayer.battery.status.temperature_max_dC = BMS_highest_cell_temperature * 10; +#endif //!SKIP_TEMPERATURE_SENSOR_NUMBER // Update webserver datalayer datalayer_extended.bydAtto3.SOC_method = SOC_method; @@ -147,6 +181,16 @@ void update_values_battery() { //This function maps all the values fetched via datalayer_extended.bydAtto3.SOC_polled = BMS_SOC; datalayer_extended.bydAtto3.voltage_periodic = battery_voltage; datalayer_extended.bydAtto3.voltage_polled = BMS_voltage; + datalayer_extended.bydAtto3.battery_temperatures[0] = battery_daughterboard_temperatures[0]; + datalayer_extended.bydAtto3.battery_temperatures[1] = battery_daughterboard_temperatures[1]; + datalayer_extended.bydAtto3.battery_temperatures[2] = battery_daughterboard_temperatures[2]; + datalayer_extended.bydAtto3.battery_temperatures[3] = battery_daughterboard_temperatures[3]; + datalayer_extended.bydAtto3.battery_temperatures[4] = battery_daughterboard_temperatures[4]; + datalayer_extended.bydAtto3.battery_temperatures[5] = battery_daughterboard_temperatures[5]; + datalayer_extended.bydAtto3.battery_temperatures[6] = battery_daughterboard_temperatures[6]; + datalayer_extended.bydAtto3.battery_temperatures[7] = battery_daughterboard_temperatures[7]; + datalayer_extended.bydAtto3.battery_temperatures[8] = battery_daughterboard_temperatures[8]; + datalayer_extended.bydAtto3.battery_temperatures[9] = battery_daughterboard_temperatures[9]; } void handle_incoming_can_frame_battery(CAN_frame rx_frame) { @@ -217,6 +261,15 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { break; case 0x43D: datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + battery_frame_index = rx_frame.data.u8[0]; + + if (battery_frame_index < (NOF_CELLS / 3)) { + uint8_t base_index = battery_frame_index * 3; + for (uint8_t i = 0; i < 3; i++) { + battery_cellvoltages[base_index + i] = + (((rx_frame.data.u8[2 * (i + 1)] & 0x0F) << 8) | rx_frame.data.u8[2 * i + 1]); + } + } break; case 0x444: datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; @@ -443,6 +496,9 @@ void update_values_battery2() { //This function maps all the values fetched via datalayer.battery2.status.temperature_min_dC = BMS2_lowest_cell_temperature * 10; // Add decimals datalayer.battery2.status.temperature_max_dC = BMS2_highest_cell_temperature * 10; + + //Map all cell voltages to the global array + memcpy(datalayer.battery2.status.cell_voltages_mV, battery2_cellvoltages, NOF_CELLS * sizeof(uint16_t)); } void handle_incoming_can_frame_battery2(CAN_frame rx_frame) { @@ -511,6 +567,14 @@ void handle_incoming_can_frame_battery2(CAN_frame rx_frame) { break; case 0x43D: datalayer.battery2.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + battery2_frame_index = rx_frame.data.u8[0]; + if (battery2_frame_index < (NOF_CELLS / 3)) { + uint8_t base2_index = battery2_frame_index * 3; + for (uint8_t i = 0; i < 3; i++) { + battery2_cellvoltages[base2_index + i] = + (((rx_frame.data.u8[2 * (i + 1)] & 0x0F) << 8) | rx_frame.data.u8[2 * i + 1]); + } + } break; case 0x444: datalayer.battery2.status.CAN_battery_still_alive = CAN_STILL_ALIVE; diff --git a/Software/src/battery/BYD-ATTO-3-BATTERY.h b/Software/src/battery/BYD-ATTO-3-BATTERY.h index 57f943ff..f726ebc6 100644 --- a/Software/src/battery/BYD-ATTO-3-BATTERY.h +++ b/Software/src/battery/BYD-ATTO-3-BATTERY.h @@ -8,6 +8,10 @@ #define MAXPOWER_CHARGE_W 10000 #define MAXPOWER_DISCHARGE_W 10000 +//Uncomment and configure this line, if you want to filter out a broken temperature sensor (1-10) +//Make sure you understand risks associated with disabling. Values can be read via "More Battery info" +//#define SKIP_TEMPERATURE_SENSOR_NUMBER 1 + /* Do not modify the rows below */ #define BATTERY_SELECTED #define MAX_PACK_VOLTAGE_DV 4410 //5000 = 500.0V diff --git a/Software/src/battery/DALY-BMS.cpp b/Software/src/battery/DALY-BMS.cpp new file mode 100644 index 00000000..0be25c0f --- /dev/null +++ b/Software/src/battery/DALY-BMS.cpp @@ -0,0 +1,206 @@ +#include "DALY-BMS.h" +#include +#include "../include.h" +#include "RJXZS-BMS.h" +#ifdef DALY_BMS +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" +#include "RENAULT-TWIZY.h" + +/* Do not change code below unless you are sure what you are doing */ + +static uint32_t lastPacket = 0; +static int16_t temperature_min_dC = 0; +static int16_t temperature_max_dC = 0; +static int16_t current_dA = 0; +static uint16_t voltage_dV = 0; +static uint32_t remaining_capacity_mAh = 0; +static uint16_t cellvoltages_mV[48] = {0}; +static uint16_t cellvoltage_min_mV = 0; +static uint16_t cellvoltage_max_mV = 0; +static uint16_t SOC = 0; +static bool has_fault = false; + +void update_values_battery() { + datalayer.battery.status.real_soc = SOC; + datalayer.battery.status.voltage_dV = voltage_dV; //value is *10 (3700 = 370.0) + datalayer.battery.status.current_dA = current_dA; //value is *10 (150 = 15.0) + datalayer.battery.status.remaining_capacity_Wh = (remaining_capacity_mAh * (uint32_t)voltage_dV) / 10000; + + datalayer.battery.status.max_charge_power_W = (BATTERY_MAX_CHARGE_AMP * voltage_dV) / 100; + datalayer.battery.status.max_discharge_power_W = (BATTERY_MAX_DISCHARGE_AMP * voltage_dV) / 100; + + // limit power when SoC is low or high + uint32_t adaptive_power_limit = 999999; + if (SOC < 2000) + adaptive_power_limit = ((uint32_t)SOC * POWER_PER_PERCENT) / 100; + else if (SOC > 8000) + adaptive_power_limit = ((10000 - (uint32_t)SOC) * POWER_PER_PERCENT) / 100; + + if (adaptive_power_limit < datalayer.battery.status.max_charge_power_W) + datalayer.battery.status.max_charge_power_W = adaptive_power_limit; + if (SOC < 2000 && adaptive_power_limit < datalayer.battery.status.max_discharge_power_W) + datalayer.battery.status.max_discharge_power_W = adaptive_power_limit; + + // always allow to charge at least a little bit + if (datalayer.battery.status.max_charge_power_W < POWER_PER_PERCENT) + datalayer.battery.status.max_charge_power_W = POWER_PER_PERCENT; + + memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages_mV, sizeof(cellvoltages_mV)); + datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min_mV; + datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max_mV; + + datalayer.battery.status.temperature_min_dC = temperature_min_dC; + datalayer.battery.status.temperature_max_dC = temperature_max_dC; + + datalayer.battery.status.real_bms_status = has_fault ? BMS_FAULT : BMS_ACTIVE; +} + +void setup_battery(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "DALY RS485", 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer.battery.info.number_of_cells = CELL_COUNT; + datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV; + datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; + datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV; + datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV; + datalayer.battery.info.total_capacity_Wh = BATTERY_WH_MAX; +} + +uint8_t calculate_checksum(uint8_t buff[12]) { + uint8_t check = 0; + for (uint8_t i = 0; i < 12; i++) { + check += buff[i]; + } + return check; +} + +uint16_t decode_uint16be(uint8_t data[8], uint8_t offset) { + uint16_t upper = data[offset]; + uint16_t lower = data[offset + 1]; + return (upper << 8) | lower; +} +int16_t decode_int16be(uint8_t data[8], uint8_t offset) { + int16_t upper = data[offset]; + int16_t lower = data[offset + 1]; + return (upper << 8) | lower; +} +uint32_t decode_uint32be(uint8_t data[8], uint8_t offset) { + return (((uint32_t)data[offset]) << 24) | (((uint32_t)data[offset + 1]) << 16) | (((uint32_t)data[offset + 2]) << 8) | + ((uint32_t)data[offset + 3]); +} + +#ifdef DEBUG_VIA_USB +void dump_buff(const char* msg, uint8_t* buff, uint8_t len) { + Serial.print("[DALY-BMS] "); + Serial.print(msg); + for (int i = 0; i < len; i++) { + Serial.print(buff[i] >> 4, HEX); + Serial.print(buff[i] & 0xf, HEX); + Serial.print(" "); + } + Serial.println(); +} +#endif + +void decode_packet(uint8_t command, uint8_t data[8]) { + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + + switch (command) { + case 0x90: + voltage_dV = decode_uint16be(data, 0); + current_dA = decode_int16be(data, 4) - 30000; + SOC = decode_uint16be(data, 6) * 10; + break; + case 0x91: + cellvoltage_max_mV = decode_uint16be(data, 0); + cellvoltage_min_mV = decode_uint16be(data, 3); + break; + case 0x92: + temperature_max_dC = (data[0] - 40) * 10; + temperature_min_dC = (data[2] - 40) * 10; + break; + case 0x93: + remaining_capacity_mAh = decode_uint32be(data, 4); + break; + case 0x94: + break; + case 0x95: + if (data[0] > 0 && data[0] <= 16) { + uint8_t frame_index = (data[0] - 1) * 3; + cellvoltages_mV[frame_index + 0] = decode_uint16be(data, 1); + cellvoltages_mV[frame_index + 1] = decode_uint16be(data, 3); + cellvoltages_mV[frame_index + 2] = decode_uint16be(data, 5); + } + break; + case 0x96: + break; + case 0x97: + break; + case 0x98: + // for now we do not handle individual faults. All of them are 0 when ok, and 1 when a fault occurs + has_fault = false; + for (int i = 0; i < 8; i++) { + if (data[i] != 0x00) { + has_fault = true; + } + } + break; + } +} + +void transmit_rs485() { + static uint8_t nextCommand = 0x90; + + if (millis() - lastPacket > 60) { + uint8_t tx_buff[13] = {0}; + tx_buff[0] = 0xA5; + tx_buff[1] = 0x40; + tx_buff[2] = nextCommand; + tx_buff[3] = 8; + tx_buff[12] = calculate_checksum(tx_buff); + +#ifdef DEBUG_VIA_USB + dump_buff("transmitting: ", tx_buff, 13); +#endif + + Serial2.write(tx_buff, 13); + lastPacket = millis(); + + nextCommand++; + if (nextCommand > 0x98) + nextCommand = 0x90; + } +} + +void receive_RS485() { + static uint8_t recv_buff[13] = {0}; + static uint8_t recv_len = 0; + + while (Serial2.available()) { + recv_buff[recv_len] = Serial2.read(); + + recv_len++; + + if (recv_len > 0 && recv_buff[0] != 0xA5 || recv_len > 1 && recv_buff[1] != 0x01 || + recv_len > 2 && (recv_buff[2] < 0x90 || recv_buff[2] > 0x98) || recv_len > 3 && recv_buff[3] != 8 || + recv_len > 12 && recv_buff[12] != calculate_checksum(recv_buff)) { + +#ifdef DEBUG_VIA_USB + dump_buff("dropping partial rx: ", recv_buff, recv_len); +#endif + recv_len = 0; + } + + if (recv_len > 12) { +#ifdef DEBUG_VIA_USB + dump_buff("decoding successfull rx: ", recv_buff, recv_len); +#endif + decode_packet(recv_buff[2], &recv_buff[4]); + recv_len = 0; + lastPacket = millis(); + } + } +} + +#endif diff --git a/Software/src/battery/DALY-BMS.h b/Software/src/battery/DALY-BMS.h new file mode 100644 index 00000000..dfc3df02 --- /dev/null +++ b/Software/src/battery/DALY-BMS.h @@ -0,0 +1,17 @@ +#ifndef DALY_BMS_H +#define DALY_BMS_H + +/* Tweak these according to your battery build */ +#define CELL_COUNT 14 +#define MAX_PACK_VOLTAGE_DV 588 //588 = 58.8V +#define MIN_PACK_VOLTAGE_DV 518 //518 = 51.8V +#define MAX_CELL_VOLTAGE_MV 4250 //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 +#define POWER_PER_PERCENT 50 // below 20% and above 80% limit power to 50W * SOC (i.e. 150W at 3%, 500W at 10%, ...) + +/* Do not modify any rows below*/ +#define BATTERY_SELECTED +#define RS485_BATTERY_SELECTED +#define RS485_BAUDRATE 9600 + +#endif diff --git a/Software/src/battery/IMIEV-CZERO-ION-BATTERY.h b/Software/src/battery/IMIEV-CZERO-ION-BATTERY.h index 4191ab67..048fc1d4 100644 --- a/Software/src/battery/IMIEV-CZERO-ION-BATTERY.h +++ b/Software/src/battery/IMIEV-CZERO-ION-BATTERY.h @@ -6,7 +6,7 @@ #define BATTERY_SELECTED #define MAX_PACK_VOLTAGE_DV 3696 //5000 = 500.0V #define MIN_PACK_VOLTAGE_DV 3160 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 250 #define MAX_CELL_VOLTAGE_MV 4150 //Battery is put into emergency stop if one cell goes over this value #define MIN_CELL_VOLTAGE_MV 2750 //Battery is put into emergency stop if one cell goes below this value diff --git a/Software/src/battery/JAGUAR-IPACE-BATTERY.h b/Software/src/battery/JAGUAR-IPACE-BATTERY.h index adf120a1..d89e4770 100644 --- a/Software/src/battery/JAGUAR-IPACE-BATTERY.h +++ b/Software/src/battery/JAGUAR-IPACE-BATTERY.h @@ -5,7 +5,7 @@ #define BATTERY_SELECTED #define MAX_PACK_VOLTAGE_DV 4546 //5000 = 500.0V #define MIN_PACK_VOLTAGE_DV 3370 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 250 #define MAX_CELL_VOLTAGE_MV 4250 //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 diff --git a/Software/src/battery/KIA-HYUNDAI-64-BATTERY.cpp b/Software/src/battery/KIA-HYUNDAI-64-BATTERY.cpp index af621981..a8d85114 100644 --- a/Software/src/battery/KIA-HYUNDAI-64-BATTERY.cpp +++ b/Software/src/battery/KIA-HYUNDAI-64-BATTERY.cpp @@ -40,6 +40,7 @@ static int8_t powerRelayTemperature = 0; static bool startedUp = false; #ifdef DOUBLE_BATTERY +static uint8_t counter_200_2 = 0; static uint16_t battery2_soc_calculated = 0; static uint16_t battery2_SOC_BMS = 0; static uint16_t battery2_SOC_Display = 0; @@ -69,23 +70,28 @@ static int8_t battery2_temperature_water_inlet = 0; static int8_t battery2_heatertemp = 0; static int8_t battery2_powerRelayTemperature = 0; static bool battery2_startedUp = false; -#endif //DOUBLE_BATTERY +CAN_frame KIA_HYUNDAI_200_2 = {.FD = false, + .ext_ID = false, + .DLC = 8, + .ID = 0x200, + .data = {0x00, 0x80, 0xD8, 0x04, 0x00, 0x17, 0xD0, 0x00}}; //2nd battery +#endif //DOUBLE_BATTERY CAN_frame KIA_HYUNDAI_200 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x200, - .data = {0x00, 0x80, 0xD8, 0x04, 0x00, 0x17, 0xD0, 0x00}}; //Mid log value + .data = {0x00, 0x80, 0xD8, 0x04, 0x00, 0x17, 0xD0, 0x00}}; CAN_frame KIA_HYUNDAI_523 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x523, - .data = {0x08, 0x38, 0x36, 0x36, 0x33, 0x34, 0x00, 0x01}}; //Mid log value + .data = {0x08, 0x38, 0x36, 0x36, 0x33, 0x34, 0x00, 0x01}}; CAN_frame KIA_HYUNDAI_524 = {.FD = false, .ext_ID = false, .DLC = 8, .ID = 0x524, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Initial value + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; //553 Needed frame 200ms CAN_frame KIA64_553 = {.FD = false, .ext_ID = false, @@ -724,7 +730,7 @@ void handle_incoming_can_frame_battery2(CAN_frame rx_frame) { case 0x10: //"PID Header" if (rx_frame.data.u8[4] == battery2_poll_data_pid) { transmit_can_frame(&KIA64_7E4_ack, - can_config.battery); //Send ack to BMS if the same frame is sent as polled + can_config.battery_double); //Send ack to BMS if the same frame is sent as polled } break; case 0x21: //First frame in PID group @@ -997,8 +1003,49 @@ void transmit_can_battery() { transmit_can_frame(&KIA_HYUNDAI_524, can_config.battery); #ifdef DOUBLE_BATTERY + if (battery2_startedUp && datalayer.system.status.battery2_allows_contactor_closing) { - transmit_can_frame(&KIA_HYUNDAI_200, can_config.battery_double); + switch (counter_200_2) { + case 0: + KIA_HYUNDAI_200_2.data.u8[5] = 0x17; + ++counter_200_2; + break; + case 1: + KIA_HYUNDAI_200_2.data.u8[5] = 0x57; + ++counter_200_2; + break; + case 2: + KIA_HYUNDAI_200_2.data.u8[5] = 0x97; + ++counter_200_2; + break; + case 3: + KIA_HYUNDAI_200_2.data.u8[5] = 0xD7; + ++counter_200_2; + break; + case 4: + KIA_HYUNDAI_200_2.data.u8[3] = 0x10; + KIA_HYUNDAI_200_2.data.u8[5] = 0xFF; + ++counter_200_2; + break; + case 5: + KIA_HYUNDAI_200_2.data.u8[5] = 0x3B; + ++counter_200_2; + break; + case 6: + KIA_HYUNDAI_200_2.data.u8[5] = 0x7B; + ++counter_200_2; + break; + case 7: + KIA_HYUNDAI_200_2.data.u8[5] = 0xBB; + ++counter_200_2; + break; + case 8: + KIA_HYUNDAI_200_2.data.u8[5] = 0xFB; + counter_200_2 = 5; + break; + } + + transmit_can_frame(&KIA_HYUNDAI_200_2, can_config.battery_double); transmit_can_frame(&KIA_HYUNDAI_523, can_config.battery_double); diff --git a/Software/src/battery/MEB-BATTERY.cpp b/Software/src/battery/MEB-BATTERY.cpp index ea2b24ac..fa261c14 100644 --- a/Software/src/battery/MEB-BATTERY.cpp +++ b/Software/src/battery/MEB-BATTERY.cpp @@ -51,11 +51,6 @@ static uint16_t battery_allowed_charge_power = 0; static uint16_t battery_allowed_discharge_power = 0; static uint16_t cellvoltages_polled[108]; static uint16_t tempval = 0; -static uint8_t BMS_5A2_CRC = 0; -static uint8_t BMS_5CA_CRC = 0; -static uint8_t BMS_0CF_CRC = 0; -static uint8_t BMS_578_CRC = 0; -static uint8_t BMS_0C0_CRC = 0; static uint8_t BMS_16A954A6_CRC = 0; static uint8_t BMS_5A2_counter = 0; static uint8_t BMS_5CA_counter = 0; @@ -386,7 +381,7 @@ uint32_t can_msg_received = 0; * @see https://www.autosar.org/fileadmin/user_upload/standards/classic/4-3/AUTOSAR_SWS_CRCLibrary.pdf * @see https://web.archive.org/web/20221105210302/https://www.autosar.org/fileadmin/user_upload/standards/classic/4-3/AUTOSAR_SWS_CRCLibrary.pdf */ -uint8_t vw_crc_calc(uint8_t* inputBytes, uint8_t length, uint16_t address) { +uint8_t vw_crc_calc(uint8_t* inputBytes, uint8_t length, uint32_t address) { const uint8_t poly = 0x2F; const uint8_t xor_output = 0xFF; @@ -395,6 +390,8 @@ uint8_t vw_crc_calc(uint8_t* inputBytes, uint8_t length, uint16_t address) { 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40}; const uint8_t MB00C0[16] = {0x2f, 0x44, 0x72, 0xd3, 0x07, 0xf2, 0x39, 0x09, 0x8d, 0x6f, 0x57, 0x20, 0x37, 0xf9, 0x9b, 0xfa}; + const uint8_t MB00CF[16] = {0xee, 0x80, 0x6e, 0x4e, 0x29, 0xc6, 0x92, 0xc0, + 0x65, 0xaa, 0x3a, 0xa1, 0x8f, 0xcd, 0xe6, 0x90}; const uint8_t MB00FC[16] = {0x77, 0x5c, 0xa0, 0x89, 0x4b, 0x7c, 0xbb, 0xd6, 0x1f, 0x6c, 0x4f, 0xf6, 0x20, 0x2b, 0x43, 0xdd}; const uint8_t MB00FD[16] = {0xb4, 0xef, 0xf8, 0x49, 0x1e, 0xe5, 0xc2, 0xc0, @@ -423,6 +420,8 @@ uint8_t vw_crc_calc(uint8_t* inputBytes, uint8_t length, uint16_t address) { 0x1e, 0x0d, 0x24, 0xcd, 0x8c, 0xa6, 0x2f, 0x41}; const uint8_t MB0578[16] = {0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48}; + const uint8_t MB05A2[16] = {0xeb, 0x4c, 0x44, 0xaf, 0x21, 0x8d, 0x01, 0x58, + 0xfa, 0x93, 0xdb, 0x89, 0x15, 0x10, 0x4a, 0x61}; const uint8_t MB05CA[16] = {0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43}; const uint8_t MB0641[16] = {0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, @@ -445,6 +444,9 @@ uint8_t vw_crc_calc(uint8_t* inputBytes, uint8_t length, uint16_t address) { case 0x00C0: // magicByte = MB00C0[counter]; break; + case 0x00CF: //BMS + magicByte = MB00CF[counter]; + break; case 0x00FC: magicByte = MB00FC[counter]; break; @@ -487,6 +489,9 @@ uint8_t vw_crc_calc(uint8_t* inputBytes, uint8_t length, uint16_t address) { case 0x0578: // BMS DC magicByte = MB0578[counter]; break; + case 0x05A2: // BMS + magicByte = MB05A2[counter]; + break; case 0x05CA: // BMS magicByte = MB05CA[counter]; break; @@ -503,7 +508,9 @@ uint8_t vw_crc_calc(uint8_t* inputBytes, uint8_t length, uint16_t address) { magicByte = MB16A954A6[counter]; break; default: // this won't lead to correct CRC checksums - logging.println("Checksum request uknown"); +#ifdef DEBUG_LOG + logging.println("Checksum request unknown"); +#endif magicByte = 0x00; break; } @@ -625,6 +632,26 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { #endif first_can_msg = last_can_msg_timestamp; } + + /* CRC check on messages with CRC */ + switch (rx_frame.ID) { + case 0x0CF: + case 0x578: + case 0x5A2: + case 0x5CA: + case 0x16A954A6: + if (rx_frame.data.u8[0] != + vw_crc_calc(rx_frame.data.u8, rx_frame.DLC, rx_frame.ID)) { //If CRC does not match calc + datalayer.battery.status.CAN_error_counter++; +#ifdef DEBUG_LOG + logging.printf("MEB: Msg 0x%04X CRC error\n", rx_frame.ID); +#endif + return; + } + default: + break; + } + switch (rx_frame.ID) { case 0x17F0007B: // BMS 500ms can_msg_received |= RX_0x17F0007B; @@ -707,7 +734,6 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { break; case 0x16A954A6: // BMS can_msg_received |= RX_0x16A954A6; - BMS_16A954A6_CRC = rx_frame.data.u8[0]; // Can be used to check CAN signal integrity later on BMS_16A954A6_counter = (rx_frame.data.u8[1] & 0x0F); // Can be used to check CAN signal integrity later on isolation_fault = (rx_frame.data.u8[2] & 0xE0) >> 5; isolation_status = (rx_frame.data.u8[2] & 0x1E) >> 1; @@ -944,7 +970,6 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { energy_extracted_from_battery = ((rx_frame.data.u8[7] & 0x7F) << 8) | rx_frame.data.u8[6]; break; case 0x578: // BMS 100ms - BMS_578_CRC = rx_frame.data.u8[0]; // Can be used to check CAN signal integrity later on BMS_578_counter = (rx_frame.data.u8[1] & 0x0F); // Can be used to check CAN signal integrity later on BMS_Status_DCLS = ((rx_frame.data.u8[1] & 0x30) >> 4); DC_voltage_DCLS = (rx_frame.data.u8[2] << 6) | (rx_frame.data.u8[1] >> 6); @@ -953,7 +978,6 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { break; case 0x5A2: // BMS 500ms normal, 100ms fast can_msg_received |= RX_0x5A2; - BMS_5A2_CRC = rx_frame.data.u8[0]; // Can be used to check CAN signal integrity later on BMS_5A2_counter = (rx_frame.data.u8[1] & 0x0F); // Can be used to check CAN signal integrity later on service_disconnect_switch_missing = (rx_frame.data.u8[1] & 0x20) >> 5; pilotline_open = (rx_frame.data.u8[1] & 0x10) >> 4; @@ -969,10 +993,9 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { break; case 0x5CA: // BMS 500ms can_msg_received |= RX_0x5CA; - BMS_5CA_CRC = rx_frame.data.u8[0]; // Can be used to check CAN signal integrity later on - BMS_5CA_counter = (rx_frame.data.u8[1] & 0x0F); // Can be used to check CAN signal integrity later on - balancing_request = (rx_frame.data.u8[5] & 0x08) >> - 3; // BMS requests a low current end charge to support balancing, maybe unused. + BMS_5CA_counter = (rx_frame.data.u8[1] & 0x0F); + balancing_request = (rx_frame.data.u8[5] & 0x08) >> 3; + // balancing_request: BMS requests a low current end charge to support balancing, maybe unused. battery_diagnostic = (rx_frame.data.u8[3] & 0x07); battery_Wh_left = (rx_frame.data.u8[2] << 4) | (rx_frame.data.u8[1] >> 4); //*50 ! Not usable, seems to always contain 0x7F0 @@ -985,8 +1008,7 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { break; case 0x0CF: //BMS 10ms can_msg_received |= RX_0x0CF; - BMS_0CF_CRC = rx_frame.data.u8[0]; // Can be used to check CAN signal integrity later on - BMS_0CF_counter = (rx_frame.data.u8[1] & 0x0F); // Can be used to check CAN signal integrity later on + BMS_0CF_counter = (rx_frame.data.u8[1] & 0x0F); BMS_welded_contactors_status = (rx_frame.data.u8[1] & 0x60) >> 5; BMS_ext_limits_active = (rx_frame.data.u8[1] & 0x80) >> 7; BMS_mode = (rx_frame.data.u8[2] & 0x07); diff --git a/Software/src/battery/NISSAN-LEAF-BATTERY.cpp b/Software/src/battery/NISSAN-LEAF-BATTERY.cpp index ddd7c91e..8926be0f 100644 --- a/Software/src/battery/NISSAN-LEAF-BATTERY.cpp +++ b/Software/src/battery/NISSAN-LEAF-BATTERY.cpp @@ -202,7 +202,11 @@ void update_values_battery() { /* This function maps all the values fetched via datalayer.battery.status.current_dA = (battery_Current2 * 5); //0.5A/bit, multiply by 5 to get Amp+1decimal (5,5A = 11) - datalayer.battery.info.total_capacity_Wh = (battery_Max_GIDS * WH_PER_GID); + if (battery_Max_GIDS == 273) { //battery_Max_GIDS is stuck at 273 on ZE0 + datalayer.battery.info.total_capacity_Wh = ((battery_Max_GIDS * WH_PER_GID * battery_StateOfHealth) / 100); + } else { //battery_Max_GIDS updates on newer generations, making for a total_capacity_Wh value that makes sense + datalayer.battery.info.total_capacity_Wh = (battery_Max_GIDS * WH_PER_GID); + } datalayer.battery.status.remaining_capacity_Wh = battery_Wh_Remaining; @@ -381,7 +385,11 @@ void update_values_battery2() { // Handle the values coming in from battery #2 datalayer.battery2.status.current_dA = (battery2_Current2 * 5); //0.5A/bit, multiply by 5 to get Amp+1decimal (5,5A = 11) - datalayer.battery2.info.total_capacity_Wh = (battery2_Max_GIDS * WH_PER_GID); + if (battery2_Max_GIDS == 273) { //battery2_Max_GIDS is stuck at 273 on 24kWh packs + datalayer.battery2.info.total_capacity_Wh = ((battery2_Max_GIDS * WH_PER_GID * battery2_StateOfHealth) / 100); + } else { //battery_Max_GIDS updates on newer generations, making for a total_capacity_Wh value that makes sense + datalayer.battery2.info.total_capacity_Wh = (battery2_Max_GIDS * WH_PER_GID); + } datalayer.battery2.status.remaining_capacity_Wh = battery2_Wh_Remaining; diff --git a/Software/src/battery/NISSAN-LEAF-BATTERY.h b/Software/src/battery/NISSAN-LEAF-BATTERY.h index f5e7adcd..25564d26 100644 --- a/Software/src/battery/NISSAN-LEAF-BATTERY.h +++ b/Software/src/battery/NISSAN-LEAF-BATTERY.h @@ -6,7 +6,7 @@ #define BATTERY_SELECTED #define MAX_PACK_VOLTAGE_DV 4040 //5000 = 500.0V #define MIN_PACK_VOLTAGE_DV 2600 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 150 #define MAX_CELL_VOLTAGE_MV 4250 //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 diff --git a/Software/src/battery/ORION-BMS.cpp b/Software/src/battery/ORION-BMS.cpp new file mode 100644 index 00000000..f0ba33a8 --- /dev/null +++ b/Software/src/battery/ORION-BMS.cpp @@ -0,0 +1,151 @@ +#include "../include.h" +#ifdef ORION_BMS +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" +#include "ORION-BMS.h" + +/* Do not change code below unless you are sure what you are doing */ +static uint16_t cellvoltages[MAX_AMOUNT_CELLS]; //array with all the cellvoltages +static uint16_t Maximum_Cell_Voltage = 3700; +static uint16_t Minimum_Cell_Voltage = 3700; +static uint16_t Pack_Health = 99; +static int16_t Pack_Current = 0; +static int16_t Average_Temperature = 0; +static uint16_t Pack_Summed_Voltage = 0; +static int16_t Average_Current = 0; +static uint16_t High_Temperature = 0; +static uint16_t Pack_SOC_ppt = 0; +static uint16_t Pack_CCL = 0; //Charge current limit (A) +static uint16_t Pack_DCL = 0; //Discharge current limit (A) +static uint16_t Maximum_Pack_Voltage = 0; +static uint16_t Minimum_Pack_Voltage = 0; +static uint16_t CellID = 0; +static uint16_t CellVoltage = 0; +static uint16_t CellResistance = 0; +static uint16_t CellOpenVoltage = 0; +static uint16_t Checksum = 0; +static uint16_t CellBalancing = 0; +static uint8_t amount_of_detected_cells = 0; + +void findMinMaxCellvoltages(const uint16_t arr[], size_t size, uint16_t& Minimum_Cell_Voltage, + uint16_t& Maximum_Cell_Voltage) { + Minimum_Cell_Voltage = std::numeric_limits::max(); + Maximum_Cell_Voltage = 0; + bool foundValidValue = false; + + for (size_t i = 0; i < size; ++i) { + if (arr[i] != 0) { // Skip zero values + if (arr[i] < Minimum_Cell_Voltage) + Minimum_Cell_Voltage = arr[i]; + if (arr[i] > Maximum_Cell_Voltage) + Maximum_Cell_Voltage = arr[i]; + foundValidValue = true; + } + } + + // If all values were zero, set min and max to 3700 + if (!foundValidValue) { + Minimum_Cell_Voltage = 3700; + Maximum_Cell_Voltage = 3700; + } +} + +void update_values_battery() { + + datalayer.battery.status.real_soc = Pack_SOC_ppt * 10; + + datalayer.battery.status.soh_pptt = Pack_Health * 100; + + datalayer.battery.status.voltage_dV = (Pack_Summed_Voltage / 10); + + datalayer.battery.status.current_dA = Average_Current; + + datalayer.battery.status.max_charge_power_W = (Pack_CCL * datalayer.battery.status.voltage_dV) / 100; + + datalayer.battery.status.max_discharge_power_W = (Pack_DCL * datalayer.battery.status.voltage_dV) / 100; + + datalayer.battery.status.remaining_capacity_Wh = static_cast( + (static_cast(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); + + datalayer.battery.status.temperature_min_dC = (High_Temperature - 10); + + datalayer.battery.status.temperature_max_dC = High_Temperature; + + //Map all cell voltages to the global array + memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages, MAX_AMOUNT_CELLS * sizeof(uint16_t)); + + //Find min and max cellvoltage from the array + findMinMaxCellvoltages(cellvoltages, MAX_AMOUNT_CELLS, Minimum_Cell_Voltage, Maximum_Cell_Voltage); + + datalayer.battery.status.cell_max_voltage_mV = Maximum_Cell_Voltage; + + datalayer.battery.status.cell_min_voltage_mV = Minimum_Cell_Voltage; + + //If user did not configure amount of cells correctly in the header file, update the value + if ((amount_of_detected_cells > NUMBER_OF_CELLS) && (amount_of_detected_cells < MAX_AMOUNT_CELLS)) { + datalayer.battery.info.number_of_cells = amount_of_detected_cells; + } +} + +void handle_incoming_can_frame_battery(CAN_frame rx_frame) { + switch (rx_frame.ID) { + case 0x356: + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + Pack_Summed_Voltage = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]; + Average_Current = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]; + High_Temperature = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]; + break; + case 0x351: + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + Maximum_Pack_Voltage = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]; + Pack_CCL = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]; + Pack_DCL = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]; + Minimum_Pack_Voltage = (rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]; + break; + case 0x355: + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + Pack_Health = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]; + Pack_SOC_ppt = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]; + break; + case 0x35A: + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x36: + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + CellID = rx_frame.data.u8[0]; + CellBalancing = (rx_frame.data.u8[3] & 0x80) >> 7; + CellVoltage = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]; + CellResistance = ((rx_frame.data.u8[3] & 0x7F) << 8) | rx_frame.data.u8[4]; + CellOpenVoltage = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + Checksum = rx_frame.data.u8[7]; //Value = (0x36 + 8 + byte0 + byte1 + ... + byte6) & 0xFF + + if (CellID >= MAX_AMOUNT_CELLS) { + CellID = MAX_AMOUNT_CELLS; + } + cellvoltages[CellID] = (CellVoltage / 10); + if (CellID > amount_of_detected_cells) { + amount_of_detected_cells = CellID; + } + break; + default: + break; + } +} + +void transmit_can_battery() { + unsigned long currentMillis = millis(); + // No transmission needed for this integration +} + +void setup_battery(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "DIY battery with Orion BMS (Victron setting)", 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer.battery.info.number_of_cells = NUMBER_OF_CELLS; + 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.system.status.battery_allows_contactor_closing = true; +} + +#endif diff --git a/Software/src/battery/ORION-BMS.h b/Software/src/battery/ORION-BMS.h new file mode 100644 index 00000000..6412d92f --- /dev/null +++ b/Software/src/battery/ORION-BMS.h @@ -0,0 +1,19 @@ +#ifndef ORION_BMS_H +#define ORION_BMS_H +#include +#include "../include.h" + +#define BATTERY_SELECTED + +/* Change the following to suit your battery */ +#define NUMBER_OF_CELLS 96 +#define MAX_PACK_VOLTAGE_DV 5000 //5000 = 500.0V +#define MIN_PACK_VOLTAGE_DV 1500 +#define MAX_CELL_VOLTAGE_MV 4250 //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 +#define MAX_CELL_DEVIATION_MV 150 + +void setup_battery(void); +void transmit_can_frame(CAN_frame* tx_frame, int interface); + +#endif diff --git a/Software/src/battery/PYLON-BATTERY.h b/Software/src/battery/PYLON-BATTERY.h index 8d17c047..026a8c75 100644 --- a/Software/src/battery/PYLON-BATTERY.h +++ b/Software/src/battery/PYLON-BATTERY.h @@ -10,7 +10,7 @@ #define MIN_PACK_VOLTAGE_DV 1500 #define MAX_CELL_VOLTAGE_MV 4250 //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 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 150 void setup_battery(void); void transmit_can_frame(CAN_frame* tx_frame, int interface); diff --git a/Software/src/battery/RANGE-ROVER-PHEV-BATTERY.h b/Software/src/battery/RANGE-ROVER-PHEV-BATTERY.h index 9dfcaf06..874ad76d 100644 --- a/Software/src/battery/RANGE-ROVER-PHEV-BATTERY.h +++ b/Software/src/battery/RANGE-ROVER-PHEV-BATTERY.h @@ -6,11 +6,11 @@ #define BATTERY_SELECTED /* Change the following to suit your battery */ -#define MAX_PACK_VOLTAGE_DV 5000 //TODO: Configure -#define MIN_PACK_VOLTAGE_DV 0 //TODO: Configure -#define MAX_CELL_VOLTAGE_MV 4250 //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 -#define MAX_CELL_DEVIATION_MV 500 //TODO: Configure +#define MAX_PACK_VOLTAGE_DV 5000 //TODO: Configure +#define MIN_PACK_VOLTAGE_DV 0 //TODO: Configure +#define MAX_CELL_VOLTAGE_MV 4250 //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 +#define MAX_CELL_DEVIATION_MV 150 void setup_battery(void); void transmit_can_frame(CAN_frame* tx_frame, int interface); diff --git a/Software/src/battery/RENAULT-KANGOO-BATTERY.h b/Software/src/battery/RENAULT-KANGOO-BATTERY.h index 25f739c6..c7cb9cf7 100644 --- a/Software/src/battery/RENAULT-KANGOO-BATTERY.h +++ b/Software/src/battery/RENAULT-KANGOO-BATTERY.h @@ -6,7 +6,7 @@ #define BATTERY_SELECTED #define MAX_PACK_VOLTAGE_DV 4150 //5000 = 500.0V #define MIN_PACK_VOLTAGE_DV 2500 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 150 #define MAX_CELL_VOLTAGE_MV 4250 //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 #define MAX_CHARGE_POWER_W 5000 // Battery can be charged with this amount of power diff --git a/Software/src/battery/RENAULT-TWIZY.h b/Software/src/battery/RENAULT-TWIZY.h index 8454d35b..98adddaa 100644 --- a/Software/src/battery/RENAULT-TWIZY.h +++ b/Software/src/battery/RENAULT-TWIZY.h @@ -5,7 +5,7 @@ #define BATTERY_SELECTED #define MAX_PACK_VOLTAGE_DV 579 // 57.9V at 100% SOC (with 70% SOH, new one might be higher) #define MIN_PACK_VOLTAGE_DV 480 // 48.4V at 13.76% SOC -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 150 #define MAX_CELL_VOLTAGE_MV 4200 //Battery is put into emergency stop if one cell goes over this value #define MIN_CELL_VOLTAGE_MV 3400 //Battery is put into emergency stop if one cell goes below this value diff --git a/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.h b/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.h index 1a3968bf..5b092b23 100644 --- a/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.h +++ b/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.h @@ -3,10 +3,9 @@ #include "../include.h" #define BATTERY_SELECTED -#define MAX_CELL_DEVIATION_MV 500 #define MAX_PACK_VOLTAGE_DV 4200 //5000 = 500.0V #define MIN_PACK_VOLTAGE_DV 3000 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 150 #define MAX_CELL_VOLTAGE_MV 4250 //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 diff --git a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h index cfcc65d7..bfb39e1f 100644 --- a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h +++ b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h @@ -5,7 +5,7 @@ #define BATTERY_SELECTED #define MAX_PACK_VOLTAGE_DV 4100 //5000 = 500.0V #define MIN_PACK_VOLTAGE_DV 3000 -#define MAX_CELL_DEVIATION_MV 500 +#define MAX_CELL_DEVIATION_MV 150 #define MAX_CELL_VOLTAGE_MV 4250 //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 diff --git a/Software/src/battery/RJXZS-BMS.cpp b/Software/src/battery/RJXZS-BMS.cpp index 345e8eae..f08e9659 100644 --- a/Software/src/battery/RJXZS-BMS.cpp +++ b/Software/src/battery/RJXZS-BMS.cpp @@ -74,6 +74,8 @@ static uint16_t fan_start_setting_value = 0; static uint16_t ptc_heating_start_setting_value = 0; static uint16_t default_channel_state = 0; static uint8_t timespent_without_soc = 0; +static bool charging_active = false; +static bool discharging_active = false; void update_values_battery() { @@ -96,7 +98,13 @@ void update_values_battery() { datalayer.battery.status.voltage_dV = total_voltage; - datalayer.battery.status.current_dA = total_current; + if (charging_active) { + datalayer.battery.status.current_dA = total_current; + } else if (discharging_active) { + datalayer.battery.status.current_dA = -total_current; + } else { //No direction data. Should never occur, but send current as charging, better than nothing + datalayer.battery.status.current_dA = total_current; + } // Charge power is set in .h file if (datalayer.battery.status.real_soc > 9900) { @@ -204,6 +212,13 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { host_temperature = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]; status_accounting = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]; equalization_starting_voltage = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + if ((rx_frame.data.u8[4] & 0x40) >> 6) { + charging_active = true; + discharging_active = false; + } else { + charging_active = false; + discharging_active = true; + } } else if (mux == 0x07) { // Cellvoltages 1-3 cellvoltages[0] = (rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]; cellvoltages[1] = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]; @@ -507,6 +522,12 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) { low_temperature_protection_setting_value = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; protecting_historical_logs = rx_frame.data.u8[7]; + if ((protecting_historical_logs & 0x0F) > 0) { + set_event(EVENT_RJXZS_LOG, 0); + } else { + clear_event(EVENT_RJXZS_LOG); + } + if (protecting_historical_logs == 0x01) { // Overcurrent protection set_event(EVENT_DISCHARGE_LIMIT_EXCEEDED, 0); // could also be EVENT_CHARGE_LIMIT_EXCEEDED diff --git a/Software/src/battery/TESLA-BATTERY.h b/Software/src/battery/TESLA-BATTERY.h index fd878484..e2fc1acf 100644 --- a/Software/src/battery/TESLA-BATTERY.h +++ b/Software/src/battery/TESLA-BATTERY.h @@ -9,11 +9,10 @@ #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 (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 RAMPDOWN_SOC 900 // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00% +#define RAMPDOWNPOWERALLOWED 10000 // 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 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 diff --git a/Software/src/battery/VOLVO-SPA-HYBRID-BATTERY.cpp b/Software/src/battery/VOLVO-SPA-HYBRID-BATTERY.cpp new file mode 100644 index 00000000..233af5ca --- /dev/null +++ b/Software/src/battery/VOLVO-SPA-HYBRID-BATTERY.cpp @@ -0,0 +1,667 @@ +#include "../include.h" +#ifdef VOLVO_SPA_HYBRID_BATTERY +#include "../datalayer/datalayer.h" +#include "../datalayer/datalayer_extended.h" //For "More battery info" webpage +#include "../devboard/utils/events.h" +#include "VOLVO-SPA-HYBRID-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 +static float MAX_U = 0; //0x3A +static float MIN_U = 0; //0x3A +static float BATT_I = 0; //0x3A +static int32_t CHARGE_ENERGY = 0; //0x1A1 +static uint8_t BATT_ERR_INDICATION = 0; //0x413 +static float BATT_T_MAX = 0; //0x413 +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 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[102]; //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, 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, + .DLC = 8, + .ID = 0x372, + .data = {0x00, 0xA6, 0x07, 0x14, 0x04, 0x00, 0x80, 0x00}}; //Ambient Temp -->>VERIFY this data content!!!<<-- +CAN_frame VOLVO_CELL_U_Req = { + .FD = false, + .ext_ID = false, + .DLC = 8, + .ID = 0x735, + .data = {0x03, 0x22, 0x48, 0x06, 0x00, 0x00, 0x00, 0x00}}; //Cell voltage request frame // changed +CAN_frame VOLVO_FlowControl = {.FD = false, + .ext_ID = false, + .DLC = 8, + .ID = 0x735, + .data = {0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Flowcontrol +CAN_frame VOLVO_SOH_Req = {.FD = false, + .ext_ID = 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.VolvoHybrid.soc_bms = SOC_BMS; + datalayer_extended.VolvoHybrid.soc_calc = SOC_CALC; + datalayer_extended.VolvoHybrid.soc_rescaled = datalayer.battery.status.reported_soc; + datalayer_extended.VolvoHybrid.soh_bms = datalayer.battery.status.soh_pptt; + + datalayer_extended.VolvoHybrid.BECMBatteryVoltage = BATT_U; + datalayer_extended.VolvoHybrid.BECMBatteryCurrent = BATT_I; + datalayer_extended.VolvoHybrid.BECMUDynMaxLim = MAX_U; + datalayer_extended.VolvoHybrid.BECMUDynMinLim = MIN_U; + + datalayer_extended.VolvoHybrid.HvBattPwrLimDcha1 = HvBattPwrLimDcha1; + datalayer_extended.VolvoHybrid.HvBattPwrLimDchaSoft = HvBattPwrLimDchaSoft; + //datalayer_extended.VolvoHybrid.HvBattPwrLimDchaSlowAgi = HvBattPwrLimDchaSlowAgi; + //datalayer_extended.VolvoHybrid.HvBattPwrLimChrgSlowAgi = HvBattPwrLimChrgSlowAgi; + + // Update requests from webserver datalayer + if (datalayer_extended.VolvoHybrid.UserRequestDTCreset) { + transmit_can_frame(&VOLVO_DTC_Erase, can_config.battery); //Send global DTC erase command + datalayer_extended.VolvoHybrid.UserRequestDTCreset = false; + } + if (datalayer_extended.VolvoHybrid.UserRequestBECMecuReset) { + transmit_can_frame(&VOLVO_BECM_ECUreset, can_config.battery); //Send BECM ecu reset command + datalayer_extended.VolvoHybrid.UserRequestBECMecuReset = false; + } + if (datalayer_extended.VolvoHybrid.UserRequestDTCreadout) { + transmit_can_frame(&VOLVO_DTCreadout, can_config.battery); //Send DTC readout command + datalayer_extended.VolvoHybrid.UserRequestDTCreadout = false; + } + + remaining_capacity = (18830 - 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 + SOC_CALC = remaining_capacity / 19; // Use calculated SOC based on remaining_capacity + + datalayer.battery.status.real_soc = SOC_CALC * 10; + + if (BATT_U > MAX_U) // Protect if overcharged + { + datalayer.battery.status.real_soc = 10000; + } else if (BATT_U < MIN_U) //Protect if undercharged + { + datalayer.battery.status.real_soc = 0; + } + + datalayer.battery.status.voltage_dV = BATT_U * 10; + datalayer.battery.status.current_dA = BATT_I * 10; + datalayer.battery.status.remaining_capacity_Wh = remaining_capacity; + + datalayer.battery.status.max_discharge_power_W = 6600; //default power on charge connector + datalayer.battery.status.max_charge_power_W = 6600; //default power on charge connector + datalayer.battery.status.temperature_min_dC = BATT_T_MIN; + datalayer.battery.status.temperature_max_dC = BATT_T_MAX; + + datalayer.battery.status.cell_max_voltage_mV = CELL_U_MAX; // Use min/max reported from BMS + datalayer.battery.status.cell_min_voltage_mV = CELL_U_MIN; + + //Map all cell voltages to the global array + for (int i = 0; i < 102; ++i) { + datalayer.battery.status.cell_voltages_mV[i] = cell_voltages[i]; + } + +#ifdef DEBUG_LOG + logging.print("BMS reported SOC%: "); + logging.println(SOC_BMS); + logging.print("Calculated SOC%: "); + logging.println(SOC_CALC); + logging.print("Rescaled SOC%: "); + logging.println(datalayer.battery.status.reported_soc / 100); + logging.print("Battery current: "); + logging.println(BATT_I); + logging.print("Battery voltage: "); + logging.println(BATT_U); + logging.print("Battery maximum voltage limit: "); + logging.println(MAX_U); + logging.print("Battery minimum voltage limit: "); + logging.println(MIN_U); + logging.print("Remaining Energy: "); + logging.println(remaining_capacity); + logging.print("Discharge limit: "); + logging.println(HvBattPwrLimDchaSoft); + logging.print("Battery Error Indication: "); + logging.println(BATT_ERR_INDICATION); + logging.print("Maximum battery temperature: "); + logging.println(BATT_T_MAX / 10); + logging.print("Minimum battery temperature: "); + logging.println(BATT_T_MIN / 10); + logging.print("Average battery temperature: "); + logging.println(BATT_T_AVG / 10); + logging.print("BMS Highest cell voltage: "); + logging.println(CELL_U_MAX); + logging.print("BMS Lowest cell voltage: "); + logging.println(CELL_U_MIN); + logging.print("BMS Highest cell nr: "); + logging.println(CELL_ID_U_MAX); + logging.print("Highest cell voltage: "); + logging.println(min_max_voltage[1]); + logging.print("Lowest cell voltage: "); + logging.println(min_max_voltage[0]); + logging.print("Cell voltage,"); + while (cnt < 102) { + logging.print(cell_voltages[cnt++]); + logging.print(","); + } + logging.println(";"); +#endif +} + +void handle_incoming_can_frame_battery(CAN_frame rx_frame) { + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + switch (rx_frame.ID) { + case 0x3A: + if ((rx_frame.data.u8[6] & 0x80) == 0x80) + BATT_I = (0 - ((((rx_frame.data.u8[6] & 0x7F) * 256.0 + rx_frame.data.u8[7]) * 0.1) - 1638)); + else { + BATT_I = 0; +#ifdef DEBUG_LOG + logging.println("BATT_I not valid"); +#endif + } + + if ((rx_frame.data.u8[2] & 0x08) == 0x08) + MAX_U = (((rx_frame.data.u8[2] & 0x07) * 256.0 + rx_frame.data.u8[3]) * 0.25); + else { + //MAX_U = 0; + //logging.println("MAX_U not valid"); // Value toggles between true/false from BMS + } + + if ((rx_frame.data.u8[4] & 0x08) == 0x08) + MIN_U = (((rx_frame.data.u8[4] & 0x07) * 256.0 + rx_frame.data.u8[5]) * 0.25); + else { + //MIN_U = 0; + //logging.println("MIN_U not valid"); // Value toggles between true/false from BMS + } + + if ((rx_frame.data.u8[0] & 0x08) == 0x08) + BATT_U = (((rx_frame.data.u8[0] & 0x07) * 256.0 + rx_frame.data.u8[1]) * 0.25); + else { + BATT_U = 0; +#ifdef DEBUG_LOG + logging.println("BATT_U not valid"); +#endif + } + + if ((rx_frame.data.u8[0] & 0x40) == 0x40) + datalayer_extended.VolvoHybrid.HVSysRlySts = ((rx_frame.data.u8[0] & 0x30) >> 4); + else + datalayer_extended.VolvoHybrid.HVSysRlySts = 0xFF; + + if ((rx_frame.data.u8[2] & 0x40) == 0x40) + datalayer_extended.VolvoHybrid.HVSysDCRlySts1 = ((rx_frame.data.u8[2] & 0x30) >> 4); + else + datalayer_extended.VolvoHybrid.HVSysDCRlySts1 = 0xFF; + if ((rx_frame.data.u8[2] & 0x80) == 0x80) + datalayer_extended.VolvoHybrid.HVSysDCRlySts2 = ((rx_frame.data.u8[4] & 0x30) >> 4); + else + datalayer_extended.VolvoHybrid.HVSysDCRlySts2 = 0xFF; + if ((rx_frame.data.u8[0] & 0x80) == 0x80) + datalayer_extended.VolvoHybrid.HVSysIsoRMonrSts = ((rx_frame.data.u8[4] & 0xC0) >> 6); + else + datalayer_extended.VolvoHybrid.HVSysIsoRMonrSts = 0xFF; + + break; + case 0x1A1: + if ((rx_frame.data.u8[4] & 0x10) == 0x10) + CHARGE_ENERGY = ((((rx_frame.data.u8[4] & 0x0F) * 256.0 + rx_frame.data.u8[5]) * 50) - 500); + else { + CHARGE_ENERGY = 0; + set_event(EVENT_KWH_PLAUSIBILITY_ERROR, CHARGE_ENERGY); + } + break; + case 0x413: + if ((rx_frame.data.u8[0] & 0x80) == 0x80) + BATT_ERR_INDICATION = ((rx_frame.data.u8[0] & 0x40) >> 6); + else { + BATT_ERR_INDICATION = 0; +#ifdef DEBUG_LOG + logging.println("BATT_ERR_INDICATION not valid"); +#endif + } + if ((rx_frame.data.u8[0] & 0x20) == 0x20) { + BATT_T_MAX = ((rx_frame.data.u8[2] & 0x1F) * 256.0 + rx_frame.data.u8[3]); + BATT_T_MIN = ((rx_frame.data.u8[4] & 0x1F) * 256.0 + rx_frame.data.u8[5]); + BATT_T_AVG = ((rx_frame.data.u8[0] & 0x1F) * 256.0 + rx_frame.data.u8[1]); + } else { + BATT_T_MAX = 0; + BATT_T_MIN = 0; + BATT_T_AVG = 0; +#ifdef DEBUG_LOG + logging.println("BATT_T not valid"); +#endif + } + break; + case 0x369: + if ((rx_frame.data.u8[0] & 0x80) == 0x80) { + HvBattPwrLimDchaSoft = (((rx_frame.data.u8[6] & 0x03) * 256 + rx_frame.data.u8[6]) >> 2); + } else { + HvBattPwrLimDchaSoft = 0; +#ifdef DEBUG_LOG + logging.println("HvBattPwrLimDchaSoft not valid"); +#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]); + } else { + SOC_BMS = 0; +#ifdef DEBUG_LOG + logging.println("SOC_BMS not valid"); +#endif + } + + if ((rx_frame.data.u8[0] & 0x04) == 0x04) + //CELL_U_MAX = ((rx_frame.data.u8[2] & 0x01) * 256 + rx_frame.data.u8[3]); + ; + else { + //CELL_U_MAX = 0; + ; +#ifdef DEBUG_LOG + logging.println("CELL_U_MAX not valid"); +#endif + } + + if ((rx_frame.data.u8[0] & 0x02) == 0x02) + //CELL_U_MIN = ((rx_frame.data.u8[0] & 0x01) * 256.0 + rx_frame.data.u8[1]); + ; + else { + //CELL_U_MIN = 0; + ; +#ifdef DEBUG_LOG + logging.println("CELL_U_MIN not valid"); +#endif + } + + if ((rx_frame.data.u8[0] & 0x08) == 0x08) + //CELL_ID_U_MAX = ((rx_frame.data.u8[4] & 0x01) * 256.0 + rx_frame.data.u8[5]); + ; + else { + //CELL_ID_U_MAX = 0; + ; +#ifdef DEBUG_LOG + logging.println("CELL_ID_U_MAX not valid"); +#endif + } + break; + case 0x635: // Diag request response + if ((rx_frame.data.u8[0] == 0x07) && (rx_frame.data.u8[1] == 0x62) && (rx_frame.data.u8[2] == 0x49) && + (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.VolvoHybrid.BECMsupplyVoltage = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]); + } else if ((rx_frame.data.u8[0] == 0x10) && (rx_frame.data.u8[2] == 0x62) && (rx_frame.data.u8[3] == 0x48) && + (rx_frame.data.u8[4] == 0x06)) // First response frame of cell voltages //changed + { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]); + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x22) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x23) && (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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x24) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x25) && (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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x26) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x27) && (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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x28) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x29) && (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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2A) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2B) && (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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2C) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2D) && (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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2E) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2F) && (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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + rxConsecutiveFrames = 2; + } else if ((rx_frame.data.u8[0] == 0x20) && (rxConsecutiveFrames == 2)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x21) && (rxConsecutiveFrames == 2)) { + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x22) && (rxConsecutiveFrames == 2)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x23) && (rxConsecutiveFrames == 2)) { + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x24) && (rxConsecutiveFrames == 2)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x25) && (rxConsecutiveFrames == 2)) { + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x26) && (rxConsecutiveFrames == 2)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x27) && (rxConsecutiveFrames == 2)) { + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x28) && (rxConsecutiveFrames == 2)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x29) && (rxConsecutiveFrames == 2)) { + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2A) && (rxConsecutiveFrames == 2)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2B) && (rxConsecutiveFrames == 2)) { + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2C) && (rxConsecutiveFrames == 2)) { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]); + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]); + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]; + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + } else if ((rx_frame.data.u8[0] == 0x2D) && (rxConsecutiveFrames == 2)) { + 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]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + //cell_voltages[battery_request_idx++] = (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]; + //transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + transmit_can_frame(&VOLVO_FlowControl, can_config.battery); // Send flow control + + if (false) // Run until last pack is read + { + //VOLVO_CELL_U_Req.data.u8[3] = batteryModuleNumber++; + //transmit_can_frame(&VOLVO_CELL_U_Req, can_config.battery); //Send cell voltage read request for next module + ; + } else { + min_max_voltage[0] = 9999; + min_max_voltage[1] = 0; + for (cellcounter = 0; cellcounter < 102; cellcounter++) { + if (min_max_voltage[0] > cell_voltages[cellcounter]) + min_max_voltage[0] = cell_voltages[cellcounter]; + if (min_max_voltage[1] < cell_voltages[cellcounter]) { + min_max_voltage[1] = cell_voltages[cellcounter]; + CELL_ID_U_MAX = cellcounter; + } + } + CELL_U_MAX = min_max_voltage[1]; + CELL_U_MIN = min_max_voltage[0]; + + transmit_can_frame(&VOLVO_SOH_Req, can_config.battery); //Send SOH read request + } + rxConsecutiveFrames = 0; + } + break; + default: + break; + } +} + +void readCellVoltages() { + battery_request_idx = 0; + //batteryModuleNumber = 0x10; + rxConsecutiveFrames = 0; + //VOLVO_CELL_U_Req.data.u8[3] = batteryModuleNumber++; + transmit_can_frame(&VOLVO_CELL_U_Req, can_config.battery); //Send cell voltage read request for first module +} + +void transmit_can_battery() { + unsigned long currentMillis = millis(); + // Send 100ms CAN Message + if (currentMillis - previousMillis100 >= INTERVAL_100_MS) { + // Check if sending of CAN messages has been delayed too much. + if ((currentMillis - previousMillis100 >= INTERVAL_100_MS_DELAYED) && (currentMillis > BOOTUP_TIME)) { + set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis100)); + } else { + clear_event(EVENT_CAN_OVERRUN); + } + previousMillis100 = currentMillis; + + 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) && startedUp) { + datalayer.system.status.battery_allows_contactor_closing = true; + //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) { + previousMillis60s = currentMillis; + if (true) { + readCellVoltages(); +#ifdef DEBUG_LOG + logging.println("Requesting cell voltages"); +#endif + } + } +} + +void setup_battery(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "Volvo PHEV battery", 63); //changed + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer.battery.info.number_of_cells = 102; //was 108, changed + 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; +} +#endif diff --git a/Software/src/battery/VOLVO-SPA-HYBRID-BATTERY.h b/Software/src/battery/VOLVO-SPA-HYBRID-BATTERY.h new file mode 100644 index 00000000..1e392705 --- /dev/null +++ b/Software/src/battery/VOLVO-SPA-HYBRID-BATTERY.h @@ -0,0 +1,16 @@ +#ifndef VOLVO_SPA_HYBRID_BATTERY_H +#define VOLVO_SPA_HYBRID_BATTERY_H +#include +#include "../include.h" + +#define BATTERY_SELECTED +#define MAX_PACK_VOLTAGE_DV 4294 //5000 = 500.0V +#define MIN_PACK_VOLTAGE_DV 2754 +#define MAX_CELL_DEVIATION_MV 250 +#define MAX_CELL_VOLTAGE_MV 4210 //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 + +void setup_battery(void); +void transmit_can_frame(CAN_frame* tx_frame, int interface); + +#endif diff --git a/Software/src/communication/can/comm_can.cpp b/Software/src/communication/can/comm_can.cpp index 880d7b5e..264b237a 100644 --- a/Software/src/communication/can/comm_can.cpp +++ b/Software/src/communication/can/comm_can.cpp @@ -109,7 +109,9 @@ void transmit_can() { return; //Global block of CAN messages } +#ifndef RS485_BATTERY_SELECTED transmit_can_battery(); +#endif #ifdef CAN_INVERTER_SELECTED transmit_can_inverter(); @@ -302,7 +304,9 @@ void map_can_frame_to_variable(CAN_frame* rx_frame, int interface) { #endif if (interface == can_config.battery) { +#ifndef RS485_BATTERY_SELECTED handle_incoming_can_frame_battery(*rx_frame); +#endif #ifdef CHADEMO_BATTERY ISA_handleFrame(rx_frame); #endif diff --git a/Software/src/communication/rs485/comm_rs485.cpp b/Software/src/communication/rs485/comm_rs485.cpp index feec33aa..4d5ba508 100644 --- a/Software/src/communication/rs485/comm_rs485.cpp +++ b/Software/src/communication/rs485/comm_rs485.cpp @@ -29,9 +29,9 @@ void init_rs485() { pinMode(PIN_5V_EN, OUTPUT); digitalWrite(PIN_5V_EN, HIGH); #endif // PIN_5V_EN -#ifdef RS485_INVERTER_SELECTED - Serial2.begin(57600, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); -#endif // RS485_INVERTER_SELECTED +#if defined(RS485_INVERTER_SELECTED) || defined(RS485_BATTERY_SELECTED) + Serial2.begin(RS485_BAUDRATE, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); +#endif // RS485_INVERTER_SELECTED || RS485_BATTERY_SELECTED #ifdef MODBUS_INVERTER_SELECTED #ifdef BYD_MODBUS // Init Static data to the RTU Modbus diff --git a/Software/src/datalayer/datalayer.h b/Software/src/datalayer/datalayer.h index e9b9f664..b1a02cbc 100644 --- a/Software/src/datalayer/datalayer.h +++ b/Software/src/datalayer/datalayer.h @@ -124,6 +124,12 @@ 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; + /** Parameters for keeping track of the limiting factor in the system */ + bool user_settings_limit_discharge = false; + bool user_settings_limit_charge = false; + bool inverter_limits_discharge = false; + bool inverter_limits_charge = false; + /** 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; @@ -203,10 +209,12 @@ typedef struct { typedef struct { /** array with type of battery used, for displaying on webserver */ char battery_protocol[64] = {0}; - /** array with type of inverter used, for displaying on webserver */ + /** array with type of inverter protocol used, for displaying on webserver */ char inverter_protocol[64] = {0}; /** array with type of battery used, for displaying on webserver */ char shunt_protocol[64] = {0}; + /** array with type of inverter brand used, for displaying on webserver */ + char inverter_brand[8] = {0}; /** array with incoming CAN messages, for displaying on webserver */ char logged_can_messages[15000] = {0}; size_t logged_can_messages_offset = 0; diff --git a/Software/src/datalayer/datalayer_extended.h b/Software/src/datalayer/datalayer_extended.h index 6b05fdd7..fcb4b04a 100644 --- a/Software/src/datalayer/datalayer_extended.h +++ b/Software/src/datalayer/datalayer_extended.h @@ -181,7 +181,9 @@ typedef struct { /** uint16_t */ /** Voltage polled OBD2*/ uint16_t voltage_polled = 0; - + /** int16_t */ + /** All the temperature sensors inside the battery pack*/ + int16_t battery_temperatures[10]; } DATALAYER_INFO_BYDATTO3; typedef struct { @@ -641,6 +643,36 @@ typedef struct { } DATALAYER_INFO_VOLVO_POLESTAR; +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_HYBRID; + typedef struct { /** uint16_t */ /** Values WIP*/ @@ -700,6 +732,7 @@ class DataLayerExtended { DATALAYER_INFO_NISSAN_LEAF nissanleaf; DATALAYER_INFO_MEB meb; DATALAYER_INFO_VOLVO_POLESTAR VolvoPolestar; + DATALAYER_INFO_VOLVO_HYBRID VolvoHybrid; DATALAYER_INFO_ZOE_PH2 zoePH2; }; diff --git a/Software/src/devboard/safety/safety.cpp b/Software/src/devboard/safety/safety.cpp index fe422558..cf2a0cdb 100644 --- a/Software/src/devboard/safety/safety.cpp +++ b/Software/src/devboard/safety/safety.cpp @@ -21,8 +21,8 @@ battery_pause_status emulator_pause_status = NORMAL; void update_machineryprotection() { // Start checking that the battery is within reason. Incase we see any funny business, raise an event! - // Pause function is on - if (emulator_pause_request_ON) { + // Pause function is on OR we have a critical fault event active + if (emulator_pause_request_ON || (datalayer.battery.status.bms_status == FAULT)) { datalayer.battery.status.max_discharge_power_W = 0; datalayer.battery.status.max_charge_power_W = 0; } @@ -41,6 +41,14 @@ void update_machineryprotection() { clear_event(EVENT_BATTERY_FROZEN); } + if (labs(datalayer.battery.status.temperature_max_dC - datalayer.battery.status.temperature_min_dC) > + BATTERY_MAX_TEMPERATURE_DEVIATION) { + set_event_latched(EVENT_BATTERY_TEMP_DEVIATION_HIGH, + datalayer.battery.status.temperature_max_dC - datalayer.battery.status.temperature_min_dC); + } else { + clear_event(EVENT_BATTERY_TEMP_DEVIATION_HIGH); + } + // Battery voltage is over designed max voltage! if (datalayer.battery.status.voltage_dV > datalayer.battery.info.max_design_voltage_dV) { set_event(EVENT_BATTERY_OVERVOLTAGE, datalayer.battery.status.voltage_dV); diff --git a/Software/src/devboard/utils/events.cpp b/Software/src/devboard/utils/events.cpp index a2f0dfbd..5d72931e 100644 --- a/Software/src/devboard/utils/events.cpp +++ b/Software/src/devboard/utils/events.cpp @@ -210,6 +210,7 @@ void init_events(void) { events.entries[EVENT_RESET_EFUSE].level = EVENT_LEVEL_INFO; events.entries[EVENT_RESET_PWR_GLITCH].level = EVENT_LEVEL_INFO; events.entries[EVENT_RESET_CPU_LOCKUP].level = EVENT_LEVEL_WARNING; + events.entries[EVENT_RJXZS_LOG].level = EVENT_LEVEL_INFO; events.entries[EVENT_PAUSE_BEGIN].level = EVENT_LEVEL_WARNING; events.entries[EVENT_PAUSE_END].level = EVENT_LEVEL_INFO; events.entries[EVENT_WIFI_CONNECT].level = EVENT_LEVEL_INFO; @@ -218,6 +219,7 @@ void init_events(void) { events.entries[EVENT_MQTT_DISCONNECT].level = EVENT_LEVEL_INFO; events.entries[EVENT_EQUIPMENT_STOP].level = EVENT_LEVEL_ERROR; events.entries[EVENT_SD_INIT_FAILED].level = EVENT_LEVEL_WARNING; + events.entries[EVENT_BATTERY_TEMP_DEVIATION_HIGH].level = EVENT_LEVEL_WARNING; events.entries[EVENT_EEPROM_WRITE].log = false; // Don't log the logger... @@ -254,6 +256,9 @@ void reset_all_events() { } events.level = EVENT_LEVEL_INFO; update_bms_status(); +#ifdef DEBUG_LOG + logging.println("All events have been cleared."); +#endif } void set_event_MQTTpublished(EVENTS_ENUM_TYPE event) { @@ -430,6 +435,8 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) { return "The board was reset due to a detected power glitch"; case EVENT_RESET_CPU_LOCKUP: return "The board was reset due to CPU lockup. Inform developers!"; + case EVENT_RJXZS_LOG: + return "Error code active in RJXZS BMS. Clear via their smartphone app!"; case EVENT_PAUSE_BEGIN: return "The emulator is trying to pause the battery."; case EVENT_PAUSE_END: diff --git a/Software/src/devboard/utils/events.h b/Software/src/devboard/utils/events.h index 4874ee65..fe0a6b08 100644 --- a/Software/src/devboard/utils/events.h +++ b/Software/src/devboard/utils/events.h @@ -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 0x0023 // 0x0000 to 0xFFFF +#define EE_MAGIC_HEADER_VALUE 0x0024 // 0x0000 to 0xFFFF #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, @@ -107,6 +107,7 @@ XX(EVENT_RESET_EFUSE) \ XX(EVENT_RESET_PWR_GLITCH) \ XX(EVENT_RESET_CPU_LOCKUP) \ + XX(EVENT_RJXZS_LOG) \ XX(EVENT_PAUSE_BEGIN) \ XX(EVENT_PAUSE_END) \ XX(EVENT_WIFI_CONNECT) \ @@ -116,6 +117,7 @@ XX(EVENT_EQUIPMENT_STOP) \ XX(EVENT_AUTOMATIC_PRECHARGE_FAILURE) \ XX(EVENT_SD_INIT_FAILED) \ + XX(EVENT_BATTERY_TEMP_DEVIATION_HIGH) \ XX(EVENT_NOF_EVENTS) typedef enum { EVENTS_ENUM_TYPE(GENERATE_ENUM) } EVENTS_ENUM_TYPE; diff --git a/Software/src/devboard/webserver/advanced_battery_html.cpp b/Software/src/devboard/webserver/advanced_battery_html.cpp index 3a70d325..95e8d52f 100644 --- a/Software/src/devboard/webserver/advanced_battery_html.cpp +++ b/Software/src/devboard/webserver/advanced_battery_html.cpp @@ -469,6 +469,16 @@ String advanced_battery_processor(const String& var) { content += "

SOC OBD2: " + String(datalayer_extended.bydAtto3.SOC_polled) + "

"; content += "

Voltage periodic: " + String(datalayer_extended.bydAtto3.voltage_periodic) + "

"; content += "

Voltage OBD2: " + String(datalayer_extended.bydAtto3.voltage_polled) + "

"; + content += "

Temperature sensor 1: " + String(datalayer_extended.bydAtto3.battery_temperatures[0]) + "

"; + content += "

Temperature sensor 2: " + String(datalayer_extended.bydAtto3.battery_temperatures[1]) + "

"; + content += "

Temperature sensor 3: " + String(datalayer_extended.bydAtto3.battery_temperatures[2]) + "

"; + content += "

Temperature sensor 4: " + String(datalayer_extended.bydAtto3.battery_temperatures[3]) + "

"; + content += "

Temperature sensor 5: " + String(datalayer_extended.bydAtto3.battery_temperatures[4]) + "

"; + content += "

Temperature sensor 6: " + String(datalayer_extended.bydAtto3.battery_temperatures[5]) + "

"; + content += "

Temperature sensor 7: " + String(datalayer_extended.bydAtto3.battery_temperatures[6]) + "

"; + content += "

Temperature sensor 8: " + String(datalayer_extended.bydAtto3.battery_temperatures[7]) + "

"; + content += "

Temperature sensor 9: " + String(datalayer_extended.bydAtto3.battery_temperatures[8]) + "

"; + content += "

Temperature sensor 10: " + String(datalayer_extended.bydAtto3.battery_temperatures[9]) + "

"; #endif //BYD_ATTO_3_BATTERY #ifdef SIMPBMS_BATTERY @@ -1320,11 +1330,101 @@ String advanced_battery_processor(const String& var) { content += ""; #endif // VOLVO_SPA_BATTERY +#ifdef VOLVO_SPA_HYBRID_BATTERY + content += "

BECM reported SOC: " + String(datalayer_extended.VolvoHybrid.soc_bms) + "

"; + content += "

Calculated SOC: " + String(datalayer_extended.VolvoHybrid.soc_calc) + "

"; + content += "

Rescaled SOC: " + String(datalayer_extended.VolvoHybrid.soc_rescaled / 10) + "

"; + content += "

BECM reported SOH: " + String(datalayer_extended.VolvoHybrid.soh_bms) + "

"; + content += "

BECM supply voltage: " + String(datalayer_extended.VolvoHybrid.BECMsupplyVoltage) + " mV

"; + + content += "

HV voltage: " + String(datalayer_extended.VolvoHybrid.BECMBatteryVoltage) + " V

"; + content += "

HV current: " + String(datalayer_extended.VolvoHybrid.BECMBatteryCurrent) + " A

"; + content += "

Dynamic max voltage: " + String(datalayer_extended.VolvoHybrid.BECMUDynMaxLim) + " V

"; + content += "

Dynamic min voltage: " + String(datalayer_extended.VolvoHybrid.BECMUDynMinLim) + " V

"; + + content += "

Discharge power limit 1: " + String(datalayer_extended.VolvoHybrid.HvBattPwrLimDcha1) + " kW

"; + content += + "

Discharge soft power limit: " + String(datalayer_extended.VolvoHybrid.HvBattPwrLimDchaSoft) + " kW

"; + + content += "

HV system relay status: "; + switch (datalayer_extended.VolvoHybrid.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 += "

HV system relay status 1: "; + switch (datalayer_extended.VolvoHybrid.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 += "

HV system relay status 2: "; + switch (datalayer_extended.VolvoHybrid.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 += "

HV system isolation resistance monitoring status: "; + switch (datalayer_extended.VolvoHybrid.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 += "


"; + content += "
"; + content += ""; +#endif // VOLVO_SPA_HYBRID_BATTERY + #if !defined(BMW_PHEV_BATTERY) && !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) && !defined(VOLVO_SPA_BATTERY) && \ - !defined(KIA_HYUNDAI_64_BATTERY) && !defined(SIMPBMS_BATTERY)//Only the listed types have extra info + !defined(MEB_BATTERY) && !defined(VOLVO_SPA_BATTERY) && !defined(VOLVO_SPA_HYBRID_BATTERY) && \ + !defined(KIA_HYUNDAI_64_BATTERY) && !defined(SIMPBMS_BATTERY) //Only the listed types have extra info content += "No extra information available for this battery type"; #endif diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index d61212db..64efbdd5 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -430,6 +430,33 @@ void init_webserver() { request->send(200, "text/plain", "Updated successfully"); }); + // Route for erasing DTC on Volvo hybrid batteries + server.on("/volvoEraseDTC", HTTP_GET, [](AsyncWebServerRequest* request) { + if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) { + return request->requestAuthentication(); + } + datalayer_extended.VolvoHybrid.UserRequestDTCreset = true; + request->send(200, "text/plain", "Updated successfully"); + }); + + // Route for reading DTC on Volvo hybrid batteries + server.on("/volvoReadDTC", HTTP_GET, [](AsyncWebServerRequest* request) { + if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) { + return request->requestAuthentication(); + } + datalayer_extended.VolvoHybrid.UserRequestDTCreadout = true; + request->send(200, "text/plain", "Updated successfully"); + }); + + // Route for performing ECU reset on Volvo hybrid batteries + server.on("/volvoBECMecuReset", HTTP_GET, [](AsyncWebServerRequest* request) { + if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) { + return request->requestAuthentication(); + } + datalayer_extended.VolvoHybrid.UserRequestBECMecuReset = true; + request->send(200, "text/plain", "Updated successfully"); + }); + #ifdef TEST_FAKE_BATTERY // Route for editing FakeBatteryVoltage server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) { @@ -784,6 +811,8 @@ String processor(const String& var) { // Display which components are used content += "

Inverter protocol: "; content += datalayer.system.info.inverter_protocol; + content += " "; + content += datalayer.system.info.inverter_brand; content += "

"; content += "

Battery protocol: "; content += datalayer.system.info.battery_protocol; @@ -885,8 +914,18 @@ String processor(const String& var) { } else { content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1); content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1); - content += "

Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A

"; - content += "

Max charge current: " + String(maxCurrentChargeFloat, 1) + " A

"; + content += "

Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A"; + if (datalayer.battery.settings.user_settings_limit_discharge) { + content += " (Manual)

"; + } else { + content += " (BMS)"; + } + content += "

Max charge current: " + String(maxCurrentChargeFloat, 1) + " A"; + if (datalayer.battery.settings.user_settings_limit_charge) { + content += " (Manual)

"; + } else { + content += " (BMS)"; + } } content += "

Cell max: " + String(datalayer.battery.status.cell_max_voltage_mV) + " mV

"; @@ -947,9 +986,28 @@ String processor(const String& var) { if (datalayer.battery.status.current_dA == 0) { content += "

Battery idle

"; } else if (datalayer.battery.status.current_dA < 0) { - content += "

Battery discharging!

"; - } else { // > 0 - content += "

Battery charging!

"; + content += "

Battery discharging!"; + if (datalayer.battery.settings.inverter_limits_discharge) { + content += " (Inverter limiting)

"; + } else { + if (datalayer.battery.settings.user_settings_limit_discharge) { + content += " (Settings limiting)"; + } else { + content += " (Battery limiting)"; + } + } + content += ""; + } else { // > 0 , positive current + content += "

Battery charging!"; + if (datalayer.battery.settings.inverter_limits_charge) { + content += " (Inverter limiting)

"; + } else { + if (datalayer.battery.settings.user_settings_limit_charge) { + content += " (Settings limiting)"; + } else { + content += " (Battery limiting)"; + } + } } content += "

Automatic contactor closing allowed:

"; diff --git a/Software/src/devboard/webserver/webserver.h b/Software/src/devboard/webserver/webserver.h index 3e2a71a1..f7dc5629 100644 --- a/Software/src/devboard/webserver/webserver.h +++ b/Software/src/devboard/webserver/webserver.h @@ -4,10 +4,10 @@ #include #include #include "../../include.h" +#include "../../lib/ESP32Async-AsyncTCP/src/AsyncTCP.h" #include "../../lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h" #include "../../lib/YiannisBourkelis-Uptime-Library/src/uptime_formatter.h" #include "../../lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h" -#include "../../lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" extern const char* version_number; // The current software version, shown on webserver diff --git a/Software/src/inverter/BYD-CAN.cpp b/Software/src/inverter/BYD-CAN.cpp index a8ebde6f..ee051c68 100644 --- a/Software/src/inverter/BYD-CAN.cpp +++ b/Software/src/inverter/BYD-CAN.cpp @@ -74,7 +74,6 @@ CAN_frame BYD_210 = {.FD = false, .ID = 0x210, .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; -static uint8_t inverter_name[7] = {0}; static int16_t temperature_average = 0; static uint16_t inverter_voltage = 0; static uint16_t inverter_SOC = 0; @@ -127,6 +126,16 @@ void update_values_can_inverter() { //This function maps all the values fetched //SOC (100.00%) BYD_150.data.u8[0] = (datalayer.battery.status.reported_soc >> 8); BYD_150.data.u8[1] = (datalayer.battery.status.reported_soc & 0x00FF); + if (datalayer.battery.status.max_charge_current_dA == 0) { + //Force to 100.00% incase battery no longer wants to charge + BYD_150.data.u8[0] = (10000 >> 8); + BYD_150.data.u8[1] = (10000 & 0x00FF); + } + if (datalayer.battery.status.max_discharge_current_dA == 0) { + //Force to 0% incase battery no longer wants to discharge + BYD_150.data.u8[0] = 0; + BYD_150.data.u8[1] = 0; + } //StateOfHealth (100.00%) BYD_150.data.u8[2] = (datalayer.battery.status.soh_pptt >> 8); BYD_150.data.u8[3] = (datalayer.battery.status.soh_pptt & 0x00FF); @@ -153,16 +162,6 @@ void update_values_can_inverter() { //This function maps all the values fetched //Temperature min BYD_210.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8); BYD_210.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF); - -#ifdef DEBUG_LOG - if (inverter_name[0] != 0) { - logging.print("Detected inverter: "); - for (uint8_t i = 0; i < 7; i++) { - logging.print((char)inverter_name[i]); - } - logging.println(); - } -#endif } void map_can_frame_to_variable_inverter(CAN_frame rx_frame) { @@ -174,8 +173,9 @@ void map_can_frame_to_variable_inverter(CAN_frame rx_frame) { send_intial_data(); } else { // We can identify what inverter type we are connected to for (uint8_t i = 0; i < 7; i++) { - inverter_name[i] = rx_frame.data.u8[i + 1]; + datalayer.system.info.inverter_brand[i] = rx_frame.data.u8[i + 1]; } + datalayer.system.info.inverter_brand[7] = '\0'; } break; case 0x091: diff --git a/Software/src/inverter/FERROAMP-CAN.cpp b/Software/src/inverter/FERROAMP-CAN.cpp new file mode 100644 index 00000000..edcc4815 --- /dev/null +++ b/Software/src/inverter/FERROAMP-CAN.cpp @@ -0,0 +1,499 @@ +#include "../include.h" +#ifdef FERROAMP_CAN +#include "../datalayer/datalayer.h" +#include "FERROAMP-CAN.h" + +//#define SEND_0 //If defined, the messages will have ID ending with 0 (useful for some inverters) +#define SEND_1 //If defined, the messages will have ID ending with 1 (useful for some inverters) +#define INVERT_LOW_HIGH_BYTES //If defined, certain frames will have inverted low/high bytes \ + //useful for some inverters like Sofar that report the voltages incorrect otherwise +#define SET_30K_OFFSET //If defined, current values are sent with a 30k offest (useful for ferroamp) + +/* Some inverters need to see a specific amount of cells/modules to emulate a specific Pylon battery. +Change the following only if your inverter is generating fault codes about voltage range */ +#define TOTAL_CELL_AMOUNT 120 //Adjust this parameter in steps of 120 to add another 14,2kWh of capacity +#define MODULES_IN_SERIES 4 +#define CELLS_PER_MODULE 30 +#define VOLTAGE_LEVEL 384 +#define AH_CAPACITY 37 + +/* Do not change code below unless you are sure what you are doing */ +//Actual content messages +CAN_frame PYLON_7310 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x7310, + .data = {0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x00, 0x00}}; +CAN_frame PYLON_7311 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x7311, + .data = {0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x00, 0x00}}; +CAN_frame PYLON_7320 = { + .FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x7320, + .data = {TOTAL_CELL_AMOUNT, (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES, CELLS_PER_MODULE, VOLTAGE_LEVEL, + (uint8_t)(VOLTAGE_LEVEL >> 8), AH_CAPACITY, (uint8_t)(AH_CAPACITY >> 8)}}; +CAN_frame PYLON_7321 = { + .FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x7321, + .data = {TOTAL_CELL_AMOUNT, (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES, CELLS_PER_MODULE, VOLTAGE_LEVEL, + (uint8_t)(VOLTAGE_LEVEL >> 8), AH_CAPACITY, (uint8_t)(AH_CAPACITY >> 8)}}; +CAN_frame PYLON_4210 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4210, + .data = {0xA5, 0x09, 0x30, 0x75, 0x9D, 0x04, 0x2E, 0x64}}; +CAN_frame PYLON_4220 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4220, + .data = {0x8C, 0x0A, 0xE9, 0x07, 0x4A, 0x79, 0x4A, 0x79}}; +CAN_frame PYLON_4230 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4230, + .data = {0xDF, 0x0C, 0xDA, 0x0C, 0x03, 0x00, 0x06, 0x00}}; +CAN_frame PYLON_4240 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4240, + .data = {0x7E, 0x04, 0x62, 0x04, 0x11, 0x00, 0x03, 0x00}}; +CAN_frame PYLON_4250 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4250, + .data = {0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame PYLON_4260 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4260, + .data = {0xAC, 0xC7, 0x74, 0x27, 0x03, 0x00, 0x02, 0x00}}; +CAN_frame PYLON_4270 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4270, + .data = {0x7E, 0x04, 0x62, 0x04, 0x05, 0x00, 0x01, 0x00}}; +CAN_frame PYLON_4280 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4280, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame PYLON_4290 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4290, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame PYLON_4211 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4211, + .data = {0xA5, 0x09, 0x30, 0x75, 0x9D, 0x04, 0x2E, 0x64}}; +CAN_frame PYLON_4221 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4221, + .data = {0x8C, 0x0A, 0xE9, 0x07, 0x4A, 0x79, 0x4A, 0x79}}; +CAN_frame PYLON_4231 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4231, + .data = {0xDF, 0x0C, 0xDA, 0x0C, 0x03, 0x00, 0x06, 0x00}}; +CAN_frame PYLON_4241 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4241, + .data = {0x7E, 0x04, 0x62, 0x04, 0x11, 0x00, 0x03, 0x00}}; +CAN_frame PYLON_4251 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4251, + .data = {0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame PYLON_4261 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4261, + .data = {0xAC, 0xC7, 0x74, 0x27, 0x03, 0x00, 0x02, 0x00}}; +CAN_frame PYLON_4271 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4271, + .data = {0x7E, 0x04, 0x62, 0x04, 0x05, 0x00, 0x01, 0x00}}; +CAN_frame PYLON_4281 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4281, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame PYLON_4291 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4291, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + +static uint16_t cell_tweaked_max_voltage_mV = 3300; +static uint16_t cell_tweaked_min_voltage_mV = 3300; + +void update_values_can_inverter() { //This function maps all the values fetched from battery CAN to the correct CAN messages + //There are more mappings that could be added, but this should be enough to use as a starting point + // Note we map both 0 and 1 messages + + //Ferroamp only supports LFP batteries. We need to fake an LFP voltage range if the battery used is not LFP + if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) { + //Already LFP, pass thru value + cell_tweaked_max_voltage_mV = datalayer.battery.status.cell_max_voltage_mV; + cell_tweaked_min_voltage_mV = datalayer.battery.status.cell_min_voltage_mV; + } else { //linear interpolation to remap the value from the range [2500-4200] to [2500-3400] + cell_tweaked_max_voltage_mV = + (2500 + ((datalayer.battery.status.cell_max_voltage_mV - 2500) * (3400 - 2500)) / (4200 - 2500)); + cell_tweaked_min_voltage_mV = + (2500 + ((datalayer.battery.status.cell_min_voltage_mV - 2500) * (3400 - 2500)) / (4200 - 2500)); + } + + //Charge / Discharge allowed + PYLON_4280.data.u8[0] = 0; + PYLON_4280.data.u8[1] = 0; + PYLON_4280.data.u8[2] = 0; + PYLON_4280.data.u8[3] = 0; + PYLON_4281.data.u8[0] = 0; + PYLON_4281.data.u8[1] = 0; + PYLON_4281.data.u8[2] = 0; + PYLON_4281.data.u8[3] = 0; + + //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); + PYLON_4211.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8); + PYLON_4211.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF); + + //Current (15.0) + 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); + + // BMS Temperature (We dont have BMS temp, send max cell voltage instead) +#ifdef INVERT_LOW_HIGH_BYTES //Useful for Sofar inverters + PYLON_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF); + 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 + 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 + //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 + + //StateOfHealth (100.00%) + PYLON_4210.data.u8[7] = (datalayer.battery.status.soh_pptt / 100); + PYLON_4211.data.u8[7] = (datalayer.battery.status.soh_pptt / 100); + + // Status=Bit 0,1,2= 0:Sleep, 1:Charge, 2:Discharge 3:Idle. Bit3 ForceChargeReq. Bit4 Balance charge Request + if (datalayer.battery.status.bms_status == FAULT) { + PYLON_4251.data.u8[0] = (0x00); // Sleep + } else if (datalayer.battery.status.current_dA < 0) { + PYLON_4251.data.u8[0] = (0x01); // Charge + } else if (datalayer.battery.status.current_dA > 0) { + PYLON_4251.data.u8[0] = (0x02); // Discharge + } else if (datalayer.battery.status.current_dA == 0) { + PYLON_4251.data.u8[0] = (0x03); // Idle + } + +#ifdef INVERT_LOW_HIGH_BYTES //Useful for Sofar inverters + //Voltage (370.0) + PYLON_4210.data.u8[0] = (datalayer.battery.status.voltage_dV & 0x00FF); + PYLON_4210.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8); + PYLON_4211.data.u8[0] = (datalayer.battery.status.voltage_dV & 0x00FF); + PYLON_4211.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8); + +#ifdef SET_30K_OFFSET + //Current (15.0) + PYLON_4210.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF); + 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 + 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 + + // 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); + 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); + + //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); + + //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); + +#ifdef SET_30K_OFFSET + //Max ChargeCurrent + PYLON_4220.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF); + PYLON_4220.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8); + PYLON_4221.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF); + PYLON_4221.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8); + + //Max DischargeCurrent + PYLON_4220.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF); + 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 + //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); + PYLON_4221.data.u8[4] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); + PYLON_4221.data.u8[5] = (datalayer.battery.status.max_charge_current_dA >> 8); + + //Max DishargeCurrent + PYLON_4220.data.u8[6] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); + 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 + + //Max cell voltage + PYLON_4230.data.u8[0] = (cell_tweaked_max_voltage_mV & 0x00FF); + PYLON_4230.data.u8[1] = (cell_tweaked_max_voltage_mV >> 8); + PYLON_4231.data.u8[0] = (cell_tweaked_max_voltage_mV & 0x00FF); + PYLON_4231.data.u8[1] = (cell_tweaked_max_voltage_mV >> 8); + + //Min cell voltage + PYLON_4230.data.u8[2] = (cell_tweaked_min_voltage_mV & 0x00FF); + PYLON_4230.data.u8[3] = (cell_tweaked_min_voltage_mV >> 8); + PYLON_4231.data.u8[2] = (cell_tweaked_min_voltage_mV & 0x00FF); + PYLON_4231.data.u8[3] = (cell_tweaked_min_voltage_mV >> 8); + + //Max temperature per cell + PYLON_4240.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + PYLON_4240.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8); + PYLON_4241.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + PYLON_4241.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8); + + //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); + + //Max temperature per module + PYLON_4270.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + PYLON_4270.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8); + PYLON_4271.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + PYLON_4271.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8); + + //Min temperature per module + PYLON_4270.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF); + 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 + //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); + PYLON_4211.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8); + PYLON_4211.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF); + +#ifdef SET_30K_OFFSET + //Current (15.0) + PYLON_4210.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) >> 8); + 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 + 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 + + // 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); + 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); + + //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); + + //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); + +#ifdef SET_30K_OFFSET + //Max ChargeCurrent + PYLON_4220.data.u8[4] = ((max_charge_current + 30000) >> 8); + PYLON_4220.data.u8[5] = ((max_charge_current + 30000) & 0x00FF); + PYLON_4221.data.u8[4] = ((max_charge_current + 30000) >> 8); + PYLON_4221.data.u8[5] = ((max_charge_current + 30000) & 0x00FF); + + //Max DischargeCurrent + PYLON_4220.data.u8[6] = ((30000 - max_discharge_current) >> 8); + 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 + //Max ChargeCurrent + PYLON_4220.data.u8[4] = (max_charge_current >> 8); + PYLON_4220.data.u8[5] = (max_charge_current & 0x00FF); + PYLON_4221.data.u8[4] = (max_charge_current >> 8); + PYLON_4221.data.u8[5] = (max_charge_current & 0x00FF); + + //Max DishargeCurrent + PYLON_4220.data.u8[6] = (max_discharge_current >> 8); + 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 + + //Max cell voltage + PYLON_4230.data.u8[0] = (cell_tweaked_max_voltage_mV >> 8); + PYLON_4230.data.u8[1] = (cell_tweaked_max_voltage_mV & 0x00FF); + PYLON_4231.data.u8[0] = (cell_tweaked_max_voltage_mV >> 8); + PYLON_4231.data.u8[1] = (cell_tweaked_max_voltage_mV & 0x00FF); + + //Min cell voltage + PYLON_4230.data.u8[2] = (cell_tweaked_min_voltage_mV >> 8); + PYLON_4230.data.u8[3] = (cell_tweaked_min_voltage_mV & 0x00FF); + PYLON_4231.data.u8[2] = (cell_tweaked_min_voltage_mV >> 8); + PYLON_4231.data.u8[3] = (cell_tweaked_min_voltage_mV & 0x00FF); + + //Max 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_4241.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8); + PYLON_4241.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + + //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); + + //Max 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_4271.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8); + PYLON_4271.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + + //Min temperature per module + PYLON_4270.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8); + 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] = (cell_tweaked_max_voltage_mV >> 8); + PYLON_4230.data.u8[1] = (cell_tweaked_max_voltage_mV & 0x00FF); + PYLON_4230.data.u8[2] = (cell_tweaked_min_voltage_mV >> 8); + PYLON_4230.data.u8[3] = (cell_tweaked_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); + + //In case we run into any errors/faults, we can set charge / discharge forbidden + if (datalayer.battery.status.bms_status == FAULT) { + PYLON_4280.data.u8[0] = 0xAA; + PYLON_4280.data.u8[1] = 0xAA; + PYLON_4280.data.u8[2] = 0xAA; + PYLON_4280.data.u8[3] = 0xAA; + PYLON_4281.data.u8[0] = 0xAA; + PYLON_4281.data.u8[1] = 0xAA; + PYLON_4281.data.u8[2] = 0xAA; + PYLON_4281.data.u8[3] = 0xAA; + } +} + +void map_can_frame_to_variable_inverter(CAN_frame rx_frame) { + switch (rx_frame.ID) { + case 0x4200: //Message originating from inverter. Depending on which data is required, act accordingly + datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; + if (rx_frame.data.u8[0] == 0x02) { + send_setup_info(); + } + if (rx_frame.data.u8[0] == 0x00) { + send_system_data(); + } + break; + default: + break; + } +} + +void transmit_can_inverter() { + // No periodic sending, we only react on received can messages +} + +void send_setup_info() { //Ensemble information +#ifdef SEND_0 + transmit_can_frame(&PYLON_7310, can_config.inverter); + transmit_can_frame(&PYLON_7320, can_config.inverter); +#endif +#ifdef SEND_1 + transmit_can_frame(&PYLON_7311, can_config.inverter); + transmit_can_frame(&PYLON_7321, can_config.inverter); +#endif +} + +void send_system_data() { //System equipment information +#ifdef SEND_0 + transmit_can_frame(&PYLON_4210, can_config.inverter); + transmit_can_frame(&PYLON_4220, can_config.inverter); + transmit_can_frame(&PYLON_4230, can_config.inverter); + transmit_can_frame(&PYLON_4240, can_config.inverter); + transmit_can_frame(&PYLON_4250, can_config.inverter); + transmit_can_frame(&PYLON_4260, can_config.inverter); + transmit_can_frame(&PYLON_4270, can_config.inverter); + transmit_can_frame(&PYLON_4280, can_config.inverter); + transmit_can_frame(&PYLON_4290, can_config.inverter); +#endif +#ifdef SEND_1 + transmit_can_frame(&PYLON_4211, can_config.inverter); + transmit_can_frame(&PYLON_4221, can_config.inverter); + transmit_can_frame(&PYLON_4231, can_config.inverter); + transmit_can_frame(&PYLON_4241, can_config.inverter); + transmit_can_frame(&PYLON_4251, can_config.inverter); + transmit_can_frame(&PYLON_4261, can_config.inverter); + transmit_can_frame(&PYLON_4271, can_config.inverter); + transmit_can_frame(&PYLON_4281, can_config.inverter); + transmit_can_frame(&PYLON_4291, can_config.inverter); +#endif +} +void setup_inverter(void) { // Performs one time setup at startup over CAN bus + strncpy(datalayer.system.info.inverter_protocol, "Ferroamp Pylon battery over CAN bus", 63); + datalayer.system.info.inverter_protocol[63] = '\0'; +} +#endif diff --git a/Software/src/inverter/FERROAMP-CAN.h b/Software/src/inverter/FERROAMP-CAN.h new file mode 100644 index 00000000..06bae78a --- /dev/null +++ b/Software/src/inverter/FERROAMP-CAN.h @@ -0,0 +1,12 @@ +#ifndef FERROAMP_CAN_H +#define FERROAMP_CAN_H +#include "../include.h" + +#define CAN_INVERTER_SELECTED + +void send_system_data(); +void send_setup_info(); +void transmit_can_frame(CAN_frame* tx_frame, int interface); +void setup_inverter(void); + +#endif diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index 674af2f8..0dd8c37d 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -164,11 +164,16 @@ void update_values_can_inverter() { //This function maps all the values fetched } //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); + if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage + //User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) + GROWATT_3110.data.u8[0] = (datalayer.battery.settings.max_user_set_charge_voltage_dV >> 8); + GROWATT_3110.data.u8[1] = (datalayer.battery.settings.max_user_set_charge_voltage_dV & 0x00FF); + } else { + //Battery max voltage used as 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); @@ -239,9 +244,15 @@ void update_values_can_inverter() { //This function maps all the values fetched 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); + if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage + //Use user specified voltage as Discharge cutoff voltage (0.1V) [0-1000V] + GROWATT_3150.data.u8[0] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV >> 8); + GROWATT_3150.data.u8[1] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV & 0x00FF); + } else { + //Use battery min design voltage as 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); diff --git a/Software/src/inverter/INVERTERS.h b/Software/src/inverter/INVERTERS.h index a5b40c94..3a73ff5f 100644 --- a/Software/src/inverter/INVERTERS.h +++ b/Software/src/inverter/INVERTERS.h @@ -19,6 +19,10 @@ #include "KOSTAL-RS485.h" #endif +#ifdef FERROAMP_CAN +#include "FERROAMP-CAN.h" +#endif + #ifdef FOXESS_CAN #include "FOXESS-CAN.h" #endif diff --git a/Software/src/inverter/KOSTAL-RS485.cpp b/Software/src/inverter/KOSTAL-RS485.cpp index 881907f5..b8a6f18e 100644 --- a/Software/src/inverter/KOSTAL-RS485.cpp +++ b/Software/src/inverter/KOSTAL-RS485.cpp @@ -27,59 +27,44 @@ union f32b { byte b[4]; }; -uint8_t frame1[40] = {0x06, 0xE2, 0xFF, 0x02, 0xFF, 0x29, // Frame header - 0x01, 0x08, 0x80, 0x43, // 256.063 Nominal voltage / 5*51.2=256 first byte 0x01 or 0x04 - 0xE4, 0x70, 0x8A, 0x5C, // These might be Umin & Unax, Uint16 - 0xB5, 0x02, 0xD3, 0x01, // Battery Serial number? Modbus register 527 - 0x01, 0x05, 0xC8, 0x41, // 25.0024 ? - 0xC2, 0x18, // Battery Firmware, modbus register 586 - 0x01, 0x03, 0x59, 0x42, // 0x00005942 = 54.25 ?? - 0x01, 0x01, 0x01, 0x02, 0x05, 0x02, 0xA0, 0x01, 0x01, 0x02, - 0x4D, // CRC - 0x00}; // +uint8_t BATTERY_INFO[40] = { + 0x06, 0xE2, 0xFF, 0x02, 0xFF, 0x29, // Frame header + 0x01, 0x08, 0x80, 0x43, // 256.063 Nominal voltage / 5*51.2=256 first byte 0x01 or 0x04 + 0xE4, 0x70, 0x8A, 0x5C, // These might be Umin & Unax, Uint16 + 0xB5, 0x02, 0xD3, 0x01, // Battery Serial number? Modbus register 527 + 0x01, 0x05, 0xC8, 0x41, // 25.0024 ? + 0xC2, 0x18, // Battery Firmware, modbus register 586 + 0x01, 0x03, 0x59, 0x42, // 0x00005942 = 54.25 ?? + 0x01, 0x01, 0x01, 0x02, 0x05, 0x02, 0xA0, 0x01, 0x01, 0x02, + 0x4D, // CRC + 0x00}; // -// values in frame2 will be overwritten at update_modbus_registers_inverter() +// values in CyclicData will be overwritten at update_modbus_registers_inverter() -uint8_t frame2[64] = { - 0x0A, // This may also been 0x06, seen at startup when live values not valid, but also occasionally single frames. +uint8_t CyclicData[64] = { + 0x00, // First zero byte pointer 0xE2, 0xFF, 0x02, 0xFF, 0x29, // frame Header + 0x1D, 0x5A, 0x85, 0x43, // Current Voltage (float) Modbus register 216, Bytes 6-9 + 0x00, 0x00, 0x8D, 0x43, // Max Voltage (2 byte float), Bytes 12-13 + 0x00, 0x00, 0xAC, 0x41, // BAttery Temperature (2 byte float) Modbus register 214, Bytes 16-17 + 0x00, 0x00, 0x00, 0x00, // Peak Current (1s period?), Bytes 18-21 + 0x00, 0x00, 0x00, 0x00, // Avg current (1s period?), Bytes 22-25 + 0x00, 0x00, 0x48, 0x42, // Max discharge current (2 byte float), Bit 26-29, // Sunspec: ADisChaMax + 0x00, 0x00, 0xC8, 0x41, // Battery gross capacity, Ah (2 byte float) , Bytes 30-33, Modbus 512 + 0x00, 0x00, 0xA0, 0x41, // Max charge current (2 byte float) Bit 36-37, ZERO WHEN SOC=100 // Sunspec: AChaMax + 0xCD, 0xCC, 0xB4, 0x41, // MaxCellTemp (4 byte float) Bit 38-41 + 0x00, 0x00, 0xA4, 0x41, // MinCellTemp (4 byte float) Bit 42-45 + 0xA4, 0x70, 0x55, 0x40, // MaxCellVolt (float), Bit 46-49 + 0x7D, 0x3F, 0x55, 0x40, // MinCellVolt (float), Bit 50-53 - 0x1D, 0x5A, 0x85, 0x43, // Current Voltage (float) Modbus register 216, Bytes 6-9 - 0x01, 0x03, // Unknown, 0x03 seen also 0x0F, 0x07, might hava something to do with current - 0x8D, 0x43, // Max Voltage (2 byte float), Bytes 12-13 - 0x01, 0x03, 0xAC, 0x41, // BAttery Temperature (2 byte float) Modbus register 214, Bytes 16-17 - 0x01, 0x01, 0x01, - 0x01, // Peak Current (1s period?), Bytes 18-21 - Communication fault seen with some values (>10A?) - 0x01, 0x01, 0x01, - 0x01, // Avg current (1s period?), Bytes 22-25 - Communication fault seen with some values (>10A?) - - 0x01, 0x03, 0x48, 0x42, // Max discharge current (2 byte float), Bit 26-29, - // Sunspec: ADisChaMax - - 0x01, 0x03, // Unknown - 0xC8, 0x41, // Battery gross capacity, Ah (2 byte float) , Bytes 30-33, Modbus 512 - - 0x01, // Unknown - 0x16, // This seems to have something to do with cell temperatures - - 0xA0, 0x41, // Max charge current (2 byte float) Bit 36-37, ZERO WHEN SOC=100 - // Sunspec: AChaMax - - 0xCD, 0xCC, 0xB4, 0x41, // MaxCellTemp (4 byte float) Bit 38-41 - 0x01, 0x0C, 0xA4, 0x41, // MinCellTemp (4 byte float) Bit 42-45 - - 0xA4, 0x70, 0x55, 0x40, // MaxCellVolt (float), Bit 46-49 - 0x7D, 0x3F, 0x55, 0x40, // MinCellVolt (float), Bit 50-53 - - 0xFE, // Cylce count , Bit 54 - 0x04, // Cycle count? , Bit 55 - 0x01, // Byte 56 - 0x40, // When SOC=100 Byte57=0x40, at startup 0x03 (about 7 times), otherwise 0x02 - 0x64, // SOC , Bit 58 - 0x01, // Unknown, when byte 57 = 0x03, this 0x02, otherwise 0x01 - 0x01, // Unknown, Seen only 0x01 - 0x02, // Unknown, Mostly 0x02. seen also 0x01 - 0x00, // CRC (inverted sum of bytes 1-62 + 0xC0), Bit 62 + 0xFE, 0x04, // Cycle count, + 0x00, // Byte 56 + 0x40, // When SOC=100 Byte57=0x40, at startup 0x03 (about 7 times), otherwise 0x02 + 0x64, // SOC , Bit 58 + 0x00, // Unknown, + 0x00, // Unknown, + 0x02, // Unknown, Mostly 0x02. seen also 0x01 + 0x00, // CRC (inverted sum of bytes 1-62 + 0xC0), Bit 62 0x00}; // FE 04 01 40 xx 01 01 02 yy (fully charged) @@ -97,7 +82,7 @@ uint8_t frame4[8] = {0x07, 0xE3, 0xFF, 0x02, 0xFF, 0x29, 0xF4, 0x00}; uint8_t frameB1[10] = {0x07, 0x63, 0xFF, 0x02, 0xFF, 0x29, 0x5E, 0x02, 0x16, 0x00}; uint8_t frameB1b[8] = {0x07, 0xE3, 0xFF, 0x02, 0xFF, 0x29, 0xF4, 0x00}; -uint8_t RS485_RXFRAME[10]; +uint8_t RS485_RXFRAME[300]; bool register_content_ok = false; @@ -110,13 +95,6 @@ void float2frame(byte* arr, float value, byte framepointer) { arr[framepointer + 3] = g.b[3]; } -void float2frameMSB(byte* arr, float value, byte framepointer) { - f32b g; - g.f = value; - arr[framepointer + 0] = g.b[2]; - arr[framepointer + 1] = g.b[3]; -} - static void dbg_timestamp(void) { #ifdef DEBUG_KOSTAL_RS485_DATA logging.print("["); @@ -148,17 +126,32 @@ static void dbg_message(const char* msg) { #endif } +/* https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing#Encoding_examples */ + +void null_stuffer(byte* lfc, int len) { + int last_null_byte = 0; + for (int i = 0; i < len; i++) { + if (lfc[i] == '\0') { + lfc[last_null_byte] = (byte)(i - last_null_byte); + last_null_byte = i; + } + } +} + 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) { +byte calculate_kostal_crc(byte* lfc, int len) { unsigned int sum = 0; - for (int i = 0; i < lastbyte; ++i) { + if (lfc[0] != 0) { + logging.printf("WARNING: first byte should be 0, but is 0x%02x\n", lfc[0]); + } + for (int i = 1; i < len; i++) { sum += lfc[i]; } - return ((byte) ~(sum + 0xc0) & 0xff); + return (byte)(-sum & 0xff); } byte calculate_frame1_crc(byte* lfc, int lastbyte) { @@ -191,60 +184,65 @@ void update_RS485_registers_inverter() { if (datalayer.system.status.battery_allows_contactor_closing & datalayer.system.status.inverter_allows_contactor_closing) { - float2frame(frame2, (float)datalayer.battery.status.voltage_dV / 10, 6); // Confirmed OK mapping - frame2[0] = 0x0A; + float2frame(CyclicData, (float)datalayer.battery.status.voltage_dV / 10, 6); // Confirmed OK mapping + CyclicData[0] = 0x0A; } else { - frame2[0] = 0x06; - float2frame(frame2, 0.0, 6); + CyclicData[0] = 0x06; + float2frame(CyclicData, 0.0, 6); } // Set nominal voltage to value between min and max voltage set by battery (Example 400 and 300 results in 350V) nominal_voltage_dV = (((datalayer.battery.info.max_design_voltage_dV - datalayer.battery.info.min_design_voltage_dV) / 2) + datalayer.battery.info.min_design_voltage_dV); - float2frameMSB(frame1, (float)nominal_voltage_dV / 10, 8); + float2frame(BATTERY_INFO, (float)nominal_voltage_dV / 10, 6); - float2frameMSB(frame2, (float)datalayer.battery.info.max_design_voltage_dV / 10, 12); + float2frame(CyclicData, (float)datalayer.battery.info.max_design_voltage_dV / 10, 10); - float2frameMSB(frame2, (float)average_temperature_dC / 10, 16); + float2frame(CyclicData, (float)average_temperature_dC / 10, 14); // Some current values causes communication error, must be resolved, why. - // float2frameMSB(frame2, (float)datalayer.battery.status.current_dA / 10, 20); // Peak discharge? current (2 byte float) - // float2frameMSB(frame2, (float)datalayer.battery.status.current_dA / 10, 24); + // float2frame(CyclicData, (float)datalayer.battery.status.current_dA / 10, 18); // Peak discharge? current (2 byte float) + // float2frame(CyclicData, (float)datalayer.battery.status.current_dA / 10, 22); - float2frameMSB(frame2, (float)datalayer.battery.status.max_discharge_current_dA / 10, 28); // BAttery capacity Ah - - float2frameMSB(frame2, (float)datalayer.battery.status.max_discharge_current_dA / 10, 32); + float2frame(CyclicData, (float)datalayer.battery.status.max_discharge_current_dA / 10, 26); // When SOC = 100%, drop down allowed charge current down. if ((datalayer.battery.status.reported_soc / 100) < 100) { - float2frameMSB(frame2, (float)datalayer.battery.status.max_charge_current_dA / 10, 36); - frame2[57] = 0x02; - frame2[59] = 0x01; + float2frame(CyclicData, (float)datalayer.battery.status.max_charge_current_dA / 10, 34); } else { - float2frameMSB(frame2, 0.0, 36); - //frame2[57]=0x40; - frame2[57] = 0x02; - frame2[59] = 0x01; + float2frame(CyclicData, 0.0, 34); } - // On startup, byte 57 seems to be always 0x03 couple of frames,. + // On startup, byte 56 seems to be always 0x00 couple of frames,. + if (f2_startup_count < 9) { + CyclicData[56] = 0x00; + } else { + CyclicData[56] = 0x01; + } + + // On startup, byte 59 seems to be always 0x02 couple of frames,. if (f2_startup_count < 14) { - frame2[57] = 0x03; - frame2[59] = 0x02; + CyclicData[59] = 0x02; + } else { + CyclicData[59] = 0x00; } - float2frame(frame2, (float)datalayer.battery.status.temperature_max_dC / 10, 38); - float2frame(frame2, (float)datalayer.battery.status.temperature_min_dC / 10, 42); + if (nominal_voltage_dV > 0) { + float2frame(CyclicData, (float)(datalayer.battery.info.total_capacity_Wh / nominal_voltage_dV * 10), + 30); // BAttery capacity Ah + } + float2frame(CyclicData, (float)datalayer.battery.status.temperature_max_dC / 10, 38); + float2frame(CyclicData, (float)datalayer.battery.status.temperature_min_dC / 10, 42); - float2frame(frame2, (float)datalayer.battery.status.cell_max_voltage_mV / 1000, 46); - float2frame(frame2, (float)datalayer.battery.status.cell_min_voltage_mV / 1000, 50); + float2frame(CyclicData, (float)datalayer.battery.status.cell_max_voltage_mV / 1000, 46); + float2frame(CyclicData, (float)datalayer.battery.status.cell_min_voltage_mV / 1000, 50); - frame2[58] = (byte)(datalayer.battery.status.reported_soc / 100); // Confirmed OK mapping + CyclicData[58] = (byte)(datalayer.battery.status.reported_soc / 100); // Confirmed OK mapping register_content_ok = true; - frame1[38] = calculate_frame1_crc(frame1, 38); + BATTERY_INFO[38] = calculate_frame1_crc(BATTERY_INFO, 38); if (incoming_message_counter > 0) { incoming_message_counter--; @@ -328,7 +326,7 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream } if (headerA && (RS485_RXFRAME[6] == 0x4A) && (RS485_RXFRAME[7] == 0x08)) { // "frame 1" - send_kostal(frame1, 40); + send_kostal(BATTERY_INFO, 40); if (!startupMillis) { startupMillis = currentMillis; } @@ -340,13 +338,10 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream f2_startup_count++; } byte tmpframe[64]; //copy values to prevent data manipulation during rewrite/crc calculation - memcpy(tmpframe, frame2, 64); - for (int i = 1; i < 63; i++) { - if (tmpframe[i] == 0x00) { - tmpframe[i] = 0x01; - } - } - tmpframe[62] = calculate_longframe_crc(tmpframe, 62); + memcpy(tmpframe, CyclicData, 64); + + tmpframe[62] = calculate_kostal_crc(tmpframe, 62); + null_stuffer(tmpframe, 64); send_kostal(tmpframe, 64); } if (headerA && (RS485_RXFRAME[6] == 0x53) && (RS485_RXFRAME[7] == 0x03)) { // "frame 3" diff --git a/Software/src/inverter/KOSTAL-RS485.h b/Software/src/inverter/KOSTAL-RS485.h index 6f638ac6..0d3c3f09 100644 --- a/Software/src/inverter/KOSTAL-RS485.h +++ b/Software/src/inverter/KOSTAL-RS485.h @@ -4,6 +4,7 @@ #include "../include.h" #define RS485_INVERTER_SELECTED +#define RS485_BAUDRATE 57600 //#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) diff --git a/Software/src/inverter/PYLON-CAN.cpp b/Software/src/inverter/PYLON-CAN.cpp index b45f0108..c55c737f 100644 --- a/Software/src/inverter/PYLON-CAN.cpp +++ b/Software/src/inverter/PYLON-CAN.cpp @@ -310,7 +310,7 @@ void update_values_can_inverter() { //This function maps all the values fetched PYLON_4271.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8); #else // Not INVERT_LOW_HIGH_BYTES //Voltage (370.0) - PYLON_4210.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8; + PYLON_4210.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8); PYLON_4210.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF); PYLON_4211.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8); PYLON_4211.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF); @@ -348,28 +348,28 @@ void update_values_can_inverter() { //This function maps all the values fetched #ifdef SET_30K_OFFSET //Max ChargeCurrent - PYLON_4220.data.u8[4] = ((max_charge_current + 30000) >> 8); - PYLON_4220.data.u8[5] = ((max_charge_current + 30000) & 0x00FF); - PYLON_4221.data.u8[4] = ((max_charge_current + 30000) >> 8); - PYLON_4221.data.u8[5] = ((max_charge_current + 30000) & 0x00FF); + PYLON_4220.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8); + PYLON_4220.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF); + PYLON_4221.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8); + PYLON_4221.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF); //Max DischargeCurrent - PYLON_4220.data.u8[6] = ((30000 - max_discharge_current) >> 8); - 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); + PYLON_4220.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8); + PYLON_4220.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF); + PYLON_4221.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8); + PYLON_4221.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF); #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); - PYLON_4221.data.u8[4] = (max_charge_current >> 8); - PYLON_4221.data.u8[5] = (max_charge_current & 0x00FF); + PYLON_4220.data.u8[4] = (datalayer.battery.status.max_charge_current_dA >> 8); + PYLON_4220.data.u8[5] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); + PYLON_4221.data.u8[4] = (datalayer.battery.status.max_charge_current_dA >> 8); + PYLON_4221.data.u8[5] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); //Max DishargeCurrent - PYLON_4220.data.u8[6] = (max_discharge_current >> 8); - 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); + PYLON_4220.data.u8[6] = (datalayer.battery.status.max_discharge_current_dA >> 8); + PYLON_4220.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); + PYLON_4221.data.u8[6] = (datalayer.battery.status.max_discharge_current >> 8); + PYLON_4221.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); #endif //SET_30K_OFFSET //Max cell voltage diff --git a/Software/src/inverter/PYLON-LV-CAN.cpp b/Software/src/inverter/PYLON-LV-CAN.cpp index ddc2ce5a..725c6451 100644 --- a/Software/src/inverter/PYLON-LV-CAN.cpp +++ b/Software/src/inverter/PYLON-LV-CAN.cpp @@ -39,28 +39,43 @@ CAN_frame PYLON_35E = {.FD = false, MANUFACTURER_NAME[7], }}; +// when e.g. the min temperature is 0, max is 100 and the warning percent is 80% +// a warning should be generated at 20 (i.e. at 20% of the value range) +// this function calculates this 20% point for a given min/max +int16_t warning_threshold_of_min(int16_t min_val, int16_t max_val) { + int16_t diff = max_val - min_val; + return min_val + (diff * (100 - WARNINGS_PERCENT)) / 100; +} + void update_values_can_inverter() { // This function maps all the values fetched from battery CAN to the correct CAN messages - // TODO: officially this value is "battery charge voltage". Do we need to add something here to the actual voltage? - PYLON_351.data.u8[0] = datalayer.battery.status.voltage_dV & 0xff; - PYLON_351.data.u8[1] = datalayer.battery.status.voltage_dV >> 8; + // Set "battery charge voltage" to volts + 1 or user supplied value + uint16_t charge_voltage_dV = datalayer.battery.info.max_design_voltage_dV; + if (datalayer.battery.settings.user_set_voltage_limits_active) + charge_voltage_dV = datalayer.battery.settings.max_user_set_charge_voltage_dV; + if (charge_voltage_dV > datalayer.battery.info.max_design_voltage_dV) + charge_voltage_dV = datalayer.battery.info.max_design_voltage_dV; + PYLON_351.data.u8[0] = charge_voltage_dV & 0xff; + PYLON_351.data.u8[1] = charge_voltage_dV >> 8; PYLON_351.data.u8[2] = datalayer.battery.status.max_charge_current_dA & 0xff; PYLON_351.data.u8[3] = datalayer.battery.status.max_charge_current_dA >> 8; PYLON_351.data.u8[4] = datalayer.battery.status.max_discharge_current_dA & 0xff; PYLON_351.data.u8[5] = datalayer.battery.status.max_discharge_current_dA >> 8; - PYLON_355.data.u8[0] = (datalayer.battery.status.reported_soc / 10) & 0xff; - PYLON_355.data.u8[1] = (datalayer.battery.status.reported_soc / 10) >> 8; - PYLON_355.data.u8[2] = (datalayer.battery.status.soh_pptt / 10) & 0xff; - PYLON_355.data.u8[3] = (datalayer.battery.status.soh_pptt / 10) >> 8; + PYLON_355.data.u8[0] = (datalayer.battery.status.reported_soc / 100) & 0xff; + PYLON_355.data.u8[1] = (datalayer.battery.status.reported_soc / 100) >> 8; + PYLON_355.data.u8[2] = (datalayer.battery.status.soh_pptt / 100) & 0xff; + PYLON_355.data.u8[3] = (datalayer.battery.status.soh_pptt / 100) >> 8; - PYLON_356.data.u8[0] = datalayer.battery.status.voltage_dV & 0xff; - PYLON_356.data.u8[1] = datalayer.battery.status.voltage_dV >> 8; + int16_t voltage_cV = datalayer.battery.status.voltage_dV * 10; + int16_t temperature = (datalayer.battery.status.temperature_min_dC + datalayer.battery.status.temperature_max_dC) / 2; + PYLON_356.data.u8[0] = voltage_cV & 0xff; + PYLON_356.data.u8[1] = voltage_cV >> 8; PYLON_356.data.u8[2] = datalayer.battery.status.current_dA & 0xff; PYLON_356.data.u8[3] = datalayer.battery.status.current_dA >> 8; - PYLON_356.data.u8[4] = datalayer.battery.status.temperature_max_dC & 0xff; - PYLON_356.data.u8[5] = datalayer.battery.status.temperature_max_dC >> 8; + PYLON_356.data.u8[4] = temperature & 0xff; + PYLON_356.data.u8[5] = temperature >> 8; // initialize all errors and warnings to 0 PYLON_359.data.u8[0] = 0x00; @@ -78,20 +93,23 @@ void update_values_can_inverter() { PYLON_359.data.u8[0] |= 0x10; if (datalayer.battery.status.temperature_max_dC >= BATTERY_MAXTEMPERATURE) PYLON_359.data.u8[0] |= 0x0C; - if (datalayer.battery.status.voltage_dV * 100 <= datalayer.battery.info.min_cell_voltage_mV) + if (datalayer.battery.status.voltage_dV <= datalayer.battery.info.min_design_voltage_dV) PYLON_359.data.u8[0] |= 0x04; - // we never set PYLON_359.data.u8[1] |= 0x80 called "BMS internal" + if (datalayer.battery.status.bms_status == FAULT) + PYLON_359.data.u8[1] |= 0x80; if (datalayer.battery.status.current_dA <= -1 * datalayer.battery.status.max_charge_current_dA) PYLON_359.data.u8[1] |= 0x01; // WARNINGS (using same rules as errors but reporting earlier) if (datalayer.battery.status.current_dA >= datalayer.battery.status.max_discharge_current_dA * WARNINGS_PERCENT / 100) PYLON_359.data.u8[2] |= 0x80; - if (datalayer.battery.status.temperature_min_dC <= BATTERY_MINTEMPERATURE * WARNINGS_PERCENT / 100) + if (datalayer.battery.status.temperature_min_dC <= + warning_threshold_of_min(BATTERY_MINTEMPERATURE, BATTERY_MAXTEMPERATURE)) PYLON_359.data.u8[2] |= 0x10; if (datalayer.battery.status.temperature_max_dC >= BATTERY_MAXTEMPERATURE * WARNINGS_PERCENT / 100) PYLON_359.data.u8[2] |= 0x0C; - if (datalayer.battery.status.voltage_dV * 100 <= datalayer.battery.info.min_cell_voltage_mV + 100) + if (datalayer.battery.status.voltage_dV <= warning_threshold_of_min(datalayer.battery.info.min_design_voltage_dV, + datalayer.battery.info.max_design_voltage_dV)) PYLON_359.data.u8[2] |= 0x04; // we never set PYLON_359.data.u8[3] |= 0x80 called "BMS internal" if (datalayer.battery.status.current_dA <= @@ -99,10 +117,17 @@ void update_values_can_inverter() { PYLON_359.data.u8[3] |= 0x01; PYLON_35C.data.u8[0] = 0xC0; // enable charging and discharging - PYLON_35C.data.u8[1] = 0x00; - if (datalayer.battery.status.real_soc <= datalayer.battery.settings.min_percentage) + if (datalayer.battery.status.bms_status == FAULT) + PYLON_35C.data.u8[0] = 0x00; // disable all + else if (datalayer.battery.settings.user_set_voltage_limits_active && + datalayer.battery.status.voltage_dV > datalayer.battery.settings.max_user_set_charge_voltage_dV) + PYLON_35C.data.u8[0] = 0x40; // only allow discharging + else if (datalayer.battery.settings.user_set_voltage_limits_active && + datalayer.battery.status.voltage_dV < datalayer.battery.settings.max_user_set_discharge_voltage_dV) PYLON_35C.data.u8[0] = 0xA0; // enable charing, set charge immediately - if (datalayer.battery.status.real_soc >= datalayer.battery.settings.max_percentage) + else if (datalayer.battery.status.real_soc <= datalayer.battery.settings.min_percentage) + PYLON_35C.data.u8[0] = 0xA0; // enable charing, set charge immediately + else if (datalayer.battery.status.real_soc >= datalayer.battery.settings.max_percentage) PYLON_35C.data.u8[0] = 0x40; // enable discharging only // PYLON_35E is pre-filled with the manufacturer name @@ -119,12 +144,35 @@ void map_can_frame_to_variable_inverter(CAN_frame rx_frame) { } } +#ifdef DEBUG_VIA_USB +void dump_frame(CAN_frame* frame) { + Serial.print("[PYLON-LV] sending CAN frame "); + Serial.print(frame->ID, HEX); + Serial.print(": "); + for (int i = 0; i < 8; i++) { + Serial.print(frame->data.u8[i] >> 4, HEX); + Serial.print(frame->data.u8[i] & 0xf, HEX); + Serial.print(" "); + } + Serial.println(); +} +#endif + void transmit_can_inverter() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis1000ms >= 1000) { previousMillis1000ms = currentMillis; +#ifdef DEBUG_VIA_USB + dump_frame(&PYLON_351); + dump_frame(&PYLON_355); + dump_frame(&PYLON_356); + dump_frame(&PYLON_359); + dump_frame(&PYLON_35C); + dump_frame(&PYLON_35E); +#endif + transmit_can_frame(&PYLON_351, can_config.inverter); transmit_can_frame(&PYLON_355, can_config.inverter); transmit_can_frame(&PYLON_356, can_config.inverter); diff --git a/Software/src/inverter/SOLAX-CAN.cpp b/Software/src/inverter/SOLAX-CAN.cpp index cddc845d..05675b1c 100644 --- a/Software/src/inverter/SOLAX-CAN.cpp +++ b/Software/src/inverter/SOLAX-CAN.cpp @@ -198,10 +198,10 @@ void update_values_can_inverter() { //This function maps all the values fetched SOLAX_1801.data.u8[4] = 1; //Ultra messages - SOLAX_187E.data.u8[0] = (uint8_t)datalayer.battery.status.reported_remaining_capacity_Wh; - SOLAX_187E.data.u8[1] = (datalayer.battery.status.reported_remaining_capacity_Wh >> 8); - SOLAX_187E.data.u8[2] = (datalayer.battery.status.reported_remaining_capacity_Wh >> 16); - SOLAX_187E.data.u8[3] = (datalayer.battery.status.reported_remaining_capacity_Wh >> 24); + SOLAX_187E.data.u8[0] = (uint8_t)datalayer.battery.info.total_capacity_Wh; + SOLAX_187E.data.u8[1] = (datalayer.battery.info.total_capacity_Wh >> 8); + SOLAX_187E.data.u8[2] = (datalayer.battery.info.total_capacity_Wh >> 16); + SOLAX_187E.data.u8[3] = (datalayer.battery.info.total_capacity_Wh >> 24); SOLAX_187E.data.u8[5] = (uint8_t)(datalayer.battery.status.reported_soc / 100); } diff --git a/Software/src/lib/ESP32Async-AsyncTCP/CMakeLists.txt b/Software/src/lib/ESP32Async-AsyncTCP/CMakeLists.txt new file mode 100644 index 00000000..f52e1c93 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/CMakeLists.txt @@ -0,0 +1,15 @@ +set(COMPONENT_SRCDIRS + "src" +) + +set(COMPONENT_ADD_INCLUDEDIRS + "src" +) + +set(COMPONENT_REQUIRES + "arduino-esp32" +) + +register_component() + +target_compile_options(${COMPONENT_TARGET} PRIVATE -fno-rtti) diff --git a/Software/src/lib/ESP32Async-AsyncTCP/CODE_OF_CONDUCT.md b/Software/src/lib/ESP32Async-AsyncTCP/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..4fcdc2f5 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socioeconomic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +https://sidweb.nl/cms3/en/contact. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/LICENSE b/Software/src/lib/ESP32Async-AsyncTCP/LICENSE similarity index 100% rename from Software/src/lib/mathieucarbou-AsyncTCPSock/LICENSE rename to Software/src/lib/ESP32Async-AsyncTCP/LICENSE diff --git a/Software/src/lib/ESP32Async-AsyncTCP/README.md b/Software/src/lib/ESP32Async-AsyncTCP/README.md new file mode 100644 index 00000000..edda6266 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/README.md @@ -0,0 +1,55 @@ +![https://avatars.githubusercontent.com/u/195753706?s=96&v=4](https://avatars.githubusercontent.com/u/195753706?s=96&v=4) + +# AsyncTCP + +[![License: LGPL 3.0](https://img.shields.io/badge/License-LGPL%203.0-yellow.svg)](https://opensource.org/license/lgpl-3-0/) +[![Continuous Integration](https://github.com/ESP32Async/AsyncTCP/actions/workflows/ci.yml/badge.svg)](https://github.com/ESP32Async/AsyncTCP/actions/workflows/ci.yml) +[![PlatformIO Registry](https://badges.registry.platformio.org/packages/ESP32Async/library/AsyncTCP.svg)](https://registry.platformio.org/libraries/ESP32Async/AsyncTCP) + +Discord Server: [https://discord.gg/X7zpGdyUcY](https://discord.gg/X7zpGdyUcY) + +## Async TCP Library for ESP32 Arduino + +This is a fully asynchronous TCP library, aimed at enabling trouble-free, multi-connection network environment for Espressif's ESP32 MCUs. + +This library is the base for [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) + +## How to install + +The library can be downloaded from the releases page at [https://github.com/ESP32Async/AsyncTCP/releases](https://github.com/ESP32Async/AsyncTCP/releases). + +It is also deployed in these registries: + +- Arduino Library Registry: [https://github.com/arduino/library-registry](https://github.com/arduino/library-registry) + +- ESP Component Registry [https://components.espressif.com/components/esp32async/asynctcp/](https://components.espressif.com/components/esp32async/asynctcp/) + +- PlatformIO Registry: [https://registry.platformio.org/libraries/esp32async/AsyncTCP](https://registry.platformio.org/libraries/esp32async/AsyncTCP) + + - Use: `lib_deps=ESP32Async/AsyncTCP` to point to latest version + - Use: `lib_deps=ESP32Async/AsyncTCP @ ^` to point to latest version with the same major version + - Use: `lib_deps=ESP32Async/AsyncTCP @ ` to always point to the same version (reproductible build) + +## AsyncClient and AsyncServer + +The base classes on which everything else is built. They expose all possible scenarios, but are really raw and require more skills to use. + +## Important recommendations + +Most of the crashes are caused by improper configuration of the library for the project. +Here are some recommendations to avoid them. + +I personally use the following configuration in my projects: + +```c++ + -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=5000 // (keep default) + -D CONFIG_ASYNC_TCP_PRIORITY=10 // (keep default) + -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 // (keep default) + -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 // force async_tcp task to be on same core as the app (default is core 0) + -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 // reduce the stack size (default is 16K) +``` + +## Compatibility + +- ESP32 +- Arduino Core 2.x and 3.x diff --git a/Software/src/lib/ESP32Async-AsyncTCP/component.mk b/Software/src/lib/ESP32Async-AsyncTCP/component.mk new file mode 100644 index 00000000..bb5bb161 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/component.mk @@ -0,0 +1,3 @@ +COMPONENT_ADD_INCLUDEDIRS := src +COMPONENT_SRCDIRS := src +CXXFLAGS += -fno-rtti diff --git a/Software/src/lib/ESP32Async-AsyncTCP/library.json b/Software/src/lib/ESP32Async-AsyncTCP/library.json new file mode 100644 index 00000000..6e725736 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/library.json @@ -0,0 +1,31 @@ +{ + "name": "AsyncTCP", + "version": "3.3.5", + "description": "Asynchronous TCP Library for ESP32", + "keywords": "async,tcp", + "repository": { + "type": "git", + "url": "https://github.com/ESP32Async/AsyncTCP.git" + }, + "authors": + { + "name": "ESP32Async", + "maintainer": true + }, + "license": "LGPL-3.0", + "frameworks": "arduino", + "platforms": [ + "espressif32", + "libretiny" + ], + "export": { + "include": [ + "examples", + "src", + "library.json", + "library.properties", + "LICENSE", + "README.md" + ] + } +} diff --git a/Software/src/lib/ESP32Async-AsyncTCP/library.properties b/Software/src/lib/ESP32Async-AsyncTCP/library.properties new file mode 100644 index 00000000..8c2495f2 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/library.properties @@ -0,0 +1,11 @@ +name=Async TCP +includes=AsyncTCP.h +version=3.3.5 +author=ESP32Async +maintainer=ESP32Async +sentence=Async TCP Library for ESP32 +paragraph=Async TCP Library for ESP32 +category=Other +url=https://github.com/ESP32Async/AsyncTCP.git +architectures=* +license=LGPL-3.0 diff --git a/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCP.cpp b/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCP.cpp new file mode 100644 index 00000000..a92ed1a6 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCP.cpp @@ -0,0 +1,1665 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "Arduino.h" + +#include "AsyncTCP.h" + +extern "C" { +#include "lwip/dns.h" +#include "lwip/err.h" +#include "lwip/inet.h" +#include "lwip/opt.h" +#include "lwip/tcp.h" +} + +#if CONFIG_ASYNC_TCP_USE_WDT +#include "esp_task_wdt.h" +#endif + +// Required for: +// https://github.com/espressif/arduino-esp32/blob/3.0.3/libraries/Network/src/NetworkInterface.cpp#L37-L47 +#if ESP_IDF_VERSION_MAJOR >= 5 +#include +#endif + +#define TAG "AsyncTCP" + +// https://github.com/espressif/arduino-esp32/issues/10526 +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING +#define TCP_MUTEX_LOCK() \ + if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { \ + LOCK_TCPIP_CORE(); \ + } + +#define TCP_MUTEX_UNLOCK() \ + if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { \ + UNLOCK_TCPIP_CORE(); \ + } +#else // CONFIG_LWIP_TCPIP_CORE_LOCKING +#define TCP_MUTEX_LOCK() +#define TCP_MUTEX_UNLOCK() +#endif // CONFIG_LWIP_TCPIP_CORE_LOCKING + +#define INVALID_CLOSED_SLOT -1 + +/* + TCP poll interval is specified in terms of the TCP coarse timer interval, which is called twice a second + https://github.com/espressif/esp-lwip/blob/2acf959a2bb559313cd2bf9306c24612ba3d0e19/src/core/tcp.c#L1895 +*/ +#define CONFIG_ASYNC_TCP_POLL_TIMER 1 + +/* + * TCP/IP Event Task + * */ + +typedef enum { + LWIP_TCP_SENT, + LWIP_TCP_RECV, + LWIP_TCP_FIN, + LWIP_TCP_ERROR, + LWIP_TCP_POLL, + LWIP_TCP_CLEAR, + LWIP_TCP_ACCEPT, + LWIP_TCP_CONNECTED, + LWIP_TCP_DNS +} lwip_tcp_event_t; + +typedef struct { + lwip_tcp_event_t event; + void *arg; + union { + struct { + tcp_pcb *pcb; + int8_t err; + } connected; + struct { + int8_t err; + } error; + struct { + tcp_pcb *pcb; + uint16_t len; + } sent; + struct { + tcp_pcb *pcb; + pbuf *pb; + int8_t err; + } recv; + struct { + tcp_pcb *pcb; + int8_t err; + } fin; + struct { + tcp_pcb *pcb; + } poll; + struct { + AsyncClient *client; + } accept; + struct { + const char *name; + ip_addr_t addr; + } dns; + }; +} lwip_tcp_event_packet_t; + +static QueueHandle_t _async_queue = NULL; +static TaskHandle_t _async_service_task_handle = NULL; + +static SemaphoreHandle_t _slots_lock = NULL; +static const int _number_of_closed_slots = CONFIG_LWIP_MAX_ACTIVE_TCP; +static uint32_t _closed_slots[_number_of_closed_slots]; +static uint32_t _closed_index = []() { + _slots_lock = xSemaphoreCreateBinary(); + configASSERT(_slots_lock); // Add sanity check + xSemaphoreGive(_slots_lock); + for (int i = 0; i < _number_of_closed_slots; ++i) { + _closed_slots[i] = 1; + } + return 1; +}(); + +static inline bool _init_async_event_queue() { + if (!_async_queue) { + _async_queue = xQueueCreate(CONFIG_ASYNC_TCP_QUEUE_SIZE, sizeof(lwip_tcp_event_packet_t *)); + if (!_async_queue) { + return false; + } + } + return true; +} + +static inline bool _send_async_event(lwip_tcp_event_packet_t **e, TickType_t wait = portMAX_DELAY) { + return _async_queue && xQueueSend(_async_queue, e, wait) == pdPASS; +} + +static inline bool _prepend_async_event(lwip_tcp_event_packet_t **e, TickType_t wait = portMAX_DELAY) { + return _async_queue && xQueueSendToFront(_async_queue, e, wait) == pdPASS; +} + +static inline bool _get_async_event(lwip_tcp_event_packet_t **e) { + while (true) { + if (!_async_queue) { + break; + } + +#if CONFIG_ASYNC_TCP_USE_WDT + // need to return periodically to feed the dog + if (xQueueReceive(_async_queue, e, pdMS_TO_TICKS(1000)) != pdPASS) { + break; + } +#else + if (xQueueReceive(_async_queue, e, portMAX_DELAY) != pdPASS) { + break; + } +#endif + + if ((*e)->event != LWIP_TCP_POLL) { + return true; + } + + /* + Let's try to coalesce two (or more) consecutive poll events into one + this usually happens with poor implemented user-callbacks that are runs too long and makes poll events to stack in the queue + if consecutive user callback for a same connection runs longer that poll time then it will fill the queue with events until it deadlocks. + This is a workaround to mitigate such poor designs and won't let other events/connections to starve the task time. + It won't be effective if user would run multiple simultaneous long running callbacks due to message interleaving. + todo: implement some kind of fair dequeuing or (better) simply punish user for a bad designed callbacks by resetting hog connections + */ + lwip_tcp_event_packet_t *next_pkt = NULL; + while (xQueuePeek(_async_queue, &next_pkt, 0) == pdPASS) { + // if the next event that will come is a poll event for the same connection, we can discard it and continue + if (next_pkt->arg == (*e)->arg && next_pkt->event == LWIP_TCP_POLL) { + if (xQueueReceive(_async_queue, &next_pkt, 0) == pdPASS) { + free(next_pkt); + next_pkt = NULL; + log_d("coalescing polls, network congestion or async callbacks might be too slow!"); + continue; + } + } + + // quit while loop if next incoming event can't be discarded (not a poll event) + break; + } + + /* + now we have to decide if to proceed with poll callback handler or discard it? + poor designed apps using asynctcp without proper dataflow control could flood the queue with interleaved pool/ack events. + I.e. on each poll app would try to generate more data to send, which in turn results in additional ack event triggering chain effect + for long connections. Or poll callback could take long time starving other connections. Anyway our goal is to keep the queue length + grows under control (if possible) and poll events are the safest to discard. + Let's discard poll events processing using linear-increasing probability curve when queue size grows over 3/4 + Poll events are periodic and connection could get another chance next time + */ + if (uxQueueMessagesWaiting(_async_queue) > (rand() % CONFIG_ASYNC_TCP_QUEUE_SIZE / 4 + CONFIG_ASYNC_TCP_QUEUE_SIZE * 3 / 4)) { + free(*e); + *e = NULL; + log_d("discarding poll due to queue congestion"); + continue; // continue main loop to dequeue next event which we know is not a poll event + } + return true; // queue not nearly full, caller can process the poll event + } + return false; +} + +static bool _remove_events_with_arg(void *arg) { + if (!_async_queue) { + return false; + } + + lwip_tcp_event_packet_t *first_packet = NULL; + lwip_tcp_event_packet_t *packet = NULL; + + // figure out which is the first non-matching packet so we can keep the order + while (!first_packet) { + if (xQueueReceive(_async_queue, &first_packet, 0) != pdPASS) { + return false; + } + // discard packet if matching + if ((uintptr_t)first_packet->arg == (uintptr_t)arg) { + free(first_packet); + first_packet = NULL; + } else if (xQueueSend(_async_queue, &first_packet, 0) != pdPASS) { + // try to return first packet to the back of the queue + // we can't wait here if queue is full, because this call has been done from the only consumer task of this queue + // otherwise it would deadlock, we have to discard the event + free(first_packet); + first_packet = NULL; + return false; + } + } + + while (xQueuePeek(_async_queue, &packet, 0) == pdPASS && packet != first_packet) { + if (xQueueReceive(_async_queue, &packet, 0) != pdPASS) { + return false; + } + if ((uintptr_t)packet->arg == (uintptr_t)arg) { + // remove matching event + free(packet); + packet = NULL; + // otherwise try to requeue it + } else if (xQueueSend(_async_queue, &packet, 0) != pdPASS) { + // we can't wait here if queue is full, because this call has been done from the only consumer task of this queue + // otherwise it would deadlock, we have to discard the event + free(packet); + packet = NULL; + return false; + } + } + return true; +} + +static void _handle_async_event(lwip_tcp_event_packet_t *e) { + if (e->arg == NULL) { + // do nothing when arg is NULL + // ets_printf("event arg == NULL: 0x%08x\n", e->recv.pcb); + } else if (e->event == LWIP_TCP_CLEAR) { + _remove_events_with_arg(e->arg); + } else if (e->event == LWIP_TCP_RECV) { + // ets_printf("-R: 0x%08x\n", e->recv.pcb); + AsyncClient::_s_recv(e->arg, e->recv.pcb, e->recv.pb, e->recv.err); + } else if (e->event == LWIP_TCP_FIN) { + // ets_printf("-F: 0x%08x\n", e->fin.pcb); + AsyncClient::_s_fin(e->arg, e->fin.pcb, e->fin.err); + } else if (e->event == LWIP_TCP_SENT) { + // ets_printf("-S: 0x%08x\n", e->sent.pcb); + AsyncClient::_s_sent(e->arg, e->sent.pcb, e->sent.len); + } else if (e->event == LWIP_TCP_POLL) { + // ets_printf("-P: 0x%08x\n", e->poll.pcb); + AsyncClient::_s_poll(e->arg, e->poll.pcb); + } else if (e->event == LWIP_TCP_ERROR) { + // ets_printf("-E: 0x%08x %d\n", e->arg, e->error.err); + AsyncClient::_s_error(e->arg, e->error.err); + } else if (e->event == LWIP_TCP_CONNECTED) { + // ets_printf("C: 0x%08x 0x%08x %d\n", e->arg, e->connected.pcb, e->connected.err); + AsyncClient::_s_connected(e->arg, e->connected.pcb, e->connected.err); + } else if (e->event == LWIP_TCP_ACCEPT) { + // ets_printf("A: 0x%08x 0x%08x\n", e->arg, e->accept.client); + AsyncServer::_s_accepted(e->arg, e->accept.client); + } else if (e->event == LWIP_TCP_DNS) { + // ets_printf("D: 0x%08x %s = %s\n", e->arg, e->dns.name, ipaddr_ntoa(&e->dns.addr)); + AsyncClient::_s_dns_found(e->dns.name, &e->dns.addr, e->arg); + } + free((void *)(e)); +} + +static void _async_service_task(void *pvParameters) { +#if CONFIG_ASYNC_TCP_USE_WDT + if (esp_task_wdt_add(NULL) != ESP_OK) { + log_w("Failed to add async task to WDT"); + } +#endif + lwip_tcp_event_packet_t *packet = NULL; + for (;;) { + if (_get_async_event(&packet)) { + _handle_async_event(packet); + } +#if CONFIG_ASYNC_TCP_USE_WDT + esp_task_wdt_reset(); +#endif + } +#if CONFIG_ASYNC_TCP_USE_WDT + esp_task_wdt_delete(NULL); +#endif + vTaskDelete(NULL); + _async_service_task_handle = NULL; +} +/* +static void _stop_async_task(){ + if(_async_service_task_handle){ + vTaskDelete(_async_service_task_handle); + _async_service_task_handle = NULL; + } +} +*/ + +static bool customTaskCreateUniversal( + TaskFunction_t pxTaskCode, const char *const pcName, const uint32_t usStackDepth, void *const pvParameters, UBaseType_t uxPriority, + TaskHandle_t *const pxCreatedTask, const BaseType_t xCoreID +) { +#ifndef CONFIG_FREERTOS_UNICORE + if (xCoreID >= 0 && xCoreID < 2) { + return xTaskCreatePinnedToCore(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask, xCoreID); + } else { +#endif + return xTaskCreate(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask); +#ifndef CONFIG_FREERTOS_UNICORE + } +#endif +} + +static bool _start_async_task() { + if (!_init_async_event_queue()) { + return false; + } + if (!_async_service_task_handle) { + customTaskCreateUniversal( + _async_service_task, "async_tcp", CONFIG_ASYNC_TCP_STACK_SIZE, NULL, CONFIG_ASYNC_TCP_PRIORITY, &_async_service_task_handle, CONFIG_ASYNC_TCP_RUNNING_CORE + ); + if (!_async_service_task_handle) { + return false; + } + } + return true; +} + +/* + * LwIP Callbacks + * */ + +static int8_t _tcp_clear_events(void *arg) { + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return ERR_MEM; + } + e->event = LWIP_TCP_CLEAR; + e->arg = arg; + if (!_prepend_async_event(&e)) { + free((void *)(e)); + return ERR_TIMEOUT; + } + return ERR_OK; +} + +static int8_t _tcp_connected(void *arg, tcp_pcb *pcb, int8_t err) { + // ets_printf("+C: 0x%08x\n", pcb); + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return ERR_MEM; + } + e->event = LWIP_TCP_CONNECTED; + e->arg = arg; + e->connected.pcb = pcb; + e->connected.err = err; + if (!_prepend_async_event(&e)) { + free((void *)(e)); + return ERR_TIMEOUT; + } + return ERR_OK; +} + +static int8_t _tcp_poll(void *arg, struct tcp_pcb *pcb) { + // throttle polling events queueing when event queue is getting filled up, let it handle _onack's + // log_d("qs:%u", uxQueueMessagesWaiting(_async_queue)); + if (uxQueueMessagesWaiting(_async_queue) > (rand() % CONFIG_ASYNC_TCP_QUEUE_SIZE / 2 + CONFIG_ASYNC_TCP_QUEUE_SIZE / 4)) { + log_d("throttling"); + return ERR_OK; + } + + // ets_printf("+P: 0x%08x\n", pcb); + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return ERR_MEM; + } + e->event = LWIP_TCP_POLL; + e->arg = arg; + e->poll.pcb = pcb; + // poll events are not critical 'cause those are repetitive, so we may not wait the queue in any case + if (!_send_async_event(&e, 0)) { + free((void *)(e)); + return ERR_TIMEOUT; + } + return ERR_OK; +} + +static int8_t _tcp_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, int8_t err) { + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return ERR_MEM; + } + e->arg = arg; + if (pb) { + // ets_printf("+R: 0x%08x\n", pcb); + e->event = LWIP_TCP_RECV; + e->recv.pcb = pcb; + e->recv.pb = pb; + e->recv.err = err; + } else { + // ets_printf("+F: 0x%08x\n", pcb); + e->event = LWIP_TCP_FIN; + e->fin.pcb = pcb; + e->fin.err = err; + // close the PCB in LwIP thread + AsyncClient::_s_lwip_fin(e->arg, e->fin.pcb, e->fin.err); + } + if (!_send_async_event(&e)) { + free((void *)(e)); + return ERR_TIMEOUT; + } + return ERR_OK; +} + +static int8_t _tcp_sent(void *arg, struct tcp_pcb *pcb, uint16_t len) { + // ets_printf("+S: 0x%08x\n", pcb); + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return ERR_MEM; + } + e->event = LWIP_TCP_SENT; + e->arg = arg; + e->sent.pcb = pcb; + e->sent.len = len; + if (!_send_async_event(&e)) { + free((void *)(e)); + return ERR_TIMEOUT; + } + return ERR_OK; +} + +static void _tcp_error(void *arg, int8_t err) { + // ets_printf("+E: 0x%08x\n", arg); + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return; + } + e->event = LWIP_TCP_ERROR; + e->arg = arg; + e->error.err = err; + if (!_send_async_event(&e)) { + free((void *)(e)); + } +} + +static void _tcp_dns_found(const char *name, struct ip_addr *ipaddr, void *arg) { + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return; + } + // ets_printf("+DNS: name=%s ipaddr=0x%08x arg=%x\n", name, ipaddr, arg); + e->event = LWIP_TCP_DNS; + e->arg = arg; + e->dns.name = name; + if (ipaddr) { + memcpy(&e->dns.addr, ipaddr, sizeof(struct ip_addr)); + } else { + memset(&e->dns.addr, 0, sizeof(e->dns.addr)); + } + if (!_send_async_event(&e)) { + free((void *)(e)); + } +} + +// Used to switch out from LwIP thread +static int8_t _tcp_accept(void *arg, AsyncClient *client) { + lwip_tcp_event_packet_t *e = (lwip_tcp_event_packet_t *)malloc(sizeof(lwip_tcp_event_packet_t)); + if (!e) { + log_e("Failed to allocate event packet"); + return ERR_MEM; + } + e->event = LWIP_TCP_ACCEPT; + e->arg = arg; + e->accept.client = client; + if (!_prepend_async_event(&e)) { + free((void *)(e)); + return ERR_TIMEOUT; + } + return ERR_OK; +} + +/* + * TCP/IP API Calls + * */ + +#include "lwip/priv/tcpip_priv.h" + +typedef struct { + struct tcpip_api_call_data call; + tcp_pcb *pcb; + int8_t closed_slot; + int8_t err; + union { + struct { + const char *data; + size_t size; + uint8_t apiflags; + } write; + size_t received; + struct { + ip_addr_t *addr; + uint16_t port; + tcp_connected_fn cb; + } connect; + struct { + ip_addr_t *addr; + uint16_t port; + } bind; + uint8_t backlog; + }; +} tcp_api_call_t; + +static err_t _tcp_output_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if (msg->closed_slot == INVALID_CLOSED_SLOT || !_closed_slots[msg->closed_slot]) { + msg->err = tcp_output(msg->pcb); + } + return msg->err; +} + +static esp_err_t _tcp_output(tcp_pcb *pcb, int8_t closed_slot) { + if (!pcb) { + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + tcpip_api_call(_tcp_output_api, (struct tcpip_api_call_data *)&msg); + return msg.err; +} + +static err_t _tcp_write_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if (msg->closed_slot == INVALID_CLOSED_SLOT || !_closed_slots[msg->closed_slot]) { + msg->err = tcp_write(msg->pcb, msg->write.data, msg->write.size, msg->write.apiflags); + } + return msg->err; +} + +static esp_err_t _tcp_write(tcp_pcb *pcb, int8_t closed_slot, const char *data, size_t size, uint8_t apiflags) { + if (!pcb) { + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + msg.write.data = data; + msg.write.size = size; + msg.write.apiflags = apiflags; + tcpip_api_call(_tcp_write_api, (struct tcpip_api_call_data *)&msg); + return msg.err; +} + +static err_t _tcp_recved_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if (msg->closed_slot == INVALID_CLOSED_SLOT || !_closed_slots[msg->closed_slot]) { + // if(msg->closed_slot != INVALID_CLOSED_SLOT && !_closed_slots[msg->closed_slot]) { + // if(msg->closed_slot != INVALID_CLOSED_SLOT) { + msg->err = 0; + tcp_recved(msg->pcb, msg->received); + } + return msg->err; +} + +static esp_err_t _tcp_recved(tcp_pcb *pcb, int8_t closed_slot, size_t len) { + if (!pcb) { + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + msg.received = len; + tcpip_api_call(_tcp_recved_api, (struct tcpip_api_call_data *)&msg); + return msg.err; +} + +static err_t _tcp_close_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if (msg->closed_slot == INVALID_CLOSED_SLOT || !_closed_slots[msg->closed_slot]) { + msg->err = tcp_close(msg->pcb); + } + return msg->err; +} + +static esp_err_t _tcp_close(tcp_pcb *pcb, int8_t closed_slot) { + if (!pcb) { + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + tcpip_api_call(_tcp_close_api, (struct tcpip_api_call_data *)&msg); + return msg.err; +} + +static err_t _tcp_abort_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = ERR_CONN; + if (msg->closed_slot == INVALID_CLOSED_SLOT || !_closed_slots[msg->closed_slot]) { + tcp_abort(msg->pcb); + } + return msg->err; +} + +static esp_err_t _tcp_abort(tcp_pcb *pcb, int8_t closed_slot) { + if (!pcb) { + return ERR_CONN; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + tcpip_api_call(_tcp_abort_api, (struct tcpip_api_call_data *)&msg); + return msg.err; +} + +static err_t _tcp_connect_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = tcp_connect(msg->pcb, msg->connect.addr, msg->connect.port, msg->connect.cb); + return msg->err; +} + +static esp_err_t _tcp_connect(tcp_pcb *pcb, int8_t closed_slot, ip_addr_t *addr, uint16_t port, tcp_connected_fn cb) { + if (!pcb) { + return ESP_FAIL; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = closed_slot; + msg.connect.addr = addr; + msg.connect.port = port; + msg.connect.cb = cb; + tcpip_api_call(_tcp_connect_api, (struct tcpip_api_call_data *)&msg); + return msg.err; +} + +static err_t _tcp_bind_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = tcp_bind(msg->pcb, msg->bind.addr, msg->bind.port); + return msg->err; +} + +static esp_err_t _tcp_bind(tcp_pcb *pcb, ip_addr_t *addr, uint16_t port) { + if (!pcb) { + return ESP_FAIL; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = -1; + msg.bind.addr = addr; + msg.bind.port = port; + tcpip_api_call(_tcp_bind_api, (struct tcpip_api_call_data *)&msg); + return msg.err; +} + +static err_t _tcp_listen_api(struct tcpip_api_call_data *api_call_msg) { + tcp_api_call_t *msg = (tcp_api_call_t *)api_call_msg; + msg->err = 0; + msg->pcb = tcp_listen_with_backlog(msg->pcb, msg->backlog); + return msg->err; +} + +static tcp_pcb *_tcp_listen_with_backlog(tcp_pcb *pcb, uint8_t backlog) { + if (!pcb) { + return NULL; + } + tcp_api_call_t msg; + msg.pcb = pcb; + msg.closed_slot = -1; + msg.backlog = backlog ? backlog : 0xFF; + tcpip_api_call(_tcp_listen_api, (struct tcpip_api_call_data *)&msg); + return msg.pcb; +} + +/* + Async TCP Client + */ + +AsyncClient::AsyncClient(tcp_pcb *pcb) + : _connect_cb(0), _connect_cb_arg(0), _discard_cb(0), _discard_cb_arg(0), _sent_cb(0), _sent_cb_arg(0), _error_cb(0), _error_cb_arg(0), _recv_cb(0), + _recv_cb_arg(0), _pb_cb(0), _pb_cb_arg(0), _timeout_cb(0), _timeout_cb_arg(0), _poll_cb(0), _poll_cb_arg(0), _ack_pcb(true), _tx_last_packet(0), + _rx_timeout(0), _rx_last_ack(0), _ack_timeout(CONFIG_ASYNC_TCP_MAX_ACK_TIME), _connect_port(0), prev(NULL), next(NULL) { + _pcb = pcb; + _closed_slot = INVALID_CLOSED_SLOT; + if (_pcb) { + _rx_last_packet = millis(); + tcp_arg(_pcb, this); + tcp_recv(_pcb, &_tcp_recv); + tcp_sent(_pcb, &_tcp_sent); + tcp_err(_pcb, &_tcp_error); + tcp_poll(_pcb, &_tcp_poll, CONFIG_ASYNC_TCP_POLL_TIMER); + if (!_allocate_closed_slot()) { + _close(); + } + } +} + +AsyncClient::~AsyncClient() { + if (_pcb) { + _close(); + } + _free_closed_slot(); +} + +/* + * Operators + * */ + +AsyncClient &AsyncClient::operator=(const AsyncClient &other) { + if (_pcb) { + _close(); + } + + _pcb = other._pcb; + _closed_slot = other._closed_slot; + if (_pcb) { + _rx_last_packet = millis(); + tcp_arg(_pcb, this); + tcp_recv(_pcb, &_tcp_recv); + tcp_sent(_pcb, &_tcp_sent); + tcp_err(_pcb, &_tcp_error); + tcp_poll(_pcb, &_tcp_poll, CONFIG_ASYNC_TCP_POLL_TIMER); + } + return *this; +} + +bool AsyncClient::operator==(const AsyncClient &other) { + return _pcb == other._pcb; +} + +AsyncClient &AsyncClient::operator+=(const AsyncClient &other) { + if (next == NULL) { + next = (AsyncClient *)(&other); + next->prev = this; + } else { + AsyncClient *c = next; + while (c->next != NULL) { + c = c->next; + } + c->next = (AsyncClient *)(&other); + c->next->prev = c; + } + return *this; +} + +/* + * Callback Setters + * */ + +void AsyncClient::onConnect(AcConnectHandler cb, void *arg) { + _connect_cb = cb; + _connect_cb_arg = arg; +} + +void AsyncClient::onDisconnect(AcConnectHandler cb, void *arg) { + _discard_cb = cb; + _discard_cb_arg = arg; +} + +void AsyncClient::onAck(AcAckHandler cb, void *arg) { + _sent_cb = cb; + _sent_cb_arg = arg; +} + +void AsyncClient::onError(AcErrorHandler cb, void *arg) { + _error_cb = cb; + _error_cb_arg = arg; +} + +void AsyncClient::onData(AcDataHandler cb, void *arg) { + _recv_cb = cb; + _recv_cb_arg = arg; +} + +void AsyncClient::onPacket(AcPacketHandler cb, void *arg) { + _pb_cb = cb; + _pb_cb_arg = arg; +} + +void AsyncClient::onTimeout(AcTimeoutHandler cb, void *arg) { + _timeout_cb = cb; + _timeout_cb_arg = arg; +} + +void AsyncClient::onPoll(AcConnectHandler cb, void *arg) { + _poll_cb = cb; + _poll_cb_arg = arg; +} + +/* + * Main Public Methods + * */ + +bool AsyncClient::_connect(ip_addr_t addr, uint16_t port) { + if (_pcb) { + log_d("already connected, state %d", _pcb->state); + return false; + } + if (!_start_async_task()) { + log_e("failed to start task"); + return false; + } + + if (!_allocate_closed_slot()) { + log_e("failed to allocate: closed slot full"); + return false; + } + + TCP_MUTEX_LOCK(); + tcp_pcb *pcb = tcp_new_ip_type(addr.type); + if (!pcb) { + TCP_MUTEX_UNLOCK(); + log_e("pcb == NULL"); + return false; + } + tcp_arg(pcb, this); + tcp_err(pcb, &_tcp_error); + tcp_recv(pcb, &_tcp_recv); + tcp_sent(pcb, &_tcp_sent); + tcp_poll(pcb, &_tcp_poll, CONFIG_ASYNC_TCP_POLL_TIMER); + TCP_MUTEX_UNLOCK(); + + esp_err_t err = _tcp_connect(pcb, _closed_slot, &addr, port, (tcp_connected_fn)&_tcp_connected); + return err == ESP_OK; +} + +bool AsyncClient::connect(const IPAddress &ip, uint16_t port) { + ip_addr_t addr; +#if ESP_IDF_VERSION_MAJOR < 5 + addr.u_addr.ip4.addr = ip; + addr.type = IPADDR_TYPE_V4; +#else + ip.to_ip_addr_t(&addr); +#endif + + return _connect(addr, port); +} + +#if LWIP_IPV6 && ESP_IDF_VERSION_MAJOR < 5 +bool AsyncClient::connect(const IPv6Address &ip, uint16_t port) { + auto ipaddr = static_cast(ip); + ip_addr_t addr = IPADDR6_INIT(ipaddr[0], ipaddr[1], ipaddr[2], ipaddr[3]); + + return _connect(addr, port); +} +#endif + +bool AsyncClient::connect(const char *host, uint16_t port) { + ip_addr_t addr; + + if (!_start_async_task()) { + log_e("failed to start task"); + return false; + } + + TCP_MUTEX_LOCK(); + err_t err = dns_gethostbyname(host, &addr, (dns_found_callback)&_tcp_dns_found, this); + TCP_MUTEX_UNLOCK(); + if (err == ERR_OK) { +#if ESP_IDF_VERSION_MAJOR < 5 +#if LWIP_IPV6 + if (addr.type == IPADDR_TYPE_V6) { + return connect(IPv6Address(addr.u_addr.ip6.addr), port); + } + return connect(IPAddress(addr.u_addr.ip4.addr), port); +#else + return connect(IPAddress(addr.addr), port); +#endif +#else + return _connect(addr, port); +#endif + } else if (err == ERR_INPROGRESS) { + _connect_port = port; + return true; + } + log_d("error: %d", err); + return false; +} + +void AsyncClient::close(bool now) { + if (_pcb) { + _tcp_recved(_pcb, _closed_slot, _rx_ack_len); + } + _close(); +} + +int8_t AsyncClient::abort() { + if (_pcb) { + _tcp_abort(_pcb, _closed_slot); + _pcb = NULL; + } + return ERR_ABRT; +} + +size_t AsyncClient::space() { + if ((_pcb != NULL) && (_pcb->state == ESTABLISHED)) { + return tcp_sndbuf(_pcb); + } + return 0; +} + +size_t AsyncClient::add(const char *data, size_t size, uint8_t apiflags) { + if (!_pcb || size == 0 || data == NULL) { + return 0; + } + size_t room = space(); + if (!room) { + return 0; + } + size_t will_send = (room < size) ? room : size; + int8_t err = ERR_OK; + err = _tcp_write(_pcb, _closed_slot, data, will_send, apiflags); + if (err != ERR_OK) { + return 0; + } + return will_send; +} + +bool AsyncClient::send() { + auto backup = _tx_last_packet; + _tx_last_packet = millis(); + if (_tcp_output(_pcb, _closed_slot) == ERR_OK) { + return true; + } + _tx_last_packet = backup; + return false; +} + +size_t AsyncClient::ack(size_t len) { + if (len > _rx_ack_len) { + len = _rx_ack_len; + } + if (len) { + _tcp_recved(_pcb, _closed_slot, len); + } + _rx_ack_len -= len; + return len; +} + +void AsyncClient::ackPacket(struct pbuf *pb) { + if (!pb) { + return; + } + _tcp_recved(_pcb, _closed_slot, pb->len); + pbuf_free(pb); +} + +/* + * Main Private Methods + * */ + +int8_t AsyncClient::_close() { + // ets_printf("X: 0x%08x\n", (uint32_t)this); + int8_t err = ERR_OK; + if (_pcb) { + TCP_MUTEX_LOCK(); + tcp_arg(_pcb, NULL); + tcp_sent(_pcb, NULL); + tcp_recv(_pcb, NULL); + tcp_err(_pcb, NULL); + tcp_poll(_pcb, NULL, 0); + TCP_MUTEX_UNLOCK(); + _tcp_clear_events(this); + err = _tcp_close(_pcb, _closed_slot); + if (err != ERR_OK) { + err = abort(); + } + _free_closed_slot(); + _pcb = NULL; + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + } + return err; +} + +bool AsyncClient::_allocate_closed_slot() { + bool allocated = false; + if (xSemaphoreTake(_slots_lock, portMAX_DELAY) == pdTRUE) { + uint32_t closed_slot_min_index = 0; + allocated = _closed_slot != INVALID_CLOSED_SLOT; + if (!allocated) { + for (int i = 0; i < _number_of_closed_slots; ++i) { + if ((_closed_slot == INVALID_CLOSED_SLOT || _closed_slots[i] <= closed_slot_min_index) && _closed_slots[i] != 0) { + closed_slot_min_index = _closed_slots[i]; + _closed_slot = i; + } + } + allocated = _closed_slot != INVALID_CLOSED_SLOT; + if (allocated) { + _closed_slots[_closed_slot] = 0; + } + } + xSemaphoreGive(_slots_lock); + } + return allocated; +} + +void AsyncClient::_free_closed_slot() { + xSemaphoreTake(_slots_lock, portMAX_DELAY); + if (_closed_slot != INVALID_CLOSED_SLOT) { + _closed_slots[_closed_slot] = _closed_index; + _closed_slot = INVALID_CLOSED_SLOT; + ++_closed_index; + } + xSemaphoreGive(_slots_lock); +} + +/* + * Private Callbacks + * */ + +int8_t AsyncClient::_connected(tcp_pcb *pcb, int8_t err) { + _pcb = reinterpret_cast(pcb); + if (_pcb) { + _rx_last_packet = millis(); + } + if (_connect_cb) { + _connect_cb(_connect_cb_arg, this); + } + return ERR_OK; +} + +void AsyncClient::_error(int8_t err) { + if (_pcb) { + TCP_MUTEX_LOCK(); + tcp_arg(_pcb, NULL); + if (_pcb->state == LISTEN) { + tcp_sent(_pcb, NULL); + tcp_recv(_pcb, NULL); + tcp_err(_pcb, NULL); + tcp_poll(_pcb, NULL, 0); + } + TCP_MUTEX_UNLOCK(); + _free_closed_slot(); + _pcb = NULL; + } + if (_error_cb) { + _error_cb(_error_cb_arg, this, err); + } + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } +} + +// In LwIP Thread +int8_t AsyncClient::_lwip_fin(tcp_pcb *pcb, int8_t err) { + if (!_pcb || pcb != _pcb) { + log_d("0x%08x != 0x%08x", (uint32_t)pcb, (uint32_t)_pcb); + return ERR_OK; + } + tcp_arg(_pcb, NULL); + if (_pcb->state == LISTEN) { + tcp_sent(_pcb, NULL); + tcp_recv(_pcb, NULL); + tcp_err(_pcb, NULL); + tcp_poll(_pcb, NULL, 0); + } + if (tcp_close(_pcb) != ERR_OK) { + tcp_abort(_pcb); + } + _free_closed_slot(); + _pcb = NULL; + return ERR_OK; +} + +// In Async Thread +int8_t AsyncClient::_fin(tcp_pcb *pcb, int8_t err) { + _tcp_clear_events(this); + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + return ERR_OK; +} + +int8_t AsyncClient::_sent(tcp_pcb *pcb, uint16_t len) { + _rx_last_ack = _rx_last_packet = millis(); + if (_sent_cb) { + _sent_cb(_sent_cb_arg, this, len, (_rx_last_packet - _tx_last_packet)); + } + return ERR_OK; +} + +int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { + while (pb != NULL) { + _rx_last_packet = millis(); + // we should not ack before we assimilate the data + _ack_pcb = true; + pbuf *b = pb; + pb = b->next; + b->next = NULL; + if (_pb_cb) { + _pb_cb(_pb_cb_arg, this, b); + } else { + if (_recv_cb) { + _recv_cb(_recv_cb_arg, this, b->payload, b->len); + } + if (!_ack_pcb) { + _rx_ack_len += b->len; + } else if (_pcb) { + _tcp_recved(_pcb, _closed_slot, b->len); + } + } + pbuf_free(b); + } + return ERR_OK; +} + +int8_t AsyncClient::_poll(tcp_pcb *pcb) { + if (!_pcb) { + // log_d("pcb is NULL"); + return ERR_OK; + } + if (pcb != _pcb) { + log_d("0x%08x != 0x%08x", (uint32_t)pcb, (uint32_t)_pcb); + return ERR_OK; + } + + uint32_t now = millis(); + + // ACK Timeout + if (_ack_timeout) { + const uint32_t one_day = 86400000; + bool last_tx_is_after_last_ack = (_rx_last_ack - _tx_last_packet + one_day) < one_day; + if (last_tx_is_after_last_ack && (now - _tx_last_packet) >= _ack_timeout) { + log_d("ack timeout %d", pcb->state); + if (_timeout_cb) { + _timeout_cb(_timeout_cb_arg, this, (now - _tx_last_packet)); + } + return ERR_OK; + } + } + // RX Timeout + if (_rx_timeout && (now - _rx_last_packet) >= (_rx_timeout * 1000)) { + log_d("rx timeout %d", pcb->state); + _close(); + return ERR_OK; + } + // Everything is fine + if (_poll_cb) { + _poll_cb(_poll_cb_arg, this); + } + return ERR_OK; +} + +void AsyncClient::_dns_found(struct ip_addr *ipaddr) { +#if ESP_IDF_VERSION_MAJOR < 5 + if (ipaddr && IP_IS_V4(ipaddr)) { + connect(IPAddress(ip_addr_get_ip4_u32(ipaddr)), _connect_port); +#if LWIP_IPV6 + } else if (ipaddr && ipaddr->u_addr.ip6.addr) { + connect(IPv6Address(ipaddr->u_addr.ip6.addr), _connect_port); +#endif +#else + if (ipaddr) { + IPAddress ip; + ip.from_ip_addr_t(ipaddr); + connect(ip, _connect_port); +#endif + } else { + if (_error_cb) { + _error_cb(_error_cb_arg, this, -55); + } + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + } +} + +/* + * Public Helper Methods + * */ + +bool AsyncClient::free() { + if (!_pcb) { + return true; + } + if (_pcb->state == CLOSED || _pcb->state > ESTABLISHED) { + return true; + } + return false; +} + +size_t AsyncClient::write(const char *data, size_t size, uint8_t apiflags) { + size_t will_send = add(data, size, apiflags); + if (!will_send || !send()) { + return 0; + } + return will_send; +} + +void AsyncClient::setRxTimeout(uint32_t timeout) { + _rx_timeout = timeout; +} + +uint32_t AsyncClient::getRxTimeout() { + return _rx_timeout; +} + +uint32_t AsyncClient::getAckTimeout() { + return _ack_timeout; +} + +void AsyncClient::setAckTimeout(uint32_t timeout) { + _ack_timeout = timeout; +} + +void AsyncClient::setNoDelay(bool nodelay) { + if (!_pcb) { + return; + } + if (nodelay) { + tcp_nagle_disable(_pcb); + } else { + tcp_nagle_enable(_pcb); + } +} + +bool AsyncClient::getNoDelay() { + if (!_pcb) { + return false; + } + return tcp_nagle_disabled(_pcb); +} + +void AsyncClient::setKeepAlive(uint32_t ms, uint8_t cnt) { + if (ms != 0) { + _pcb->so_options |= SOF_KEEPALIVE; // Turn on TCP Keepalive for the given pcb + // Set the time between keepalive messages in milli-seconds + _pcb->keep_idle = ms; + _pcb->keep_intvl = ms; + _pcb->keep_cnt = cnt; // The number of unanswered probes required to force closure of the socket + } else { + _pcb->so_options &= ~SOF_KEEPALIVE; // Turn off TCP Keepalive for the given pcb + } +} + +uint16_t AsyncClient::getMss() { + if (!_pcb) { + return 0; + } + return tcp_mss(_pcb); +} + +uint32_t AsyncClient::getRemoteAddress() { + if (!_pcb) { + return 0; + } +#if LWIP_IPV4 && LWIP_IPV6 + return _pcb->remote_ip.u_addr.ip4.addr; +#else + return _pcb->remote_ip.addr; +#endif +} + +#if LWIP_IPV6 +ip6_addr_t AsyncClient::getRemoteAddress6() { + if (!_pcb) { + ip6_addr_t nulladdr; + ip6_addr_set_zero(&nulladdr); + return nulladdr; + } + return _pcb->remote_ip.u_addr.ip6; +} + +ip6_addr_t AsyncClient::getLocalAddress6() { + if (!_pcb) { + ip6_addr_t nulladdr; + ip6_addr_set_zero(&nulladdr); + return nulladdr; + } + return _pcb->local_ip.u_addr.ip6; +} +#if ESP_IDF_VERSION_MAJOR < 5 +IPv6Address AsyncClient::remoteIP6() { + return IPv6Address(getRemoteAddress6().addr); +} + +IPv6Address AsyncClient::localIP6() { + return IPv6Address(getLocalAddress6().addr); +} +#else +IPAddress AsyncClient::remoteIP6() { + if (!_pcb) { + return IPAddress(IPType::IPv6); + } + IPAddress ip; + ip.from_ip_addr_t(&(_pcb->remote_ip)); + return ip; +} + +IPAddress AsyncClient::localIP6() { + if (!_pcb) { + return IPAddress(IPType::IPv6); + } + IPAddress ip; + ip.from_ip_addr_t(&(_pcb->local_ip)); + return ip; +} +#endif +#endif + +uint16_t AsyncClient::getRemotePort() { + if (!_pcb) { + return 0; + } + return _pcb->remote_port; +} + +uint32_t AsyncClient::getLocalAddress() { + if (!_pcb) { + return 0; + } +#if LWIP_IPV4 && LWIP_IPV6 + return _pcb->local_ip.u_addr.ip4.addr; +#else + return _pcb->local_ip.addr; +#endif +} + +uint16_t AsyncClient::getLocalPort() { + if (!_pcb) { + return 0; + } + return _pcb->local_port; +} + +IPAddress AsyncClient::remoteIP() { +#if ESP_IDF_VERSION_MAJOR < 5 + return IPAddress(getRemoteAddress()); +#else + if (!_pcb) { + return IPAddress(); + } + IPAddress ip; + ip.from_ip_addr_t(&(_pcb->remote_ip)); + return ip; +#endif +} + +uint16_t AsyncClient::remotePort() { + return getRemotePort(); +} + +IPAddress AsyncClient::localIP() { +#if ESP_IDF_VERSION_MAJOR < 5 + return IPAddress(getLocalAddress()); +#else + if (!_pcb) { + return IPAddress(); + } + IPAddress ip; + ip.from_ip_addr_t(&(_pcb->local_ip)); + return ip; +#endif +} + +uint16_t AsyncClient::localPort() { + return getLocalPort(); +} + +uint8_t AsyncClient::state() { + if (!_pcb) { + return 0; + } + return _pcb->state; +} + +bool AsyncClient::connected() { + if (!_pcb) { + return false; + } + return _pcb->state == ESTABLISHED; +} + +bool AsyncClient::connecting() { + if (!_pcb) { + return false; + } + return _pcb->state > CLOSED && _pcb->state < ESTABLISHED; +} + +bool AsyncClient::disconnecting() { + if (!_pcb) { + return false; + } + return _pcb->state > ESTABLISHED && _pcb->state < TIME_WAIT; +} + +bool AsyncClient::disconnected() { + if (!_pcb) { + return true; + } + return _pcb->state == CLOSED || _pcb->state == TIME_WAIT; +} + +bool AsyncClient::freeable() { + if (!_pcb) { + return true; + } + return _pcb->state == CLOSED || _pcb->state > ESTABLISHED; +} + +bool AsyncClient::canSend() { + return space() > 0; +} + +const char *AsyncClient::errorToString(int8_t error) { + switch (error) { + case ERR_OK: return "OK"; + case ERR_MEM: return "Out of memory error"; + case ERR_BUF: return "Buffer error"; + case ERR_TIMEOUT: return "Timeout"; + case ERR_RTE: return "Routing problem"; + case ERR_INPROGRESS: return "Operation in progress"; + case ERR_VAL: return "Illegal value"; + case ERR_WOULDBLOCK: return "Operation would block"; + case ERR_USE: return "Address in use"; + case ERR_ALREADY: return "Already connected"; + case ERR_CONN: return "Not connected"; + case ERR_IF: return "Low-level netif error"; + case ERR_ABRT: return "Connection aborted"; + case ERR_RST: return "Connection reset"; + case ERR_CLSD: return "Connection closed"; + case ERR_ARG: return "Illegal argument"; + case -55: return "DNS failed"; + default: return "UNKNOWN"; + } +} + +const char *AsyncClient::stateToString() { + switch (state()) { + case 0: return "Closed"; + case 1: return "Listen"; + case 2: return "SYN Sent"; + case 3: return "SYN Received"; + case 4: return "Established"; + case 5: return "FIN Wait 1"; + case 6: return "FIN Wait 2"; + case 7: return "Close Wait"; + case 8: return "Closing"; + case 9: return "Last ACK"; + case 10: return "Time Wait"; + default: return "UNKNOWN"; + } +} + +/* + * Static Callbacks (LwIP C2C++ interconnect) + * */ + +void AsyncClient::_s_dns_found(const char *name, struct ip_addr *ipaddr, void *arg) { + reinterpret_cast(arg)->_dns_found(ipaddr); +} + +int8_t AsyncClient::_s_poll(void *arg, struct tcp_pcb *pcb) { + return reinterpret_cast(arg)->_poll(pcb); +} + +int8_t AsyncClient::_s_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, int8_t err) { + return reinterpret_cast(arg)->_recv(pcb, pb, err); +} + +int8_t AsyncClient::_s_fin(void *arg, struct tcp_pcb *pcb, int8_t err) { + return reinterpret_cast(arg)->_fin(pcb, err); +} + +int8_t AsyncClient::_s_lwip_fin(void *arg, struct tcp_pcb *pcb, int8_t err) { + return reinterpret_cast(arg)->_lwip_fin(pcb, err); +} + +int8_t AsyncClient::_s_sent(void *arg, struct tcp_pcb *pcb, uint16_t len) { + return reinterpret_cast(arg)->_sent(pcb, len); +} + +void AsyncClient::_s_error(void *arg, int8_t err) { + reinterpret_cast(arg)->_error(err); +} + +int8_t AsyncClient::_s_connected(void *arg, struct tcp_pcb *pcb, int8_t err) { + return reinterpret_cast(arg)->_connected(pcb, err); +} + +/* + Async TCP Server + */ + +AsyncServer::AsyncServer(IPAddress addr, uint16_t port) + : _port(port) +#if ESP_IDF_VERSION_MAJOR < 5 + , + _bind4(true), _bind6(false) +#else + , + _bind4(addr.type() != IPType::IPv6), _bind6(addr.type() == IPType::IPv6) +#endif + , + _addr(addr), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) { +} + +#if ESP_IDF_VERSION_MAJOR < 5 +AsyncServer::AsyncServer(IPv6Address addr, uint16_t port) + : _port(port), _bind4(false), _bind6(true), _addr6(addr), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) {} +#endif + +AsyncServer::AsyncServer(uint16_t port) + : _port(port), _bind4(true), _bind6(false), _addr((uint32_t)IPADDR_ANY) +#if ESP_IDF_VERSION_MAJOR < 5 + , + _addr6() +#endif + , + _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) { +} + +AsyncServer::~AsyncServer() { + end(); +} + +void AsyncServer::onClient(AcConnectHandler cb, void *arg) { + _connect_cb = cb; + _connect_cb_arg = arg; +} + +void AsyncServer::begin() { + if (_pcb) { + return; + } + + if (!_start_async_task()) { + log_e("failed to start task"); + return; + } + int8_t err; + TCP_MUTEX_LOCK(); + _pcb = tcp_new_ip_type(_bind4 && _bind6 ? IPADDR_TYPE_ANY : (_bind6 ? IPADDR_TYPE_V6 : IPADDR_TYPE_V4)); + TCP_MUTEX_UNLOCK(); + if (!_pcb) { + log_e("_pcb == NULL"); + return; + } + + ip_addr_t local_addr; +#if ESP_IDF_VERSION_MAJOR < 5 + if (_bind6) { // _bind6 && _bind4 both at the same time is not supported on Arduino 2 in this lib API + local_addr.type = IPADDR_TYPE_V6; + memcpy(local_addr.u_addr.ip6.addr, static_cast(_addr6), sizeof(uint32_t) * 4); + } else { + local_addr.type = IPADDR_TYPE_V4; + local_addr.u_addr.ip4.addr = _addr; + } +#else + _addr.to_ip_addr_t(&local_addr); +#endif + err = _tcp_bind(_pcb, &local_addr, _port); + + if (err != ERR_OK) { + _tcp_close(_pcb, -1); + _pcb = NULL; + log_e("bind error: %d", err); + return; + } + + static uint8_t backlog = 5; + _pcb = _tcp_listen_with_backlog(_pcb, backlog); + if (!_pcb) { + log_e("listen_pcb == NULL"); + return; + } + TCP_MUTEX_LOCK(); + tcp_arg(_pcb, (void *)this); + tcp_accept(_pcb, &_s_accept); + TCP_MUTEX_UNLOCK(); +} + +void AsyncServer::end() { + if (_pcb) { + TCP_MUTEX_LOCK(); + tcp_arg(_pcb, NULL); + tcp_accept(_pcb, NULL); + if (tcp_close(_pcb) != ERR_OK) { + TCP_MUTEX_UNLOCK(); + _tcp_abort(_pcb, -1); + } else { + TCP_MUTEX_UNLOCK(); + } + _pcb = NULL; + } +} + +// runs on LwIP thread +int8_t AsyncServer::_accept(tcp_pcb *pcb, int8_t err) { + // ets_printf("+A: 0x%08x\n", pcb); + if (_connect_cb) { + AsyncClient *c = new (std::nothrow) AsyncClient(pcb); + if (c) { + c->setNoDelay(_noDelay); + const int8_t err = _tcp_accept(this, c); + if (err != ERR_OK) { + tcp_abort(pcb); + delete c; + } + return err; + } + } + tcp_abort(pcb); + log_d("_accept failed"); + return ERR_OK; +} + +int8_t AsyncServer::_accepted(AsyncClient *client) { + if (_connect_cb) { + _connect_cb(_connect_cb_arg, client); + } + return ERR_OK; +} + +void AsyncServer::setNoDelay(bool nodelay) { + _noDelay = nodelay; +} + +bool AsyncServer::getNoDelay() { + return _noDelay; +} + +uint8_t AsyncServer::status() { + if (!_pcb) { + return 0; + } + return _pcb->state; +} + +int8_t AsyncServer::_s_accept(void *arg, tcp_pcb *pcb, int8_t err) { + return reinterpret_cast(arg)->_accept(pcb, err); +} + +int8_t AsyncServer::_s_accepted(void *arg, AsyncClient *client) { + return reinterpret_cast(arg)->_accepted(client); +} diff --git a/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCP.h b/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCP.h new file mode 100644 index 00000000..389692fd --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCP.h @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef ASYNCTCP_H_ +#define ASYNCTCP_H_ + +#include "AsyncTCPVersion.h" +#define ASYNCTCP_FORK_ESP32Async + +#include "IPAddress.h" +#if ESP_IDF_VERSION_MAJOR < 5 +#include "IPv6Address.h" +#endif +#include "lwip/ip6_addr.h" +#include "lwip/ip_addr.h" +#include + +#ifndef LIBRETINY +#include "sdkconfig.h" +extern "C" { +#include "freertos/semphr.h" +#include "lwip/pbuf.h" +} +#else +extern "C" { +#include +#include +} +#define CONFIG_ASYNC_TCP_RUNNING_CORE -1 // any available core +#endif + +// 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 -1 // any available core +#endif + +// guard AsyncTCP task with watchdog +#ifndef CONFIG_ASYNC_TCP_USE_WDT +#define CONFIG_ASYNC_TCP_USE_WDT 1 +#endif + +#ifndef CONFIG_ASYNC_TCP_STACK_SIZE +#define CONFIG_ASYNC_TCP_STACK_SIZE 8192 * 2 +#endif + +#ifndef CONFIG_ASYNC_TCP_PRIORITY +#define CONFIG_ASYNC_TCP_PRIORITY 10 +#endif + +#ifndef CONFIG_ASYNC_TCP_QUEUE_SIZE +#define CONFIG_ASYNC_TCP_QUEUE_SIZE 64 +#endif + +#ifndef CONFIG_ASYNC_TCP_MAX_ACK_TIME +#define CONFIG_ASYNC_TCP_MAX_ACK_TIME 5000 +#endif + +class AsyncClient; + +#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. + +typedef std::function AcConnectHandler; +typedef std::function AcAckHandler; +typedef std::function AcErrorHandler; +typedef std::function AcDataHandler; +typedef std::function AcPacketHandler; +typedef std::function AcTimeoutHandler; + +struct tcp_pcb; +struct ip_addr; + +class AsyncClient { +public: + AsyncClient(tcp_pcb *pcb = 0); + ~AsyncClient(); + + AsyncClient &operator=(const AsyncClient &other); + AsyncClient &operator+=(const AsyncClient &other); + + bool operator==(const AsyncClient &other); + + bool operator!=(const AsyncClient &other) { + return !(*this == other); + } + bool connect(const IPAddress &ip, uint16_t port); +#if ESP_IDF_VERSION_MAJOR < 5 + bool connect(const IPv6Address &ip, uint16_t port); +#endif + bool connect(const char *host, uint16_t port); + /** + * @brief close connection + * + * @param now - ignored + */ + void close(bool now = false); + // same as close() + void stop() { + close(false); + }; + int8_t abort(); + bool free(); + + // ack is not pending + bool canSend(); + // TCP buffer space available + size_t space(); + + /** + * @brief add data to be send (but do not send yet) + * @note add() would call lwip's tcp_write() + By default apiflags=ASYNC_WRITE_FLAG_COPY + You could try to use 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 at least + 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 + * + * @param data + * @param size + * @param apiflags + * @return size_t amount of data that has been copied + */ + size_t add(const char *data, size_t size, uint8_t apiflags = ASYNC_WRITE_FLAG_COPY); + + /** + * @brief send data previously add()'ed + * + * @return true on success + * @return false on error + */ + bool send(); + + /** + * @brief add and enqueue data for sending + * @note it is same as add() + send() + * @note only make sense when canSend() == true + * + * @param data + * @param size + * @param apiflags + * @return size_t + */ + size_t write(const char *data, size_t size, uint8_t apiflags = ASYNC_WRITE_FLAG_COPY); + + /** + * @brief add and enqueue data for sending + * @note treats data as null-terminated string + * + * @param data + * @return size_t + */ + size_t write(const char *data) { + return data == NULL ? 0 : write(data, strlen(data)); + }; + + uint8_t state(); + bool connecting(); + bool connected(); + bool disconnecting(); + bool disconnected(); + + // disconnected or disconnecting + bool freeable(); + + uint16_t getMss(); + + uint32_t getRxTimeout(); + // no RX data timeout for the connection in seconds + void setRxTimeout(uint32_t timeout); + + uint32_t getAckTimeout(); + // no ACK timeout for the last sent packet in milliseconds + void setAckTimeout(uint32_t timeout); + + void setNoDelay(bool nodelay); + bool getNoDelay(); + + void setKeepAlive(uint32_t ms, uint8_t cnt); + + uint32_t getRemoteAddress(); + uint16_t getRemotePort(); + uint32_t getLocalAddress(); + uint16_t getLocalPort(); +#if LWIP_IPV6 + ip6_addr_t getRemoteAddress6(); + ip6_addr_t getLocalAddress6(); +#if ESP_IDF_VERSION_MAJOR < 5 + IPv6Address remoteIP6(); + IPv6Address localIP6(); +#else + IPAddress remoteIP6(); + IPAddress localIP6(); +#endif +#endif + + // compatibility + IPAddress remoteIP(); + uint16_t remotePort(); + IPAddress localIP(); + uint16_t localPort(); + + // set callback - on successful connect + void onConnect(AcConnectHandler cb, void *arg = 0); + // set callback - disconnected + void onDisconnect(AcConnectHandler cb, void *arg = 0); + // set callback - ack received + void onAck(AcAckHandler cb, void *arg = 0); + // set callback - unsuccessful connect or error + void onError(AcErrorHandler cb, void *arg = 0); + // set callback - data received (called if onPacket is not used) + void onData(AcDataHandler cb, void *arg = 0); + // set callback - data received + // !!! You MUST call ackPacket() or free the pbuf yourself to prevent memory leaks + void onPacket(AcPacketHandler cb, void *arg = 0); + // set callback - ack timeout + void onTimeout(AcTimeoutHandler cb, void *arg = 0); + // set callback - every 125ms when connected + void onPoll(AcConnectHandler cb, void *arg = 0); + + // ack pbuf from onPacket + void ackPacket(struct pbuf *pb); + // ack data that you have not acked using the method below + size_t ack(size_t len); + // will not ack the current packet. Call from onData + void ackLater() { + _ack_pcb = false; + } + + static const char *errorToString(int8_t error); + const char *stateToString(); + + // internal callbacks - Do NOT call any of the functions below in user code! + static int8_t _s_poll(void *arg, struct tcp_pcb *tpcb); + static int8_t _s_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *pb, int8_t err); + static int8_t _s_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); + static int8_t _s_lwip_fin(void *arg, struct tcp_pcb *tpcb, int8_t err); + static void _s_error(void *arg, int8_t err); + static int8_t _s_sent(void *arg, struct tcp_pcb *tpcb, uint16_t len); + static int8_t _s_connected(void *arg, struct tcp_pcb *tpcb, int8_t err); + static void _s_dns_found(const char *name, struct ip_addr *ipaddr, void *arg); + + int8_t _recv(tcp_pcb *pcb, pbuf *pb, int8_t err); + tcp_pcb *pcb() { + return _pcb; + } + +protected: + bool _connect(ip_addr_t addr, uint16_t port); + + tcp_pcb *_pcb; + int8_t _closed_slot; + + 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; + AcPacketHandler _pb_cb; + void *_pb_cb_arg; + AcTimeoutHandler _timeout_cb; + void *_timeout_cb_arg; + AcConnectHandler _poll_cb; + void *_poll_cb_arg; + + bool _ack_pcb; + uint32_t _tx_last_packet; + uint32_t _rx_ack_len; + uint32_t _rx_last_packet; + uint32_t _rx_timeout; + uint32_t _rx_last_ack; + uint32_t _ack_timeout; + uint16_t _connect_port; + + int8_t _close(); + void _free_closed_slot(); + bool _allocate_closed_slot(); + int8_t _connected(tcp_pcb *pcb, int8_t err); + void _error(int8_t err); + int8_t _poll(tcp_pcb *pcb); + int8_t _sent(tcp_pcb *pcb, uint16_t len); + int8_t _fin(tcp_pcb *pcb, int8_t err); + int8_t _lwip_fin(tcp_pcb *pcb, int8_t err); + void _dns_found(struct ip_addr *ipaddr); + +public: + AsyncClient *prev; + AsyncClient *next; +}; + +class AsyncServer { +public: + AsyncServer(IPAddress addr, uint16_t port); +#if ESP_IDF_VERSION_MAJOR < 5 + AsyncServer(IPv6Address addr, uint16_t port); +#endif + AsyncServer(uint16_t port); + ~AsyncServer(); + void onClient(AcConnectHandler cb, void *arg); + void begin(); + void end(); + void setNoDelay(bool nodelay); + bool getNoDelay(); + uint8_t status(); + + // Do not use any of the functions below! + static int8_t _s_accept(void *arg, tcp_pcb *newpcb, int8_t err); + static int8_t _s_accepted(void *arg, AsyncClient *client); + +protected: + uint16_t _port; + bool _bind4 = false; + bool _bind6 = false; + IPAddress _addr; +#if ESP_IDF_VERSION_MAJOR < 5 + IPv6Address _addr6; +#endif + bool _noDelay; + tcp_pcb *_pcb; + AcConnectHandler _connect_cb; + void *_connect_cb_arg; + + int8_t _accept(tcp_pcb *newpcb, int8_t err); + int8_t _accepted(AsyncClient *client); +}; + +#endif /* ASYNCTCP_H_ */ diff --git a/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCPVersion.h b/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCPVersion.h new file mode 100644 index 00000000..f2c4c377 --- /dev/null +++ b/Software/src/lib/ESP32Async-AsyncTCP/src/AsyncTCPVersion.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** Major version number (X.x.x) */ +#define ASYNCTCP_VERSION_MAJOR 3 +/** Minor version number (x.X.x) */ +#define ASYNCTCP_VERSION_MINOR 3 +/** Patch version number (x.x.X) */ +#define ASYNCTCP_VERSION_PATCH 5 + +/** + * Macro to convert version number into an integer + * + * To be used in comparisons, such as ASYNCTCP_VERSION >= ASYNCTCP_VERSION_VAL(2, 0, 0) + */ +#define ASYNCTCP_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch)) + +/** + * Current version, as an integer + * + * To be used in comparisons, such as ASYNCTCP_VERSION_NUM >= ASYNCTCP_VERSION_VAL(2, 0, 0) + */ +#define ASYNCTCP_VERSION_NUM ASYNCTCP_VERSION_VAL(ASYNCTCP_VERSION_MAJOR, ASYNCTCP_VERSION_MINOR, ASYNCTCP_VERSION_PATCH) + +/** + * Current version, as string + */ +#define df2xstr(s) #s +#define df2str(s) df2xstr(s) +#define ASYNCTCP_VERSION df2str(ASYNCTCP_VERSION_MAJOR) "." df2str(ASYNCTCP_VERSION_MINOR) "." df2str(ASYNCTCP_VERSION_PATCH) + +#ifdef __cplusplus +} +#endif diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h index 7bc9cb9f..f7ee21fa 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h @@ -23,7 +23,7 @@ #include #ifdef ESP32 - #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" + #include "../../ESP32Async-AsyncTCP/src/AsyncTCP.h" #include #ifndef SSE_MAX_QUEUED_MESSAGES #define SSE_MAX_QUEUED_MESSAGES 32 diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h index acf9d82a..eccb60b1 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h @@ -23,7 +23,7 @@ #include #ifdef ESP32 - #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" + #include "../../ESP32Async-AsyncTCP/src/AsyncTCP.h" #include #ifndef WS_MAX_QUEUED_MESSAGES #define WS_MAX_QUEUED_MESSAGES 32 diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h index f36483dc..8296a09a 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h @@ -32,7 +32,7 @@ #include #ifdef ESP32 - #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" + #include "../../ESP32Async-AsyncTCP/src/AsyncTCP.h" #include #elif defined(ESP8266) #include diff --git a/Software/src/lib/ayushsharma82-ElegantOTA/library.json b/Software/src/lib/ayushsharma82-ElegantOTA/library.json index 75cf5619..8d105f50 100644 --- a/Software/src/lib/ayushsharma82-ElegantOTA/library.json +++ b/Software/src/lib/ayushsharma82-ElegantOTA/library.json @@ -19,11 +19,11 @@ { "owner": "mathieucarbou", "name": "ESPAsyncWebServer", - "version": "^3.1.1", + "version": "^3.3.11", "platforms": ["espressif8266", "espressif32"] } ], - "version": "3.1.5", + "version": "3.1.6", "frameworks": "arduino", "platforms": ["espressif8266", "espressif32", "raspberrypi"], "build": { diff --git a/Software/src/lib/ayushsharma82-ElegantOTA/library.properties b/Software/src/lib/ayushsharma82-ElegantOTA/library.properties index 631251b2..7185f57a 100644 --- a/Software/src/lib/ayushsharma82-ElegantOTA/library.properties +++ b/Software/src/lib/ayushsharma82-ElegantOTA/library.properties @@ -1,5 +1,5 @@ name=ElegantOTA -version=3.1.5 +version=3.1.6 author=Ayush Sharma category=Communication maintainer=Ayush Sharma diff --git a/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.cpp b/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.cpp index ec1b2a37..acf22d70 100644 --- a/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.cpp +++ b/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.cpp @@ -43,9 +43,6 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username, return request->requestAuthentication(); } - // Pre-OTA update callback - if (preUpdateCallback != NULL) preUpdateCallback(); - // Get header x-ota-mode value, if present OTA_Mode mode = OTA_MODE_FIRMWARE; // Get mode from arg @@ -76,7 +73,7 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username, #endif // Pre-OTA update callback - //if (preUpdateCallback != NULL) preUpdateCallback(); + if (preUpdateCallback != NULL) preUpdateCallback(); // Start update process #if defined(ESP8266) @@ -179,9 +176,9 @@ void ElegantOTAClass::begin(ELEGANTOTA_WEBSERVER *server, const char * username, update_size = ((size_t)&_FS_end - (size_t)&_FS_start); LittleFS.end(); } else { - FSInfo64 i; + FSInfo i; LittleFS.begin(); - LittleFS.info64(i); + LittleFS.info(i); update_size = i.totalBytes - i.usedBytes; } // Start update process diff --git a/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h b/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h index 566aa7d9..aa9e654a 100644 --- a/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h +++ b/Software/src/lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h @@ -64,7 +64,7 @@ _____ _ _ ___ _____ _ #include "Update.h" #include "StreamString.h" #if ELEGANTOTA_USE_ASYNC_WEBSERVER == 1 - #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" + #include "../../ESP32Async-AsyncTCP/src/AsyncTCP.h" #include "../../ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h" #define ELEGANTOTA_WEBSERVER AsyncWebServer #else diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/library.json b/Software/src/lib/mathieucarbou-AsyncTCPSock/library.json deleted file mode 100644 index 5d2b5ed1..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/library.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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 - } -} diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/library.properties b/Software/src/lib/mathieucarbou-AsyncTCPSock/library.properties deleted file mode 100644 index 5c12940e..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/library.properties +++ /dev/null @@ -1,9 +0,0 @@ -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=* diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.cpp b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.cpp deleted file mode 100644 index e13fa764..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.cpp +++ /dev/null @@ -1,1301 +0,0 @@ -/* - 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 -*/ - -#include "Arduino.h" - -#include "AsyncTCP.h" -#include "esp_task_wdt.h" - -#include -#include -#include - -#include - -#include - -#undef close -#undef connect -#undef write -#undef read - -static TaskHandle_t _asyncsock_service_task_handle = NULL; -static SemaphoreHandle_t _asyncsock_mutex = NULL; - -typedef std::list::iterator sockIterator; - -void _asynctcpsock_task(void *); - -#define ASYNCTCPSOCK_POLL_INTERVAL 125 - -#define MAX_PAYLOAD_SIZE 1360 - -// Since the only task reading from these sockets is the asyncTcpPSock task -// and all socket clients are serviced sequentially, only one read buffer -// is needed, and it can therefore be statically allocated -static uint8_t _readBuffer[MAX_PAYLOAD_SIZE]; - -// Start async socket task -static bool _start_asyncsock_task(void) -{ - if (!_asyncsock_service_task_handle) { - log_i("Creating asyncTcpSock task running in core %d (-1 for any available core)...", CONFIG_ASYNC_TCP_RUNNING_CORE); - xTaskCreateUniversal( - _asynctcpsock_task, - CONFIG_ASYNC_TCP_TASK_NAME, - CONFIG_ASYNC_TCP_STACK, - NULL, - CONFIG_ASYNC_TCP_TASK_PRIORITY, - &_asyncsock_service_task_handle, - CONFIG_ASYNC_TCP_RUNNING_CORE); - if (!_asyncsock_service_task_handle) return false; - } - return true; -} - -// Actual asynchronous socket task -void _asynctcpsock_task(void *) -{ - auto & _socketBaseList = AsyncSocketBase::_getSocketBaseList(); - - while (true) { - sockIterator it; - fd_set sockSet_r; - fd_set sockSet_w; - int max_sock = 0; - - std::list sockList; - - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - - // Collect all of the active sockets into socket set. Half-destroyed - // connections should have set _socket to -1 and therefore should not - // end up in the sockList. - FD_ZERO(&sockSet_r); FD_ZERO(&sockSet_w); - for (it = _socketBaseList.begin(); it != _socketBaseList.end(); it++) { - if ((*it)->_socket != -1) { -#ifdef CONFIG_LWIP_MAX_SOCKETS - if (!(*it)->_isServer() || _socketBaseList.size() < CONFIG_LWIP_MAX_SOCKETS) { -#endif - FD_SET((*it)->_socket, &sockSet_r); - if (max_sock <= (*it)->_socket) max_sock = (*it)->_socket + 1; -#ifdef CONFIG_LWIP_MAX_SOCKETS - } -#endif - if ((*it)->_pendingWrite()) { - FD_SET((*it)->_socket, &sockSet_w); - if (max_sock <= (*it)->_socket) max_sock = (*it)->_socket + 1; - } - (*it)->_selected = true; - } - } - - // Wait for activity on all monitored sockets - struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = ASYNCTCPSOCK_POLL_INTERVAL * 1000; - - xSemaphoreGiveRecursive(_asyncsock_mutex); - - int r = select(max_sock, &sockSet_r, &sockSet_w, NULL, &tv); - - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - - // Check all sockets to see which ones are active - uint32_t nActive = 0; - if (r > 0) { - // Collect and notify all writable sockets. Half-destroyed connections - // should have set _socket to -1 and therefore should not end up in - // the sockList. - for (it = _socketBaseList.begin(); it != _socketBaseList.end(); it++) { - if ((*it)->_selected && FD_ISSET((*it)->_socket, &sockSet_w)) { - sockList.push_back(*it); - } - } - for (it = sockList.begin(); it != sockList.end(); it++) { -#if CONFIG_ASYNC_TCP_USE_WDT - if (esp_task_wdt_add(NULL) != ESP_OK) { - log_e("Failed to add async task to WDT"); - } -#endif - if ((*it)->_sockIsWriteable()) { - (*it)->_sock_lastactivity = millis(); - nActive++; - } -#if CONFIG_ASYNC_TCP_USE_WDT - if (esp_task_wdt_delete(NULL) != ESP_OK) { - log_e("Failed to remove loop task from WDT"); - } -#endif - } - sockList.clear(); - - // Collect and notify all readable sockets. Half-destroyed connections - // should have set _socket to -1 and therefore should not end up in - // the sockList. - for (it = _socketBaseList.begin(); it != _socketBaseList.end(); it++) { - if ((*it)->_selected && FD_ISSET((*it)->_socket, &sockSet_r)) { - sockList.push_back(*it); - } - } - for (it = sockList.begin(); it != sockList.end(); it++) { -#if CONFIG_ASYNC_TCP_USE_WDT - if (esp_task_wdt_add(NULL) != ESP_OK) { - log_e("Failed to add async task to WDT"); - } -#endif - (*it)->_sock_lastactivity = millis(); - (*it)->_sockIsReadable(); - nActive++; -#if CONFIG_ASYNC_TCP_USE_WDT - if (esp_task_wdt_delete(NULL) != ESP_OK) { - log_e("Failed to remove loop task from WDT"); - } -#endif - } - sockList.clear(); - } - - // Collect and notify all sockets waiting for DNS completion - for (it = _socketBaseList.begin(); it != _socketBaseList.end(); it++) { - // Collect socket that has finished resolving DNS (with or without error) - if ((*it)->_isdnsfinished) { - sockList.push_back(*it); - } - } - for (it = sockList.begin(); it != sockList.end(); it++) { -#if CONFIG_ASYNC_TCP_USE_WDT - if(esp_task_wdt_add(NULL) != ESP_OK){ - log_e("Failed to add async task to WDT"); - } -#endif - (*it)->_isdnsfinished = false; - (*it)->_sockDelayedConnect(); -#if CONFIG_ASYNC_TCP_USE_WDT - if(esp_task_wdt_delete(NULL) != ESP_OK){ - log_e("Failed to remove loop task from WDT"); - } -#endif - } - sockList.clear(); - - xSemaphoreGiveRecursive(_asyncsock_mutex); - - // Collect and run activity poll on all pollable sockets - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - for (it = _socketBaseList.begin(); it != _socketBaseList.end(); it++) { - (*it)->_selected = false; - if (millis() - (*it)->_sock_lastactivity >= ASYNCTCPSOCK_POLL_INTERVAL) { - (*it)->_sock_lastactivity = millis(); - sockList.push_back(*it); - } - } - - // Run activity poll on all pollable sockets - for (it = sockList.begin(); it != sockList.end(); it++) { -#if CONFIG_ASYNC_TCP_USE_WDT - if(esp_task_wdt_add(NULL) != ESP_OK){ - log_e("Failed to add async task to WDT"); - } -#endif - (*it)->_sockPoll(); -#if CONFIG_ASYNC_TCP_USE_WDT - if(esp_task_wdt_delete(NULL) != ESP_OK){ - log_e("Failed to remove loop task from WDT"); - } -#endif - } - sockList.clear(); - - xSemaphoreGiveRecursive(_asyncsock_mutex); - } - - vTaskDelete(NULL); - _asyncsock_service_task_handle = NULL; -} - -AsyncSocketBase::AsyncSocketBase() -{ - if (_asyncsock_mutex == NULL) _asyncsock_mutex = xSemaphoreCreateRecursiveMutex(); - - _sock_lastactivity = millis(); - _selected = false; - - // Add this base socket to the monitored list - auto & _socketBaseList = AsyncSocketBase::_getSocketBaseList(); - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - _socketBaseList.push_back(this); - xSemaphoreGiveRecursive(_asyncsock_mutex); -} - -std::list & AsyncSocketBase::_getSocketBaseList(void) -{ - // List of monitored socket objects - static std::list _socketBaseList; - return _socketBaseList; -} - -AsyncSocketBase::~AsyncSocketBase() -{ - // Remove this base socket from the monitored list - auto & _socketBaseList = AsyncSocketBase::_getSocketBaseList(); - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - _socketBaseList.remove(this); - xSemaphoreGiveRecursive(_asyncsock_mutex); -} - - -AsyncClient::AsyncClient(int sockfd) -: _connect_cb(0) -, _connect_cb_arg(0) -, _discard_cb(0) -, _discard_cb_arg(0) -, _sent_cb(0) -, _sent_cb_arg(0) -, _error_cb(0) -, _error_cb_arg(0) -, _recv_cb(0) -, _recv_cb_arg(0) -, _timeout_cb(0) -, _timeout_cb_arg(0) -, _rx_last_packet(0) -, _rx_since_timeout(0) -, _ack_timeout(ASYNC_MAX_ACK_TIME) -, _connect_port(0) -#if ASYNC_TCP_SSL_ENABLED -, _root_ca_len(0) -, _root_ca(NULL) -, _cli_cert_len(0) -, _cli_cert(NULL) -, _cli_key_len(0) -, _cli_key(NULL) -, _secure(false) -, _handshake_done(true) -, _psk_ident(0) -, _psk(0) -, _sslctx(NULL) -#endif // ASYNC_TCP_SSL_ENABLED -, _writeSpaceRemaining(TCP_SND_BUF) -, _conn_state(0) -{ - _write_mutex = xSemaphoreCreateMutex(); - if (sockfd != -1) { - fcntl( sockfd, F_SETFL, fcntl( sockfd, F_GETFL, 0 ) | O_NONBLOCK ); - - // Updating state visible to asyncTcpSock task - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - _conn_state = 4; - _socket = sockfd; - _rx_last_packet = millis(); - xSemaphoreGiveRecursive(_asyncsock_mutex); - } -} - -AsyncClient::~AsyncClient() -{ - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - if (_socket != -1) _close(); - _removeAllCallbacks(); - vSemaphoreDelete(_write_mutex); - _write_mutex = NULL; - xSemaphoreGiveRecursive(_asyncsock_mutex); -} - -void AsyncClient::setRxTimeout(uint32_t timeout){ - _rx_since_timeout = timeout; -} - -uint32_t AsyncClient::getRxTimeout(){ - return _rx_since_timeout; -} - -uint32_t AsyncClient::getAckTimeout(){ - return _ack_timeout; -} - -void AsyncClient::setAckTimeout(uint32_t timeout){ - _ack_timeout = timeout; -} - -void AsyncClient::setNoDelay(bool nodelay){ - if (_socket == -1) return; - - int flag = nodelay; - int res = setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(int)); - if(res < 0) { - log_e("fail on fd %d, errno: %d, \"%s\"", _socket, errno, strerror(errno)); - } -} - -bool AsyncClient::getNoDelay(){ - if (_socket == -1) return false; - - int flag = 0; - socklen_t size = sizeof(int); - int res = getsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, &size); - if(res < 0) { - log_e("fail on fd %d, errno: %d, \"%s\"", _socket, errno, strerror(errno)); - } - return flag; -} - -/* - * Callback Setters - * */ - -void AsyncClient::onConnect(AcConnectHandler cb, void* arg){ - _connect_cb = cb; - _connect_cb_arg = arg; -} - -void AsyncClient::onDisconnect(AcConnectHandler cb, void* arg){ - _discard_cb = cb; - _discard_cb_arg = arg; -} - -void AsyncClient::onAck(AcAckHandler cb, void* arg){ - _sent_cb = cb; - _sent_cb_arg = arg; -} - -void AsyncClient::onError(AcErrorHandler cb, void* arg){ - _error_cb = cb; - _error_cb_arg = arg; -} - -void AsyncClient::onData(AcDataHandler cb, void* arg){ - _recv_cb = cb; - _recv_cb_arg = arg; -} - -void AsyncClient::onTimeout(AcTimeoutHandler cb, void* arg){ - _timeout_cb = cb; - _timeout_cb_arg = arg; -} - -void AsyncClient::onPoll(AcConnectHandler cb, void* arg){ - _poll_cb = cb; - _poll_cb_arg = arg; -} - -bool AsyncClient::connected(){ - if (_socket == -1) { - return false; - } - return _conn_state == 4; -} - -bool AsyncClient::freeable(){ - if (_socket == -1) { - return true; - } - return _conn_state == 0 || _conn_state > 4; -} - -uint32_t AsyncClient::getRemoteAddress() { - if(_socket == -1) { - return 0; - } - - struct sockaddr_storage addr; - socklen_t len = sizeof addr; - getpeername(_socket, (struct sockaddr*)&addr, &len); - struct sockaddr_in *s = (struct sockaddr_in *)&addr; - - return s->sin_addr.s_addr; -} - -uint16_t AsyncClient::getRemotePort() { - if(_socket == -1) { - return 0; - } - - struct sockaddr_storage addr; - socklen_t len = sizeof addr; - getpeername(_socket, (struct sockaddr*)&addr, &len); - struct sockaddr_in *s = (struct sockaddr_in *)&addr; - - return ntohs(s->sin_port); -} - -uint32_t AsyncClient::getLocalAddress() { - if(_socket == -1) { - return 0; - } - - struct sockaddr_storage addr; - socklen_t len = sizeof addr; - getsockname(_socket, (struct sockaddr*)&addr, &len); - struct sockaddr_in *s = (struct sockaddr_in *)&addr; - - return s->sin_addr.s_addr; -} - -uint16_t AsyncClient::getLocalPort() { - if(_socket == -1) { - return 0; - } - - struct sockaddr_storage addr; - socklen_t len = sizeof addr; - getsockname(_socket, (struct sockaddr*)&addr, &len); - struct sockaddr_in *s = (struct sockaddr_in *)&addr; - - return ntohs(s->sin_port); -} - -IPAddress AsyncClient::remoteIP() { - return IPAddress(getRemoteAddress()); -} - -uint16_t AsyncClient::remotePort() { - return getRemotePort(); -} - -IPAddress AsyncClient::localIP() { - return IPAddress(getLocalAddress()); -} - -uint16_t AsyncClient::localPort() { - return getLocalPort(); -} - - -#if ASYNC_TCP_SSL_ENABLED -bool AsyncClient::connect(IPAddress ip, uint16_t port, bool secure) -#else -bool AsyncClient::connect(IPAddress ip, uint16_t port) -#endif // ASYNC_TCP_SSL_ENABLED -{ - if (_socket != -1) { - log_w("already connected, state %d", _conn_state); - return false; - } - - if(!_start_asyncsock_task()){ - log_e("failed to start task"); - return false; - } - -#if ASYNC_TCP_SSL_ENABLED - _secure = secure; - _handshake_done = !secure; -#endif // ASYNC_TCP_SSL_ENABLED - - int sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) { - log_e("socket: %d", errno); - return false; - } - int r = fcntl( sockfd, F_SETFL, fcntl( sockfd, F_GETFL, 0 ) | O_NONBLOCK ); - - uint32_t ip_addr = ip; - struct sockaddr_in serveraddr; - memset(&serveraddr, 0, sizeof(serveraddr)); - serveraddr.sin_family = AF_INET; - memcpy(&(serveraddr.sin_addr.s_addr), &ip_addr, 4); - serveraddr.sin_port = htons(port); - -#ifdef EINPROGRESS - #if EINPROGRESS != 119 - #error EINPROGRESS invalid - #endif -#endif - - //Serial.printf("DEBUG: connect to %08x port %d using IP... ", ip_addr, port); - errno = 0; r = ::connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); - //Serial.printf("r=%d errno=%d\r\n", r, errno); - if (r < 0 && errno != EINPROGRESS) { - //Serial.println("\t(connect failed)"); - log_e("connect on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno)); - ::close(sockfd); - return false; - } - - // Updating state visible to asyncTcpSock task - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - _conn_state = 2; - _socket = sockfd; - _rx_last_packet = millis(); - xSemaphoreGiveRecursive(_asyncsock_mutex); - - // Socket is now connecting. Should become writable in asyncTcpSock task - //Serial.printf("\twaiting for connect finished on socket: %d\r\n", _socket); - return true; -} - -void _tcpsock_dns_found(const char * name, struct ip_addr * ipaddr, void * arg); -#if ASYNC_TCP_SSL_ENABLED -bool AsyncClient::connect(const char* host, uint16_t port, bool secure){ -#else -bool AsyncClient::connect(const char* host, uint16_t port){ -#endif // ASYNC_TCP_SSL_ENABLED - ip_addr_t addr; - - if(!_start_asyncsock_task()){ - log_e("failed to start task"); - return false; - } - - log_v("connect to %s port %d using DNS...", host, port); - err_t err = dns_gethostbyname(host, &addr, (dns_found_callback)&_tcpsock_dns_found, this); - if(err == ERR_OK) { - log_v("\taddr resolved as %08x, connecting...", addr.u_addr.ip4.addr); -#if ASYNC_TCP_SSL_ENABLED - _hostname = host; - return connect(IPAddress(addr.u_addr.ip4.addr), port, secure); -#else - return connect(IPAddress(addr.u_addr.ip4.addr), port); -#endif // ASYNC_TCP_SSL_ENABLED - } else if(err == ERR_INPROGRESS) { - log_v("\twaiting for DNS resolution"); - _conn_state = 1; - _connect_port = port; -#if ASYNC_TCP_SSL_ENABLED - _hostname = host; - _secure = secure; - _handshake_done = !secure; -#endif // ASYNC_TCP_SSL_ENABLED - return true; - } - log_e("error: %d", err); - return false; -} - -// This function runs in the LWIP thread -void _tcpsock_dns_found(const char * name, struct ip_addr * ipaddr, void * arg) -{ - AsyncClient * c = (AsyncClient *)arg; - if (ipaddr) { - memcpy(&(c->_connect_addr), ipaddr, sizeof(struct ip_addr)); - } else { - memset(&(c->_connect_addr), 0, sizeof(struct ip_addr)); - } - - // Updating state visible to asyncTcpSock task - // MUST NOT take _asyncsock_mutex lock, risks a deadlock if task holding lock - // attempts a LWIP network call. - c->_isdnsfinished = true; - - // TODO: actually use name -} - -// DNS resolving has finished. Check for error or connect -void AsyncClient::_sockDelayedConnect(void) -{ - if (_connect_addr.u_addr.ip4.addr) { -#if ASYNC_TCP_SSL_ENABLED - connect(IPAddress(_connect_addr.u_addr.ip4.addr), _connect_port, _secure); -#else - connect(IPAddress(_connect_addr.u_addr.ip4.addr), _connect_port); -#endif - } else { - _conn_state = 0; - if(_error_cb) { - _error_cb(_error_cb_arg, this, -55); - } - if(_discard_cb) { - _discard_cb(_discard_cb_arg, this); - } - } -} - -#if ASYNC_TCP_SSL_ENABLED -int AsyncClient::_runSSLHandshakeLoop(void) -{ - int res = 0; - - while (!_handshake_done) { - res = _sslctx->runSSLHandshake(); - if (res == 0) { - // Handshake successful - _handshake_done = true; - } else if (ASYNCTCP_TLS_CAN_RETRY(res)) { - // Ran out of readable data or writable space on socket, must continue later - break; - } else { - // SSL handshake for AsyncTCP does not inform SSL errors - log_e("TLS setup failed with error %d, closing socket...", res); - _close(); - // _sslctx should be NULL after this - break; - } - } - - return res; -} -#endif - -bool AsyncClient::_sockIsWriteable(void) -{ - int res; - int sockerr; - socklen_t len; - bool activity = false; - - int sent_errno = 0; - std::deque notifylist; - - // Socket is now writeable. What should we do? - switch (_conn_state) { - case 2: - case 3: - // Socket has finished connecting. What happened? - len = (socklen_t)sizeof(int); - res = getsockopt(_socket, SOL_SOCKET, SO_ERROR, &sockerr, &len); - if (res < 0) { - _error(errno); - } else if (sockerr != 0) { - _error(sockerr); - } else { -#if ASYNC_TCP_SSL_ENABLED - if (_secure) { - int res = 0; - - if (_sslctx == NULL) { - String remIP_str = remoteIP().toString(); - const char * host_or_ip = _hostname.isEmpty() - ? remIP_str.c_str() - : _hostname.c_str(); - - _sslctx = new AsyncTCP_TLS_Context(); - if (_root_ca != NULL) { - res = _sslctx->startSSLClient(_socket, host_or_ip, - (const unsigned char *)_root_ca, _root_ca_len, - (const unsigned char *)_cli_cert, _cli_cert_len, - (const unsigned char *)_cli_key, _cli_key_len); - } else if (_psk_ident != NULL) { - res = _sslctx->startSSLClient(_socket, host_or_ip, - _psk_ident, _psk); - } else { - res = _sslctx->startSSLClientInsecure(_socket, host_or_ip); - } - - if (res != 0) { - // SSL setup for AsyncTCP does not inform SSL errors - log_e("TLS setup failed with error %d, closing socket...", res); - _close(); - // _sslctx should be NULL after this - } - } - - // _handshake_done is set to FALSE on connect() if encrypted connection - if (_sslctx != NULL && res == 0) res = _runSSLHandshakeLoop(); - - if (!_handshake_done) return ASYNCTCP_TLS_CAN_RETRY(res); - - // Fallthrough to ordinary successful connection - } -#endif - - // Socket is now fully connected - _conn_state = 4; - activity = true; - _rx_last_packet = millis(); - _ack_timeout_signaled = false; - - if(_connect_cb) { - _connect_cb(_connect_cb_arg, this); - } - } - break; - case 4: - default: - // Socket can accept some new data... - xSemaphoreTake(_write_mutex, (TickType_t)portMAX_DELAY); - if (_writeQueue.size() > 0) { - activity = _flushWriteQueue(); - _collectNotifyWrittenBuffers(notifylist, sent_errno); - } - xSemaphoreGive(_write_mutex); - - _notifyWrittenBuffers(notifylist, sent_errno); - - break; - } - - return activity; -} - -bool AsyncClient::_flushWriteQueue(void) -{ - bool activity = false; - - if (_socket == -1) return false; - - for (auto it = _writeQueue.begin(); it != _writeQueue.end(); it++) { - // Abort iteration if error found while writing a buffer - if (it->write_errno != 0) break; - - // Skip over head buffers already fully written - if (it->written >= it->length) continue; - - bool keep_writing = true; - do { - uint8_t * p = it->data + it->written; - size_t n = it->length - it->written; - errno = 0; - ssize_t r; - -#if ASYNC_TCP_SSL_ENABLED - if (_sslctx != NULL) { - r = _sslctx->write(p, n); - if (ASYNCTCP_TLS_CAN_RETRY(r)) { - r = -1; - errno = EAGAIN; - } else if (ASYNCTCP_TLS_EOF(r)) { - r = -1; - errno = EPIPE; - } else if (r < 0) { - if (errno == 0) errno = EIO; - } - } else { -#endif - r = lwip_write(_socket, p, n); -#if ASYNC_TCP_SSL_ENABLED - } -#endif - - if (r >= 0) { - // Written some data into the socket - it->written += r; - _writeSpaceRemaining += r; - activity = true; - - if (it->written >= it->length) { - it->written_at = millis(); - if (it->owned) ::free(it->data); - it->data = NULL; - } - } else if (errno == EAGAIN || errno == EWOULDBLOCK) { - // Socket is full, could not write anything - keep_writing = false; - } else { - // A write error happened that should be reported - it->write_errno = errno; - keep_writing = false; - log_e("socket %d lwip_write() failed errno=%d", _socket, it->write_errno); - } - } while (keep_writing && it->written < it->length); - } - - return activity; -} - -// This method MUST be called with _write_mutex held -void AsyncClient::_collectNotifyWrittenBuffers(std::deque & notifyqueue, int & write_errno) -{ - write_errno = 0; - notifyqueue.clear(); - - while (_writeQueue.size() > 0) { - if (_writeQueue.front().write_errno != 0) { - write_errno = _writeQueue.front().write_errno; - return; - } - - if (_writeQueue.front().written >= _writeQueue.front().length) { - // Collect information on fully-written buffer, and stash it into notify queue - if (_writeQueue.front().written_at > _rx_last_packet) { - _rx_last_packet = _writeQueue.front().written_at; - } - if (_writeQueue.front().owned && _writeQueue.front().data != NULL) ::free(_writeQueue.front().data); - - notify_writebuf noti; - noti.length = _writeQueue.front().length; - noti.delay = _writeQueue.front().written_at - _writeQueue.front().queued_at; - _writeQueue.pop_front(); - notifyqueue.push_back(noti); - } else { - // Found first not-fully-written buffer, stop here - return; - } - } -} - -void AsyncClient::_notifyWrittenBuffers(std::deque & notifyqueue, int write_errno) -{ - while (notifyqueue.size() > 0) { - if (notifyqueue.front().length > 0 && _sent_cb) { - _sent_cb(_sent_cb_arg, this, notifyqueue.front().length, notifyqueue.front().delay); - } - notifyqueue.pop_front(); - } - - if (write_errno != 0) _error(write_errno); -} - -void AsyncClient::_sockIsReadable(void) -{ - _rx_last_packet = millis(); - errno = 0; - ssize_t r; - -#if ASYNC_TCP_SSL_ENABLED - if (_sslctx != NULL) { - if (!_handshake_done) { - // Handshake process has stopped for want of data, must be - // continued here for connection to complete. - _runSSLHandshakeLoop(); - - // If handshake was successful, this will be recognized when the socket - // next becomes writable. No other read operation should be done here. - return; - } else { - r = _sslctx->read(_readBuffer, MAX_PAYLOAD_SIZE); - if (ASYNCTCP_TLS_CAN_RETRY(r)) { - r = -1; - errno = EAGAIN; - } else if (ASYNCTCP_TLS_EOF(r)) { - // Simulate "successful" end-of-stream condition - r = 0; - } else if (r < 0) { - if (errno == 0) errno = EIO; - } - } - } else { -#endif - r = lwip_read(_socket, _readBuffer, MAX_PAYLOAD_SIZE); -#if ASYNC_TCP_SSL_ENABLED - } -#endif - - if (r > 0) { - if(_recv_cb) { - _recv_cb(_recv_cb_arg, this, _readBuffer, r); - } - } else if (r == 0) { - // A successful read of 0 bytes indicates remote side closed connection - _close(); - } else if (r < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - // Do nothing, will try later - } else { - _error(errno); - } - } -} - -void AsyncClient::_sockPoll(void) -{ - if (!connected()) return; - - // The AsyncClient::send() call may be invoked from tasks other than "asyncTcpSock" - // and may have written buffers via _flushWriteQueue(), but the ack callbacks have - // not been called yet, nor buffers removed from the write queue. For consistency, - // written buffers are now acked here. - std::deque notifylist; - int sent_errno = 0; - xSemaphoreTake(_write_mutex, (TickType_t)portMAX_DELAY); - if (_writeQueue.size() > 0) { - _collectNotifyWrittenBuffers(notifylist, sent_errno); - } - xSemaphoreGive(_write_mutex); - - _notifyWrittenBuffers(notifylist, sent_errno); - - /* Connection migh be closed after ACK notification. */ - if (!connected()) return; - - uint32_t now = millis(); - - // ACK Timeout - simulated by write queue staleness - xSemaphoreTake(_write_mutex, (TickType_t)portMAX_DELAY); - if (_writeQueue.size() > 0 && !_ack_timeout_signaled && _ack_timeout) { - uint32_t sent_delay = now - _writeQueue.front().queued_at; - if (sent_delay >= _ack_timeout && _writeQueue.front().written_at == 0) { - _ack_timeout_signaled = true; - //log_w("ack timeout %d", pcb->state); - xSemaphoreGive(_write_mutex); - if(_timeout_cb) - _timeout_cb(_timeout_cb_arg, this, sent_delay); - return; - } - } - xSemaphoreGive(_write_mutex); - - // RX Timeout? Check for readable socket before bailing out - if (_rx_since_timeout && (now - _rx_last_packet) >= (_rx_since_timeout * 1000)) { - fd_set sockSet_r; - struct timeval tv; - - FD_ZERO(&sockSet_r); - FD_SET(_socket, &sockSet_r); - tv.tv_sec = 0; - tv.tv_usec = 0; - - int r = select(_socket + 1, &sockSet_r, NULL, NULL, &tv); - if (r > 0) _rx_last_packet = now; - } - - // RX Timeout - if (_rx_since_timeout && (now - _rx_last_packet) >= (_rx_since_timeout * 1000)) { - //log_w("rx timeout %d", pcb->state); - _close(); - return; - } - // Everything is fine - if(_poll_cb) { - _poll_cb(_poll_cb_arg, this); - } -} - -void AsyncClient::_removeAllCallbacks(void) -{ - _connect_cb = NULL; - _connect_cb_arg = NULL; - _discard_cb = NULL; - _discard_cb_arg = NULL; - _sent_cb = NULL; - _sent_cb_arg = NULL; - _error_cb = NULL; - _error_cb_arg = NULL; - _recv_cb = NULL; - _recv_cb_arg = NULL; - _timeout_cb = NULL; - _timeout_cb_arg = NULL; - _poll_cb = NULL; - _poll_cb_arg = NULL; -} - -void AsyncClient::_close(void) -{ - //Serial.print("AsyncClient::_close: "); Serial.println(_socket); - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - _conn_state = 0; - ::close(_socket); - _socket = -1; -#if ASYNC_TCP_SSL_ENABLED - if (_sslctx != NULL) { - delete _sslctx; - _sslctx = NULL; - } -#endif - xSemaphoreGiveRecursive(_asyncsock_mutex); - - _clearWriteQueue(); - if (_discard_cb) _discard_cb(_discard_cb_arg, this); -} - -void AsyncClient::_error(int8_t err) -{ - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - _conn_state = 0; - ::close(_socket); - _socket = -1; -#if ASYNC_TCP_SSL_ENABLED - if (_sslctx != NULL) { - delete _sslctx; - _sslctx = NULL; - } -#endif - xSemaphoreGiveRecursive(_asyncsock_mutex); - - _clearWriteQueue(); - if (_error_cb) _error_cb(_error_cb_arg, this, err); - if (_discard_cb) _discard_cb(_discard_cb_arg, this); -} - -size_t AsyncClient::space() -{ - if (!connected()) return 0; - return _writeSpaceRemaining; -} - -size_t AsyncClient::add(const char* data, size_t size, uint8_t apiflags) -{ - queued_writebuf n_entry; - - if (!connected() || data == NULL || size <= 0) return 0; - - size_t room = space(); - if (!room) return 0; - - size_t will_send = (room < size) ? room : size; - if (apiflags & ASYNC_WRITE_FLAG_COPY) { - n_entry.data = (uint8_t *)malloc(will_send); - if (n_entry.data == NULL) { - return 0; - } - memcpy(n_entry.data, data, will_send); - n_entry.owned = true; - } else { - n_entry.data = (uint8_t *)data; - n_entry.owned = false; - } - n_entry.length = will_send; - n_entry.written = 0; - n_entry.queued_at = millis(); - n_entry.written_at = 0; - n_entry.write_errno = 0; - - xSemaphoreTake(_write_mutex, (TickType_t)portMAX_DELAY); - _writeQueue.push_back(n_entry); - _writeSpaceRemaining -= will_send; - _ack_timeout_signaled = false; - xSemaphoreGive(_write_mutex); - - return will_send; -} - -bool AsyncClient::send() -{ - if (!connected()) return false; - - fd_set sockSet_w; - struct timeval tv; - - FD_ZERO(&sockSet_w); - FD_SET(_socket, &sockSet_w); - tv.tv_sec = 0; - tv.tv_usec = 0; - - // Write as much data as possible from queue if socket is writable - xSemaphoreTake(_write_mutex, (TickType_t)portMAX_DELAY); - int r = select(_socket + 1, NULL, &sockSet_w, NULL, &tv); - if (r > 0) _flushWriteQueue(); - xSemaphoreGive(_write_mutex); - return true; -} - -bool AsyncClient::_pendingWrite(void) -{ - xSemaphoreTake(_write_mutex, (TickType_t)portMAX_DELAY); - bool pending = ((_conn_state > 0 && _conn_state < 4) || _writeQueue.size() > 0); - xSemaphoreGive(_write_mutex); - return pending; -} - -// In normal operation this should be a no-op. Will only free something in case -// of errors before all data was written. -void AsyncClient::_clearWriteQueue(void) -{ - xSemaphoreTake(_write_mutex, (TickType_t)portMAX_DELAY); - while (_writeQueue.size() > 0) { - if (_writeQueue.front().owned) { - if (_writeQueue.front().data != NULL) ::free(_writeQueue.front().data); - } - _writeQueue.pop_front(); - } - xSemaphoreGive(_write_mutex); -} - -bool AsyncClient::free(){ - if (_socket == -1) return true; - return (_conn_state == 0 || _conn_state > 4); -} - -size_t AsyncClient::write(const char* data) { - if(data == NULL) { - return 0; - } - return write(data, strlen(data)); -} - -size_t AsyncClient::write(const char* data, size_t size, uint8_t apiflags) { - size_t will_send = add(data, size, apiflags); - if(!will_send || !send()) { - return 0; - } - return will_send; -} - -void AsyncClient::close(bool now) -{ - if (_socket != -1) _close(); -} - -int8_t AsyncClient::abort(){ - if (_socket != -1) { - // Note: needs LWIP_SO_LINGER to be enabled in order to work, otherwise - // this call is equivalent to close(). - struct linger l; - l.l_onoff = 1; - l.l_linger = 0; - setsockopt(_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)); - _close(); - } - return ERR_ABRT; -} - -#if ASYNC_TCP_SSL_ENABLED -void AsyncClient::setRootCa(const char* rootca, const size_t len) { - _root_ca = (char*)rootca; - _root_ca_len = len; -} - -void AsyncClient::setClientCert(const char* cli_cert, const size_t len) { - _cli_cert = (char*)cli_cert; - _cli_cert_len = len; -} - -void AsyncClient::setClientKey(const char* cli_key, const size_t len) { - _cli_key = (char*)cli_key; - _cli_key_len = len; -} - -void AsyncClient::setPsk(const char* psk_ident, const char* psk) { - _psk_ident = psk_ident; - _psk = psk; -} -#endif // ASYNC_TCP_SSL_ENABLED - -const char * AsyncClient::errorToString(int8_t error){ - switch(error){ - case ERR_OK: return "OK"; - case ERR_MEM: return "Out of memory error"; - case ERR_BUF: return "Buffer error"; - case ERR_TIMEOUT: return "Timeout"; - case ERR_RTE: return "Routing problem"; - case ERR_INPROGRESS: return "Operation in progress"; - case ERR_VAL: return "Illegal value"; - case ERR_WOULDBLOCK: return "Operation would block"; - case ERR_USE: return "Address in use"; - case ERR_ALREADY: return "Already connected"; - case ERR_CONN: return "Not connected"; - case ERR_IF: return "Low-level netif error"; - case ERR_ABRT: return "Connection aborted"; - case ERR_RST: return "Connection reset"; - case ERR_CLSD: return "Connection closed"; - case ERR_ARG: return "Illegal argument"; - case -55: return "DNS failed"; - default: return "UNKNOWN"; - } -} -/* -const char * AsyncClient::stateToString(){ - switch(state()){ - case 0: return "Closed"; - case 1: return "Listen"; - case 2: return "SYN Sent"; - case 3: return "SYN Received"; - case 4: return "Established"; - case 5: return "FIN Wait 1"; - case 6: return "FIN Wait 2"; - case 7: return "Close Wait"; - case 8: return "Closing"; - case 9: return "Last ACK"; - case 10: return "Time Wait"; - default: return "UNKNOWN"; - } -} -*/ - - - -/* - Async TCP Server - */ - -AsyncServer::AsyncServer(IPAddress addr, uint16_t port) -: _port(port) -, _addr(addr) -, _noDelay(false) -, _connect_cb(0) -, _connect_cb_arg(0) -{} - -AsyncServer::AsyncServer(uint16_t port) -: _port(port) -, _addr((uint32_t) IPADDR_ANY) -, _noDelay(false) -, _connect_cb(0) -, _connect_cb_arg(0) -{} - -AsyncServer::~AsyncServer(){ - end(); -} - -void AsyncServer::onClient(AcConnectHandler cb, void* arg){ - _connect_cb = cb; - _connect_cb_arg = arg; -} - -void AsyncServer::begin() -{ - if (_socket != -1) return; - - if (!_start_asyncsock_task()) { - log_e("failed to start task"); - return; - } - - int sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) return; - - struct sockaddr_in server; - server.sin_family = AF_INET; - server.sin_addr.s_addr = (uint32_t) _addr; - server.sin_port = htons(_port); - if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0) { - ::close(sockfd); - log_e("bind error: %d - %s", errno, strerror(errno)); - return; - } - - static uint8_t backlog = 5; - if (listen(sockfd , backlog) < 0) { - ::close(sockfd); - log_e("listen error: %d - %s", errno, strerror(errno)); - return; - } - fcntl(sockfd, F_SETFL, O_NONBLOCK); - - // Updating state visible to asyncTcpSock task - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - _socket = sockfd; - xSemaphoreGiveRecursive(_asyncsock_mutex); -} - -void AsyncServer::end() -{ - if (_socket == -1) return; - xSemaphoreTakeRecursive(_asyncsock_mutex, (TickType_t)portMAX_DELAY); - ::close(_socket); - _socket = -1; - xSemaphoreGiveRecursive(_asyncsock_mutex); -} - -void AsyncServer::_sockIsReadable(void) -{ - //Serial.print("AsyncServer::_sockIsReadable: "); Serial.println(_socket); - - if (_connect_cb) { - struct sockaddr_in client; - size_t cs = sizeof(struct sockaddr_in); - errno = 0; int accepted_sockfd = ::accept(_socket, (struct sockaddr *)&client, (socklen_t*)&cs); - //Serial.printf("\t new sockfd=%d errno=%d\r\n", accepted_sockfd, errno); - if (accepted_sockfd < 0) { - log_e("accept error: %d - %s", errno, strerror(errno)); - return; - } - - AsyncClient * c = new AsyncClient(accepted_sockfd); - if (c) { - c->setNoDelay(_noDelay); - _connect_cb(_connect_cb_arg, c); - } - } -} - diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h deleted file mode 100644 index 47a6f2d4..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h +++ /dev/null @@ -1,321 +0,0 @@ -/* - 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 -#include -#include -#if ASYNC_TCP_SSL_ENABLED -#include -#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 AcConnectHandler; -typedef std::function AcAckHandler; -typedef std::function AcErrorHandler; -typedef std::function AcDataHandler; -//typedef std::function AcPacketHandler; -typedef std::function AcTimeoutHandler; - -class AsyncSocketBase -{ -private: - static std::list & _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 _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 &, int &); - void _notifyWrittenBuffers(std::deque &, 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 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_ */ diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_SSL.h b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_SSL.h deleted file mode 100644 index fc70073d..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_SSL.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef ASYNCTCP_SSL_H_ -#define ASYNCTCP_SSL_H_ - -#include "AsyncTCP_SSL.hpp" - -#endif /* ASYNCTCP_SSL_H_ */ \ No newline at end of file diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_SSL.hpp b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_SSL.hpp deleted file mode 100644 index 8f088d0b..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_SSL.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef ASYNCTCP_SSL_HPP -#define ASYNCTCP_SSL_HPP - -#ifdef ASYNC_TCP_SSL_ENABLED - -#include - -#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 \ No newline at end of file diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_TLS_Context.cpp b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_TLS_Context.cpp deleted file mode 100644 index 677146a7..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_TLS_Context.cpp +++ /dev/null @@ -1,346 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - - -#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= '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 \ No newline at end of file diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_TLS_Context.h b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_TLS_Context.h deleted file mode 100644 index 4ef03327..00000000 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP_TLS_Context.h +++ /dev/null @@ -1,79 +0,0 @@ -#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 \ No newline at end of file