How to write a function to print different types of String to a LCD?

Hi,

First time posting and new to C / C++ programming, so please be gentle. :slight_smile:

I'm using the LiquidCrystal_I2C library for a project, and my sketch is littered with snippets like:

lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Some flash string")); // Flash String
lcd.setCursor(0, 1);
lcd.print(someCharArray); // Char Array
lcd.setCursor(0,2);
lcd.print(WiFi.localIP()); // IPAddress Data Type

I am exploring the possibility of defining a function that'd allow me to simply write a line like display(F("Some flash string"), someCharArray, WiFi.localIP());. I think the arguments all implement the Printable interface, but I'm not sure how to use it in the function / function signature.

If that is not possible, and overloading the function with every possible permutation of char array, flash string, and IPAddress for the 4 lines of my 20 x 4 LCD display is the only way, I would probably go for void display(byte line, const char* text) { ... } and overloading it twice more for flash strings and IPAddresses.

Thanks in advance for your advice!

one way to this is to use the SafeString-library

variable-type String can eat up all memory over time and make your program crash when used the wrong way.

The name SafeString is program. Safe to use and lots of debugging features for debugging what might be wrong with the string itself
best regards Stefan

I would define the function as

void func(const byte x, const byte y, const char* text);
void func(const byte x, const byte y, const __FlashStringHelper *text);

Thank you @StefanL38 & @blh64 for your input!

Are you able to confirm it is not possible to use a Printable interface (or similar) as function argument to accept all string types in one function?

I'm not very deep into how programming classes and objects works.

I did look inside the SafeString-library and saw there a lot of definitions that seem to be do that assigning any kind of variable-type to a variable of type SafeString with the same += operator

I'm far away from understanding how this works but from taking this short look at it my guessing is that you define it for types and then it works.

especially fro strings if you use a pointer to were the content of your string starts you simple printout just until the zero-terminnating byte is reached

best regards Stefan

i use the following, which also supports debugging by sending each string to the serial interface

void dispOled(
    const char  *s0,
    const char  *s1,
    const char  *s2,
    const char  *s3,
    bool         clr )
{
    char  s [40];

    if (clr)
        display.clear();

    display.setTextAlignment(TEXT_ALIGN_LEFT);

    if (s0)  {
        display.drawString(0, DISP_Y0,  s0);
        if (debug && NUL != *s0)  {
            sprintf (s, "... %s", s0);
            if (ST_ECHO & state)
                Serial.println (s);
        }
    }
    if (s1)  {
        display.drawString(0, DISP_Y1, s1);
        if (debug && NUL != *s1)  {
            sprintf (s, "    %s", s1);
            if (ST_ECHO & state)
                Serial.println (s);
        }
    }
    if (s2)  {
        display.drawString(0, DISP_Y2, s2);
        if (debug && NUL != *s2)  {
            sprintf (s, "    %s", s2);
            if (ST_ECHO & state)
                Serial.println (s);
        }
    }
    if (s3)  {
        display.drawString(0, DISP_Y3, s3);
        if (debug && NUL != *s3)  {
            sprintf (s, "    %s", s3);
            if (ST_ECHO & state)
                Serial.println (s);
        }
    }
    display.display();
}

Do you also define

void dispOled(  const char  *s0, bool clr ) { dispOled( s0, nullptr, nullptr, nullptr, clr); }
void dispOled(  const char  *s0, const char *s1, bool clr ) { dispOled( s0, s1, nullptr, nullptr, clr ); }
...

so you don't have to provide all 4 string arguments every time?
You could also default clr so you don't need to specify it either

no because sometimes you want blank lines (e.g. 1, 3, 4). so just specify NULL for unused lines

Hey @StefanL38, I took some time to read up on SafeString and it does not appear to solve my problem. It seems to help with string manipulation, which I do not have trouble with. It also does not appear to support the IPAddress data type, nor a string in PROGMEM.

To be clear, I print a variety of character types to an LCD using the LiquidCrystal_I2C library. Sometimes, it is a string in PROGMEM, sometimes it is a char array in memory, sometimes it is an IPAddress. The LiquidCrystal_I2C::print() function can handle all these data types with no issue.

The problem arises when I try to define a function that will abstract away the verbosity of the LiquidCrystal_I2C function calls. I am looking for an Interface for this function signature that will allow me to accept all the different data types without duplicating the function code.

This is an example of what the original code looks like:

#include <LiquidCrystal_I2C.h>
#include <IPAddress.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

void setup() {
	lcd.init();
	lcd.backlight();
	lcd.clear();

	char line1[21] = "Line One";
	IPAddress ip(192, 168, 1, 1);

	lcd.setCursor(0, 0);
	lcd.print(F("Line Zero"));
	lcd.setCursor(0, 1);
	lcd.print(line1);
	lcd.setCursor(0, 2);
	lcd.print("Line Two");
	lcd.setCursor(0, 3);
	lcd.print(ip);
}

void loop() {
}

This a working example of how it looks with an overloaded display() function. While this may appear lengthier, it makes the code both more concise and readable each time I have to print to the LCD. Note how each of the display() functions hold identical code, varying only in the function signature.

#include <LiquidCrystal_I2C.h>
#include <IPAddress.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

void display(const byte row, const char* line) {
	lcd.setCursor(0, row);
	lcd.print(line);
}

void display(const byte row, const __FlashStringHelper *line) {
	lcd.setCursor(0, row);
	lcd.print(line);
}

void display(const byte row, const IPAddress line) {
	lcd.setCursor(0, row);
	lcd.print(line);
}

void setup() {
	lcd.init();
	lcd.backlight();
	lcd.clear();

	char line1[21] = "Line One";
	IPAddress ip(192, 168, 1, 1);

	display(0, F("Line Zero"));
	display(1, line1);
	display(2, "Line Two");
	display(3, ip);
}

void loop() {
}

The solution I am looking for, if it exists, is a way to use the Printable, or some similar interface to get this code below to work:

#include <LiquidCrystal_I2C.h>
#include <IPAddress.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

void display(const byte row, <**WHAT CAN I USE HERE?**> line) {
	lcd.setCursor(0, row);
	lcd.print(line);
}

void setup() {
	lcd.init();
	lcd.backlight();
	lcd.clear();

	char line1[21] = "Line One";
	IPAddress ip(192, 168, 1, 1);

	display(0, F("Line Zero"));
	display(1, line1);
	display(2, "Line Two");
	display(3, ip);
}

void loop() {
}

If that were possible, I would then be able to refine the function further to make each instance of displaying something to the LCD even more concise, like so:

#include <LiquidCrystal_I2C.h>
#include <IPAddress.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

void display(
    <**WHAT CAN I USE HERE?**> line0 = "",
    <**WHAT CAN I USE HERE?**> line1 = "",
    <**WHAT CAN I USE HERE?**> line2 = "",
    <**WHAT CAN I USE HERE?**> line3 = "") {
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print(line0);
	lcd.setCursor(0, 1);
	lcd.print(line1);
	lcd.setCursor(0, 2);
	lcd.print(line2);
	lcd.setCursor(0, 3);
	lcd.print(line3);
}

void setup() {
	lcd.init();
	lcd.backlight();

	char line1[21] = "Line One";
	IPAddress ip(192, 168, 1, 1);

	display(F("Line Zero"), line1, "Line Two", ip);
}

void loop() {
}

Sorry for the wall of text, but I hope there is now no ambiguity as to what I am trying to achieve. If you still believe this can be done with SafeString or some interface, I'd really appreciate a code example to demonstrate this. Thank you!

Hey @gcjr, thank you for your comments. Can the dispOled() function be altered to also support strings in PROGMEM and IPAddress? I have described what I hope to achieve in further detail in the comment immediately before this one. Thanks in advance!

it can be passed any c-string. of course it could be an IpAddress.
the calling routine would need to extract a PROGMEM string and pass it as an arg.

1/3:

I don't see a big advantage of having the row number as a separate parameter.
You can achieve a very similar look a like of your source if you write your code in one line:

    lcd.setCursor(0, 0); lcd.print(F("Line Zero"));
	lcd.setCursor(0, 1); lcd.print(line1);
	lcd.setCursor(0, 2); lcd.print("Line Two");
	lcd.setCursor(0, 3); lcd.print(ip);

any other programmer will see imidiatly that you want to set the cursor to a specific row and then print something.

if you disklike the number of characters you have to type in, make 4 functions: r0, r1, r2, r3 and call them before your lcd.print.

2/3:
if you really want to have all possibilities as overload, see the print.h ... you will need at least 10 overloaded functions to make it work for each combination:

    size_t print(const __FlashStringHelper *);
    size_t print(const String &);
    size_t print(const char[]);
    size_t print(char);
    size_t print(unsigned char, int = DEC);
    size_t print(int, int = DEC);
    size_t print(unsigned int, int = DEC);
    size_t print(long, int = DEC);
    size_t print(unsigned long, int = DEC);
    size_t print(double, int = 2);
    size_t print(const Printable&);

3/3:
if you still want to go your way of a new function - consider to use a template.

compiles but not tested with hardware:

#include <LiquidCrystal_I2C.h>
#include <IPAddress.h>
LiquidCrystal_I2C lcd(0x27, 20, 4);

template <class T>
void display (byte row, T something)
{
  lcd.setCursor(0, row);
  lcd.print(something);
}

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  lcd.init();
  char line1[21] = "Line One\0";
  display(0, F("Line Zero"));
  display(1, line1);
  display(2, "Line Two");
  
}

void loop() {
  // put your main code here, to run repeatedly:

}
1 Like

ZOMG, thank you so much! This is exactly what I was looking for. As stated, I am new to C / C++ programming, and an Interface is what one would use in Java equivalent code. I'll definitely go learn everything I can about templates after this. Thanks again!

Also leaving this here for posterity (and the memory-conscious):

O͟r͟i͟g͟i͟n͟a͟l͟

#include <LiquidCrystal_I2C.h>
#include <IPAddress.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

void setup() {
	lcd.init();
	lcd.backlight();
	lcd.clear();

	char line1[21] = "Line One";
	IPAddress ip(192, 168, 1, 1);

	lcd.setCursor(0, 0);
	lcd.print(F("Line Zero"));
	lcd.setCursor(0, 1);
	lcd.print(line1);
	lcd.setCursor(0, 2);
	lcd.print("Line Two");
	lcd.setCursor(0, 3);
	lcd.print(ip);
}

void loop() {
}
Executable segment sizes:
ICACHE : 16384           - flash instruction cache 
IROM   : 236700          - code in flash         (default or ICACHE_FLASH_ATTR) 
IRAM   : 27777   / 49152 - code in IRAM          (IRAM_ATTR, ISRs...) 
DATA   : 1504  )         - initialized variables (global, static) in RAM/HEAP 
RODATA : 1048  ) / 81920 - constants             (global, static) in RAM/HEAP 
BSS    : 26032 )         - zeroed variables      (global, static) in RAM/HEAP 
Sketch uses 267029 bytes (25%) of program storage space. Maximum is 1044464 bytes.
Global variables use 28584 bytes (34%) of dynamic memory, leaving 53336 bytes for local variables. Maximum is 81920 bytes.

T͟e͟m͟p͟l͟a͟t͟e͟

#include <LiquidCrystal_I2C.h>
#include <IPAddress.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

template <class T>
void display(const byte row, T printable) {
  lcd.setCursor(0, row);
  lcd.print(printable);
}

void setup() {
	lcd.init();
	lcd.backlight();
	lcd.clear();

	char line1[21] = "Line One";
	IPAddress ip(192, 168, 1, 1);

	display(0, F("Line Zero"));
	display(1, line1);
	display(2, "Line Two");
	display(3, ip);
}

void loop() {
}
Executable segment sizes:
ICACHE : 16384           - flash instruction cache 
IROM   : 236716          - code in flash         (default or ICACHE_FLASH_ATTR) 
IRAM   : 27777   / 49152 - code in IRAM          (IRAM_ATTR, ISRs...) 
DATA   : 1504  )         - initialized variables (global, static) in RAM/HEAP 
RODATA : 1048  ) / 81920 - constants             (global, static) in RAM/HEAP 
BSS    : 26032 )         - zeroed variables      (global, static) in RAM/HEAP 
Sketch uses 267045 bytes (25%) of program storage space. Maximum is 1044464 bytes.
Global variables use 28584 bytes (34%) of dynamic memory, leaving 53336 bytes for local variables. Maximum is 81920 bytes.

Conclusion:
No difference in SRAM usage. Negligibly more (16 bytes) Flash usage with template, based on printing 4 lines to LCD once. Flash storage with template should be less if printing more times.