diff --git a/README.md b/README.md index 3e34d254..0ee4b3e6 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,12 @@ This video explains all the above mentioned steps: https://youtu.be/_mH2AjnAjDk ## Dependencies 📖 -This code uses two libraries, ESP32-Arduino-CAN (https://github.com/miwagner/ESP32-Arduino-CAN/) slightly modified for this usecase, and the eModbus library (https://github.com/eModbus/eModbus). Both these are already located in the Software folder for an easy start. +This code uses the following libraries, already located in the lib folder for an easy start: +- ESP32-Arduino-CAN (https://github.com/miwagner/ESP32-Arduino-CAN/) slightly modified for this usecase +- eModbus library (https://github.com/eModbus/eModbus) +- Adafruit Neopixel (https://github.com/adafruit/Adafruit_NeoPixel) +- mackelec SerialDataLink (https://github.com/mackelec/SerialDataLink) +- pierremolinaro acan2515 (https://github.com/pierremolinaro/acan2515) It is also based on the info found in the following excellent repositories/websites: - https://gitlab.com/pelle8/gen24 diff --git a/Software/Software.ino b/Software/Software.ino index e02a1979..d01a10c9 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -11,12 +11,15 @@ #include "src/lib/eModbus-eModbus/Logging.h" #include "src/lib/eModbus-eModbus/ModbusServerRTU.h" #include "src/lib/eModbus-eModbus/scripts/mbServerFCs.h" +#include "src/lib/mackelec-SerialDataLink/SerialDataLink.h" #include "src/lib/miwagner-ESP32-Arduino-CAN/CAN_config.h" #include "src/lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" // Interval settings int intervalUpdateValues = 4800; // Interval at which to update inverter values / Modbus registers +const int interval1 = 1; // Interval for 1ms tasks const int interval10 = 10; // Interval for 10ms tasks +unsigned long previousMillis1ms = 0; unsigned long previousMillis10ms = 50; unsigned long previousMillisUpdateVal = 0; @@ -128,6 +131,9 @@ void loop() { #ifdef DUAL_CAN receive_can2(); #endif +#ifdef SERIAL_LINK_TRANSMITTER_INVERTER + receive_serial(); +#endif // Process if (millis() - previousMillis10ms >= interval10) // Every 10ms @@ -150,6 +156,9 @@ void loop() { #ifdef DUAL_CAN send_can2(); #endif +#ifdef SERIAL_LINK_RECEIVER_FROM_BATTERY + send_serial(); +#endif } // Initialization functions @@ -217,6 +226,13 @@ void init_modbus() { pinMode(PIN_5V_EN, OUTPUT); digitalWrite(PIN_5V_EN, HIGH); +#if defined(SERIAL_LINK_RECEIVER_FROM_BATTERY) || defined(SERIAL_LINK_TRANSMITTER_INVERTER) + Serial2.begin(9600); // If the Modbus RTU port will be used for serial link +#if defined(BYD_MODBUS) || defined(LUNA2000_MODBUS) +#error Modbus pins cannot be used for Serial and Modbus at the same time! +#endif +#endif + #ifdef BYD_MODBUS // Init Static data to the RTU Modbus handle_static_data_modbus_byd(); @@ -386,6 +402,26 @@ void send_can() { #endif } +#ifdef SERIAL_LINK_RECEIVER_FROM_BATTERY +void send_serial() { + static unsigned long currentMillis = millis(); + if (currentMillis - previousMillis1ms >= interval1) { + previousMillis1ms = currentMillis; + manageSerialLinkReceiver(); + } +} +#endif + +#ifdef SERIAL_LINK_TRANSMITTER_INVERTER +void receive_serial() { + static unsigned long currentMillis = millis(); + if (currentMillis - previousMillis1ms >= interval1) { + previousMillis1ms = currentMillis; + manageSerialLinkTransmitter(); + } +} +#endif + #ifdef DUAL_CAN void receive_can2() { // This function is similar to receive_can, but just takes care of inverters in the 2nd bus. // Depending on which inverter is selected, we forward this to their respective CAN routines diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index c05068c6..24d2491e 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -42,5 +42,7 @@ //#define CONTACTOR_CONTROL //Enable this line to have pins 25,32,33 handle automatic precharge/contactor+/contactor- closing sequence //#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM logic for contactors, which lower power consumption and heat generation //#define DUAL_CAN //Enable this line to activate an isolated secondary CAN Bus using add-on MCP2515 controller (Needed for FoxESS inverters) +//#define SERIAL_LINK_RECEIVER_FROM_BATTERY //Enable this line to send battery data over Modbus pins to another Lilygo (This LilyGo interfaces with battery) +//#define SERIAL_LINK_TRANSMITTER_INVERTER //Enable this line to receive battery data over Modbus pins from another Lilygo (This LilyGo interfaces with inverter) #endif diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index d68a892f..1880d8e8 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -32,4 +32,9 @@ #ifdef TEST_FAKE_BATTERY #include "TEST-FAKE-BATTERY.h" //See this file for more Fake battery settings #endif + +#ifdef SERIAL_LINK_RECEIVER_FROM_BATTERY +#include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h" +#endif + #endif diff --git a/Software/src/battery/SERIAL-LINK-RECEIVER-FROM-BATTERY.cpp b/Software/src/battery/SERIAL-LINK-RECEIVER-FROM-BATTERY.cpp new file mode 100644 index 00000000..9d286324 --- /dev/null +++ b/Software/src/battery/SERIAL-LINK-RECEIVER-FROM-BATTERY.cpp @@ -0,0 +1,115 @@ +// SERIAL-LINK-RECEIVER-FROM-BATTERY.cpp + +#include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h" + +//#define INVERTER_SEND_NUM_VARIABLES 3 //--- comment out if nothing to send +#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 + +// txid,rxid, num_send,num_recv +SerialDataLink dataLinkReceive(Serial2, 0, 0x01, sendingNumVariables, + INVERTER_RECV_NUM_VARIABLES); // ... + +void __getData() { + SOC = (uint16_t)dataLinkReceive.getReceivedData(0); + StateOfHealth = (uint16_t)dataLinkReceive.getReceivedData(1); + battery_voltage = (uint16_t)dataLinkReceive.getReceivedData(2); + battery_current = (uint16_t)dataLinkReceive.getReceivedData(3); + capacity_Wh = (uint16_t)dataLinkReceive.getReceivedData(4); + remaining_capacity_Wh = (uint16_t)dataLinkReceive.getReceivedData(5); + max_target_discharge_power = (uint16_t)dataLinkReceive.getReceivedData(6); + max_target_charge_power = (uint16_t)dataLinkReceive.getReceivedData(7); + bms_status = (uint16_t)dataLinkReceive.getReceivedData(8); + bms_char_dis_status = (uint16_t)dataLinkReceive.getReceivedData(9); + stat_batt_power = (uint16_t)dataLinkReceive.getReceivedData(10); + temperature_min = (uint16_t)dataLinkReceive.getReceivedData(11); + temperature_max = (uint16_t)dataLinkReceive.getReceivedData(12); + cell_max_voltage = (uint16_t)dataLinkReceive.getReceivedData(13); + cell_min_voltage = (uint16_t)dataLinkReceive.getReceivedData(14); + batteryAllowsContactorClosing = (uint16_t)dataLinkReceive.getReceivedData(15); +} + +void updateData() { + // --- update with fresh data + /* + dataLinkReceive.updateData(0,var1); + dataLinkReceive.updateData(1,var2); + dataLinkReceive.updateData(2,var3); + */ +} + +/* +* @ 9600bps, assume void manageSerialLinkReceiver() +* is called every 1mS +*/ + +void manageSerialLinkReceiver() { + static bool lasterror = false; + static unsigned long lastGood; + static uint16_t lastGoodMaxCharge; + static uint16_t lastGoodMaxDischarge; + static bool initLink = false; + + 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 + LEDcolor = GREEN; + if (readError) { + LEDcolor = RED; + bms_status = 4; //FAULT + Serial.print(currentTime); + Serial.println(" - ERROR: Serial Data Link - Read Error"); + lasterror = true; + } else { + if (lasterror) { + lasterror = false; + Serial.print(currentTime); + Serial.println(" - RECOVERY: Serial Data Link - Read GOOD"); + } + lastGood = currentTime; + } + if (dataLinkReceive.checkNewData(true)) // true = clear Flag + { + __getData(); + lastGoodMaxCharge = max_target_charge_power; + lastGoodMaxDischarge = max_target_discharge_power; + } + + unsigned long minutesLost = (currentTime - lastGood) / 60000UL; + ; + if (minutesLost > 0 && lastGood > 0) { + // lose 25% each minute of data loss + if (minutesLost < 4) { + max_target_charge_power = (lastGoodMaxCharge * (4 - minutesLost)) / 4; + max_target_discharge_power = (lastGoodMaxDischarge * (4 - minutesLost)) / 4; + } else { + max_target_charge_power = 0; + max_target_discharge_power = 0; + } + } + + 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 +} diff --git a/Software/src/battery/SERIAL-LINK-RECEIVER-FROM-BATTERY.h b/Software/src/battery/SERIAL-LINK-RECEIVER-FROM-BATTERY.h new file mode 100644 index 00000000..65dc04fd --- /dev/null +++ b/Software/src/battery/SERIAL-LINK-RECEIVER-FROM-BATTERY.h @@ -0,0 +1,38 @@ +// SERIAL-LINK-RECEIVER-FROM-BATTERY.h + +#ifndef SERIAL_LINK_RECEIVER_FROM_BATTERY_H +#define SERIAL_LINK_RECEIVER_FROM_BATTERY_H + +#include +#include "../../USER_SETTINGS.h" +#include "../devboard/config.h" // Needed for LED defines +#include "../lib/mackelec-SerialDataLink/SerialDataLink.h" + +// https://github.com/mackelec/SerialDataLink + +#define ABSOLUTE_MAX_VOLTAGE \ + 4040 // 404.4V,if battery voltage goes over this, charging is not possible (goes into forced discharge) +#define ABSOLUTE_MIN_VOLTAGE 3100 // 310.0V if battery voltage goes under this, discharging further is disabled + +// These parameters need to be mapped for the inverter +extern uint16_t SOC; //SOC%, 0-100.00 (0-10000) +extern uint16_t StateOfHealth; //SOH%, 0-100.00 (0-10000) +extern uint16_t battery_voltage; //V+1, 0-500.0 (0-5000) +extern uint16_t battery_current; //A+1, Goes thru convert2unsignedint16 function (5.0A = 50, -5.0A = 65485) +extern uint16_t capacity_Wh; //Wh, 0-60000 +extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 +extern uint16_t max_target_discharge_power; //W, 0-60000 +extern uint16_t max_target_charge_power; //W, 0-60000 +extern uint16_t bms_status; //Enum, 0-5 +extern uint16_t bms_char_dis_status; //Enum, 0-2 +extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) +extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) +extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) +extern uint16_t cell_max_voltage; //mV, 0-4350 +extern uint16_t cell_min_voltage; //mV, 0-4350 +extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false +extern uint8_t LEDcolor; //Enum, 0-10 + +void manageSerialLinkReceiver(); + +#endif diff --git a/Software/src/inverter/INVERTERS.h b/Software/src/inverter/INVERTERS.h index 97527c2b..9d8825f7 100644 --- a/Software/src/inverter/INVERTERS.h +++ b/Software/src/inverter/INVERTERS.h @@ -29,4 +29,8 @@ #include "SOLAX-CAN.h" #endif +#ifdef SERIAL_LINK_TRANSMITTER_INVERTER +#include "SERIAL-LINK-TRANSMITTER-INVERTER.h" +#endif + #endif diff --git a/Software/src/inverter/SERIAL-LINK-TRANSMITTER-INVERTER.cpp b/Software/src/inverter/SERIAL-LINK-TRANSMITTER-INVERTER.cpp new file mode 100644 index 00000000..229d0bd5 --- /dev/null +++ b/Software/src/inverter/SERIAL-LINK-TRANSMITTER-INVERTER.cpp @@ -0,0 +1,85 @@ +//SERIAL-LINK-TRANSMITTER-INVERTER.cpp + +#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 3 //--- comment out if nothing to receive + +#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 _getData() { + /* + var1 = dataLinkTransmit.getReceivedData(0); + var2 = dataLinkTransmit.getReceivedData(1); + var3 = dataLinkTransmit.getReceivedData(2); + */ +} + +void manageSerialLinkTransmitter() { + static bool initLink = false; + static unsigned long updateTime = 0; + static bool lasterror = false; + + 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 (millis() - updateTime > 100) { + updateTime = millis(); + if (!initLink) { + initLink = true; + // sends variables every 5000mS even if no change + dataLinkTransmit.setUpdateInterval(5000); + } + bool sendError = dataLinkTransmit.checkTransmissionError(true); + LEDcolor = GREEN; + if (sendError) { + LEDcolor = RED; + Serial.print(millis()); + Serial.println(" - ERROR: Serial Data Link - SEND Error"); + lasterror = true; + } else { + if (lasterror) { + lasterror = false; + Serial.print(millis()); + Serial.println(" - RECOVERY: Serial Data Link - Send GOOD"); + } + } + + dataLinkTransmit.updateData(0, SOC); + dataLinkTransmit.updateData(1, StateOfHealth); + dataLinkTransmit.updateData(2, battery_voltage); + dataLinkTransmit.updateData(3, battery_current); + dataLinkTransmit.updateData(4, capacity_Wh); + dataLinkTransmit.updateData(5, remaining_capacity_Wh); + dataLinkTransmit.updateData(6, max_target_discharge_power); + dataLinkTransmit.updateData(7, max_target_charge_power); + dataLinkTransmit.updateData(8, bms_status); + dataLinkTransmit.updateData(9, bms_char_dis_status); + dataLinkTransmit.updateData(10, stat_batt_power); + dataLinkTransmit.updateData(11, temperature_min); + dataLinkTransmit.updateData(12, temperature_max); + dataLinkTransmit.updateData(13, cell_max_voltage); + dataLinkTransmit.updateData(14, cell_min_voltage); + dataLinkTransmit.updateData(15, batteryAllowsContactorClosing); + } +} diff --git a/Software/src/inverter/SERIAL-LINK-TRANSMITTER-INVERTER.h b/Software/src/inverter/SERIAL-LINK-TRANSMITTER-INVERTER.h new file mode 100644 index 00000000..68625b89 --- /dev/null +++ b/Software/src/inverter/SERIAL-LINK-TRANSMITTER-INVERTER.h @@ -0,0 +1,32 @@ +//SERIAL-LINK-TRANSMITTER-INVERTER.h + +#ifndef SERIAL_LINK_TRANSMITTER_INVERTER_H +#define SERIAL_LINK_TRANSMITTER_INVERTER_H + +#include +#include "../../USER_SETTINGS.h" +#include "../devboard/config.h" // Needed for LED defines +#include "../lib/mackelec-SerialDataLink/SerialDataLink.h" + +// These parameters need to be mapped for the inverter +extern uint16_t SOC; //SOC%, 0-100.00 (0-10000) +extern uint16_t StateOfHealth; //SOH%, 0-100.00 (0-10000) +extern uint16_t battery_voltage; //V+1, 0-500.0 (0-5000) +extern uint16_t battery_current; //A+1, Goes thru convert2unsignedint16 function (5.0A = 50, -5.0A = 65485) +extern uint16_t capacity_Wh; //Wh, 0-60000 +extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 +extern uint16_t max_target_discharge_power; //W, 0-60000 +extern uint16_t max_target_charge_power; //W, 0-60000 +extern uint16_t bms_status; //Enum, 0-5 +extern uint16_t bms_char_dis_status; //Enum, 0-2 +extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) +extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) +extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) +extern uint16_t cell_max_voltage; //mV, 0-4350 +extern uint16_t cell_min_voltage; //mV, 0-4350 +extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false +extern uint8_t LEDcolor; //Enum, 0-10 + +void manageSerialLinkTransmitter(); + +#endif diff --git a/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.cpp b/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.cpp new file mode 100644 index 00000000..31a6902c --- /dev/null +++ b/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.cpp @@ -0,0 +1,568 @@ +// 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; + + + + + +// 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; +} + +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::Transmitting; + } + break; + + case DataLinkState::Transmitting: + if (isTransmitting) + { + sendNextByte(); // Continue sending the current data + } + else + { + + constructPacket(); // Construct a new packet if not currently transmitting + + if (muteAcknowledgement) + { + 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); + } + } + + if (maxIndexTX < 1) + { + currentState = DataLinkState::Receiving; + } + // 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; + isTransmitting = false; + //handleAckTimeout(); + //--- if no ACK's etc received may as well move to Transmitting + currentState = DataLinkState::Transmitting; + } + if (ackReceived()) + { + // No data to send from the other device + currentState = DataLinkState::Transmitting; + } + if (requestToSend) + { + // The other device has data to send (indicated by ACK+RTT) + currentState = DataLinkState::Receiving; + } + break; + + + case DataLinkState::Receiving: + read(); + if (readComplete) + { + readComplete = false; + // transition to transmit mode + 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 + if (serial.available() > 0) + { + // Peek at the next byte without removing it from the buffer + uint8_t nextByte = serial.peek(); + + if (nextByte == headerChar) + { + requestToSend = true; + return false; + } + + uint8_t receivedByte = serial.read(); + + switch (receivedByte) + { + case ACK_CODE: + // Handle standard ACK + return true; + + case ACK_RTT_CODE: + // Handle ACK with request to transmit + requestToSend = true; + return true; + + case NACK_RTT_CODE: + requestToSend = true; + case NACK_CODE: + transmissionError = true; + return false; + + 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; + if (serial.available()) + { + //Serial.print("."); + 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; +} diff --git a/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.h b/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.h new file mode 100644 index 00000000..258939c4 --- /dev/null +++ b/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.h @@ -0,0 +1,176 @@ +/** + * @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 + +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); + 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, + Transmitting, + WaitingForAck, + Receiving, + 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; + 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]; + + unsigned long updateInterval = 500; + unsigned long ACK_TIMEOUT = 100; + unsigned long PACKET_TIMEOUT = 100; // Timeout in milliseconds + + unsigned long lastStateChangeTime = 0; + unsigned long stateChangeTimeout = 200; + + // 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