Combining variables into strings without serial

Hi, I am fairly new to Arduino and have ran into an issue in designing a continuity tester bit of software. I am trying to return any error messages to an LCD display for the user to see meaning I have no plugin to a PC to just serial print to the display.

I am specifically struggling to insert byte variables into a string to display a specific fault as everything online I find just says to use sprintf or combining strings which I don't have storage for.

This seems to be an incredibly difficult issue for what on the surface appears to be an easy problem so I was wondering if there is just a small bit of code I can use to change my code below to read: "Open circuit on J15 pin 4"?

// i and FAULTCOUNT are predefined
//i = 4
//FAULTCOUNT = 4

      if (((Cable715Begin[i]) > 206)&&((Cable715Begin[i]) < 212)){
      byte F1 = "J15";
      char f1 = char(F1);
      char I = char(i);

      char BOX[] = {"FaultLine" + FAULTCOUNT};
  
       BOX = char("Open circuit on ") + char("f1") + char(" pin ") + char("I");
      }

Ive tried every possible combination I could find online as to how to write my BOX character but these obviously wont compile. Any help would be massively appreciated.

If you don't have RAM for the String then whatever you do (str(ln)cat, s(n)printf, or String class concatenation) is doomed...

Can you clarify what you mean with the memory issue you are referring to?


Side note:

from a C++ perspective, the native type of "J15" is a const char *

if i is an integer, char(i); is not the decimal ASCII representation of the value held in i. In C++, casting an int to a char using char(i) results in the integer being interpreted as its corresponding ASCII (or extended ASCII) character code.
For example:

  • If i = 65, char(i) would produce 'A', because 65 is the ASCII code for 'A'.
  • If i = 48, char(i) would produce '0', because 48 is the ASCII code for '0'.

if you want the text "65" if i = 65 then you need to use sprintf() or itoa() or the String class to get that.


My view though, is that if it's all about printing to a LCD, it's like printing to the Serial monitor, just issue multiple successive prints and don't build the complete string...

Sorry, I mean that my code already utilises alot of dynamic memory and I assumed that if I end up writing lots and lots of strings it could potentially run out of storage.

Thank you for the side not about printing my "i" value I would not of noticed that.

I will investigate using the EEPROM to store my fault messages then recalling it later if its easier to write into that.

Thank you.

can you take a step back and clarify on you want to display and (if you do) manage the log of error messages ?

If you can bound the length of the string, you can use a stack buffer as the print target. This prevents any dynamic allocation. This is a very common approach on memory constrained systems.

You should use snprintf to handle this use case:

constexpr int bufsize = 32;
char buf[bufsize];
if (snprintf(buf, bufsize, "FC:%d OC %d %d", F1, f1, I) >= bufsize) {
  // Not enough space in buf. Panic? Do something else?
}
// Do something with buf.

For example, if my continuity tester were to detect a fault, I would like it to be able to write that fault into "memory" and then after it has finished testing it displays an error menu with all the found faults listed on my LCD screen for the user to scroll through :

"Open circuit on J15 pin 2"
"Open circuit on J15 pin 4"
"Short between J15(1) & J14(3)"
...

I have already created a menu and am writing this to the LCD but I just need to update the menu with the found faults.

Thank you.

It remains to be seen if you are running low on memory. By composing the messages in the fly with snprintf() you can save memory.

Just find common phrases and store them for combing later.

  char buffer[32];
  char *xx = "Hello ";
  char *yy = "World!";

  sprintf(buffer, "%s%s", xx, yy);

  Serial.println(buffer);

I did not test that, and someone will have to say how or if those phrases can be stored in the kind of memory you may have more of, if need be.

a7

byte is a tiny number in range 0-255, there is no way for it to store a 3-character string.

char is a single char and can't contain a string...

It looks like you need to take a C language textbook and start reading from the very beginning - from basic data types and their interactions.

1 Like

I assume most of your messages are combining canned text (that you know in advance) and some numbers.

with a small buffer the size of the LCD line + 1 byte for the trailing null char, you could do some fun stuff, with all the text in Flash memory and get some flexibility.

Here is an example

I create a type for describing what I want in flash memory (here just a format string)

struct ErrorDescriptor {
  char format[50];
};

then I define a list of formats that I find suitable for my needs

static const ErrorDescriptor errorMessage[] PROGMEM =
{
  {"Memory low, HCF"},
  {"Error #%d"},
  {"Error #%d, x >= %04d"},
  {"Error %d, %S"}, // %S means text in PROGMEM
  {"%S"}, // some text from PROGMEM
  {"%2d/%02d/%02d - %2d:%02d:%02d"}, // DD/MM/YY - HH:MM:SS
};

All this is in Flash memory. The descriptor corresponds to what printf() can do with the addition of dealing with flash stored c-strings with %S (capital S). this means I can deal with text stored in flash like

static const char textTryAgain[] PROGMEM = "Try again";
static const char welcome[] PROGMEM = "   -- WELCOME! --";

then I wrote a function that takes a position on screen, an descriptor for the message and then the associated parameters
showErrorMessage(byte x, byte y, const ErrorDescriptor& descriptor, ...)

and now if I call:

  showErrorMessage(0, 0, errorMessage[0] ); // "Memory low, HCF"
  showErrorMessage(0, 1, errorMessage[1], 42 ); // Error #42
  int xMin = 55;
  showErrorMessage(0, 2, errorMessage[2], 42, xMin); // Error #42, x >= 0055
  showErrorMessage(0, 3, errorMessage[3], 10, textTryAgain); // Error 10, Try again

I will see

or if I call

  showErrorMessage(0, 0, errorMessage[5], 7, 11, 24, 14, 7, 2 );
  showErrorMessage(0, 1, errorMessage[4], welcome);

I see

all the text is in flash memory and I'm pretty flexible on the formatting string, within the constrains of printf for AVR

click to see the code
/* ============================================
  code is placed under the MIT license
  Copyright (c) 2024 J-M-L
  For the Arduino Forum : https://forum.arduino.cc/u/j-m-l / https://forum.arduino.cc/t/finding-a-good-search-term/1311566?u=j-m-l

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
  ===============================================
*/

#include <Wire.h>
#include <hd44780.h>                        // main hd44780 header
#include <hd44780ioClass/hd44780_I2Cexp.h>  // i2c expander i/o class header

const uint8_t nbCols = 20;
const uint8_t nbRows = 4;
hd44780_I2Cexp lcd;

struct ErrorDescriptor {
  char format[50];
};

static const ErrorDescriptor errorMessage[] PROGMEM =
{
  {"Memory low, HCF"},
  {"Error #%d"},
  {"Error #%d, x >= %04d"},
  {"Error %d, %S"}, // %S means text in PROGMEM
  {"%S"}, // some text from PROGMEM
  {"%2d/%02d/%02d - %2d:%02d:%02d"}, // DD/MM/YY - HH:MM:SS
};

static const char textTryAgain[] PROGMEM = "Try again";
static const char welcome[] PROGMEM = "   -- WELCOME! --";


void showErrorMessage(byte x, byte y, const ErrorDescriptor& descriptor, ...) {
  char buffer[nbCols + 1];
  va_list args;
  va_start(args, descriptor);
  vsnprintf_P(buffer, sizeof(buffer), descriptor.format, args);
  va_end(args);
  lcd.setCursor(x, y);
  lcd.print(buffer);
}

void setup() {
  Serial.begin(115200);
  int result = lcd.begin(nbCols, nbRows);
  if (result) {
    Serial.print("LCD initialization failed: ");
    Serial.println(result);
    hd44780::fatalError(result);
  }

  showErrorMessage(0, 0, errorMessage[0] ); // "Memory low, HCF"
  showErrorMessage(0, 1, errorMessage[1], 42 ); // Error #42
  int xMin = 55;
  showErrorMessage(0, 2, errorMessage[2], 42, xMin); // Error #42, x >= 0055
  showErrorMessage(0, 3, errorMessage[3], 10, textTryAgain); // Error 10, Try again

  delay(3000);

  lcd.clear();
  showErrorMessage(0, 0, errorMessage[5], 7, 11, 24, 14, 7, 2 );
  showErrorMessage(0, 1, errorMessage[4], welcome);
}

void loop() {}

2 Likes

I dunno if the AVR compiler is special somehow, but on the ESP32 or RP2040 platforms this type of thing is unnecessary. A format string passed as a constant will also reside in flash, so you can just put the format string directly where you need it. The only reason to define the formats in a separate object would be if you needed to use them in multiple places for some reason.

Yes, you are correct, on ESP32 the compiler does the right thing and string literals are in flash memory.

That's not the case for AVR based platforms*, hence the PROGMEM complication.

I assumed @chimney12 is using a small Arduino since he is worried about RAM availability and my example is specifically written with AVR in mind.

* it uses an Harvard architecture, which means it uses separate memory spaces for program code (in flash memory) and data (SRAM). This allows for simultaneous access to both instructions and data, improving performance compared to the von Neumann architecture, where code and data share the same memory space but it creates a challenge as the pointers are not unified nor mapped to the same space: if you pass a pointer, the compiler will assume it's in SRAM not in flash... hence the special xxx_P functions which handle data in flash.


not even that, the compiler can recognize that part of a string literal (up till the trailing nul) is already in flash memory somewhere and just reuse that pointer. if you do

const char * text1 = "Hello world!";
const char * text2 = "world!";

the compiler will not allocate any memory for text2 as it exists already in text1, so I will just use the pointer to the right place. Same applies if you use the very same string literal in multiple places, only one version will end up in flash.

1 Like

I had originally had it as int just testing to see if it was variable type was incompatible.

And you appeared to have missed what I was referencing if you go and read some of the examples. I am not trying to define a string with a char, you should really read what the question is referencing before you make comments about unrelated topics.

Maybe you do it unintentionally, but it is exactly what you do in the line

It is the reason why I would recommend you to read beginner's C++ textbook.

Nice. One more thing would be to add an enum or some other way to refer to the formats with a nice name

  showErrorMessage(0, 0, errorMessage[LOW_HCF] );

or even a step further I can't work out just now bouncing down the road

  showErrorMessage(0, 0, LOW_HFC);

a7

I think you misunderstood but I was just referencing that this was the code I found online to do this which doesn't work so I wasn't sure of what the syntax is for the correct format.

yes that was left to the reader as an exercise :slight_smile:

alternatively the formats could just be directy F() strings or like this

static const char outOfRangeFmt[] PROGMEM = "Error #%d, x >= %04d";

Thanks, this taught me something.

It strikes me that one could still put rodata into the flash memory and read it from there automatically, since the compiler knows the section that's storing the data and therefore could in principle know what instructions to generate to read it. Is there any flag you can pass to the AVR compiler to make it behave more like the more modern platforms, even tho the address spaces are different?

An int can't hold a 3 character string either. An int takes a number between -32768 and 32767

I agree that you seem to over your head with this and lacking some very basic fundamentals.

Unfortunately not because it’s down to the implementation of the functions. You pass a pointer and the receiver does not know if its flash space or sram space (think strcpy or sprintf and the likes - so that’s why there are specific _P equivalent functions)

GCC does not offer the possibility of a uniform memory system on Harvard architectures.

Ah, what a shame. I suppose you could do it for the cases where you directly reference a variable known to be in the flash space, but once you're looking at a variable whose value isn't known until runtime, guessing is right out.

Thanks for entertaining my question!