strtok, much easier to grasp
Only because you are familiar with it
On a first call, the function expects a C string as argument for str, whose first character is used as the starting location to scan for tokens. In subsequent calls, the function expects a null pointer . . .
This end of the token is automatically replaced by a null-character, and the beginning of the token is returned by the function. . .
Once the terminating null character of str is found in a call to strtok, all subsequent calls to this function (with a null pointer as the first argument) return a null pointer.
is not particularly obvious, the actual calling process changes between the first and subsequent calls.
The underlying string gets chopped up and becomes un-useable. This is unexpected and not immediately clear from the description (unless you are a low-level C programmer).
Then you will be suggesting the use of strcpy, to keep a copy of the original input, with all the errors that can bring and including the extra memory usage you were complaining about.
You also end up with a NULL pointer in your data string variable. Always a good source of subsequent errors.
dynamic allocation poking holes
In this simple sketch there will be no fragmentation even if the inputLine resizes. For non-trivial sketches see the Taming Strings guidelines.
If memory becomes an issue later on and OP has to back away from String,
No, just follow the guidelines in Taming Arduino Strings and become aware of how much memory you are using and where.
And there is always SafeString to fall back on which provides similar high level functionallity of Strings and which maintains the built in bounds checking and null terminator handling.
Now about bound checking.
without checking bounds or memory limits (this can fail and won't be caught)
receivedChars[commandIndex++] = incomingCharacter;
will discared lines if they are just 1 char too long,
input += c;
handles lines 10 times the expected size when run on a small memory UNO. If you want to check for input lines exceeding 1200 chars then you can. Most won't.
However.. your comment (and your code) does raise the point about missing NL input validation.
Here are two sketches, the first minimal sketch, revised without the substring memory issue you complained about, and no input NL validation. The second, following, checks for missing/corrupted NL terminator.
Note: the test for line length in the second sketch is to pick up a missing NL's and not to protect a fragile char[]'s from overflowing. The exact number to check the length against is not important when using Strings
I would expect most users would be happy with no NL validation sketch for a first pass as it is so simple, but the choice is theirs.
// NO Input NL Line Validation
// sample input
// L,R,D',B,U,D,F',R,B',L',B2,R,F2,D2,F,L',U2,L,U2,L,B2,L',B2,L2,U2,L2,R2,U2,B2,U2
// terminated with a NewLine char '\n' or CR NL
String inputLine;
// stopC is the char to stop at
// returns true when get stopC and input contains the input up to then
// else return false
bool readStrUntil(char stopC, String& input) {
while (Serial.available()) {
char c = Serial.read();
if (c == stopC) {
return true;
}
// else
input += c; // good for >1200 char lines on UNO
}
return false;
}
void runL() {
Serial.println("run cmd L");
}
void runR() {
Serial.println("run cmd R");
}
/// etc
void unknownCmd(String & cmd) {
Serial.print("cmd:"); Serial.print(cmd); Serial.println(" not programmed yet.");
}
void execute(String & cmd) {
cmd.trim();
if (cmd.length() == 0) {
return;
}
if (cmd == "L") {
runL();
} else if (cmd == "R") {
runR();
// . . . etc
} else {
unknownCmd(cmd);
}
}
void parseInput(String & data) { // note the String&
Serial.print(F("Data : '")); Serial.print(data); Serial.println("'");
data.trim(); // handle <CR><NL>
data += ','; // add a trailing , to simplify the logic
int endIdx = 0; int startIdx = 0;
while ((endIdx = data.indexOf(',', startIdx)) >= 0) { // not -1 i.e found ,
String cmd = data.substring(startIdx, endIdx);
execute(cmd);
startIdx = endIdx + 1; // skip the ,
}
}
void setup() {
Serial.begin(115200);
for (int i = 10; i > 0; i--) {
Serial.print(i); Serial.print(' ');
delay(500);
}
Serial.println();
}
void loop() {
if (readStrUntil('\n', inputLine)) { // got a line of input
parseInput(inputLine);
inputLine = ""; // clear for next input
}
}
This sketch detects missing NL and processes the data on a timeout.
If the missing NL causes an abnormally long input (as defined by the user) it is ignored and the following input skipped until it stops or a NL found.
// With Input NL Line Validations
// sample input
// L,R,D',B,U,D,F',R,B',L',B2,R,F2,D2,F,L',U2,L,U2,L,B2,L',B2,L2,U2,L2,R2,U2,B2,U2
// terminated with a NewLine char '\n' or CR NL
#include <millisDelay.h>
// millisDelay in included in the SafeString library available from the library manager
String inputLine;
bool skippingData = false;
millisDelay newLineTimeout; // catch if input line not terminated with NL
const unsigned long NEW_LINE_TIMEOUT_MS = 1000; // 1sec
bool returnTrueIfNotSkippingData(String& input) {
if (skippingData) {
skippingData = false;
Serial.print(F(" Finished skipping Data. Ignoring this last data '"));
Serial.print(input);
Serial.println("'");
input = ""; // skip this data
return false;
} else {
return true; // input has char upto stopC
}
}
// stopC is the char to stop at
// returns true when get stopC and input contains the input up to then
// else return false
bool readStrUntil(char stopC, String& input) {
while (Serial.available()) {
char c = Serial.read();
newLineTimeout.start(NEW_LINE_TIMEOUT_MS);
if (c == stopC) {
newLineTimeout.stop();
return returnTrueIfNotSkippingData(input);
}
// else
input += c;
// check for un-terminated input line
if (input.length() > 200) { // some large value exact size not important, two line together?
Serial.print(F("Missing or corrupted NL? Ignoring this data:'"));
Serial.print(input);
Serial.println("'");
Serial.println(F(" and skipping data until input stops or next NL received."));
skippingData = true; // start skipping
newLineTimeout.stop();
input = ""; //
}
}
if (newLineTimeout.justFinished()) {
if (skippingData) {
Serial.println(F(" Input stopped. "));
} else {
Serial.print(F("Input stopped without a NL. Parsing '"));
Serial.print(input);
Serial.println("'");
}
return returnTrueIfNotSkippingData(input);
}
//
return false;
}
void runL() {
Serial.println("run cmd L");
}
void runR() {
Serial.println("run cmd R");
}
/// etc
void unknownCmd(String & cmd) {
Serial.print("cmd:"); Serial.print(cmd); Serial.println(" not programmed yet.");
}
void execute(String & cmd) {
cmd.trim();
if (cmd.length() == 0) {
return;
}
if (cmd == "L") {
runL();
} else if (cmd == "R") {
runR();
// . . . etc
} else {
unknownCmd(cmd);
}
}
void parseInput(String & data) { // note the String&
Serial.print(F("Data : '")); Serial.print(data); Serial.println("'");
data.trim(); // handle <CR><NL>
data += ','; // add a trailing , to simplify the logic
int endIdx = 0; int startIdx = 0;
while ((endIdx = data.indexOf(',', startIdx)) >= 0) { // not -1 i.e found ,
String cmd = data.substring(startIdx, endIdx);
execute(cmd);
startIdx = endIdx + 1; // skip the ,
}
}
void setup() {
Serial.begin(115200);
for (int i = 10; i > 0; i--) {
Serial.print(i); Serial.print(' ');
delay(500);
}
Serial.println();
}
void loop() {
if (readStrUntil('\n', inputLine)) { // got a line of input
parseInput(inputLine);
inputLine = ""; // clear for next input
}
}
Adding this input validation adds noticeable more code and most users will just be happy with the basic sketch that handles 1200 chars
If the user wants this input validation then SafeString is a better choice because the SafeStringReader includes all the input timeout and skipping data logic already