Serial.readStringUntil() behavior different than documentation

Hi guys,

In working around the Serial class functions on my MKR Zero board (SAMD21 architecture), I came across the following issue.
The Serial.readStringUntil(char terminator) function is documented within the Arduino reference library to have the following return behavior:

Returns the entire string read from the serial buffer, up to the terminator character. If the terminator character can’t be found, or if there is no data before the terminator character, it will return NULL.

However, when testing with the simple code below, I found that the function will never return a NULL response. It either returns a String determined by the given terminator char (with char removed).... or just returns whatever String it has when the serial timeout expires. This persists across different terminators, using the all the line ending options in the serial monitor, etc.

void setup(){
  Serial.begin(9600);
}

void loop(){
String input = Serial.readStringUntil('\n');
Serial.println(input);
}

I noted that the function is inherited from the Stream class, so I dug into the Stream.cpp on AVR core github and found the following relevant sections.

int Stream::timedRead()
{
  int c;
  _startMillis = millis();
  do {
    c = read();
    if (c >= 0) return c;
  } while(millis() - _startMillis < _timeout);
  return -1;     // -1 indicates timeout
}


String Stream::readStringUntil(char terminator)
{
  String ret;
  int c = timedRead();
  while (c >= 0 && c != terminator)
  {
    ret += (char)c;
    c = timedRead();
  }
  return ret;
}

It seems like the behavior I'm seeing is what's coded, unless I'm missing something. Does anyone know why this is the case? The documented function is to differentiate between messages based on the presence of a terminator, but the actual function is just String or delayed String.

Below is a stand-in function for the functionality I'm looking for that I based on the github code above.

String readStringUntilWithTimeout(Stream& stream, char terminator, unsigned long timeout = Serial.getTimeout()) {
  String ret;
  unsigned long startTime = millis();
  while (millis() - startTime < timeout) {
    if (stream.available() > 0) {
      char c = stream.read();
      if (c == terminator) {
        return ret;
      } else {
        ret += c;
      }
    }
  }
  // Timeout occurred, discard the result
  ret = "";
  return ret;
}

// example loop using this function

void loop() {
  if (Serial.available()) {
    if (String input = readStringUntilWithTimeout(Serial, '\n'); input.isEmpty()) {
      Serial.println("Timed out looking for terminator.");
    } else {
      Serial.println(input);
    }
  }
}


Should this be changed in the code, or the documentation? Or am I completely missing what's going on here?

Also I did check the SAMD core libraries on github since I'm on a MKR Zero, but couldn't find a Stream class reference. I'm guessing they just take the AVR code.

Also yes, I'm aware Strings are bad. I don't think that needs to be discussed.

1 Like

How testing, with what for input, with which wiring and logic levels?

Any arbitrary text input over USB serial via the serial monitor. Input text, hit enter, and if Serial Monitor has "New Line" selected for input, the text is echoed immediately. If "no line ending" then the text is echoed at the Serial timeout limit (1000ms default).
Board is naked, USB serial is the only wiring.
Logic level is 3.3v on this board, would love to understand how that could be relevant.

The documentation is indeed incorrect.

You can report the issue here: https://www.arduino.cc/en/contact-us

1 Like

Well done detective work.

1 Like

Thanks for double-checking. Made a report.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.