At one point while working on my latest and largest Arduino project, I discovered the joys of debugging with sprintf, then was disappointed when it didn't play nice with the Arduino String class. I foolishly decided that it would be a good idea to try to fix that myself in several different ways.
Before you tell me, I know every part of this is bad practice. If I ever do make it work I almost certainly won't use it, but I think I might be able to learn something about C++ and embedded systems programming through it (before C++ the lowest-level programming language I had used was Python).
This is the .cpp file:
#ifndef Ssprintf
#define Ssprintf
#include "Arduino.h"
// Change these
#define REPLACEMENT_LEN 128
#define REPLACEMENT_TRUNC_TEXT "[...]" // NOTE: change the next line whenever you change this
#define REPLACEMENT_TRUNC_LEN 5
// Don't change these
#define FORMAT_CHARS "diuoxXfFeEgGaAcspn%S"
// Helper function
#define sprintf_trunc(replacement, format, ...) \
if (snprintf(replacement, REPLACEMENT_LEN - REPLACEMENT_TRUNC_LEN, format, __VA_ARGS__) \
> REPLACEMENT_LEN - REPLACEMENT_TRUNC_LEN) \
strcat(replacement, REPLACEMENT_TRUNC_TEXT);
inline String va_ssprintf(String format, va_list args) {
String result = "";
int strpos = 0;
int fmtStartPos;
int fmtEndPos;
while(true) { // Includes a break; statement
fmtStartPos = format.indexOf('%', strpos);
if (fmtStartPos == -1) break; // We're past the last argument
fmtEndPos = fmtStartPos + 1; // Adds one later so we don't find the first % again and think it's %%
// Find the next character
// Logic: Increment fmtEndPos while the fmtEndPosth char is not in FORMAT_CHARS.
// fmtEndPos ends up being the first character AFTER the format specifier, which is good.
while(strchr(FORMAT_CHARS, format.charAt(fmtEndPos++)) == NULL);
// Create a buffer for the substring that is big enough for the format string and \0...
char substr[fmtEndPos - fmtStartPos+1];
// ..and populate it with the substring
format.substring(fmtStartPos, fmtEndPos).toCharArray(substr, fmtEndPos - fmtStartPos + 1);
// Allocate some memory for the replacement (fixed size)
char replacement[REPLACEMENT_LEN];
// Now get the arg value according to variable type and use it (sprintf must be in the scope of each case)
switch (substr[strlen(substr) - 1]) {
// Special treatment cases first
case 'S':
{ // Arduino string
// Get the string
String* argPtr = va_arg(args, String*);
String argStr = *argPtr;
// Convert the string to a char*
char arg[argStr.length()+1];
argStr.toCharArray(arg, argStr.length()+1);
// Replace the 'S' with an 's', that's what sprintf expects
// Logic: Get a pointer to the 'S' char, defererence it into the char, set the char to 's'
*strrchr(substr, 'S') = 's';
// Put at most REPLACEMENT_LEN - REPLACEMENT_TRUNC_LEN characters into the array,
// so there's room for the truncation indicator if necessary
sprintf_trunc(replacement, substr, arg);
break;
}
case '%': // This one's special because it doesn't mean there's an argument to fetch
// The simplest way to handle this is to turn it into sprintf("%s", "%");
sprintf_trunc(replacement, "%s", "%");
break;
case 'n': // Fetch an argument to maintain the place in va_args, but don't use it
// Turn this one into sprintf("%s", "");
va_arg(args, unsigned long);
sprintf_trunc(replacement, "%s", "");
break;
// The rest of the cases are fairly straightforward (except lengths are annoying)
case 'd':
case 'i':
{
if (*strstr(substr, "hh")) {
signed char arg = va_arg(args, int); // compiler told me to use int
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'h')) {
short int arg = va_arg(args, int); // compiler told me to use int
sprintf_trunc(replacement, substr, arg);
} else if (*strstr(substr, "ll")) {
long long int arg = va_arg(args, long long int);
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'l')) {
long int arg = va_arg(args, long int);
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'j')) {
intmax_t arg = va_arg(args, intmax_t);
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'z')) {
size_t arg = va_arg(args, size_t);
} else { // Apparently ptrdiff_t isn't supported
int arg = va_arg(args, int);
sprintf_trunc(replacement, substr, arg);
}
break;
}
case 'u':
case 'o':
case 'x':
case 'X':
{
if (*strstr(substr, "hh")) {
unsigned char arg = va_arg(args, unsigned int); // compiler told me to use int
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'h')) {
unsigned short int arg = va_arg(args, unsigned int); // compiler told me to use int
sprintf_trunc(replacement, substr, arg);
} else if (*strstr(substr, "ll")) {
unsigned long long int arg = va_arg(args, unsigned long long int);
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'l')) {
unsigned long int arg = va_arg(args, unsigned long int);
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'j')) {
uintmax_t arg = va_arg(args, uintmax_t);
sprintf_trunc(replacement, substr, arg);
} else if (*strchr(substr, 'z')) {
size_t arg = va_arg(args, size_t);
} else { // Apparently ptrdiff_t isn't supported
unsigned int arg = va_arg(args, unsigned int);
sprintf_trunc(replacement, substr, arg);
}
break;
}
case 'f':
case 'F':
case 'e':
case 'E':
case 'g':
case 'G':
case 'a':
case 'A':
{
//! Here is an opportunity to do something about arduino sprintf()'s lack of float handling (but I won't)
if (*strchr(substr, 'L')) {
long double arg = va_arg(args, long double);
sprintf_trunc(replacement, substr, arg);
} else {
double arg = va_arg(args, double);
sprintf_trunc(replacement, substr, arg);
}
break;
}
case 'c':
{ // no support for wint_t
int arg = va_arg(args, int);
sprintf_trunc(replacement, substr, arg);
break;
}
case 's':
{
if (strchr(substr, 'l') != NULL) {
wchar_t* arg = va_arg(args, wchar_t*);
sprintf_trunc(replacement, substr, arg);
} else {
char* arg = va_arg(args, char*);
sprintf_trunc(replacement, substr, arg);
}
break;
}
case 'p':
{
void* arg = va_arg(args, void*);
sprintf_trunc(replacement, substr, arg);
break;
}
default: // Shouldn't happen, so this is a format error (but remember to pop an item off va_args)
va_arg(args, unsigned long);
sprintf_trunc(replacement, "%s", "__FORMAT_ERROR__");
break;
}
// Add: Everything from the end of the previous format string (strpos) to the beginning of this format string,
// then the replacement for this format string
result += format.substring(strpos, fmtStartPos);
result += replacement;
//Serial.print("result so far: ");
//Serial.println(result);
// Finally, update our current position in the string. This WON'T be reached on the last iteration
// (when fmtStartPos == -1), so it will remain the end of the last format string after the loop
strpos = fmtEndPos;
}
// Add everything from the last format string to the end of the string
//Serial.print("right-before-final result: ");
//Serial.println(result);
//Serial.print("last substring: ");
//Serial.println(format.substring(strpos));
result += format.substring(strpos);
//Serial.print("final result: ");
//Serial.println(result);
Serial.flush();
return result;
}
inline String ssprintf(String format, ...) {
va_list args;
va_start(args, format);
String result = va_ssprintf(format, args);
va_end(args);
return result;
}
#endif
I'll post the code I'm using to test it below, since it puts the post over the character limit.
I'm looking for any critique except reasons not to do this (like I said, I know not to do this outside of a programming exercise). The behavior is so erratic that I can't offer any more helpful information, so I'm hoping that experienced C++ developers can point out the many errors I'm sure I've made and that might make it more consistent.