Merge branch 'main' into feature/volvo-69kWh

This commit is contained in:
Daniel Öster 2025-03-04 21:28:26 +02:00
commit a686c14b67
25 changed files with 371 additions and 1343 deletions

View file

@ -78,7 +78,6 @@ jobs:
- TESLA_MODEL_SX_BATTERY - TESLA_MODEL_SX_BATTERY
- VOLVO_SPA_BATTERY - VOLVO_SPA_BATTERY
- TEST_FAKE_BATTERY - TEST_FAKE_BATTERY
- SERIAL_LINK_RECEIVER
# These are the emulated inverter communication protocols for which the code will be compiled. # These are the emulated inverter communication protocols for which the code will be compiled.
inverter: inverter:
- BYD_CAN - BYD_CAN

View file

@ -86,7 +86,6 @@ jobs:
- SMA_TRIPOWER_CAN - SMA_TRIPOWER_CAN
- SOFAR_CAN - SOFAR_CAN
- SOLAX_CAN - SOLAX_CAN
- SERIAL_LINK_TRANSMITTER
# These are the supported hardware platforms for which the code will be compiled. # These are the supported hardware platforms for which the code will be compiled.
hardware: hardware:
- HW_LILYGO - HW_LILYGO

View file

@ -86,7 +86,6 @@ jobs:
- SMA_TRIPOWER_CAN - SMA_TRIPOWER_CAN
- SOFAR_CAN - SOFAR_CAN
- SOLAX_CAN - SOLAX_CAN
- SERIAL_LINK_TRANSMITTER
# These are the supported hardware platforms for which the code will be compiled. # These are the supported hardware platforms for which the code will be compiled.
hardware: hardware:
- HW_LILYGO - HW_LILYGO

View file

@ -72,7 +72,6 @@ jobs:
- SOFAR_CAN - SOFAR_CAN
- SOLAX_CAN - SOLAX_CAN
- SUNGROW_CAN - SUNGROW_CAN
- SERIAL_LINK_TRANSMITTER
- NISSANLEAF_CHARGER # Last element is not an inverter, but good to also test if the charger compiles - NISSANLEAF_CHARGER # Last element is not an inverter, but good to also test if the charger compiles
# These are the supported hardware platforms for which the code will be compiled. # These are the supported hardware platforms for which the code will be compiled.
hardware: hardware:

View file

@ -92,7 +92,6 @@ This code uses the following excellent libraries:
- [ayushsharma82/ElegantOTA](https://github.com/ayushsharma82/ElegantOTA) AGPL-3.0 license - [ayushsharma82/ElegantOTA](https://github.com/ayushsharma82/ElegantOTA) AGPL-3.0 license
- [bblanchon/ArduinoJson](https://github.com/bblanchon/ArduinoJson) MIT-License - [bblanchon/ArduinoJson](https://github.com/bblanchon/ArduinoJson) MIT-License
- [eModbus/eModbus](https://github.com/eModbus/eModbus) MIT-License - [eModbus/eModbus](https://github.com/eModbus/eModbus) MIT-License
- [mackelec/SerialDataLink](https://github.com/mackelec/SerialDataLink)
- [ESP32Async/AsyncTCP](https://github.com/ESP32Async/AsyncTCP) 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 - [ESP32Async/ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) LGPL-3.0 license
- [miwagner/ESP32-Arduino-CAN](https://github.com/miwagner/ESP32-Arduino-CAN/) MIT-License - [miwagner/ESP32-Arduino-CAN](https://github.com/miwagner/ESP32-Arduino-CAN/) MIT-License

View file

@ -15,7 +15,6 @@
#include "src/communication/nvm/comm_nvm.h" #include "src/communication/nvm/comm_nvm.h"
#include "src/communication/precharge_control/precharge_control.h" #include "src/communication/precharge_control/precharge_control.h"
#include "src/communication/rs485/comm_rs485.h" #include "src/communication/rs485/comm_rs485.h"
#include "src/communication/seriallink/comm_seriallink.h"
#include "src/datalayer/datalayer.h" #include "src/datalayer/datalayer.h"
#include "src/devboard/sdcard/sdcard.h" #include "src/devboard/sdcard/sdcard.h"
#include "src/devboard/utils/events.h" #include "src/devboard/utils/events.h"
@ -50,7 +49,7 @@
volatile unsigned long long bmsResetTimeOffset = 0; volatile unsigned long long bmsResetTimeOffset = 0;
// The current software version, shown on webserver // The current software version, shown on webserver
const char* version_number = "8.7.dev"; const char* version_number = "8.8.dev";
// Interval settings // Interval settings
uint16_t intervalUpdateValues = INTERVAL_1_S; // Interval at which to update inverter values / Modbus registers uint16_t intervalUpdateValues = INTERVAL_1_S; // Interval at which to update inverter values / Modbus registers
@ -109,7 +108,6 @@ void setup() {
init_rs485(); init_rs485();
init_serialDataLink();
#if defined(CAN_INVERTER_SELECTED) || defined(MODBUS_INVERTER_SELECTED) || defined(RS485_INVERTER_SELECTED) #if defined(CAN_INVERTER_SELECTED) || defined(MODBUS_INVERTER_SELECTED) || defined(RS485_INVERTER_SELECTED)
setup_inverter(); setup_inverter();
#endif #endif
@ -237,9 +235,6 @@ void core_loop(void* task_time_us) {
#if defined(RS485_INVERTER_SELECTED) || defined(RS485_BATTERY_SELECTED) #if defined(RS485_INVERTER_SELECTED) || defined(RS485_BATTERY_SELECTED)
receive_RS485(); // Process serial2 RS485 interface receive_RS485(); // Process serial2 RS485 interface
#endif // RS485_INVERTER_SELECTED #endif // RS485_INVERTER_SELECTED
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
run_serialDataLink();
#endif // SERIAL_LINK_RECEIVER || SERIAL_LINK_TRANSMITTER
END_TIME_MEASUREMENT_MAX(comm, datalayer.system.status.time_comm_us); END_TIME_MEASUREMENT_MAX(comm, datalayer.system.status.time_comm_us);
#ifdef WEBSERVER #ifdef WEBSERVER
START_TIME_MEASUREMENT(ota); START_TIME_MEASUREMENT(ota);
@ -268,9 +263,7 @@ void core_loop(void* task_time_us) {
check_interconnect_available(); check_interconnect_available();
#endif // DOUBLE_BATTERY #endif // DOUBLE_BATTERY
update_calculated_values(); update_calculated_values();
#ifndef SERIAL_LINK_RECEIVER update_machineryprotection(); // Check safeties
update_machineryprotection(); // Check safeties (Not on serial link reciever board)
#endif // SERIAL_LINK_RECEIVER
update_values_inverter(); // Update values heading towards inverter update_values_inverter(); // Update values heading towards inverter
} }
END_TIME_MEASUREMENT_MAX(time_values, datalayer.system.status.time_values_us); END_TIME_MEASUREMENT_MAX(time_values, datalayer.system.status.time_values_us);

View file

@ -44,7 +44,6 @@
//#define VOLVO_SPA_HYBRID_BATTERY //#define VOLVO_SPA_HYBRID_BATTERY
//#define TEST_FAKE_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 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 */ /* 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 AFORE_CAN //Enable this line to emulate an "Afore battery" over CAN bus
@ -65,7 +64,6 @@
//#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus //#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus
//#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus //#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus
//#define SUNGROW_CAN //Enable this line to emulate a "Sungrow SBR064" over CAN bus //#define SUNGROW_CAN //Enable this line to emulate a "Sungrow SBR064" over CAN bus
//#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter)
/* Select hardware used for Battery-Emulator */ /* Select hardware used for Battery-Emulator */
//#define HW_LILYGO //#define HW_LILYGO
@ -82,7 +80,7 @@
//#define PERIODIC_BMS_RESET //Enable to have the emulator powercycle the connected battery every 24hours via GPIO. Useful for some batteries like Nissan LEAF //#define PERIODIC_BMS_RESET //Enable to have the emulator powercycle the connected battery every 24hours via GPIO. Useful for some batteries like Nissan LEAF
//#define REMOTE_BMS_RESET //Enable to allow the emulator to remotely trigger a powercycle of the battery via MQTT. Useful for some batteries like Nissan LEAF //#define REMOTE_BMS_RESET //Enable to allow the emulator to remotely trigger a powercycle of the battery via MQTT. Useful for some batteries like Nissan LEAF
// PERIODIC_BMS_RESET_AT Uses NTP server, internet required. In 24 Hour format WITHOUT leading 0. e.g 0230 should be 230. Time Zone is set in USER_SETTINGS.cpp // PERIODIC_BMS_RESET_AT Uses NTP server, internet required. In 24 Hour format WITHOUT leading 0. e.g 0230 should be 230. Time Zone is set in USER_SETTINGS.cpp
#define PERIODIC_BMS_RESET_AT 525 //#define PERIODIC_BMS_RESET_AT 525
/* Shunt/Contactor settings (Optional) */ /* Shunt/Contactor settings (Optional) */
//#define BMW_SBOX // SBOX relay control & battery current/voltage measurement //#define BMW_SBOX // SBOX relay control & battery current/voltage measurement

View file

@ -143,10 +143,6 @@ void setup_can_shunt();
#include "VOLVO-SPA-HYBRID-BATTERY.h" #include "VOLVO-SPA-HYBRID-BATTERY.h"
#endif #endif
#ifdef SERIAL_LINK_RECEIVER
#include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h"
#endif
void setup_battery(void); void setup_battery(void);
void update_values_battery(); void update_values_battery();

View file

@ -1,11 +1,11 @@
#include "DALY-BMS.h" #include "DALY-BMS.h"
#include <cstdint>
#include "../include.h"
#include "RJXZS-BMS.h"
#ifdef DALY_BMS #ifdef DALY_BMS
#include <cstdint>
#include "../datalayer/datalayer.h" #include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h" #include "../devboard/utils/events.h"
#include "../include.h"
#include "RENAULT-TWIZY.h" #include "RENAULT-TWIZY.h"
#include "RJXZS-BMS.h"
/* Do not change code below unless you are sure what you are doing */ /* Do not change code below unless you are sure what you are doing */

View file

@ -1,230 +0,0 @@
#include "../include.h"
#ifdef SERIAL_LINK_RECEIVER
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
#include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h"
#define INVERTER_SEND_NUM_VARIABLES 1
#define INVERTER_RECV_NUM_VARIABLES 16
#ifdef INVERTER_SEND_NUM_VARIABLES
const uint8_t sendingNumVariables = INVERTER_SEND_NUM_VARIABLES;
#else
const uint8_t sendingNumVariables = 0;
#endif
#ifdef TESTBENCH
// In the testbench environment, the receiver uses Serial3
#define SerialReceiver Serial3
#else
// In the production environment, the receiver uses Serial2
#define SerialReceiver Serial2
#endif
// txid,rxid, num_send,num_recv
SerialDataLink dataLinkReceive(SerialReceiver, 0, 0x01, sendingNumVariables,
INVERTER_RECV_NUM_VARIABLES); // ...
static bool batteryFault = false; // used locally - mainly to indicate Battery CAN failure
void __getData() {
datalayer.battery.status.real_soc = (uint16_t)dataLinkReceive.getReceivedData(0);
datalayer.battery.status.soh_pptt = (uint16_t)dataLinkReceive.getReceivedData(1);
datalayer.battery.status.voltage_dV = (uint16_t)dataLinkReceive.getReceivedData(2);
datalayer.battery.status.current_dA = (int16_t)dataLinkReceive.getReceivedData(3);
datalayer.battery.info.total_capacity_Wh =
(uint32_t)(dataLinkReceive.getReceivedData(4) * 10); //add back missing decimal
datalayer.battery.status.remaining_capacity_Wh =
(uint32_t)(dataLinkReceive.getReceivedData(5) * 10); //add back missing decimal
datalayer.battery.status.max_discharge_power_W =
(uint32_t)(dataLinkReceive.getReceivedData(6) * 10); //add back missing decimal
datalayer.battery.status.max_charge_power_W =
(uint32_t)(dataLinkReceive.getReceivedData(7) * 10); //add back missing decimal
uint16_t _system_bms_status = (uint16_t)dataLinkReceive.getReceivedData(8);
datalayer.battery.status.active_power_W =
(uint32_t)(dataLinkReceive.getReceivedData(9) * 10); //add back missing decimal
datalayer.battery.status.temperature_min_dC = (int16_t)dataLinkReceive.getReceivedData(10);
datalayer.battery.status.temperature_max_dC = (int16_t)dataLinkReceive.getReceivedData(11);
datalayer.battery.status.cell_max_voltage_mV = (uint16_t)dataLinkReceive.getReceivedData(12);
datalayer.battery.status.cell_min_voltage_mV = (uint16_t)dataLinkReceive.getReceivedData(13);
datalayer.battery.info.chemistry = (battery_chemistry_enum)dataLinkReceive.getReceivedData(14);
datalayer.system.status.battery_allows_contactor_closing = (bool)dataLinkReceive.getReceivedData(15);
batteryFault = false;
if (_system_bms_status == FAULT) {
batteryFault = true;
set_event(EVENT_SERIAL_TRANSMITTER_FAILURE, 0);
}
}
void updateData() {
// --- update with fresh data
dataLinkReceive.updateData(0, datalayer.system.status.inverter_allows_contactor_closing);
//dataLinkReceive.updateData(1,var2); // For future expansion,
//dataLinkReceive.updateData(2,var3); // if inverter needs to send data to battery
}
/*
* @ 9600bps, assume void manageSerialLinkReceiver()
* is called every 1mS
*/
void manageSerialLinkReceiver() {
static bool lasterror = false;
static unsigned long last_minutesLost = 0;
static unsigned long lastGood;
static uint16_t lastGoodMaxCharge;
static uint16_t lastGoodMaxDischarge;
static bool initLink = false;
static unsigned long reportTime = 0;
static uint16_t reads = 0;
static uint16_t errors = 0;
unsigned long currentTime = millis();
if (!initLink) {
initLink = true;
// sends variables every 5000mS even if no change
dataLinkReceive.setUpdateInterval(5000);
#ifdef SERIALDATALINK_MUTEACK
dataLinkReceive.muteACK(true);
#endif
}
dataLinkReceive.run();
bool readError = dataLinkReceive.checkReadError(true); // check for error & clear error flag
if (readError) {
logging.print(currentTime);
logging.println(" - ERROR: SerialDataLink - Read Error");
lasterror = true;
errors++;
}
if (dataLinkReceive.checkNewData(true)) // true = clear Flag
{
__getData();
reads++;
lastGoodMaxCharge = datalayer.battery.status.max_charge_power_W;
lastGoodMaxDischarge = datalayer.battery.status.max_discharge_power_W;
//--- if BatteryFault then assume Data is stale
if (!batteryFault)
lastGood = currentTime;
//bms_status = ACTIVE; // just testing
if (lasterror) {
lasterror = false;
logging.print(currentTime);
logging.println(" - RECOVERY: SerialDataLink - Read GOOD");
}
}
unsigned long minutesLost = (currentTime - lastGood) / 60000UL;
if (minutesLost > 0 && lastGood > 0) {
// lose 25% each minute of data loss
if (minutesLost < 4) {
datalayer.battery.status.max_charge_power_W = (lastGoodMaxCharge * (4 - minutesLost)) / 4;
datalayer.battery.status.max_discharge_power_W = (lastGoodMaxDischarge * (4 - minutesLost)) / 4;
set_event(EVENT_SERIAL_RX_WARNING, minutesLost);
} else {
// Times Up -
datalayer.battery.status.max_charge_power_W = 0;
datalayer.battery.status.max_discharge_power_W = 0;
set_event(EVENT_SERIAL_RX_FAILURE, uint8_t(min(minutesLost, 255uL)));
//----- Throw Error
}
// report Lost data & Max charge / Discharge reductions
if (minutesLost != last_minutesLost) {
last_minutesLost = minutesLost;
logging.print(currentTime);
if (batteryFault) {
logging.print("Battery Fault (minutes) : ");
} else {
logging.print(" - Minutes without data : ");
}
logging.print(minutesLost);
logging.print(", max Charge = ");
logging.print(datalayer.battery.status.max_charge_power_W);
logging.print(", max Discharge = ");
logging.println(datalayer.battery.status.max_discharge_power_W);
}
}
if (currentTime - reportTime > 59999) {
reportTime = currentTime;
logging.print(currentTime);
logging.print(" SerialDataLink-Receiver - NewData :");
logging.print(reads);
logging.print(" Errors : ");
logging.println(errors);
reads = 0;
errors = 0;
// --- printUsefullData();
//logging.print("SOC = ");
//logging.println(SOC);
#ifdef DEBUG_LOG
update_values_serial_link();
#endif
}
static unsigned long updateTime = 0;
#ifdef INVERTER_SEND_NUM_VARIABLES
if (currentTime - updateTime > 100) {
updateTime = currentTime;
dataLinkReceive.run();
bool sendError = dataLinkReceive.checkTransmissionError(true); // check for error & clear error flag
updateData();
}
#endif
}
void update_values_serial_link() {
logging.println("Values from battery: ");
logging.print("SOC: ");
logging.print(datalayer.battery.status.real_soc);
logging.print(" SOH: ");
logging.print(datalayer.battery.status.soh_pptt);
logging.print(" Voltage: ");
logging.print(datalayer.battery.status.voltage_dV);
logging.print(" Current: ");
logging.print(datalayer.battery.status.current_dA);
logging.print(" Capacity: ");
logging.print(datalayer.battery.info.total_capacity_Wh);
logging.print(" Remain cap: ");
logging.print(datalayer.battery.status.remaining_capacity_Wh);
logging.print(" Max discharge W: ");
logging.print(datalayer.battery.status.max_discharge_power_W);
logging.print(" Max charge W: ");
logging.print(datalayer.battery.status.max_charge_power_W);
logging.print(" BMS status: ");
logging.print(datalayer.battery.status.bms_status);
logging.print(" Power: ");
logging.print(datalayer.battery.status.active_power_W);
logging.print(" Temp min: ");
logging.print(datalayer.battery.status.temperature_min_dC);
logging.print(" Temp max: ");
logging.print(datalayer.battery.status.temperature_max_dC);
logging.print(" Cell max: ");
logging.print(datalayer.battery.status.cell_max_voltage_mV);
logging.print(" Cell min: ");
logging.print(datalayer.battery.status.cell_min_voltage_mV);
logging.print(" LFP : ");
logging.print(datalayer.battery.info.chemistry);
logging.print(" Battery Allows Contactor Closing: ");
logging.print(datalayer.system.status.battery_allows_contactor_closing);
logging.print(" Inverter Allows Contactor Closing: ");
logging.print(datalayer.system.status.inverter_allows_contactor_closing);
logging.println("");
}
void setup_battery(void) {
strncpy(datalayer.system.info.battery_protocol, "Serial link to another LilyGo board", 63);
datalayer.system.info.battery_protocol[63] = '\0';
}
// Needed to make the compiler happy
void update_values_battery() {}
void transmit_can_battery() {}
void handle_incoming_can_frame_battery(CAN_frame rx_frame) {}
#endif

View file

@ -1,15 +0,0 @@
// SERIAL-LINK-RECEIVER-FROM-BATTERY.h
#ifndef SERIAL_LINK_RECEIVER_FROM_BATTERY_H
#define SERIAL_LINK_RECEIVER_FROM_BATTERY_H
#define BATTERY_SELECTED
#include "../include.h"
#include "../lib/mackelec-SerialDataLink/SerialDataLink.h"
void manageSerialLinkReceiver();
void update_values_serial_link();
void setup_battery(void);
#endif

View file

@ -297,10 +297,18 @@ void print_can_frame(CAN_frame frame, frameDirection msgDir) {
} }
void map_can_frame_to_variable(CAN_frame* rx_frame, int interface) { void map_can_frame_to_variable(CAN_frame* rx_frame, int interface) {
if (interface !=
CANFD_NATIVE) { //Avoid printing twice due to receive_frame_canfd_addon sending to both FD interfaces
//TODO: This check can be removed later when refactored to use inline functions for logging
print_can_frame(*rx_frame, frameDirection(MSG_RX)); print_can_frame(*rx_frame, frameDirection(MSG_RX));
}
#ifdef LOG_CAN_TO_SD #ifdef LOG_CAN_TO_SD
if (interface !=
CANFD_NATIVE) { //Avoid printing twice due to receive_frame_canfd_addon sending to both FD interfaces
//TODO: This check can be removed later when refactored to use inline functions for logging
add_can_frame_to_buffer(*rx_frame, frameDirection(MSG_RX)); add_can_frame_to_buffer(*rx_frame, frameDirection(MSG_RX));
}
#endif #endif
if (interface == can_config.battery) { if (interface == can_config.battery) {

View file

@ -9,9 +9,6 @@ uint16_t mbPV[MB_RTU_NUM_VALUES]; // Process variable memory
// Create a ModbusRTU server instance listening on Serial2 with 2000ms timeout // Create a ModbusRTU server instance listening on Serial2 with 2000ms timeout
ModbusServerRTU MBserver(Serial2, 2000); ModbusServerRTU MBserver(Serial2, 2000);
#endif #endif
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
#define SERIAL_LINK_BAUDRATE 112500
#endif
// Initialization functions // Initialization functions

View file

@ -1,35 +0,0 @@
#include "comm_seriallink.h"
#include "../../include.h"
// Parameters
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
#define SERIAL_LINK_BAUDRATE 112500
#endif
// Initialization functions
void init_serialDataLink() {
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
Serial2.begin(SERIAL_LINK_BAUDRATE, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);
#endif // SERIAL_LINK_RECEIVER || SERIAL_LINK_TRANSMITTER
}
// Main functions
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
void run_serialDataLink() {
static unsigned long updateTime = 0;
unsigned long currentMillis = millis();
if ((currentMillis - updateTime) > 1) { //Every 2ms
updateTime = currentMillis;
#ifdef SERIAL_LINK_RECEIVER
manageSerialLinkReceiver();
#endif
#ifdef SERIAL_LINK_TRANSMITTER
manageSerialLinkTransmitter();
#endif
}
}
#endif // SERIAL_LINK_RECEIVER || SERIAL_LINK_TRANSMITTER

View file

@ -1,17 +0,0 @@
#ifndef _COMM_SERIALLINK_H_
#define _COMM_SERIALLINK_H_
#include "../../include.h"
/**
* @brief Initialization of serial data link
*
* @param[in] void
*
* @return void
*/
void init_serialDataLink();
void run_serialDataLink();
#endif

View file

@ -224,6 +224,10 @@ typedef struct {
size_t logged_can_messages_offset = 0; size_t logged_can_messages_offset = 0;
/** bool, determines if CAN messages should be logged for webserver */ /** bool, determines if CAN messages should be logged for webserver */
bool can_logging_active = false; bool can_logging_active = false;
/** uint8_t, enumeration which CAN interface should be used for log playback */
uint8_t can_replay_interface = CAN_NATIVE;
/** bool, determines if CAN replay should loop or not */
bool loop_playback = false;
} DATALAYER_SYSTEM_INFO_TYPE; } DATALAYER_SYSTEM_INFO_TYPE;

View file

@ -0,0 +1,147 @@
#include "can_replay_html.h"
#include <Arduino.h>
#include "../../datalayer/datalayer.h"
#include "index_html.h"
String can_replay_processor(void) {
if (!datalayer.system.info.can_logging_active) {
datalayer.system.info.logged_can_messages_offset = 0;
datalayer.system.info.logged_can_messages[0] = '\0';
}
datalayer.system.info.can_logging_active =
true; // Signal to main loop that we should log messages. Disabled by default for performance reasons
String content = index_html_header;
// Page format
content += "<style>";
content += "body { background-color: black; color: white; font-family: Arial, sans-serif; }";
content +=
"button { background-color: #505E67; color: white; border: none; padding: 10px 20px; margin-bottom: 20px; "
"cursor: pointer; border-radius: 10px; }";
content += "button:hover { background-color: #3A4A52; }";
content +=
".can-message { background-color: #404E57; margin-bottom: 5px; padding: 10px; border-radius: 5px; font-family: "
"monospace; }";
content += "</style>";
content += "<button onclick='home()'>Back to main page</button>";
// Start a new block for the CAN messages
content += "<div style='background-color: #303E47; padding: 20px; border-radius: 15px'>";
// Ask user to select which CAN interface log should be sent to
content += "<h3>Step 1: Select CAN Interface for Playback</h3>";
// Dropdown with choices
content += "<label for='canInterface'>CAN Interface:</label>";
content += "<select id='canInterface' name='canInterface'>";
content += "<option value='" + String(CAN_NATIVE) + "' " +
(datalayer.system.info.can_replay_interface == CAN_NATIVE ? "selected" : "") + ">CAN Native</option>";
content += "<option value='" + String(CANFD_NATIVE) + "' " +
(datalayer.system.info.can_replay_interface == CANFD_NATIVE ? "selected" : "") + ">CANFD Native</option>";
content += "<option value='" + String(CAN_ADDON_MCP2515) + "' " +
(datalayer.system.info.can_replay_interface == CAN_ADDON_MCP2515 ? "selected" : "") +
">CAN Addon MCP2515</option>";
content += "<option value='" + String(CANFD_ADDON_MCP2518) + "' " +
(datalayer.system.info.can_replay_interface == CANFD_ADDON_MCP2518 ? "selected" : "") +
">CANFD Addon MCP2518</option>";
content += "</select>";
// Add a button to submit the selected CAN interface
// This function writes the selection to datalayer.system.info.can_replay_interface
content += "<button onclick='sendCANSelection()'>Apply</button>";
content += "<h3>Step 2: Upload CAN Log File</h3>";
content += "<p>Click Browse to select a .txt CANdump log file to upload</p>";
content += "<input type='file' id='file-input' accept='.txt'>";
content += "<button id='upload-btn'>Upload</button>";
content += "<h3>Step 3: Playback control</h3>";
//Checkbox to see if the user wants the log to repeat once it reaches the end
content += "<input type=\"checkbox\" id=\"loopCheckbox\"> Loop ";
// Add a button to start playing the log
content += "<button onclick='startReplay()'>Start</button> ";
// Add a button to stop playing the log
content += "<button onclick='stopReplay()'>Stop</button> ";
// Status indicator
content += "<span id='statusIndicator' style='margin-left:10px; font-weight:bold;'>Stopped</span> ";
content += "<h3>Uploaded Log Preview:</h3>";
content += "<pre id='file-content'></pre>";
content += "<script>";
content += "const fileInput = document.getElementById('file-input');";
content += "const uploadBtn = document.getElementById('upload-btn');";
content += "const fileContent = document.getElementById('file-content');";
content += "let selectedFile = null;";
content += "fileInput.addEventListener('change', () => { selectedFile = fileInput.files[0]; });";
content += "uploadBtn.addEventListener('click', () => {";
content += "if (!selectedFile) { alert('Please select a file first!'); return; }";
content += "const formData = new FormData();";
content += "formData.append('file', selectedFile);";
content += "const xhr = new XMLHttpRequest();";
content += "xhr.open('POST', '/import_can_log', true);";
content +=
"xhr.onload = () => { if (xhr.status === 200) { alert('File uploaded successfully!'); const reader = new "
"FileReader(); reader.onload = function (e) { fileContent.textContent = e.target.result; }; "
"reader.readAsText(selectedFile); } else { alert('Upload failed! Server error.'); }};";
content += "xhr.send(formData);";
content += "});";
content += "</script>";
content += "</div>";
// Add JavaScript for updating status
content += "<script>";
content += "function startReplay() {";
content += " let loop = document.getElementById('loopCheckbox').checked ? 1 : 0;";
content += " fetch('/startReplay?loop=' + loop, { method: 'GET' })";
content += " .then(response => response.text())";
content += " .then(data => {";
content += " console.log(data);";
content += " document.getElementById('statusIndicator').innerText = 'Running...';";
content += " document.getElementById('statusIndicator').style.color = 'green';";
content += " if (loop === 0) {"; // If loop is not checked
content += " setTimeout(() => {";
content += " document.getElementById('statusIndicator').innerText = 'Completed';";
content += " document.getElementById('statusIndicator').style.color = 'white';";
content += " }, 5000);"; // 5-second timeout before reverting the text
content += " }";
content += " })";
content += " .catch(error => console.error('Error:', error));";
content += "}";
content += "function stopReplay() {";
content += " fetch('/stopReplay', { method: 'GET' })";
content += " .then(response => response.text())";
content += " .then(data => {";
content += " console.log(data);";
content += " document.getElementById('statusIndicator').innerText = 'Stopped';";
content += " document.getElementById('statusIndicator').style.color = 'red';";
content += " })";
content += " .catch(error => console.error('Error:', error));";
content += "}";
content += "function sendCANSelection() {";
content += " var selectedInterface = document.getElementById('canInterface').value;";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/setCANInterface?interface=' + selectedInterface, true);";
content += " xhr.onreadystatechange = function() {";
content += " if (xhr.readyState === 4) {";
content += " if (xhr.status === 200) {";
content += " alert('Success: ' + xhr.responseText);";
content += " } else {";
content += " alert('Error: ' + xhr.responseText);";
content += " }";
content += " }";
content += " };";
content += " xhr.send();";
content += "}";
content += "function home() { window.location.href = '/'; }";
content += "</script>";
content += index_html_footer;
return content;
}

View file

@ -0,0 +1,16 @@
#ifndef CANREPLAY_H
#define CANREPLAY_H
#include <Arduino.h>
#include <string>
/**
* @brief Replaces placeholder with content section in web page
*
* @param[in] var
*
* @return String
*/
String can_replay_processor(void);
#endif

View file

@ -9,6 +9,7 @@
#include "../utils/events.h" #include "../utils/events.h"
#include "../utils/led_handler.h" #include "../utils/led_handler.h"
#include "../utils/timer.h" #include "../utils/timer.h"
#include "esp_task_wdt.h"
// Create AsyncWebServer object on port 80 // Create AsyncWebServer object on port 80
AsyncWebServer server(80); AsyncWebServer server(80);
@ -18,6 +19,7 @@ unsigned long ota_progress_millis = 0;
#include "advanced_battery_html.h" #include "advanced_battery_html.h"
#include "can_logging_html.h" #include "can_logging_html.h"
#include "can_replay_html.h"
#include "cellmonitor_html.h" #include "cellmonitor_html.h"
#include "debug_logging_html.h" #include "debug_logging_html.h"
#include "events_html.h" #include "events_html.h"
@ -29,6 +31,124 @@ bool ota_active = false;
const char get_firmware_info_html[] = R"rawliteral(%X%)rawliteral"; const char get_firmware_info_html[] = R"rawliteral(%X%)rawliteral";
String importedLogs = ""; // Store the uploaded logfile contents in RAM
bool isReplayRunning = false; // Global flag to track replay state
CAN_frame currentFrame = {.FD = true, .ext_ID = false, .DLC = 64, .ID = 0x12F, .data = {0}};
void handleFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len,
bool final) {
if (!index) {
importedLogs = ""; // Clear previous logs
logging.printf("Receiving file: %s\n", filename.c_str());
}
// Append received data to the string (RAM storage)
importedLogs += String((char*)data).substring(0, len);
if (final) {
logging.println("Upload Complete!");
request->send(200, "text/plain", "File uploaded successfully");
}
}
void canReplayTask(void* param) {
std::vector<String> messages;
messages.reserve(1000); // Pre-allocate memory to reduce fragmentation
if (!importedLogs.isEmpty()) {
int lastIndex = 0;
while (true) {
int nextIndex = importedLogs.indexOf("\n", lastIndex);
if (nextIndex == -1) {
messages.push_back(importedLogs.substring(lastIndex));
break;
}
messages.push_back(importedLogs.substring(lastIndex, nextIndex));
lastIndex = nextIndex + 1;
}
do {
float firstTimestamp = -1.0;
float lastTimestamp = 0.0;
bool firstMessageSent = false; // Track first message
for (size_t i = 0; i < messages.size(); i++) {
String line = messages[i];
line.trim();
if (line.length() == 0)
continue;
int timeStart = line.indexOf("(") + 1;
int timeEnd = line.indexOf(")");
if (timeStart == 0 || timeEnd == -1)
continue;
float currentTimestamp = line.substring(timeStart, timeEnd).toFloat();
if (firstTimestamp < 0) {
firstTimestamp = currentTimestamp;
}
// Send first message immediately
if (!firstMessageSent) {
firstMessageSent = true;
firstTimestamp = currentTimestamp; // Adjust reference time
} else {
// Delay only if this isn't the first message
float deltaT = (currentTimestamp - lastTimestamp) * 1000;
vTaskDelay((int)deltaT / portTICK_PERIOD_MS);
}
lastTimestamp = currentTimestamp;
int interfaceStart = timeEnd + 2;
int interfaceEnd = line.indexOf(" ", interfaceStart);
if (interfaceEnd == -1)
continue;
int idStart = interfaceEnd + 1;
int idEnd = line.indexOf(" [", idStart);
if (idStart == -1 || idEnd == -1)
continue;
String messageID = line.substring(idStart, idEnd);
int dlcStart = idEnd + 2;
int dlcEnd = line.indexOf("]", dlcStart);
if (dlcEnd == -1)
continue;
String dlc = line.substring(dlcStart, dlcEnd);
int dataStart = dlcEnd + 2;
String dataBytes = line.substring(dataStart);
currentFrame.ID = strtol(messageID.c_str(), NULL, 16);
currentFrame.DLC = dlc.toInt();
int byteIndex = 0;
char* token = strtok((char*)dataBytes.c_str(), " ");
while (token != NULL && byteIndex < currentFrame.DLC) {
currentFrame.data.u8[byteIndex++] = strtol(token, NULL, 16);
token = strtok(NULL, " ");
}
currentFrame.FD = (datalayer.system.info.can_replay_interface == CANFD_NATIVE) ||
(datalayer.system.info.can_replay_interface == CANFD_ADDON_MCP2518);
currentFrame.ext_ID = (currentFrame.ID > 0x7F0);
transmit_can_frame(&currentFrame, datalayer.system.info.can_replay_interface);
}
} while (datalayer.system.info.loop_playback);
messages.clear(); // Free vector memory
messages.shrink_to_fit(); // Release excess memory
}
isReplayRunning = false; // Mark replay as stopped
vTaskDelete(NULL);
}
void init_webserver() { void init_webserver() {
server.on("/logout", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(401); }); server.on("/logout", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(401); });
@ -65,6 +185,63 @@ void init_webserver() {
request->send(response); request->send(response);
}); });
// Route for going to CAN replay web page
server.on("/canreplay", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
AsyncWebServerResponse* response = request->beginResponse(200, "text/html", can_replay_processor());
request->send(response);
});
server.on("/startReplay", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
// Prevent multiple replay tasks from being created
if (isReplayRunning) {
request->send(400, "text/plain", "Replay already running!");
return;
}
datalayer.system.info.loop_playback = request->hasParam("loop") && request->getParam("loop")->value().toInt() == 1;
isReplayRunning = true; // Set flag before starting task
xTaskCreatePinnedToCore(canReplayTask, "CAN_Replay", 8192, NULL, 1, NULL, 1);
request->send(200, "text/plain", "CAN replay started!");
});
// Route for stopping the CAN replay
server.on("/stopReplay", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer.system.info.loop_playback = false;
request->send(200, "text/plain", "CAN replay stopped!");
});
// Route to handle setting the CAN interface for CAN replay
server.on("/setCANInterface", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("interface")) {
String canInterface = request->getParam("interface")->value();
// Convert the received value to an integer
int interfaceValue = canInterface.toInt();
// Update the datalayer with the selected interface
datalayer.system.info.can_replay_interface = interfaceValue;
// Respond with success message
request->send(200, "text/plain", "New interface selected");
} else {
request->send(400, "text/plain", "Error: updating interface failed");
}
});
#if defined(DEBUG_VIA_WEB) || defined(LOG_TO_SD) #if defined(DEBUG_VIA_WEB) || defined(LOG_TO_SD)
// Route for going to debug logging web page // Route for going to debug logging web page
server.on("/log", HTTP_GET, [](AsyncWebServerRequest* request) { server.on("/log", HTTP_GET, [](AsyncWebServerRequest* request) {
@ -79,6 +256,14 @@ void init_webserver() {
request->send(200, "text/plain", "Logging stopped"); request->send(200, "text/plain", "Logging stopped");
}); });
// Define the handler to import can log
server.on(
"/import_can_log", HTTP_POST,
[](AsyncWebServerRequest* request) {
request->send(200, "text/plain", "Ready to receive file."); // Response when request is made
},
handleFileUpload);
#ifndef LOG_CAN_TO_SD #ifndef LOG_CAN_TO_SD
// Define the handler to export can log // Define the handler to export can log
server.on("/export_can_log", HTTP_GET, [](AsyncWebServerRequest* request) { server.on("/export_can_log", HTTP_GET, [](AsyncWebServerRequest* request) {
@ -1280,6 +1465,7 @@ String processor(const String& var) {
content += "<button onclick='Settings()'>Change Settings</button> "; content += "<button onclick='Settings()'>Change Settings</button> ";
content += "<button onclick='Advanced()'>More Battery Info</button> "; content += "<button onclick='Advanced()'>More Battery Info</button> ";
content += "<button onclick='CANlog()'>CAN logger</button> "; content += "<button onclick='CANlog()'>CAN logger</button> ";
content += "<button onclick='CANreplay()'>CAN replay</button> ";
#if defined(DEBUG_VIA_WEB) || defined(LOG_TO_SD) #if defined(DEBUG_VIA_WEB) || defined(LOG_TO_SD)
content += "<button onclick='Log()'>Log</button> "; content += "<button onclick='Log()'>Log</button> ";
#endif // DEBUG_VIA_WEB #endif // DEBUG_VIA_WEB
@ -1308,6 +1494,7 @@ String processor(const String& var) {
content += "function Settings() { window.location.href = '/settings'; }"; content += "function Settings() { window.location.href = '/settings'; }";
content += "function Advanced() { window.location.href = '/advanced'; }"; content += "function Advanced() { window.location.href = '/advanced'; }";
content += "function CANlog() { window.location.href = '/canlog'; }"; content += "function CANlog() { window.location.href = '/canlog'; }";
content += "function CANreplay() { window.location.href = '/canreplay'; }";
content += "function Log() { window.location.href = '/log'; }"; content += "function Log() { window.location.href = '/log'; }";
content += "function Events() { window.location.href = '/events'; }"; content += "function Events() { window.location.href = '/events'; }";
content += content +=

View file

@ -30,20 +30,6 @@
#endif #endif
#endif #endif
#ifdef MODBUS_INVERTER_SELECTED
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
// Check that Dual LilyGo via RS485 option isn't enabled, this collides with Modbus!
#error MODBUS CANNOT BE USED IN DOUBLE LILYGO SETUPS! CHECK USER SETTINGS!
#endif
#endif
#ifdef RS485_INVERTER_SELECTED
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
// Check that Dual LilyGo via RS485 option isn't enabled, this collides with Modbus!
#error RS485 CANNOT BE USED IN DOUBLE LILYGO SETUPS! CHECK USER SETTINGS!
#endif
#endif
#ifdef HW_LILYGO #ifdef HW_LILYGO
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) #if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
#if defined(CAN_ADDON) || defined(CANFD_ADDON) || defined(CHADEMO_BATTERY) #if defined(CAN_ADDON) || defined(CANFD_ADDON) || defined(CHADEMO_BATTERY)

View file

@ -75,10 +75,6 @@
#include "SUNGROW-CAN.h" #include "SUNGROW-CAN.h"
#endif #endif
#ifdef SERIAL_LINK_TRANSMITTER
#include "SERIAL-LINK-TRANSMITTER-INVERTER.h"
#endif
#ifdef CAN_INVERTER_SELECTED #ifdef CAN_INVERTER_SELECTED
void update_values_can_inverter(); void update_values_can_inverter();
void map_can_frame_to_variable_inverter(CAN_frame rx_frame); void map_can_frame_to_variable_inverter(CAN_frame rx_frame);

View file

@ -1,195 +0,0 @@
#include "../include.h"
#ifdef SERIAL_LINK_TRANSMITTER
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
#include "SERIAL-LINK-TRANSMITTER-INVERTER.h"
/*
* SerialDataLink
* txid=1, rxid=0 gives this the startup transmit priority_queue
* Will transmit max 16 int variable - receive none
*/
#define BATTERY_SEND_NUM_VARIABLES 16
#define BATTERY_RECV_NUM_VARIABLES 1
#ifdef BATTERY_RECV_NUM_VARIABLES
const uint8_t receivingNumVariables = BATTERY_RECV_NUM_VARIABLES;
#else
const uint8_t receivingNumVariables = 0;
#endif
// txid,rxid,num_tx,num_rx
SerialDataLink dataLinkTransmit(Serial2, 0x01, 0, BATTERY_SEND_NUM_VARIABLES, receivingNumVariables);
void printSendingValues();
void _getData() {
datalayer.system.status.inverter_allows_contactor_closing = dataLinkTransmit.getReceivedData(0);
//var2 = dataLinkTransmit.getReceivedData(1); // For future expansion,
//var3 = dataLinkTransmit.getReceivedData(2); // if inverter needs to send data to battery
}
void manageSerialLinkTransmitter() {
static bool initLink = false;
static unsigned long updateTime = 0;
static bool lasterror = false;
//static unsigned long lastNoError = 0;
static unsigned long transmitGoodSince = 0;
static unsigned long lastGood = 0;
unsigned long currentTime = millis();
dataLinkTransmit.run();
#ifdef BATTERY_RECV_NUM_VARIABLES
bool readError = dataLinkTransmit.checkReadError(true); // check for error & clear error flag
if (dataLinkTransmit.checkNewData(true)) // true = clear Flag
{
_getData();
}
#endif
if (currentTime - updateTime > INTERVAL_100_MS) {
updateTime = currentTime;
if (!initLink) {
initLink = true;
transmitGoodSince = currentTime;
dataLinkTransmit.setUpdateInterval(INTERVAL_10_S);
}
bool sendError = dataLinkTransmit.checkTransmissionError(true);
if (sendError) {
logging.print(currentTime);
logging.println(" - ERROR: Serial Data Link - SEND Error");
lasterror = true;
transmitGoodSince = currentTime;
}
/* new feature */
/* @getLastAcknowledge(bool resetFlag)
* - returns:
* -2 NACK received from receiver
* -1 no ACK received
* 0 no activity
* 1 ACK received
* resetFlag = true will clear to 0
*/
int ackReceived = dataLinkTransmit.getLastAcknowledge(true);
if (ackReceived > 0)
lastGood = currentTime;
if (lasterror && (ackReceived > 0)) {
lasterror = false;
logging.print(currentTime);
logging.println(" - RECOVERY: Serial Data Link - Send GOOD");
}
//--- reporting every 60 seconds that transmission is good
if (currentTime - transmitGoodSince > INTERVAL_60_S) {
transmitGoodSince = currentTime;
logging.print(currentTime);
logging.println(" - Transmit Good");
// printUsefullData();
#ifdef DEBUG_LOG
void printSendingValues();
#endif
}
//--- report that Errors been ocurring for > 60 seconds
if (currentTime - lastGood > INTERVAL_60_S) {
lastGood = currentTime;
logging.print(currentTime);
logging.println(" - Transmit Failed : 60 seconds");
// print the max_ data
logging.println("SerialDataLink : bms_status=4");
logging.println("SerialDataLink : LEDcolor = RED");
logging.println("SerialDataLink : max_target_discharge_power = 0");
logging.println("SerialDataLink : max_target_charge_power = 0");
datalayer.battery.status.max_discharge_power_W = 0;
datalayer.battery.status.max_charge_power_W = 0;
set_event(EVENT_SERIAL_TX_FAILURE, 0);
// throw error
}
/*
// lastMessageReceived from CAN bus (Battery)
if (currentTime - lastMessageReceived > (5 * 60000) ) // 5 minutes
{
logging.print(millis());
logging.println(" - Data Stale : 5 minutes");
// throw error
// stop transmitting until fresh
}
*/
static unsigned long updateDataTime = 0;
if (currentTime - updateDataTime > INTERVAL_1_S) {
strncpy(datalayer.system.info.inverter_protocol, "Serial link to another LilyGo board", 63);
datalayer.system.info.inverter_protocol[63] = '\0';
updateDataTime = currentTime;
dataLinkTransmit.updateData(0, datalayer.battery.status.real_soc);
dataLinkTransmit.updateData(1, datalayer.battery.status.soh_pptt);
dataLinkTransmit.updateData(2, datalayer.battery.status.voltage_dV);
dataLinkTransmit.updateData(3, datalayer.battery.status.current_dA);
dataLinkTransmit.updateData(4, datalayer.battery.info.total_capacity_Wh / 10); //u32, remove .0 to fit 16bit
dataLinkTransmit.updateData(5,
datalayer.battery.status.remaining_capacity_Wh / 10); //u32, remove .0 to fit 16bit
dataLinkTransmit.updateData(6,
datalayer.battery.status.max_discharge_power_W / 10); //u32, remove .0 to fit 16bit
dataLinkTransmit.updateData(7, datalayer.battery.status.max_charge_power_W / 10); //u32, remove .0 to fit 16bit
dataLinkTransmit.updateData(8, datalayer.battery.status.bms_status);
dataLinkTransmit.updateData(9, datalayer.battery.status.active_power_W / 10); //u32, remove .0 to fit 16bit
dataLinkTransmit.updateData(10, datalayer.battery.status.temperature_min_dC);
dataLinkTransmit.updateData(11, datalayer.battery.status.temperature_max_dC);
dataLinkTransmit.updateData(12, datalayer.battery.status.cell_max_voltage_mV);
dataLinkTransmit.updateData(13, datalayer.battery.status.cell_min_voltage_mV);
dataLinkTransmit.updateData(14, (int16_t)datalayer.battery.info.chemistry);
dataLinkTransmit.updateData(15, datalayer.system.status.battery_allows_contactor_closing);
}
}
}
void printSendingValues() {
logging.println("Values from battery: ");
logging.print("SOC: ");
logging.print(datalayer.battery.status.real_soc);
logging.print(" SOH: ");
logging.print(datalayer.battery.status.soh_pptt);
logging.print(" Voltage: ");
logging.print(datalayer.battery.status.voltage_dV);
logging.print(" Current: ");
logging.print(datalayer.battery.status.current_dA);
logging.print(" Capacity: ");
logging.print(datalayer.battery.info.total_capacity_Wh);
logging.print(" Remain cap: ");
logging.print(datalayer.battery.status.remaining_capacity_Wh);
logging.print(" Max discharge W: ");
logging.print(datalayer.battery.status.max_discharge_power_W);
logging.print(" Max charge W: ");
logging.print(datalayer.battery.status.max_charge_power_W);
logging.print(" BMS status: ");
logging.print(datalayer.battery.status.bms_status);
logging.print(" Power: ");
logging.print(datalayer.battery.status.active_power_W);
logging.print(" Temp min: ");
logging.print(datalayer.battery.status.temperature_min_dC);
logging.print(" Temp max: ");
logging.print(datalayer.battery.status.temperature_max_dC);
logging.print(" Cell max: ");
logging.print(datalayer.battery.status.cell_max_voltage_mV);
logging.print(" Cell min: ");
logging.print(datalayer.battery.status.cell_min_voltage_mV);
logging.print(" LFP : ");
logging.print(datalayer.battery.info.chemistry);
logging.print(" Battery Allows Contactor Closing: ");
logging.print(datalayer.system.status.battery_allows_contactor_closing);
logging.print(" Inverter Allows Contactor Closing: ");
logging.print(datalayer.system.status.inverter_allows_contactor_closing);
logging.println("");
}
#endif

View file

@ -1,11 +0,0 @@
#ifndef SERIAL_LINK_TRANSMITTER_INVERTER_H
#define SERIAL_LINK_TRANSMITTER_INVERTER_H
#include <Arduino.h>
#include "../include.h"
#include "../lib/mackelec-SerialDataLink/SerialDataLink.h"
void manageSerialLinkTransmitter();
void setup_inverter(void);
#endif

View file

@ -1,607 +0,0 @@
// SerialDataLink.cpp
#include "SerialDataLink.h"
const uint16_t crcTable[256] = {
0, 32773, 32783, 10, 32795, 30, 20, 32785,
32819, 54, 60, 32825, 40, 32813, 32807, 34,
32867, 102, 108, 32873, 120, 32893, 32887, 114,
80, 32853, 32863, 90, 32843, 78, 68, 32833,
32963, 198, 204, 32969, 216, 32989, 32983, 210,
240, 33013, 33023, 250, 33003, 238, 228, 32993,
160, 32933, 32943, 170, 32955, 190, 180, 32945,
32915, 150, 156, 32921, 136, 32909, 32903, 130,
33155, 390, 396, 33161, 408, 33181, 33175, 402,
432, 33205, 33215, 442, 33195, 430, 420, 33185,
480, 33253, 33263, 490, 33275, 510, 500, 33265,
33235, 470, 476, 33241, 456, 33229, 33223, 450,
320, 33093, 33103, 330, 33115, 350, 340, 33105,
33139, 374, 380, 33145, 360, 33133, 33127, 354,
33059, 294, 300, 33065, 312, 33085, 33079, 306,
272, 33045, 33055, 282, 33035, 270, 260, 33025,
33539, 774, 780, 33545, 792, 33565, 33559, 786,
816, 33589, 33599, 826, 33579, 814, 804, 33569,
864, 33637, 33647, 874, 33659, 894, 884, 33649,
33619, 854, 860, 33625, 840, 33613, 33607, 834,
960, 33733, 33743, 970, 33755, 990, 980, 33745,
33779, 1014, 1020, 33785, 1000, 33773, 33767, 994,
33699, 934, 940, 33705, 952, 33725, 33719, 946,
912, 33685, 33695, 922, 33675, 910, 900, 33665,
640, 33413, 33423, 650, 33435, 670, 660, 33425,
33459, 694, 700, 33465, 680, 33453, 33447, 674,
33507, 742, 748, 33513, 760, 33533, 33527, 754,
720, 33493, 33503, 730, 33483, 718, 708, 33473,
33347, 582, 588, 33353, 600, 33373, 33367, 594,
624, 33397, 33407, 634, 33387, 622, 612, 33377,
544, 33317, 33327, 554, 33339, 574, 564, 33329,
33299, 534, 540, 33305, 520, 33293, 33287, 514
};
union Convert
{
uint16_t u16;
int16_t i16;
struct
{
byte low;
byte high;
};
}convert;
/*
#define SET_PA6() (GPIOA->BSRR = GPIO_BSRR_BS6)
#define CLEAR_PA6() (GPIOA->BSRR = GPIO_BSRR_BR6)
// Macro to toggle PA6
#define TOGGLE_PA6() (GPIOA->ODR ^= GPIO_ODR_ODR6)
*/
// Constructor
SerialDataLink::SerialDataLink(Stream &serial, uint8_t transmitID, uint8_t receiveID, uint8_t maxIndexTX, uint8_t maxIndexRX, bool enableRetransmit)
: serial(serial), transmitID(transmitID), receiveID(receiveID), maxIndexTX(maxIndexTX), maxIndexRX(maxIndexRX), retransmitEnabled(enableRetransmit) {
// Initialize buffers and state variables
txBufferIndex = 0;
isTransmitting = false;
isReceiving = false;
transmissionError = false;
readError = false;
newData = false;
// Initialize data arrays and update flags
memset(dataArrayTX, 0, sizeof(dataArrayTX));
memset(dataArrayRX, 0, sizeof(dataArrayRX));
memset(dataUpdated, 0, sizeof(dataUpdated));
memset(lastSent , 0, sizeof(lastSent ));
// Additional initialization as required
}
void SerialDataLink::updateData(uint8_t index, int16_t value)
{
if (index < maxIndexTX)
{
if (dataArrayTX[index] != value)
{
dataArrayTX[index] = value;
dataUpdated[index] = true;
lastSent[index] = millis();
}
}
}
int16_t SerialDataLink::getReceivedData(uint8_t index)
{
if (index < dataArraySizeRX) {
return dataArrayRX[index];
} else {
// Handle the case where the index is out of bounds
return -1;
}
}
bool SerialDataLink::checkTransmissionError(bool resetFlag)
{
bool currentStatus = transmissionError;
if (resetFlag && transmissionError) {
transmissionError = false;
}
return currentStatus;
}
int SerialDataLink::getLastAcknowledge(bool resetFlag)
{
int result = lastAcknowledgeStatus;
if (resetFlag)
{
lastAcknowledgeStatus = 0; // Reset to default state
}
return result;
}
bool SerialDataLink::checkReadError(bool reset)
{
bool error = readError;
if (reset) {
readError = false;
}
return error;
}
bool SerialDataLink::checkNewData(bool resetFlag) {
bool currentStatus = newData;
if (resetFlag && newData) {
newData = false;
}
return currentStatus;
}
void SerialDataLink::muteACK(bool mute)
{
muteAcknowledgement = mute;
}
void SerialDataLink::run()
{
unsigned long currentTime = millis();
static DataLinkState oldstate;
// Check if state has not changed for a prolonged period
if (oldstate != currentState)
{
lastStateChangeTime = currentTime;
oldstate = currentState;
}
if ((currentTime - lastStateChangeTime) > stateChangeTimeout) {
// Reset the state to Idle and perform necessary cleanup
currentState = DataLinkState::Idle;
// Perform any additional cleanup or reinitialization here
// ...
lastStateChangeTime = currentTime; // Reset the last state change time
}
switch (currentState)
{
case DataLinkState::Idle:
// Decide if the device should start transmitting
currentState = DataLinkState::Receiving;
if (shouldTransmit())
{
currentState = DataLinkState::WaitTobuildPacket;
}
break;
case DataLinkState::WaitTobuildPacket:
constructPacket();
if (isTransmitting)
{
currentState = DataLinkState::Transmitting;
}
break;
case DataLinkState::Transmitting:
sendNextByte();
// Check if the transmission is complete
if (transmissionComplete)
{
transmissionComplete = false;
isTransmitting = false;
currentState = DataLinkState::WaitingForAck; // Move to WaitingForAck state
}
break;
case DataLinkState::WaitingForAck:
if (ackTimeout())
{
// Handle ACK timeout scenario
transmissionError = true;
lastAcknowledgeStatus = -1;
//--- if no ACK's etc received may as well move to Transmitting
currentState = DataLinkState::Idle;
}
if (ackReceived())
{
// No data to send from the other device
currentState = DataLinkState::Idle;
}
if (requestToSend)
{
// The other device has data to send (indicated by ACK+RTT)
currentState = DataLinkState::Receiving;
requestToSend = false;
}
break;
case DataLinkState::Receiving:
read();
if (readComplete)
{
readComplete = false;
currentState = DataLinkState::SendingAck;
}
break;
case DataLinkState::SendingAck:
constructPacket();
if (muteAcknowledgement && (needToACK || needToNACK))
{
needToACK = false;
needToNACK = false;
}
uint8_t ack;
// now it is known which acknoledge need sending since last Reception
if (needToACK)
{
needToACK = false;
ack = (txBufferIndex > 5) ? ACK_RTT_CODE : ACK_CODE;
serial.write(ack);
}
if (needToNACK)
{
needToNACK = false;
ack = (txBufferIndex > 5) ? NACK_RTT_CODE : NACK_CODE;
serial.write(ack);
}
currentState = DataLinkState::Idle;
if (isTransmitting)
{
currentState = DataLinkState::Wait;
}
break;
case DataLinkState::Wait:
{
static unsigned long waitTimer=0;
if (waitTimer == 0) waitTimer = currentTime;
if (currentTime - waitTimer > 20)
{
waitTimer=0;
currentState = DataLinkState::Transmitting;
}
}
break;
default:
currentState = DataLinkState::Idle;
}
}
void SerialDataLink::updateState(DataLinkState newState)
{
if (currentState != newState)
{
currentState = newState;
lastStateChangeTime = millis();
}
}
bool SerialDataLink::shouldTransmit()
{
// Priority condition: Device with transmitID = 1 and receiveID = 0 has the highest priority
if (transmitID == 1 && receiveID == 0)
{
return true;
}
return false;
}
void SerialDataLink::constructPacket()
{
if (maxIndexTX <1) return;
if (!isTransmitting)
{
lastTransmissionTime = millis();
txBufferIndex = 0; // Reset the TX buffer index
addToTxBuffer(headerChar);
addToTxBuffer(transmitID);
addToTxBuffer(0); // EOT position - place holder
unsigned long currentTime = millis();
int count = txBufferIndex;
for (uint8_t i = 0; i < maxIndexTX; i++)
{
if (dataUpdated[i] || (currentTime - lastSent[i] >= updateInterval))
{
addToTxBuffer(i);
convert.i16 = dataArrayTX[i];
addToTxBuffer(convert.high);
addToTxBuffer(convert.low);
dataUpdated[i] = false;
lastSent[i] = currentTime; // Update the last sent time for this index
}
}
if (count == txBufferIndex)
{
// No data was added to the buffer, so no need to send a packet
return;
}
addToTxBuffer(eotChar);
//----- assign EOT position
txBuffer[2] = txBufferIndex - 1;
uint16_t crc = calculateCRC16(txBuffer, txBufferIndex);
convert.u16 = crc;
addToTxBuffer(convert.high);
addToTxBuffer(convert.low);
isTransmitting = true;
}
}
void SerialDataLink::addToTxBuffer(uint8_t byte)
{
if (txBufferIndex < txBufferSize)
{
txBuffer[txBufferIndex] = byte;
txBufferIndex++;
}
}
bool SerialDataLink::sendNextByte()
{
if (!isTransmitting) return false;
if (txBufferIndex >= txBufferSize)
{
txBufferIndex = 0; // Reset the TX buffer index
isTransmitting = false;
return false; // Buffer was fully sent, end transmission
}
serial.write(txBuffer[sendBufferIndex]);
sendBufferIndex++;
if (sendBufferIndex >= txBufferIndex)
{
isTransmitting = false;
txBufferIndex = 0; // Reset the TX buffer index for the next packet
sendBufferIndex = 0;
transmissionComplete = true;
return true; // Packet was fully sent
}
return false; // More bytes remain to be sent
}
bool SerialDataLink::ackReceived()
{
// Check if there is data available to read
int count = 0;
if (serial.available() )
{
count++;
// Peek at the next byte without removing it from the buffer
uint8_t nextByte = serial.peek();
if (nextByte == headerChar)
{
requestToSend = true;
transmissionError = true;
return false;
}
uint8_t receivedByte = serial.read();
switch (receivedByte)
{
case ACK_CODE:
// Handle standard ACK
lastAcknowledgeStatus = 1;
return true;
case ACK_RTT_CODE:
// Handle ACK with request to transmit
requestToSend = true;
lastAcknowledgeStatus = 1;
return true;
case NACK_RTT_CODE:
requestToSend = true;
case NACK_CODE:
transmissionError = true;
lastAcknowledgeStatus = -2;
return true;
break;
default:
break;
}
}
return false; // No ACK, NACK, or new packet received
}
bool SerialDataLink::ackTimeout()
{
// Check if the current time has exceeded the last transmission time by the ACK timeout period
if (millis() - lastTransmissionTime > ACK_TIMEOUT)
{
return true; // Timeout occurred
}
return false; // No timeout
}
void SerialDataLink::read()
{
if (maxIndexRX < 1) return;
int count = 0;
while (serial.available() && count < 10)
{
count++;
if (millis() - lastHeaderTime > PACKET_TIMEOUT && rxBufferIndex > 0)
{
// Timeout occurred, reset buffer and pointer
rxBufferIndex = 0;
eotPosition = 0;
readError = true;
}
uint8_t incomingByte = serial.read();
switch (rxBufferIndex) {
case 0: // Looking for the header
if (incomingByte == headerChar)
{
lastHeaderTime = millis();
rxBuffer[rxBufferIndex] = incomingByte;
rxBufferIndex++;
}
break;
case 1: // Looking for the address
if (incomingByte == receiveID) {
rxBuffer[rxBufferIndex] = incomingByte;
rxBufferIndex++;
} else {
// Address mismatch, reset to look for a new packet
rxBufferIndex = 0;
}
break;
case 2: // EOT position
eotPosition = incomingByte;
rxBuffer[rxBufferIndex] = incomingByte;
rxBufferIndex++;
break;
default:
// Normal data handling
rxBuffer[rxBufferIndex] = incomingByte;
rxBufferIndex++;
if (isCompletePacket())
{
processPacket();
rxBufferIndex = 0; // Reset for the next packet
readComplete = true; // Indicate that read operation is complete
}
// Check for buffer overflow
if (rxBufferIndex >= rxBufferSize)
{
rxBufferIndex = 0;
}
break;
}
}
}
bool SerialDataLink::isCompletePacket() {
if (rxBufferIndex - 3 < eotPosition) return false;
// Ensure there are enough bytes for EOT + 2-byte CRC
// Check if the third-last byte is the EOT character
if (rxBuffer[eotPosition] == eotChar)
{
return true;
}
return false;
}
bool SerialDataLink::checkCRC()
{
uint16_t receivedCrc;
if (rxBufferIndex < 3)
{
// Not enough data for CRC check
return false;
}
convert.high = rxBuffer[rxBufferIndex - 2];
convert.low = rxBuffer[rxBufferIndex - 1];
receivedCrc = convert.u16;
// Calculate CRC for the received data (excluding the CRC bytes themselves)
uint16_t calculatedCrc = calculateCRC16(rxBuffer, rxBufferIndex - 2);
return receivedCrc == calculatedCrc;
}
void SerialDataLink::processPacket()
{
if (!checkCRC()) {
// CRC check failed, handle the error
readError = true;
return;
}
// Start from index 3 to skip the SOT and ADDRESS and EOT Position characters
uint8_t i = 3;
while (i < eotPosition)
{
uint8_t arrayID = rxBuffer[i++];
// Make sure there's enough data for a complete int16 (2 bytes)
if (i + 1 >= rxBufferIndex) {
readError = true;
needToNACK = true;
return; // Incomplete packet or buffer overflow
}
// Combine the next two bytes into an int16 value
int16_t value = (int16_t(rxBuffer[i]) << 8) | int16_t(rxBuffer[i + 1]);
i += 2;
// Handle the array ID and value here
if (arrayID < dataArraySizeRX) {
dataArrayRX[arrayID] = value;
}
else
{
// Handle invalid array ID
readError = true;
needToNACK = true;
return;
}
newData = true;
needToACK = true;
}
}
void SerialDataLink::setUpdateInterval(unsigned long interval) {
updateInterval = interval;
}
void SerialDataLink::setAckTimeout(unsigned long timeout) {
ACK_TIMEOUT = timeout;
}
void SerialDataLink::setPacketTimeout(unsigned long timeout) {
PACKET_TIMEOUT = timeout;
}
void SerialDataLink::setHeaderChar(char header)
{
headerChar = header;
}
void SerialDataLink::setEOTChar(char eot)
{
eotChar = eot;
}
uint16_t SerialDataLink::calculateCRC16(const uint8_t* data, size_t length)
{
uint16_t crc = 0xFFFF; // Start value for CRC
for (size_t i = 0; i < length; i++)
{
uint8_t index = (crc >> 8) ^ data[i];
crc = (crc << 8) ^ crcTable[index];
}
return crc;
}

View file

@ -1,185 +0,0 @@
/**
* @file SerialDataLink.h
* @brief Half-Duplex Serial Data Link for Arduino
*
* This file contains the definition of the SerialDataLink class, designed to facilitate
* half-duplex communication between Arduino controllers. The class employs a non-blocking,
* poll-based approach to transmit and receive data, making it suitable for applications
* where continuous monitoring and variable transfer between controllers are required.
*
* The half-duplex nature of this implementation allows for data transfer in both directions,
* but not simultaneously, ensuring a controlled communication flow and reducing the likelihood
* of data collision.
*
*
* @author MackElec
* @web https://github.com/mackelec/SerialDataLink
* @license MIT
*/
// ... Class definition ...
/**
* @class SerialDataLink
* @brief Class for managing half-duplex serial communication.
*
* Provides functions to send and receive data in a half-duplex manner over a serial link.
* It supports non-blocking operation with a polling approach to check for new data and
* transmission errors.
*
* Public Methods:
* - SerialDataLink(): Constructor to initialize the communication parameters.
* - run(): Main method to be called frequently to handle data transmission and reception.
* - updateData(): Method to update data to be transmitted.
* - getReceivedData(): Retrieves data received from the serial link.
* - checkNewData(): Checks if new data has been received.
* - checkTransmissionError(): Checks for transmission errors.
* - checkReadError(): Checks for read errors.
* - setUpdateInterval(): Sets the interval for data updates.
* - setAckTimeout(): Sets the timeout for acknowledgments.
* - setPacketTimeout(): Sets the timeout for packet reception.
* - setHeaderChar(): Sets the character used to denote the start of a packet.
* - setEOTChar(): Sets the character used to denote the end of a packet.
*/
#ifndef SERIALDATALINK_H
#define SERIALDATALINK_H
#include <Arduino.h>
class SerialDataLink {
public:
// Constructor
SerialDataLink(Stream &serial, uint8_t transmitID, uint8_t receiveID, uint8_t maxIndexTX, uint8_t maxIndexRX, bool enableRetransmit = false);
// Method to handle data transmission and reception
void run();
void updateData(uint8_t index, int16_t value);
// Check if new data has been received
bool checkNewData(bool resetFlag);
int16_t getReceivedData(uint8_t index);
// Check for errors
bool checkTransmissionError(bool resetFlag);
int getLastAcknowledge(bool resetFlag);
bool checkReadError(bool resetFlag);
// Setter methods for various parameters and special characters
void setUpdateInterval(unsigned long interval);
void setAckTimeout(unsigned long timeout);
void setPacketTimeout(unsigned long timeout);
void setHeaderChar(char header);
void setEOTChar(char eot);
void muteACK(bool mute);
private:
enum class DataLinkState
{
Idle,
WaitTobuildPacket,
Transmitting,
WaitingForAck,
Receiving,
SendingAck,
Wait,
Error
};
DataLinkState currentState;
Stream &serial;
uint8_t transmitID;
uint8_t receiveID;
// Separate max indices for TX and RX
const uint8_t maxIndexTX;
const uint8_t maxIndexRX;
// Buffer and state management
static const uint8_t txBufferSize = 128; // Adjust size as needed
static const uint8_t rxBufferSize = 128; // Adjust size as needed
uint8_t txBuffer[txBufferSize];
uint8_t rxBuffer[rxBufferSize];
uint8_t txBufferIndex;
uint8_t rxBufferIndex;
uint8_t sendBufferIndex = 0;
bool isTransmitting;
bool transmissionComplete = false;
bool isReceiving;
bool readComplete = false;
bool retransmitEnabled;
bool transmissionError = false;
int lastAcknowledgeStatus = 0;
bool readError = false;
bool muteAcknowledgement = false;
// Data arrays and update management
static const uint8_t dataArraySizeTX = 20; // Adjust size as needed for TX
static const uint8_t dataArraySizeRX = 20; // Adjust size as needed for RX
int16_t dataArrayTX[dataArraySizeTX];
int16_t dataArrayRX[dataArraySizeRX];
bool dataUpdated[dataArraySizeTX];
unsigned long lastSent[dataArraySizeTX];
// times in milliseconds
unsigned long updateInterval = 1000;
unsigned long ACK_TIMEOUT = 200;
unsigned long PACKET_TIMEOUT = 200;
unsigned long stateChangeTimeout = 300;
unsigned long lastStateChangeTime = 0;
// Special characters for packet framing
char headerChar = '<';
char eotChar = '>';
static const uint8_t ACK_CODE = 0x06; // Standard acknowledgment
static const uint8_t ACK_RTT_CODE = 0x07; // Acknowledgment with request to transmit
static const uint8_t NACK_CODE = 0x08; // Negative acknowledgment
static const uint8_t NACK_RTT_CODE = 0x09; // Negative acknowledgment with request to transmit
// Internal methods for packet construction, transmission, and reception
bool shouldTransmit();
void constructPacket();
void addToTxBuffer(uint8_t byte);
bool sendNextByte();
bool ackReceived();
bool ackTimeout();
void updateState(DataLinkState newState);
// Internal methods for reception
void read();
void handleResendRequest();
bool isCompletePacket();
void processPacket();
void sendACK();
bool checkCRC();
uint16_t calculateCRC16(const uint8_t* data, size_t length);
unsigned long lastTransmissionTime;
bool requestToSend = false;
unsigned long lastHeaderTime = 0;
bool newData = false;
bool needToACK = false;
bool needToNACK = false;
uint8_t eotPosition = 0;
};
#endif // SERIALDATALINK_H