/** SerialComsPair_HardwareSerial1.ino Using Serial1 On ESP32 Serial1 needs the default pins changed so if you want to use Serial1 you need to select the RX1_pin and TX1_pin from these pins 4,5, 12, 13, 18 to 27, 32, 33 and then use this code to begin const int RX1_pin = 4; const int TX1_pin = 5; Serial1.begin(9600,SERIAL_8N1,RX1_pin,TX1_pin); The design allows for extra time that may taken to process msgs by the loop() code, such as writing it to an SD card or POST-ing it to a web service. The design also handles SoftwareSerial connections that cannot send and receive at the same time. The code synchronizes send and recieve so that they don't happen at the same time. Both sides are very tolerant of delays anywhere in the loop(), not just in the message processing If the messages can completely fit in the Serial RX buffer, then the loop() code can be very very slow to run. That is messages < 61 bytes (allowing for the checksum and newline) should be good even for loop() code that includes those 'evil' delays MAX_RECEIVE_MSG_LENGTH = 60; // sets the maximum size of received msgs, msgs longer than this are just ignored MAX_SEND_MSG_LENGTH = 60; // sets the maximum size of send msgs, try to add more than this many chars will be ignored If your loop() is fast enought to keep up with the serial baudrate so that the RX buffer does not overflow you can set longer message lengths. Also see https://www.forward.com.au/pfod/ArduinoProgramming/Serial_IO/index.html for how to increase the RX buffer size. Different boards have different RX buffer sizes:- SoftwareSerial - 63 byte buffer (appears to be the default for boards) HardwareSerial Uno - 63 byte buffer, Mega2560 - 63 byte buffer, ESP32 - 127 byte buffer, ESP8266 - 128 byte buffer, There are two sides, the controller side and the other (non-controller side). The controller side initiates the connection, and also prompts the other side if it is slow to send a response. Once a connection is established, newline chars are sent back and forth continually, if there is no data to be sent. If you are using SoftwareSerial that side must be the controller side. If both sides are using SoftwareSerial, then either can be the controller If you can connecting to a PC, make the Arduino the controller side to simplify the PC code. Otherwise either side can be the controller. COMS_CONTROLLER = true; in the code below sets the controller side By default each message has two char HEX checksum added to the end. When connecting to a PC program you can disable the checksum to simplify the PC code by setting USE_CHECK_SUM = false in the code below. The textToSend SafeString holds the next message to be sent and is cleared once the message has been sent so you can check for if (textToSend.isEmpty()) { // last message sent can setup another one. The connectionTimeout_mS should be set to the same value for both sides. The default value is 250mS (0.25sec) This is almost always sufficient. COMS_SERIAL set the serial to use for the connection. The default baud rate for the send/receive is 9600 and is set at the top of the setup() method. DEBUG -- By default the DEBUG output uses the SafeString::Output which is set to Serial that is not being used for the connection if #define USING_SERIAL_FOR_DEBUG_IO has not been commented out below. If you are using Serial for the COMS_SERIAL connection then comment out the #define USING_SERIAL_FOR_DEBUG_IO to prevent debug output going to the COMS_SERIAL. The basic loop() code for a responder is void loop() { if (haveLineOfData()) { // valid response, not just a newline // here textReceived SafeString will have the line of data // textReceived is cleared next time haveLineOfData() is called so save what you need to here!!! DEBUG.print(F(" Received Data '")); DEBUG.print(textReceived); DEBUG.println("'"); // process textReceived here. can set textToSend in response as well if you want to textToSend = " SoftwareSerial response at "; textToSend += millis(); textToSend += "mS"; } sendNextMsg(textToSend); // send textToSend text if connected, else wait for connection. // sendNextMsg( ) clears textToSend after it is sent ready for next one } The basic loop() code for a sender is void loop() { if (haveLineOfData()) { // valid response, not just a newline // here textReceived SafeString will have the line of data // textReceived is cleared next time haveLineOfData() is called so save what you need to here!!! DEBUG.print(F(" Received Data '")); DEBUG.print(textReceived); DEBUG.println("'"); // process textReceived here if you need to } if (textToSend.isEmpty()) { // textToSend will be empty when it has been sent // set the next data to be sent textToSend = " SoftwareSerial loop data at "; textToSend += millis(); textToSend += "mS"; } sendNextMsg(textToSend); // send textToSend text if connected, else wait for connection. // sendNextMsg( ) clears textToSend after it is sent ready for next one } by Matthew Ford Copyright(c)2021 Forward Computing and Control Pty. Ltd. This example code is in the public domain. download and install the SafeString library V4.0.3+ from Arduino library manager or from www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html */ #include "SafeStringReader.h" #include "millisDelay.h" // textReceived will be filled with text when some is received and is cleared on next call to haveLineOfData() // put text to be sent in textToSend, textToSend is cleared when it has been sent // these values should match the other side, i.e. this MAX_RECEIVE_MSG_LENGTH should == other side's MAX_SEND_MSG_LENGTH const size_t MAX_RECEIVE_MSG_LENGTH = 60; // received msgs longer than this are just ignored const size_t MAX_SEND_MSG_LENGTH = 60; // try to add more chars will be ignored and raise a SafeString error // One side must have COMS_CONTROLLER = true; // and the other side have COMS_CONTROLLER = false; // if using SoftwareSerial set it as the controller // if connecting to PC set the Arduino as the controller // else either one can be the controller const bool COMS_CONTROLLER = false; // set to true for the controller side const bool USE_CHECK_SUM = true; // true to add checksum, false for no checksum, this setting must match on both sides const unsigned long connectionTimeout_mS = 250; // 0.25 sec // The connectionTimeout_mS should be set to the same value for both sides. The default value is 250mS (0.25sec) // This is almost always sufficient. #define COMS_SERIAL Serial1 // not using Serial for COMS_SERIAL so can use it for debug and for user test input // if #define COMS_SERIAL Serial // then comment out this line!! #define USING_SERIAL_FOR_DEBUG_IO // set DEBUG to where to send output // here sending via SafeString::Output which can be turned off // by commenting out this line in setup // SafeString::setOutput(Serial); // enable error msgs #define DEBUG SafeString::Output // ========= incomming and outgoing data SafeStrings // textReceived incoming data with checkSum and newline removed // reads from COMS_SERIAL accepts lines upto MAX_RECEIVE_MSG_LENGTH + checkSum and terminating '\n' char createSafeStringReader(textReceived, MAX_RECEIVE_MSG_LENGTH + 3, '\n'); // reads from COMS_SERIAL // textToSend outgoing data before adding checkSum and newline cSF(textToSend, MAX_SEND_MSG_LENGTH); // cmds upto MAX_SEND_MSG_LENGTH char long next cmd to send // =================== bool haveLineOfData(); // returns true if have received a valid msg, could be just an empty line, i.e. no data void sendNextMsg(SafeString& msg); bool isConnected(); // returns true if connected void setupSerialReader(); // this will be an empty method if USING_SERIAL_FOR_DEBUG_IO is not #defined void readUserInput(); // this will be an empty method if USING_SERIAL_FOR_DEBUG_IO is not #defined millisDelay connectionTimeout; // if we are not using Serial for coms define a reader to read from coms for user input to send test msgs #ifdef USING_SERIAL_FOR_DEBUG_IO createSafeStringReader(sfReader, MAX_SEND_MSG_LENGTH, "\r\n"); // To read from IDE terminal monitor accepts lines upto MAX_SEND_MSG_LENGTH chars terminated by '\n' or '\r' or no line ending (i.e. timeout) #endif // #ifdef USING_SERIAL_FOR_DEBUG_IO void setup() { // for ESP32 Serial1 use //select the RX1_pin and TX1_pin // from these pins 4,5, 12, 13, 18 to 27, 32, 33 and then use this code to begin //const int RX1_pin = 4; //const int TX1_pin = 5; //Serial1.begin(9600,SERIAL_8N1,RX1_pin,TX1_pin); // else just use COMS_SERIAL.begin(9600); // not too fast do this first #ifdef USING_SERIAL_FOR_DEBUG_IO // start up Serial Serial.begin(115200); for (int i = 10; i > 0; i--) { Serial.print(' '); Serial.print(i); delay(500); } Serial.println(); SafeString::setOutput(Serial); // enable error msgs to be sent to Serial #endif // #ifdef USING_SERIAL_FOR_DEBUG_IO textReceived.setTimeout(connectionTimeout_mS); // set connectionTimeout_mS sec timeout for flushInput and missing '\n' textReceived.flushInput(); textReceived.connect(COMS_SERIAL); // read from COMS_SERIAL if (connectionTimeout_mS > 0) { connectionTimeout.start(connectionTimeout_mS); } DEBUG.println(F(" ComsPair - started")); DEBUG.println(F(" Enter text to be sent in place of the millis() msg")); #ifdef USING_SERIAL_FOR_DEBUG_IO setupSerialReader(); #endif // #ifdef USING_SERIAL_FOR_DEBUG_IO } void loop() { if (haveLineOfData()) { // valid response, not just a newline // here textReceived SafeString will have the line of data // textReceived is cleared next time haveLineOfData() is called so save what you need to here!!! DEBUG.print(F(" Received Data '")); DEBUG.print(textReceived); DEBUG.println("'"); // process textReceived here. can set textToSend in response as well if you want to } // Can overwrite textToSend here if you want to send something else sendNextMsg(textToSend); // send textToSend text if connected, else wait for connection. Clears textToSend after it is sent ready for next one // to just send data ignoring textReceived if (textToSend.isEmpty()) { // textToSend will be empty when is have been sent textToSend = " HardwareSerial loop data at "; textToSend += millis(); textToSend += "mS"; } #ifdef USING_SERIAL_FOR_DEBUG_IO readUserInput(); // reads cmds from USB for to be sent testing #endif // #ifdef USING_SERIAL_FOR_DEBUG_IO // put your main code here, to run repeatedly: } // =================================== SerialComsPair support methods =============================== boolean connected = false; bool clearToSendFlag = false; bool isConnected() { return connected; } void resetConnectionTimer() { if (connectionTimeout_mS > 0) { connectionTimeout.restart(); // we are talking } } void setConnected() { resetConnectionTimer(); if (!isConnected()) { DEBUG.println(F(" Made Connection.")); connected = true; } } void lostConnection() { if (isConnected()) { DEBUG.println(F("Lost Connection")); } if (!COMS_CONTROLLER) { clearToSendFlag = false; // wait for prompt from controller } connected = false; } // calcuates the checksum to add to the end of msg as 2 hex chars void calcCheckSum(SafeString& msg, SafeString& chkHexStr) { chkHexStr.clear(); if ((msg.isEmpty()) || (!USE_CHECK_SUM)) { return; } uint8_t chksum = 0; for (size_t i = 0; i < msg.length(); i++) { chksum ^= msg[i]; } if (chksum < 16) { chkHexStr += '0'; } chkHexStr.print(chksum, HEX); // add as hex } // removes checksum 2 hex chars at end of msg // calcuates checksum char for this message and compares them bool checkCheckSum(SafeString& msg) { if (!USE_CHECK_SUM) { return true; // don't check checksum } size_t len = msg.length(); if (len < 3) { DEBUG.print(F(" CheckSum failed for '")); DEBUG.print(textReceived); DEBUG.println("'"); DEBUG.print(F(" Need at least 3 chars in msg")); DEBUG.println(); return false; // 2 Hex for checksum + at least one char for msg // empty lines don't have checksums } cSF(msgChksum, 2); msg.substring(msgChksum, len - 2, len); msg.removeLast(2); cSF(chksum, 2); calcCheckSum(msg, chksum); // calculate this msg checksum as two hex char if (chksum == msgChksum) { return true; } // else DEBUG.print(F(" CheckSum failed for '")); DEBUG.print(textReceived); DEBUG.println("'"); DEBUG.print(F(" Hex received '")); DEBUG.print(msgChksum); DEBUG.print("'"); DEBUG.print(F(" calculated '")); DEBUG.print(chksum); DEBUG.print("'"); DEBUG.println(); return false; // check sum failed } void checkConnectionTimeout() { if (connectionTimeout.justFinished()) { connectionTimeout.restart(); if ((!isConnected()) && (!clearToSendFlag) && COMS_CONTROLLER && (!COMS_SERIAL.available())) { // controller is waiting for other side to send and the other side has not started sending // so prompt it every connectionTimeOut_mS // after connectionTimeout, controller times out but skips this section since connected is still true // next time around i.e. 2*connectionTimeOut this section send a prompt. DEBUG.println(F("Prompt other side to connect")); COMS_SERIAL.print('\n'); // we are still alive } if (!COMS_SERIAL.available()) { lostConnection(); } } } bool haveLineOfData() { if (clearToSendFlag) { // we should be sending so ignore any input while (COMS_SERIAL.available()) { COMS_SERIAL.read(); // clear any following data } } // do this check second so that if non-controller takes a long time to process msg and generate response // the above code will first clear all previous newline prompts // then this code will timeout and set non-connected and clearToSendFlag == false for next prompt checkConnectionTimeout(); // if CONTROLLER the other side if not connected and this side is the controller if (!textReceived.read()) { // have not got a line of data ALWAYS call this to handle read timeouts return false; } if (textReceived.hasError()) { // previous input length exceeded or read 0 DEBUG.println(F(" textReceived hasError. Read '\\0' or Input overflowed.")); } if (textReceived.getDelimiter() == ((char) - 1)) { // no delimiter so timed out DEBUG.println(F("textReceived timeout without newline")); return false; } // Only one message allowed at a time so clear any following data while (COMS_SERIAL.available()) { COMS_SERIAL.read(); // clear any following data } // for both controller and non-controller, setConnected(); // calls resetConnectionTimer, controller is ALWAYS connected clearToSendFlag = true; // can send more data now if (textReceived.isEmpty()) { // just an empty line valid response return false; } // else non-empty return true to process if (!checkCheckSum(textReceived)) { // check check sum lostConnection(); // sets clearToSendFlag = false; controller stays in connected state return false; // do not reply => will timeout } // else return true; // valid msg } void sendNextMsg(SafeString& msg) { checkConnectionTimeout(); if ((clearToSendFlag) && (isConnected() || COMS_CONTROLLER)) { // COMS_CONTROLLER is ALWAYS connected clearToSendFlag = false; // only send one message per msg received if (!msg.isEmpty()) { msg.replace('\n', ' '); // replace \n with space DEBUG.print(F("Send Data '")); DEBUG.print(msg); DEBUG.println("'"); cSF(ckSum, 2); calcCheckSum(msg, ckSum); // calculated checksum returned in SafeString ckSum COMS_SERIAL.print(msg); COMS_SERIAL.print(ckSum); } COMS_SERIAL.print('\n'); msg = ""; // clear it for next cmd } } void setupSerialReader() { #ifdef USING_SERIAL_FOR_DEBUG_IO // ======================= this section is for testing ================== sfReader.flushInput(); // this will skip startup noise from the other side sfReader.connect(Serial); // for reading test cmds from Serial to send to COMS_SERIAL sfReader.setTimeout(1000); // set 1 sec timeout //sfReader.echoOn(); // echo IDE serial input // ======================= end of setup for testing via USB ================== #endif // #ifdef USING_SERIAL_FOR_DEBUG_IO } void readUserInput() { #ifdef USING_SERIAL_FOR_DEBUG_IO // ======================= this section is for testing ================== if (sfReader.read()) { // read from text to be sent from IDE Monitor sfReader.trim(); if (!sfReader.isEmpty()) { // skip empty input DEBUG.print(F(" Got a line of Data to send '")); DEBUG.print(sfReader); DEBUG.println("'"); textToSend = sfReader; // just overwrite any waiting msg with this input } } // ======================= end of setup for testing via USB ================== #endif // #ifdef USING_SERIAL_FOR_DEBUG_IO }