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 9042202f..358711df 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -11,6 +11,7 @@ #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" diff --git a/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.cpp b/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.cpp new file mode 100644 index 00000000..522b8811 --- /dev/null +++ b/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.cpp @@ -0,0 +1,531 @@ +// 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::run() +{ + 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 + + 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; + } +} + +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..163d9441 --- /dev/null +++ b/Software/src/lib/mackelec-SerialDataLink/SerialDataLink.h @@ -0,0 +1,170 @@ +/** + * @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); + +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; + + // 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 + + // 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(); + + // 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