Home
| pfodApps/pfodDevices
| WebStringTemplates
| Java/J2EE
| Unix
| Torches
| Superannuation
| CRPS Treatment
|
| About
Us
|
The SafeString alternative to Arduino Strings for Beginners
|
by Matthew Ford 17th October 2024 (original 1st June
2020)
© Forward Computing and Control Pty. Ltd. NSW
Australia
All rights reserved.
Update 17th October 2024: V4.1.35
– minor edit to BufferedOutput
Update 20th August
2024: V4.1.34 – fixed
SafeStringLengthTrim example (removed unsupported setLength()
calls)
Update 28th June 2024: V4.1.33
– fix for bool for SAM boards
Update 24th May
2024: V4.1.32 – added
support for Adafruit SAMD boards
Update 9th Feb 2024:
V4.1.31 – added support
for other Arduino MBED boards like Opta
Update 29th Oct
2023: V4.1.30 – added
default initializations
Update 3rd Oct 2023: V4.1.29
– removed F() and FlashStringHelper class, SafeString now
depends of the board's core to define these.
Update 23rd
Sept 2023: V4.1.28 –
revised defines for fixes for Arduino UNO R4 and R4 WiFi
Update
15th May 2023: V4.1.27 –
revised defines for Arduino Zero added PlatformIO versions
Update
22nd April 2023: V4.1.26 –
fix for Arduino IDE 2 and ESP32 for F() macro
Update
19th Jan 2023: V4.1.25 –
added readFrom(const char *, unsigned int maxCharsToRead)
See
the docs for the list
of Revisions
This is a tutorial on SafeString. The SafeString
library documentation is here. For more implementation details,
read the comments in the .cpp files.
This
SafeString library is designed for beginners to be a safe, robust and
debuggable replacement for string processing in Arduino. This
tutorial shows you how to use SafeStrings for your Arduino string
processing. As concrete examples, it includes a simple sketch to
check for user's commands, separated by spaces or commas, without
blocking the program waiting for input. It also include an OBD data
processor where the existing char* is warped in a SafeString for
processing. See the User
Commands Example and the ODB
data processing examples below.
If you have questions about how to apply the SafeString library to your code, you can post to the Arduino forum Using the SafeString library or contact me directly via the Contact link (top right)
Quick Start
Printing
to a SafeString
Fixed
Width Formatting
Passing SafeStrings to
methods as references and returning SafeString results
Wrapping
existing char arrays in a SafeString
Reading
from a const char*
An OBD data processor
example
Using SafeString for class
variables
Working with Arrays and Structs
of C-strings
Method 'static' SafeStrings and
readFrom() and writeTo()
A String
Tokenising and Number Conversion Example
When
to use firstToken()
User Commands
Example
Converting between String and
SafeString
Differences between Arduino
Strings (WString.cpp) and SafeStrings
SafeString
Error Flags and Controlling SafeString Error messages and
Debugging
What is wrong with Arduino String
processing
Conclusion
Also see Arduino For
Beginners – Next Steps
Taming
Arduino Strings
How
to write Timers and Delays in Arduino
SafeString
Processing for Beginners (this
one)
Simple
Arduino Libraries for Beginners
Simple
Multi-tasking in Arduino
Arduino
Serial I/O for the Real World
The previous option for string processing in Arduino was Arduino Strings. Both Sparkfun and Adafruit advise against using the Arduino String class but Arduino Strings are very safe and usable, if you follow the guidelines in Taming Arduino Strings. On the other hand SafeString gives you complete control over your memory usage and a richer set of readers, parsers and text functions and detailed error messages that make debugging easier. You can also build SafeString content by calling the well know print( ) methods on them. SafeString also are better then Arduino Strings for processing data returned by third party libraries in char[]s. There are also a few errors in the Arduino String class that SafeString fixes.
String processing using low level C character arrays (using strcat, srtcpy etc) is a major source of program crashes and is not recommended.
SafeStrings are easy to debug. SafeStrings provide detailed
error messages, including the name of SafeString in question, to help
you get your program running correctly.
SafeStrings are safe
and robust. SafeStrings, once created^,
never cause reboots and are always in a valid usable state, even if
your code passes null pointers or '\0' arguments or exceeds the
available capacity of the SafeString.
SafeString programs run
forever. SafeStrings completely avoid memory fragmentation and
never makes extra copies when passed as arguments.
SafeStrings
are faster. SafeStrings do not create multiple
copies or short lived objects nor do they do unnecessary
copying of the data.
SafeStrings can wrap existing c-strings.
Wrapping existing char* or char[]
in a SafeString object allows you to safely perform string
manipulations on existing data.
^When you create a SafeString by wrapping an existing char*, if the string pointed to is not correctly terminated with a '\0', SafeString cannot correct for this error and further SafeString methods may fail.
Unlike low level C character array methods,
SafeStrings will NEVER crash your program due to memory
fragmentation, null pointers, buffer overrun or invalid
argument.
V2.0.0 of SafeStrings adds
creating SafeStrings from existing char[] and char* See Wrapping
existing char arrays in a SafeString below
V2.0.5 of
SafeString adds non-blocking Serial I/O and SafeStringStreams. See
Arduino Serial I/O
for the Real World
V3.0.0 of SafeString adds loopTimer and
millisDelay and BufferedInput. See How
to write Timers and Delays in Arduino, Simple
Multi-tasking in Arduino
V4.0.0 of SafeString revises
function returns to more closely match Arduino Strings. In particular
indexOf now returns and int, and returns -1 if not found.
Also see the sections further down on Wrapping existing char arrays in a SafeString and Converting between String and SafeString
SafeString is now available via the Arduino
Library Manager, thanks to Va_Tech_EE
Start
the Arduino IDE, open the library manager via the menu Tools->Manage
Libraries..
Type SafeString
into the Filter your
search search bar and mouse over the
SafeString entry and click on Install.
You can also install the library manually if
you wish, but the Library Manager is easier.
For manual
installation, download the SafeString.zip
files to your computer, move it to your desktop or some other folder
you can easily find. This zip file is a duplicate of what the library
manager provides.
Then open the Arduino IDE and use the menu
Sketch → Import
Library → Add Library
to install it. Stop and restart the Arduino IDE.
Under File->Examples you should now see
SafeString listed (under Examples from Custom Libraries) and
under that a large number of examples sketches.
Under the
File->Examples->SafeString ,the SafeString_Test
directory has numerous examples of how the SafeString methods work.
Start with the SafeString_ConstructorAndDebugging
example and then SafeStringFromCharArray,
SafeStringFromCharPtr and
SafeStringFromCharPtrWithSize.
For illustrative real world example see the GPS_reader_processor and OBD_Processor examples
In SafeString V2+, as well as the createSafeString( ) macro, it introduces three (3) more macros createSafeStringFromCharArray( ), createSafeStringFromCharPtr( ) and createSafeStringFromCharPtrWithSize( ) (see Wrapping existing char arrays in a SafeString below). SafeString V2+ also adds the typing shortcuts cSF( ), cSFA( ), cSFP( ) and cSFPS( ) respectively for these macros. These shortcuts are #defined at the top of SafeString.h.
// define typing shortcuts #define cSF createSafeString #define cSFA createSafeStringFromCharArray #define cSFP createSafeStringFromCharPtr #define cSFPS createSafeStringFromCharPtrWithSize
You can change them as you wish or add your own defines.
As a first example try the sketch below, SafeString_Example1.ino,
Some of the important statements in this sketch are:-
createSafeString(msgStr, 5);
creates an empty
SafeString large enough to hold 5 char (plus a terminating '\0').
Here a global SafeString, msgStr, is created, but you can also create
temporary local SafeStrings inside your functions. In all cases there
is never any heap fragmentation. You can also use the typing shortcut
cSF( ) which does the same thing e.g.
cSF(msgStr,
5);
SafeString::setOutput(Serial);
set the output
Stream to send the error messages and debug() to
msgStr = F("A0");
msgStr += " =
";
msgStr += analogRead(A0);
builds up the
string to be output.
msgStr.debug(F(" After adding analogRead "));
will print the title followed by the details and contents
msgStr (but only if setOutput( ) has been called).
Running this sketch, SafeString_Example1.ino, produces the following output
10 9 8 7 6 5 4 3 2 1 Error: msgStr.concat() needs capacity of 8 for the first 3 chars of the input. Input arg was '598' msgStr cap:5 len:5 'A0 = ' After adding analogRead msgStr cap:5 len:5 'A0 = ' A0 =
The error message,
Error: msgStr.concat() needs capacity of 8 for the first 3 chars of the input. Input arg was '598' msgStr cap:5 len:5 'A0 = ' After adding analogRead msgStr cap:5 len:5 'A0 = '
indicates that the msgStr is not large enough to hold the whole message. It needs to be increased to at least a capacity of 8. Note that the msgStr is still valid. If an operation cannot be performed the SafeString is just left unchanged.
Edit the createSafeString(msgStr, 5); to read
increase it size to 10, i.e.
SafeString_Example1a.ino
createSafeString(msgStr,
10);
Re-running the sketch (SafeString_Example1a.ino) after this change gives the following complete output
10 9 8 7 6 5 4 3 2 1 After adding analogRead msgStr cap:10 len:8 'A0 = 604' A0 = 604
You can also cascade these statements, like this, by using nesting
brackets
( (msgStr = F("A0")) += " = "
) += analogRead(A0);
Finally comment out or remove the setOutput
statement, i.e. SafeString_Example1b.ino
//
SafeString::setOutput(Serial);
Re-running the sketch (SafeString_Example1b.ino) after this change gives the following output
10 9 8 7 6 5 4 3 2 1
A0 = 487
All the debug() output has been suppressed. Without the setOutput( ) statement the error messages are also suppressed, so you should include a setOutput( ); statement while testing.
There are a number of ways to add text to a SafeString. As shown
above you can use = and += to add text, numbers and other
SafeStrings. You can also pass an initial string value to
createSafeString( ) , e.g.
createSafeString(msgStr,
10, "A0");
If the
SafeString is not large enough to hold the initial value, the result
will just be an empty SafeString. If SafeString::setOutput(Serial);
has been called then an error
message will also be output.
Note:
Only an inline "string"
can be used as the initial string value for the createSafeString( )
macro.
You can also use the concat()
methods to do the same. e.g.
SafeString_Example1c.ino
The
clear() method
clears out any existing text in the SafeString. The concat()
and clear()
methods return a reference to the SafeString so
you can chain calls as shown in SafeString_Example1c.ino
Serial.println(msgStr.concat(" = ").concat(analogRead(A0)));
For more format control you can use the
familiar print() methods
to print to a SafeString. e.g. SafeString_Example1d.ino
To add the analog reading in HEX form you use
msgStr.print(analogRead(A0),HEX);
All
the print/write methods are available for adding to SafeStrings.
The output from SafeString_Example1d.ino is (the exact value depends on the analog read)
A0 = 0x25A
SafeString also has prefix() methods and the -= operator to add text to the front of the SafeString, e.g. SafeString_Example1e.ino
msgStr.clear(); // remove the initial
value
msgStr.print(analogRead(A0),HEX);
msgStr -=
" = 0x";
msgStr.prefix(F("A0"));
Serial.println(msgStr);
SafeString has a range of other methods to work on text :- compareTo, equals, == , != , < , <= , > , >= , equalsIgnoreCase, startsWith, endsWith, endsWithCharFrom, startsWithIgnoreCase, setCharAt, charAt, [ ] (access only), indexOf, lastIndexOf, indexOfCharFrom, substring, remove, removeBefore, removeFrom, removeLast, keepLast, replace, toLowerCase, toUpperCase, trim, toInt, toLong, binToLong, octToLong, hexToLong, toFloat, toDouble, stoken, read, readUntil, readFrom, writeTo, stoken, nextToken, processBackspaces.
The two 'unsafe' methods in V1 of this library:- readBuffer and writeBuffer, have been removed in V2+ and replaced with the safe readFrom() and writeTo() methods.
You can print( ) to a SafeString just like printing to Serial, but the text is accumulated in the SafeString for outputting later. (see the example sketch, SafeStringPrint.ino) All the usual print( ) and println( ) methods can be used e.g
msgStr.clear(); // remove the initial value
float
f = 5.334;
msgStr.print(f,1);
Serial.println(msgStr);
Displays on the Serial output
5.3
As well as the the usual print( ) and
println( ) methods, SafeString has a fixed width print method for
printing doubles, floats, longs and ints, left or right justifed in a
fixed width. The format is:-
print( value, decPlaces, width );
// and println( … )
where value is the number to be
printed, decPlaces is the number of digits after the . and width is
the fixed width of the field. If width is +ve the output is right
justified and and padded with spaces on the left. If width is -ve the
output is left justified and padded with spaces on the right. If the
value is too large to fit in the width, then SafeString automatically
reduces the number of digit displayed after the decimal point to fit
it. If the value won't fix with 0 decPlaces then a completely blank
field is returned and the error flag set and, optionally, and error
message printed if SafeString::setOutput( ) has been called. E.g.
d = 0.12345123; cSF(sfStr, 70); sfStr.print(F(" using sfStr.print(d,5,9)|")); sfStr.print(d, 5, 9); // 5 is prec requested will be automatically reduced if d will not fit in width 9 sfStr.println(F("| text following field")); Serial.print(sfStr);
This code outputs
using sfStr.print(d,5,9)| 0.12345| text following field
You can also print longs and ints in fixed width by setting decPlaces
to 0, e.g. sfStr.print(43,0,9);
By default the + sign is not
output, but you can force it by passing true as the optional 4th
argument, e.g. sfStr.print(43,0,9,true);
See the example sketch
SafeString_fixedWidthFormat.ino
and SafeString_snprintfAlternative.ino
for more examples
The Arduino compiler will issue an error or warning if you try and
write an incorrect method using SafeString arguments or
returns.
Note: You can also work on familiar char* to pass
and return arguments and just wrap them in SafeStrings within the
method for processing. See Wrapping existing char
arrays in a SafeString below and the
SafeString_ReadFrom_WriteTo.ino
example sketch.
To see these error and warning easily, make the following settings in the Arduino preference dialog.
Open Arduino File->Preferences and untick Show verbose output during compilation and just below that turn on Compiler Warnings: That is set Compiler Warning to something other than None, for example Default. These settings will make it easy to see the warnings/errors referred to below.
When you define a method that has a SafeString argument you
declare it as a reference, SafeString &str, e.g.
void
test(SafeString& strIn) {
. . .
}
If you forget to use the & when defining the function, you will get an error message like the the following
. . . /SafeString.h: In function 'void setup()': . . . /SafeString.h:109:5: error: 'SafeString::SafeString(const SafeString&)' is private SafeString(const SafeString& other ); // You must declare SafeStrings function arguments as a reference, SafeString&, e.g. void test(SafeString& strIn) ^ TestCode:30:11: error: within this context test(str); ^
The first line of the errors tells you where the function with the
error was called from, e.g. test( ) was called in setup()
The
third line tells you what the error was
//
You must declare SafeStrings function arguments as a reference,
SafeString&, e.g. void test(SafeString& strIn)
and
a little further down you will see which function call caused the
error
TestCode:30:11: error: within this context
test(str);
That tells you that it is the test( ) method that is missing the SafeString & in its definition. Go the where you wrote the test( ) method and add a & to the end of SafeString. That is change test(SafeString .. ) to test(SafeString& … )
Declaring the method SafeString& argument as a reference means that any changes you make to it in the method will immediately update the SafeString that you passed to the method when you called it. There is only ever one copy of each SafeString and it is that one copy which gets all the changes applied to it. You could also declare the argument as a SafeString pointer, e.g. SafeString *str, but it is easier to code the method using a SafeString & reference.
With SafeString V2+ you cannot write a method with a const SafeString & as an argument. You have to use a non-const SafeString & argument as shown above This is because SafeStrings that wrap existing char arrays (see Wrapping existing char arrays, below) are 'cleaned up' in each SafeString function. This 'clean up' protects the SafeString from the results of invalid c-string operations on the wrapped char array (see Mixing SafeString and c-string methods, below), but this means the SafeString & can not be a const. If you define a method with a const SafeString & as an argument, then you compile you will get an error/warning message like
passing 'const SafeString' as 'this' argument discards qualifiers [-fpermissive]
Compiling for Uno/Mega will issue a warning (with the setting Compiler Warnings: default). While compiling for an ESP32 will give an error.
Unlike the String class, SafeString does not support returning a SafeString from a method. To return results in a SafeString, you must update one of the SafeString & arguments you passed to the method. For example write a method that takes the number of loops and returns a SafeString for printing you would use SafeStringMethods.ino
void loopCountStr(int num, SafeString& str) { str = "loop count = "; str += num; // update it // str returned via SafeString& arg }
The caller of this method has to provide the SafeString, as an argument, for the result to be returned in. The SafeString provided as an argument can be a global SafeString OR a local SafeString created in the code calling the loopCountStr method.
Arduino Strings let you return a String or String & as the return type from the method. It does this by coping the result and passing that copy back. SafeString only ever has one instance of each SafeString and never creates another copy. If you try to pass a SafeString back as the method return you will get either a compiler error or warning as shown below.
Here are two
examples that do not work and the errors and warning they
produce.
SafeStringMethod_Error_1.ino
uses SafeString
loopCountStr(int num) and
the compiler gives the error message
SafeString(const
SafeString& other ); // You must declare SafeStrings function
arguments as a reference, SafeString&, e.g. void test(SafeString&
strIn)
That
is to return a SafeString you need to pass it into the method as a
reference and then update it as in SafeStringMethods.ino
SafeStringMethod_Error_2.ino
uses SafeString&
loopCountStr(int num) and
the compiler give the warning message
warning:
reference to local variable 'str' returned
That
is the method loopCountStr is trying to return a reference (a
pointer) to the local SafeString that was created on the local method
stack. The method stack is completely discarded when the method
returns so the data referenced is no longer valid.
You MUST heed
this warning and change the code to remove it, because sometimes a
sketch with this warning will appear to work, but it will ultimately
fail and cause a reboot.
When working with existing code and libraries you often have existing char[] or char* that contains the string data you want to process or where you want to put the results. The macros createSafeStringFromCharArray( ) or cSFA( ) and createSafeStringFromCharPtr( ) or cSFP( ) and createSafeStringFromCharPtrWithSize( ) or cSFPS( ) let you wrap the data in a SafeString for processing using the existing allocated char[] without copying the data to/from a SafeString. See the example sketches SafeStringFromCharArray.ino, SafeStringFromCharPtr.ino and SafeStringFromCharPtrWithSize.ino that are included with the SafeString library.
Once you have wrapped the existing char[] in a SafeString, any changes you make using SafeString methods are reflecting in the underlying wrapped char[].
The difference between createSafeString() / sSF() and the other createSafeStringFrom..() versions is that the createSafeString() macro creates a char[] (either globally or on the local stack) where as the other createSafeStringFrom..() macros all just wrap an existing char[] and do not use any extra char[] storage. Because the createSafeStringFrom...() macros wrap the existing data, any changes made via the SafeString are reflected in the existing data.
For example using cSFP to wrap the “test” string updates when operations are performed with the SafeString :-
char str[] = "test"; // existing char[] Serial.println(str); // prints => test cSFP(sfStr,str); // use typing short cut to create a SafeString, sfStr, that warps the existing data. sfStr.toUpperCase(); // changes the underlying data Serial.println(str); // prints => TEST
On the other hand using cSF( ) and sfStr = str, takes a copy of the initial data and so does not change the underlying char[]
char str[] = "test"; // existing char[] Serial.println(str); // prints => test cSF(sfStr, strlen(str)); // create as SafeString large enough to hold a copy of the existing data sfStr = str; // copy str to sfStr sfStr.toUpperCase(); // does NOT change the underlying data Serial.println(str); // prints => test
The macro createSafeStringFromCharArray( ) or its typing shortcut cSFA( ),wraps an existing char[..]. The char[ ] must have a specified size e.g.
char testData[25] = "initial data";
Then
cSFA(sfData, testData); // SafeString from a char[25]
gives you safe access to the testData[25] and sfData.debug() =>
SafeString sfData cap:24 len:12 'initial data'
The sfData capacity
is only 24 since one extra char storage is need to correctly
terminate the c-string.
SafeString use the sizeof(char[]) to
set the capacity if the SafeString. To check that a char[] and not
char* has been passed as the argument, cSFA( ) flags an Error is the
sizeof(char[]) == sizeof(char*).
The sizeof(char*) differs for
various Arduino boards. For Uno/Mega2560 and other 8bit boards
sizeof(char*) == 2. For ESP8266/ESP32 and other 16bit boards
sizeof(char*) == 4.
So to void having SafeString reject your cSFA(
) statement for small arrays, use a 5 as the minimum size of your
char[]s. E.g char cArray[5];
Data accessed by char* is much more common than a char[ ] with a specified size. For char* you can use either createSafeStringFromCharPtr() / sSFP( ) or createSafeStringFromCharPtrWithSize( ) / cSFPS( ) to wrap the data in a SafeString.
The macro createSafeStringFromCharPtr() or its typing shortcut sSFP( ), wraps the existing c-string pointed to by the char* and sets the SafeString capacity to it strlen(). This macro is very useful for processing incoming data. The macro checks for a NULL pointer but assumes the c-string pointed to is correctly terminated. The SafeString lets you safely process the data, but, because the SafeString's capacity is set to the strlen of the input data, you cannot extend the len. e.g.
char testData[25] = "initial data";
char *dataPtr = testData; // a pointer to the data
Then
cSFP(sfData, dataPtr); // SafeString from a char*
gives you safe access to the initial data and sfData.debug() =>
SafeString sfData cap:12 len:12 'initial data'
The sfData capacity
is only 12 since that is the only valid data length SafeString can
determine from the char *.
The macro createSafeStringFromCharPtrWithSize() or its typing shortcut sSFPS( ), wraps the existing c-string pointed to by the char* and sets the SafeString capacity to one less than the specified size. This macro is very useful for outputting data to a result c-string. The macro checks for a NULL pointer but assumes you are correctly specifying the number of size of the underlying char[].
char testData[25] = ""; // testData can hold 24 characters + terminating '\0'
char *dataPtr = testData; // a pointer to the data
Then
cSFPS(sfData, dataPtr, 25); // SafeString from a char* that can hold (25-1) = 24 char + terminating '\0'
gives you safe access to the initial data and sfData.debug() => SafeString sfData cap:24 len:12 'initial data'
NOTE: It is very important that the size passed must be the size of the underlying char[] pointed to by the char*. If in doubt use cSFP( ) which will use the strlen(dataPtr) as the capacity.
readFrom(const char*) allows you to read a limited number of chars into a SafeString without errors. This allows you to truncate input data to a length your program can handle.
cSF(sfData,10); char input[] = "some data of unknown length"; unsigned int charsRead = sfData.readFrom(input); // only the first 10 chars are stored in sfData and no SafeString errors triggered // charsRead result is 10
An car ODB data processor will be used as an example application using these SafeString wrappers. The initial attempt at this processor (local copy here) using Arduino Strings and malloc() and strdup() only ran for 30secs before the ESP32 rebooted due to heap fragmentation. That problem was solved using SafeString V1. The OBD_Processor.ino sketch illustrates how to use the SafeString wrapping macros to do the processing.
The typical code to read the OBD data (using ELMduino library) is
myELM327.sendCommand("AT SH 7E4"); // Set Header BMS if (myELM327.queryPID("220101")) { // BMS PID = hex 22 0101 => dec 34, 257 char* payload = myELM327.payload; //size_t payloadLen = myELM327.recBytes; // But ELMduino terminates the result with '\0' so can just use strlen()
but for this example sketch some static rawData will be used as
the payload
char rawData[] = "7F22127F22127F22127F22127F221203E0:620101FFF7E71:FF8A00000000832:00240ED71713133:141713000010C14:22C12800008B005:00450B0000434B6:000019A80000187:1200200EE70D018:7B0000000003E8"; … char *payload = rawData;
The data has the format
headerBytes
then one
frameNumberByte:frameDataBytes
repeated. A : starts each frame of
data and the byte just before the : is the frame number.
The results of parsing the frames will be stored in a data struct which has an array of 9 char[20] arrays. cSFA( ) will be used to wrap the each array to store the results.
struct dataFrames_struct { char frames[9][20]; // 9 frames each of 20chars }; typedef struct dataFrames_struct dataFrames; // create a simple name for this type of data dataFrames results; // this struct will hold the results
The void
processPayload(char *OBDdata, size_t datalen, dataFrames &
results) method process the
input data using cSFP( ) to wrap the OBDdata.
The int
convertToInt(char* dataFrame, size_t offset, size_t numberBytes)
method is a utility method that
uses cSFP( ) to wrap the dataFrame pointer setting the SafeString
capacity to it current strlen so bytes can be extracted and converted
to numbers.
The processing statements are :-
char *payload = rawData; processPayload(payload, results); printBatteryVolts(results); // plus other data
With the SafeString output set i.e. SafeString::setOutput(Serial); the sketch results are:-
frameIdx:0 SafeString frame cap:19 len:12 '620101FFF7E7' frameIdx:1 SafeString frame cap:19 len:14 'FF8A0000000083' frameIdx:2 SafeString frame cap:19 len:14 '00240ED7171313' frameIdx:3 SafeString frame cap:19 len:14 '141713000010C1' frameIdx:4 SafeString frame cap:19 len:14 '22C12800008B00' frameIdx:5 SafeString frame cap:19 len:14 '00450B0000434B' frameIdx:6 SafeString frame cap:19 len:14 '000019A8000018' frameIdx:7 SafeString frame cap:19 len:14 '1200200EE70D01' frameIdx:8 SafeString frame cap:19 len:14 '7B0000000003E8' hex number hexSubString cap:14 len:4 '0ED7' Battery Volts:379.9
Commenting out the // SafeString::setOutput(Serial); gives just
Battery Volts:379.9
Looking at the processPayload( ) method
void processPayload(char *OBDdata, dataFrames & results) { cSFP(data, OBDdata); // wrap in a SafeString clearResultFrames(results); int idx = data.indexOf(':'); // skip over header and find first delimiter while (idx >= 0) { int frameIdx = data[idx - 1] - '0'; // the char before : if ((frameIdx < 0) || (frameIdx > 8)) { // error in frame number skip this frame, print a message here SafeString::Output.print("frameIdx:"); SafeString::Output.print(frameIdx); SafeString::Output.print(" outside range data: "); data.debug(); idx = data.indexOf(':', idx + 1); // step over : and find next : continue; } cSFA(frame, results.frames[frameIdx]); // wrap a result frame in a SafeString to store this frame's data idx++; // step over : int nextIdx = data.indexOf(':', idx); // find next : -1 if not found if (nextIdx > 0) { data.substring(frame, idx, nextIdx - 1); // next : found so take chars upto 1 char before next : } else { data.substring(frame, idx, nextIdx); // next : not found so take all the remaining chars as this field } SafeString::Output.print("frameIdx:"); SafeString::Output.print(frameIdx); SafeString::Output.print(" "); frame.debug(); idx = nextIdx; // step onto next frame } }
The input data is wrapped for processing. The ELMduino terminates the OBDdata received data with a terminating '\0', so we can just wrap using createSaftStringFromPtr() ( cSFP() ) which uses strlen(OBDdata) to set the SaftString capacity.
cSFP(data, OBDdata);
Once the frame number has been found the corresponding char[] in the results struct is wrapped in SafeString using
cSFA(frame, results.frames[frameIdx]);
The frame SafeString is then filled with the field data via
data.substring(frame, idx, nextIdx – 1);
Looking at the convertToInt() method
int convertToInt(char* dataFrame, size_t offset, size_t numberBytes) { // define a local SafeString on the stack for this method cSFP(frame, dataFrame); cSF(hexSubString, frame.capacity()); // allow for taking entire frame as a substring frame.substring(hexSubString, offset, offset + (numberBytes * 2)); // endIdx in exclusive in SafeString V2+ hexSubString.debug(F(" hex number ")); long num = 0; if (!hexSubString.hexToLong(num)) { hexSubString.debug(F(" invalid hex number ")); } return num; }
The input char* dataFrame is wrapped in a SafeString using
cSFP(frame, dataFrame);
As you can see above the frames are not all the same length. cSFP( ) sets the SafeString capacity to the current strlen of the c-string in the dataFrame, so SafeString will detect if you try to access bytes outside the current data and output an error if SafeString::setOutput(Serial); has been called. The SafeString hexToLong() method is used to convert the hex bytes to a number and will output a message if the bytes are not valid hex.
You cannot create SafeStrings inside a C++ class. For example
class Loco { public: // Members createSafeString(_niceName, 12); createSafeString(_locoName, 12); int _locoAddress; . . .
Will give compile errors like
libraries/SafeString/src/SafeString.h:209:94: error: expected identifier before 'sizeof'
Instead you use char[]'s in the class and then wrap them in
SafeString for processing.
class Loco { private: // make these private to protect from external access char _niceName[12]; char _locoName[12]; int _locoAddress; public: // Members Loco(const char* niceName, const char* locoName, int locoAddress); void printData(Stream& out); }; Loco::Loco(const char* niceName, const char* locoName, int locoAddress) { cSFA(sfNiceName, _niceName); //wrap in SafeStrings for processing cSFA(sfLocoName, _locoName); sfNiceName = niceName; sfLocoName = locoName; _locoAddress = locoAddress; } void Loco::printData(Stream &out) { out.print(" niceName:"); out.println((char*)_niceName); out.print(" locoName:"); out.println((char*)_locoName); out.print(" location:"); out.println(_locoAddress); }
So when you want to work with the char _niceName[] array, you first
wrap it in a SafeString using
createSafeStringFromArray(sfNiceName,
_niceName); OR cSFA(sfNiceName, _niceName); for short.
Then any changes you make to the SafeString sfNiceName are reflected and saved in the char[] _niceName. Notice that cSFA(sfNiceName, _niceName); automatically picks up the valid length of the char _niceName[12]; That is capacity for 11 chars plus a terminating '\0'.
If you try to create a Loco object with a niceName that is too long. SafeString will ignore it and leave the _niceName at an empty c-string. So if you see empty fields for Loco move the construction into the setup() AFTER calling SafeString::setOutput(Serial); to see the error messages. The example sketch SafeStringsInClasses_1.ino shows the locoBad instance temporarily moved into the setup() to show the error messages.
Good C++ class design should protect its data from uncontrolled external changes. So if you need to update _niceName is should be done via a Loco class method. e.g.
void Loco::setNiceName(const char* newNiceName) { cSFA(sfNiceName,_niceName); // wrap in a SafeString sfNiceName = newNiceName; // update _niceName, checking it will fit and copying }
OR
void Loco::setNiceName(SafeString& sfNewNiceName) { cSFA(sfNiceName,_niceName); // wrap in a SafeString sfNiceName = sfNewNiceName; // update _niceName, checking it will fit and copying }
Also since the char[]s are private in the Loco class, you will need methods to access them. While you could write a method like
const char* Loco::getNiceName() {
return (const char*)_niceName;
}
this would expose the char _niceName[12] to corruption by external code.
A better method would be
void Loco::getNiceName(SafeString& result) {
result = _niceName; // copy current contents of _niceName into external SafeString
}
which gives the external code a copy that can be modified without changing the of the interval value.
If you have an array of c-strings i.e.
#define MAX_STRING_SIZE 40 char arr[][MAX_STRING_SIZE] = { "array of c string", "is fun to use", "make sure to properly", "tell the array size" };
You can easily and safely work with them by wrapping the elements in SafeStrings as needed. i.e.
createSafeStringFromCharArray(sfarr0, arr[0]); // or cSFA(sfarr0, arr[0]); for short
The SafeString sfarr0 automatically knows the maximum capacity
of arr[0] and will not let you exceed it.
When you modify
the SafeString, sfarr0, the underlying arr[0] c-string is
updated.
See the sketch SafeStringWithArraysofCstring.ino for a working example.
Using SafeString for structs containing char[] is similar
struct { char buffer_0[3]; char buffer_1[10] = "test"; char buffer_2[8]; } buffers; createSafeStringFromCharArray(sfBuffer_0, buffers.buffer_0); // or cSFA(sfBuffer_0, buffers.buffer_0); for short
sfBuffer_0 will now use buffers.buffer_0 as it backing char[] and any changes made to sfBuffer_0 will update the struct. If the buffer_0 memory has garbage in it, SafeString may complain about an untermiated string, but it will clean up the memory to put a '\0' at the end of buffer_0, i.e. in buffer_0[2], to make it valid.
SafeString automatically picks up the sizes of the buffers_.. and sets the maximum capacity of the SafeString to suit.
createSafeStringFromCharArray(sfBuffer_1, buffers.buffer_1); // or cSFA(sfBuffer_1, buffers.buffer_1); for short
Will wrap buffers.buffer_1 including the existing text. The sfBuffer_1 will have a capacity of 9 chars + the terminating '\0' and will initially contain the string 'test'
Because the createSafeStringFrom..() macros wrap existing c-string data, it is possible, but not advisable, to intermix calls to SafeString methods and unsafe c-string methods, like strcat( ). However typically you would do all your processing using SafeString methods, either in a method or within a code block { }
convertToInt() and processPayload() above are examples of doing all the processing with SafeStrings within a method. If you just have little processing to do you can do it in a small block e.g.
Serial.println(str); // => initial data { cSFP(sfStr,str); // wrap in a local SafeString for a bit of processing. sfStr.replace("data","d"); // can reduce str length just cannot increase it } Serial.println(str); // => initial d
However if you do intermix SafeString method calls with c-string methods, the wrapped SafeStrings remain valid.
Before executing each method of a wrapped SafeString:-
a) the
underlying c-string is re-terminate to the SafeString capacity() and
b) the strlen called to resynchronize the SafeString length to
length of the underlying c-string.
Once a SafeString has been created its capacity cannot be changed. It has static size. So even if the c-string pointed to has extra space and more characters are added later, the SafeString will truncate the string to the SafeString's original capacity to prevent possible memory overruns. SafeString also resynchronizes its length to the current strlen( ) to allow for external changes.
For example
char testData[50] = "initial data"; // space for more chars here char* str = testData; Serial.print(F("str => "));Serial.println(str); cSFP(sfStr, str); // create from a char* capacity is set to initial strlen sfStr.debug(F("Initial cSFP( ) contents :")); strcat(testData, "123"); // access the c-string, str, directly sfStr.debug(F("After strcat :")); // no change is capacity Serial.print(F("str => "));Serial.println(str); // underlying str was reterminated at capacity str[7] = 0; // truncate underlying str sfStr.debug(F("After str[7] = 0")); // new length picked up Serial.print(F("str => "));Serial.println(str); // underlying str was reterminated at capacity
Produces the output the following output. Note how the strcat of "123" is lost when the sfStr SafeString re-terminates the underlying c-string to the original capacity. But the new string length after str[7] = 0; is picked up.
str => initial data Initial cSFP( ) contents : sfStr cap:12 len:12 'initial data' After strcat : sfStr cap:12 len:12 'initial data' str => initial data After str[7] = 0 sfStr cap:12 len:7 'initial' str => initial
So after performing c-string operations on the underlying char *,
SafeStrings remain safe, but it intermix c-string methods and
SafeString methods is not advised as the results may not be what you
expect.
Also see the SafeStringFromCharArray.ino,
SafeStringFromCharPtr.ino and SafeStringFromCharPtrWithSize.ino
example sketches included with the SafeString library.
A final point on the createSafeStringFrom...() macros. As discussed above each time a SafeString method is called on a SafeString created with one of these macros, the current length of the underlying char[] is checked using strlen( ) and the SafeString.length() updated. This picks up any changes made directly on the underlying char[]. The more times methods are called and the larger the underlying c-string is the more processing time this takes. You can trade this processing cost for memory usage by creating a local SafeString, using createSafeString() or cSF() and copying the data to it. e.g. instead of
void processPayload(char *OBDdata, dataFrames & results) { cSFP(data, OBDdata); // wrap in a SafeString
use
void processPayload(char *OBDdata, dataFrames & results) { cSF(data, strlen(OBDdata)); // create a local SafeString large enough to hold the incoming data data = OBDdata; // copy the OBDdata to the SafeString, = does a copy
Then calling methods on the data SafeString created using cSF( ),
do not call strlen( ), because only the SafeString has access to the
data. This saves processing time at the expense of the extra stack
space used for the data char[] the cSF() macro adds. Also any changes
made via the cSF() SafeString methods do not effect the input
OBDdata.
The example sketch SafeString_ReadFrom_WriteTo.ino (included with the SafeString library), illustrates creating method 'static' SafeStrings which keep their value across method calls. That example also covers the readFrom() and writeTo() SafeString methods. SafeString V2+ removes readBuffer(),writeBuffer() and adds the safer readFrom(), writeTo() methods
Method 'static' SafeStrings are created by wrapping 'static' char[]. e.g.
size_t processInput(char* text, size_t txtIdx, char* outBuf, size_t outBufSize ) { // these two method static's keep their values between method calls static char inputArray[MAX_CMD_LENGTH + 2]; // +1 for delimiter, +1 for terminating '\0' keep this array from call to call static char tokenArray[MAX_CMD_LENGTH + 2]; // +1 for delimiter, +1 for terminating '\0' keep this array from call to call cSFA(input, inputArray); // create a 'static' SafeString string by wrapping the static array inputArray cSFA(token, tokenArray); // create a 'static' SafeString string by wrapping the static array tokenArray
The rest of the example covers reading small parts of large
buffer into the input SafeString, via readFrom(), to parse
valid commands. Any valid command found is then written to a very
small output buffer, via writeTo(). The output buffer is very slowly
printed out. As space becomes available in the output buffer the rest
of the command is written to it. All of this processing is
non-blocking. The loop() code continues to run at maximum speed.
See Arduino Serial I/O for the Real World for a simpler method of reading input
The SafeString_firstToken_nextToken_Example.ino
and SafeString_stoken_Example.ino
sketches illustrates extracting numbers from a CSV (comma separated
values) input and converting the valid fields to numbers. Also see
the SafeString_nextToken.ino and SafeString_stoken.ino
examples included with the SafeString
library
Commenting out the
//
SafeString::setOutput(Serial);
to
remove all the SafeString::Output.print(
) and
debug( ) output
gives the following
10 9 8 7 6 5 4 3 2 1 The input is 23.5, 44a ,, , -5. , +.5, 7a, 33,fred5, 6.5.3, a.5,b.3 Fields with numbers are:- Field 1 : 23.50 Field 5 : -5.00 Field 6 : 0.50 Field 8 : 33.00
The difference between firstToken/nextToken and stoken is that first/nextToken consumes the input as the tokens are returned and will return false when finished, whereas stoken keeps the input unchanged and needs the starting index incremented each time to stop over the last delimiter found, while checking for the -1 (not found) return. first/nextToken are generally easier to use. Both stoken and first/nextToken have an optional argument to return empty fields from consecutive delimiters. first/nextToken has a final optional parameter to not return the last un-terminated token, while stoken has a final optional parameter use the delimiters as the valid field chars instead of as delimiters.
If you where using the Arduino String class there would be two problems, i) there is no tokenize method and ii) fields like 44a would be interpreted as valid numeric fields.
unsigned char firstToken(token, delimiters, bool returnLastNonDelimitedToken = true);
nextToken() leaves the token delimiter in the string, so the next call to nextToken() starts by stepping over the any delimiter at the start of the string. So when you start processing a new string, if the first field is empty, i.e. the leading character is a delimiter, nextToken() will just step over it even if you have set the returnEmptyFields optional argument to true.
firstToken() handles this case and will return an empty token if the first character in the string is a delimiter. So if you want to handle and process a possibly empty first field then use firstToken(token, delimters); for the first call and then use nextToken(token, delimiter, true); for subsequent calls to handle subsequent empty fields. On the other hand, if you don't want empty fields returned, then you can just use nextToken(token, delimiter); for the first and subsequent calls.
Finally firstToken(.. ) is just calls nextToken( .. , true) where the last optional argument of nextToken is set to true, so you can also just use nextToken( ) and change the last optional argument from true for the first call to false for subsequent calls.
See Arduino Serial I/O for the Real World for a detailed tutorial on handling Serial I/O without blocking the rest of your sketch. SafeString V3 includes the SafeStringReader class that simplifies reading delimited tokens from Streams. SafeStringReader wraps up the SafeString readUntilToken() method and its supporting variables. The examples SafeStringReader_Cmds.ino and SafeStringReader_CmdsTimed.ino illustrate reading user commands.
You often need a way to enter commands to control a sketch, while it is running. The standard Arduino Stream methods, readString(), readStringUntil(), etc, do not support this because they pause the whole program while waiting for the user input. They are completely unsuitable when the rest of the sketch is controlling something in 'real time', e.g. a stepper motor.
The basic user commands sketch is simply:-
createSafeStringReader(sfReader, 5, " ,\r\n"); // create the SafeStringReader sfReader and its supporting buffers // to handle cmds upto 5 chars long delimited by space, comma, Carrage return or Newline void setup() { Serial.begin(9600); for (int i = 10; i > 0; i--) { // pause a little to give you time to open the Arduino monitor Serial.print(i); Serial.print(' '); delay(500); } Serial.println(); SafeString::setOutput(Serial); // enable error messages and SafeString.debug() output to be sent to Serial sfReader.connect(Serial); // where SafeStringReader will read from sfReader.echoOn(); // echo back all input, by default echo is off // sfReader.setTimeout(2000); // optionally set a non-blocking read timeout } void handleStartCmd() { // handle start cmd here } void handleStopCmd() { // handle stop cmd here } void loop() { if (sfReader.read()) { if (sfReader == "start") { handleStartCmd(); } else if (sfReader == "stop") { handleStopCmd(); } // else ignore unrecognized command } // else no delimited command yet // rest of code here is executed while the user typing in commands }
The process is, read some input until a delimiter is found, check the token against known commands then execute rest of the loop code, and repeat. There are no delays waiting for user input in this process, so the loop() runs at maximum speed. Although this sketch will accept an unlimited number of input characters and commands in one line, the size of the input and token SafeStrings need only be 1 more then the longest command (to allow for the delimiter), so the sketch uses very little RAM memory. If the Arduino monitor is set to No line ending, or the input is being sent character by character from a terminal program, then an optional non-blocking timeout can be set to automatically terminate the last command after chars stop coming
For most use cases the SafeStringReader is all that is needed. However the SafeString class itself also has three supporting non-blocking read methods, read( stream ), readUntil( stream, .. ) and readUntilToken( stream, … ), if you need more flexiabilty than the SafeStringReader provides.
To convert an Arduino (C++) String to a SafeString, create as SafeString of sufficient size and copy the String to the SafeString.
String arduinoStr("this is a string"); Serial.print(" Arduino String : "); Serial.println(arduinoStr); createSafeString(sfStr, arduinoStr.length()); // create a SafeString of sufficient size sfStr = arduinoStr.c_str(); // copy arduinoStr to the SafeString sfStr.debug();
This gives the output, (if SafeString::setOutput(Serial); has been called)
Arduino String : this is a string
SafeString sfStr cap:16 len:16 'this is a string'
To convert from a SafeString back to an Arduino (C++) String just use
arduinoStr = str.c_str();
The major difference is that the capacity of a SafeString is fixed at creation and does not change, where as Arduino Strings can resize dynamically using more memory then you expected and fragmenting the heap-space. SafeStrings are also more robust and provide detailed error messages when an operation cannot be preformed.
SafeString is always left unchanged and in a valid state even if
the operation cannot be preformed. On the other hand, Arduino Strings
loose all the existing data if the dynamic resize fails and some
operations result in invalid String states. e.g.
String
str = "abc";
str += '\0';
or
str[2] = '\0';
result in an invalid Arduino
String.
In SafeString, s_str += '\0'; and s_str.concat('\0');
errors are caught and ignored and generate an error message. While
str[ .. ] = is not supported because the error cannot be
caught. In SafeStrings use setCharAt( )
instead.
str.setCharAt(2,'a');
SafeStrings do not support the + operator, because it creates
temporary Strings objects and is problematic in its application. For
example
String testStr1;
testStr1 = "a"
+ 10; // this compiles and runs
testStr1 = "a" +
10 + "b"; // this does NOT compile
With SafeStrings use the += operator or the concat()
method, i.e.
createSafeString(testStr, 20);
testStr
= "a";
testStr += 10;
testStr += "b";
SafeStrings has three non-blocking read methods, readUntilToken(), readUntil() and read() which allow you to look for user input without halting the rest of your program. See the User Commands Example above. The SafeString library also includes the BufferedOutput class that provides non-blocking print() and write() methods. Together they provide practical non-blocking replacement for the Arduino Serial blocking class. See Arduino Serial I/O for the Real World
SafeString has two tokenising methods. stoken() is the low level method which can be used to split a string into tokens separated by specified delimiters or extract tokens that only contain characters from a specified valid set of characters. nextToken() is a high level method that will remove a delimited token from the SafeString and and return it for processing. See the User Commands Example above and the SafeString_stoken example sketch provided with the library.
As shown in the previous examples, SafeStrings implement the Print interface so you can print( ) to a SafeString to add text to it and use all the familiar formatting features available with print( ).
There are noticeable differences between SafeStrings and Arduino Strings for string to number conversions. In Arduino Strings, if the conversion fails the method returns 0 or 0.0 , so you cannot tell if the conversion failed or the number was actually 0 Also the Arduino String number conversions do not look at the whole string but just stop and return a result as soon as the next character is invalid. For example for Arduino String toInt() on “5a” returns 5 as the answer, and for toFloat(), “5.3.5” returns 5.3.
The SafeStrings conversion methods, on the other hand, return true or false to indicate if a valid number was found and if one is found then the argument variable is updated. For example (also see the SafeStringToNum example sketch provided with the library.)
createSafeString(str, 7, " 5.3.6 ");
float
f = 0.0;
bool rtn = str.toFloat(f);
will
return false and
leave f unchanged
createSafeString(str, 7, " 5 ");
long
ans = 0;
bool rtn = str.toLong(ans);
will
return true and set
ans to 5.
Arduino
String's toInt() method
actually converts the string to a long
SafeString has extensive error checking built-in. SafeString also has error flags your program can check, even if error messages are turned off.
SafeString has a global error flag which is set whenever an error is detected in any SafeString.
SafeString::errorDetected() will return true in there has been any errors since the last time this method was called. The global error flag is cleared each time errorDetected() is called.
Each SafeString object has its own private error flag and calling hasError() on a SafeString object will return true if there has been any error detected on that SafeString since the last call to hasError(). e.g.
cSF(sfStr,5); sfStr = "abcdeft"; if (sfStr.hasError()) { Serial.println(F("Error detected on sfStr")); }
Global SafeString are setup before the sketch starts running, so you cannot print out error messages for them. However you can check if there are any errors by checking SafeString::errorDetected() in the setup().
void setup() { Serial.begin(9600); SafeString::setOutput(Serial); // for error msgs if (SafeString::errorDetected()) { Serial.println(F("Error setting up global SafeStrings")); } . . .
To enable the error messages, you need to tell SafeString where to
send them to using the setOutput( )
method
SafeString::setOutput(Serial);
Then if your code tries to exceed the capacity of a SafeString,
you will get an error message sent to Serial e.g.
stringOne
= "abc";
stringOne += "123456789";
Error: stringOne.concat() needs capacity of 12 for the first 9 chars of the input. Input arg was '123456789' stringOne cap:8 len:3 'abc'
The error message tells you which operation failed, why it failed
and a summary of the current state of the SafeString object and it
current contents. This is enough for you to find and fix the problem.
In this case by increasing the size of stringOne to 12 in the
createSafeString( ) macro.
You can use the setVerbose( ) method to turn off/on the
printing of the current string contents in error messages at various
points in your sketch.
SafeString::setVerbose(false);
stringOne
= "abc";
stringOne += "123456789";
now outputs a more compact error message.
Error: stringOne.concat() needs capacity of 12 for the first 9 chars of the input. --- stringOne cap:8 len:3
You often want to debug your program by outputting the value of a
SafeString at various points in the program. The debug()
method does that for you. Once you have told SafeString where to send
error/debugging messages, you can use the debug( ) method to
output the current state of a SafeString. e.g.
stringOne.debug();
SafeString stringOne cap:8 len:3 'abc'
To get a compact output without the string's contents
use
stringOne.debug(false);
SafeString stringOne cap:8 len:3
You can also add a title, either a " ... " or an F( )
string or another SafeString, to the debug output
e.g.
stringOne.debug(F(" After stringOne =
\"abc\";") );
After stringOne = "abc"; stringOne cap:8 len:3 'abc'
You can use the statement
SafeString::turnOutputOff();
to
turn off all error messages and debug() output from then on. Use
SafeString::setOutput(Serial);
to
turn it back on again.
You can print debugging messages
using SafeString's Output. e.g.
SafeString::Output.println("
string debug msg.. ");
If SafeString::setOutput( ) has been called then SafeString::Output.print( will print to that Stream. Otherwise the output is discarded. This lets you add string debugging messages that will disappear when you turn off the other SafeString error messages and debugging output.
The SafeString Error messages
add a noticeable amount of code to your program. If you are running
out of program space, you can quickly remove all the error messages
code by commenting out
// #define SSTRING_DEBUG
at
the top of the SafeString.h file and recompiling your sketch. This
will remove all the error message handling but will keep all the
error checks so that your code is still safe and robust.
The debug() method and
SafeString::Output.print( methods are still available with
//#define SSTRING_DEBUG commented out, but the name of the variable
is not longer included in the debug() output. i.e.
stringOne.debug(F(" After stringOne = \"abc\";")
);
After stringOne = "abc"; cap:8 len:3 'abc'
Sending output to Serial can delay the rest of you program from running. At 9600baud it takes about 1mS per char to send the output. Once the outgoing Serial buffer fills up, your program stops and waits for some chars to be sent to free up space in the buffer so it can send the rest of the output. See Arduino Serial I/O for the Real World for how to over come this.
You can also reduce the amount of SafeString output by using setVerbose(false); This gives compact error messages which omit the printing the contents of the SafeString. debug(..,false); does the same for debug() statements
C-string methods have been the bane of programmers for over 30 years and the cause of so many programming errors and security breaches that Microsoft has banned their programmers from using them (local copy here) and text books have been written on why they should not be used. For example Secure Coding in C and C++, 2nd Edition by Robert C. Seacord, Ch5 ( local copy here) which discusses the dangers of particular c-string methods and Ch2 (local copy here) which talks about how easy it is to get coding errors. Low level operations on char[]s, a char at a time, are also a major source of errors. Hackers love the use c-string methods and char[] code as it allows them to force buffer overflows and gain access to computer systems. See the Wikipedia Buffer_overflow entry and for just the latest example of a commercial coding error using c-strings and char[] operations, see the 'bug' in Unix's sudo code. The lastest IPhone security failing is another recent example.
Both Sparkfun and Adafruit advise against using
the Arduino String class.
Sparkfun's
comment on the String class is “The String method (capital
'S') in Arduino uses a large amount of memory and tends to cause
problems in larger scale projects. ‘S’trings should be
strongly avoided in libraries.
Adafruit's
comment on the String class is “In most usages, lots of
other little String objects are used temporarily as you perform these
(String) operations, forcing the new string allocation to a new area
of the heap and leaving a big hole where the previous one was (memory
fragmentation). “
However as the tutorial on Taming Arduino Strings shows you can avoid the memory fragmentation and excessive memory usage, by following a few simple guidelines. And the good news is that on Uno and Mega2560 using Arduino Strings is extremely safe and robust, even if you run out-of-memory. If you run out-of-memory, you will just not get all the text in the Strings that you expect. The program will continue to run. See What happens when UNO runs out of heap memory in the Taming Arduino Strings tutorial
However there are some errors in Arduino's String code. The current Wstring.cpp code supplied with Arduino contains a number of bugs which cause programs using it to either fail or give un-expected results. For example
String a; a = "12345"; Serial.println(a); a += a; Serial.println(a);
causes an Arduino UNO to continually reboot.
Another example is
String aStr; aStr = ""; aStr.concat('\0'); Serial.print("aStr .length():");Serial.println(aStr .length()); Serial.print("strlen(aStr.c_str()):");Serial.println(strlen(aStr .c_str()));
which outputs
aStr.length():1
strlen(aStr.c_str()):0
That is the String object length() is no longer the same as the strlen() of the underlying char buffer. The Arduino String class has a number of these types of errors.
In contrast using SafeStrings gives
outputs of
For the first example
12345
1234512345
And for the second example gives the error message
Error: aStr.concat() of '\0'
Using SafeStrings, the first example works as expected and the second
example gives a detailed error message contain the name of the
SafeString, aStr ,
involved, and leaves the original value of aStr
unchanged.
Most, but not all of, WString's bugs could be fixed but
that would not fix the basic problems with Arduino Strings dynamic
memory allocation and lots of object creations and data copying.
In number conversion, Arduino String.toInt() can give odd errors. For example
String a_str("123456789012345"); Serial.println(a_str.toInt());
Outputs
-2045911175
While SafeString toLong(num) returns false indicating the string is not a valid long and the num argument is unchanged. See the SafeStringToNum example sketch provided with the library.
String processing using low level C character arrays (using strcat, srtcpy etc) is a major source of program crashes and is not recommended. While the Arduino Strings are very usable and safe if you follow the guidelines, if you want to keep complete control over your memory usage or you are working with a library that returns a char* or you want a richer set of readers, parsers and text functions and a library that provides detailed debug error messages, then SafeStrings are preferred.
SafeStrings are easy to debug. SafeStrings
provide detailed error messages, including the name of SafeString in
question, to help you get your program running correctly.
SafeStrings are safe and robust. SafeStrings
never cause reboots and are always in a valid usable state, even if
your code passes null pointers or '\0' arguments or exceeds the
available capacity of the SafeString.
SafeString
programs run forever. SafeStrings
completely avoid memory fragmentation and never makes extra copies
when passed as arguments.
SafeStrings are faster.
SafeStrings do not create multiple
copies or short lived objects nor do they do unnecessary copying of
the data.
SafeStrings can wrap existing c-strings.
Wrapping existing char* or char[]
in a SafeString object allows you to safely perform string
manipulations on existing data.
The SafeString library provide numerous examples and as well as simple practical sketch which process user commands while keeping running the sketch at maximum speed
For use of the Arduino name see http://arduino.cc/en/Main/FAQ
Contact Forward Computing and Control by
©Copyright 1996-2024 Forward Computing and Control Pty. Ltd.
ACN 003 669 994