Passing negative int/long/float/double values to Print.h library

A bit ago I was working on a class that I could use to make managing text on displays a bit easier, and one of the obvious functions would be a print() function/method that would display nearly any data type. It would need to convert it to a char[] and store it in a buffer.
I know this can be done with either templates or method overloading, but I figured there's probably a library that could handle just the overloading functionality, and I found the Print class that's already included as part of the Arduino core libraries, and it seems almost perfect for this.

I'm currently running into two problems with it though:

  1. float/double values passed to print() get split up. If I execute print((float)12.34), then it will get broken up into 4 different prints, the 12, . (decimal), 3 and 4 will all get broken up and handled separately.
  2. Negative integers/longs/floats/doubles will get converted into their absolute (positive) values. I suspect this is maybe related to this thread, but I'm not sure how to fix it when extending the Print class.

All of the code, as well as the example sketch and its serial output are below.

Settings.h

#pragma once

#ifndef __MYPRINT_SETTINGS__
#define __MYPRINT_SETTINGS__

const size_t    MAX_CHAR_LENGTH         = 60; // Char length of strings in UI

#endif

MyPrint.h

#pragma once

#ifndef MY_PRINT_H
#define MY_PRINT_H

#include <Arduino.h>
#include <Print.h>
#include <ArduinoSTL.h>
#include "Settings.h"

#include <stdint.h>
#include <stddef.h>

class MyPrint : public Print {

public: 

  MyPrint();
  ~MyPrint(){};

  size_t write(const uint8_t *buffer, size_t size);

  // This method is required since its extended from the parent Print class, 
  // I'm not entirely sure what its needed. It only returns  sizeof(val);
  size_t write(uint8_t val);

private:
  // Current char/string being displayed (or to be displayed)
  char _printerVal[ MAX_CHAR_LENGTH ]; 
};

#endif

MyPrint.cpp

#pragma once

#include "MyPrint.h"

MyPrint::MyPrint() : Print () {}

size_t MyPrint::write(const uint8_t *buffer, size_t size)  {
  int diff = 0;

  if ( strcmp( buffer, _printerVal ) != 0 ){
    strcpy( _printerVal, buffer );
  }
    
  size_t n = 0;
  while (size--) {
    size_t ret = write(pgm_read_byte(buffer++));
    if (ret == 0) {
      // Write of last byte didn't complete, abort additional processing
      break;
    }
    n += ret;
  }

  // Print the entire buffer
  // NOTE: This will be replaced with the display logic, but for now, it just outputs it to the console.
  std::cout << _printerVal << std::endl;

  return n;
}

size_t MyPrint::write(uint8_t val){
  // Not entirely sure what this is for..
  return sizeof(val);
}

Test_MyPrint.ino

#include "MyPrint.h"

MyPrint * MP;

void setup() {
  Serial.begin(9600);
  delay(2000);

  MP = new MyPrint();

  std::cout << "1)\t";
  MP->print("Y");
  
  std::cout << "2)\t";
  MP->print("Test with char");
  
  std::cout << "3)\t";
  MP->print(String("Test with the forbidden String"));

  std::cout << "4)\t";
  MP->print((int) 12);

  std::cout << "5)\t";
  MP->print((int) -12);

  std::cout << "6)\t";
  MP->print((uint8_t) 63);

  std::cout << "7)\t";
  MP->print((uint16_t) 63);

  std::cout << "8)\t";
  MP->print((unsigned short) 34);

  std::cout << "9)\t";
  MP->print((int) -56);

  std::cout << "10)\t";
  MP->print((int) 56);

  std::cout << "11)\t";
  MP->print((unsigned int) 32);

  std::cout << "12)\t";
  MP->print((long) -78);

  std::cout << "13)\t";
  MP->print((long) millis());

  std::cout << "14)\t";
  MP->print((unsigned long) 78);

  std::cout << "15)\t";
  MP->print((float) 12.34);

  std::cout << "16)\t";
  MP->print((float) -56.78);
}

void loop(){}

Serial output

Note: I'm running this on a Nano Every, but this will be used on other Arduinos too, such as the Uno and Mega. But this is the serial output on the Nano Every

1)	Y
2)	Test with char
3)	Test with the forbidden String
4)	12
5)	12 // Expected: -12
6)	63
7)	63
8)	34
9)	56 // Expected: -56
10)	56
11)	32
12)	78  // Expected: -78
13)	2066
14)	78
15)	12  // Expected: 12.34
.
3
4
16)	56 // Expected: -56.78
.
7
8

The odd test result #'s are 5, 9, 12 15 and 16 above.

And another question.. If you look at line 55 in Print.h, you'll see the method:

virtual size_t write(uint8_t) = 0;

And it looks like pretty much all methods in that class eventually conclude by calling this write method. But all this does is take in a uint8_t parameter and return the result of sizeof(uint8_t val), and I can't figure out what the point of that is supposed to be.

Any input is appreciated. Thanks in advance.

-J

The Print is a base interface class. It contains a methods of converting different datatypes to its character representation. The final output to real -life device should be done by write() method. But the Print class doesn't do it itself, it's write method is virtual and do nothing.
The real printing is done in the child classes, which define different write method for different devices - such as serial console , tft screen or printer

Not sure I follow... Looking at the Print.cpp file, I can see that every one of the print/println/printnumber functions eventually call the write() function. Every one of them just take the value to be printed (regardless of type), load it into a buffer, then pass it to write().

Edit: Nevermind, I get it. Were both saying the same thing I guess. This method is what I update to change how the data gets handled (ie: printing the value to the screen, etc):

        virtual size_t write(const uint8_t *buffer, size_t size);
        size_t write(const char *buffer, size_t size) {
            return write((const uint8_t *) buffer, size);
        }

But then there's this virtual write() method on line 55, which is what kinda confuses me:

        virtual size_t write(uint8_t) = 0;

I don't get what this method is for, and what I'm supposed to do with it.

The write(const uint8_t *buffer, size_t size); method seems to be exactly what I need to override/create, and that's what I did, and it seems to work fine, except for when I try to pass it certain values (as mentioned floats, doubles or negative numbers). That's what I'm struggling with now.

That's the only function you need to overload. In fact, you must overload it as it's purely virtual. It's passed a single character to print. Print it on whatever your display device is and return a value of 1.
That's it. Done. The Print class will take care of every data type it knows how to print and pass the characters to you one at a time.

No need to overload the other version of write().

Hmmm... Ok, so even string values get passed as uint8_t? If im printing an alpha character (or special char), how would you convert from uint8_t?

Edit: Nevermind, that was easy. lol.

One last question - I see how the size_t write(uint8_t val); would let me print the characters one by one, but I actually need to store the entire word in a buffer, so I can do things like compare the value currently being printed with another value. Is there a method I can use to see what the entire value being printed is? Not just one char at a time?

Yes. There's no way around that. The serial bus can only send one byte at a time. No matter what you do it will be sent one character at a time. Even if you convert it to a char array and send that, then the characters in the char array eventually find their way one at a time to the write function. It's the only one that ever actually does any writing and it only does one character at a time.

No. Nothing will be converted. What the print function will do first is asses whether or not the number is negative. If it is then it will print a negative sign character and then print the absolute value of the number one character at a time. Which is what you want it to do. But the number you had is safe, it's not going to make changes to the variable you pass to it. I believe it's even defined as having const arguments.

That thread you linked was about how to read a number from serial that has more than one character. It's not really anything to do with what you asked.

You have the value of that before you print it.

int val = 16;
Serial.print(val);

Now I don't need to read anything back from serial, I have the variable val holding what was printed. I can just look at it.

Are you now talking about on the receiving end? Whatever you do with serial, you're going to do it one byte at a time. That's just how it works.

No no, maybe I should elaborate a bit. This actually has nothing to do with serial, I'm just using that to output the data while I do the development. What I would like to do is use the Print class to re-do some of the code in a class I wrote to help with writing data to an LCD/OLED/etc display.

The reason its handy to store the entire string being printed is because if I print "Hello World" to the screen, then I re-write "Goodbye", it would be useful to know the text that was written there last so I can re-write over the end bit that would typically still be visible.

The (rather crude) library I wrote can be found here, but the part that handles overwriting the existing text with new data is (after removing a bunch of debug comments to serial and commented out code):

// This function looks at the string to be printed, and compares it to the value that was printed 
// last to see if any extra updates are needed (eg: if the previous string was longer, then the
// difference in length will need to be appended to hide the previous value)
TextAnchor * TextAnchor::print( void ){
  uint16_t  colFg = _fgColor, 
            colBg = _bgColor;

  // If highlight is true, then set the fg/bg colors to the highlight colors
  if ( _highlightStatus == true ) {
    // If no fg/bg highlight color is set, then just swap the values
    if ( _fgHlColor == NULL && _bgHlColor == NULL ){
      colFg = _bgColor;
      colBg = _fgColor;
    }
    // But if only one of the fg or bg highlight color is set, then use that
    // color only for that fg/bg (and leave the other as-is)
    else {
      if ( _fgHlColor != NULL ) colFg = _fgHlColor;

      if ( _bgHlColor != NULL ) colBg = _bgHlColor;
    }
  }

  int diff = 0;

  // If the printed text isn't the same as the text printed last time, then check the difference
  // in length (to override the text that would otherwise show from the side), and update the 
  // _printerValPrev
  if ( strcmp( _printerVal, _printerValPrev ) != 0 ) {
    //std::cout << "\t_printerVal is different than _printerValPrev: "  << std::endl;

    if ( strlen(_printerValPrev) > strlen( _printerVal ) ){
      diff = strlen(_printerValPrev) - strlen(_printerVal );
    }
  }

  // Pause interrupts while we set the font details and placement. This prevents other
  // updates that may be triggered via an interrupt from unintentionally injecting
  // print values into this location. For example, this happens if a text anchor is 
  // updated in an interrupt function triggered by an encoder (if the encoder is spun
  // too quickly). 
  if ( _allowInterruptPauses == true ) noInterrupts();

  GFXHandler->setTextSize( _textSize );
  GFXHandler->setFont( NULL );

  //int realX = 0;

  // If right align is enabled, then set X to be n characters to left of _x,
  // where n = length of _printerVal
  if ( _rightAlign == true ){
    GFXHandler->setCursor( _x - (strlen(_printerVal) + diff) * _fontCharWidth, _y );
  }
  else {
    GFXHandler->setCursor( _x, _y );
  }
  
  GFXHandler->setTextColor( colFg, colBg );

  if ( diff > 0  && _rightAlign == true ){
    for ( int i = 0; diff > i; i++ ){
      //std::cout << "\tAdding prefix space #" << i  << std::endl;
      GFXHandler->print(" ");
    }
  }

  GFXHandler->print( _printerVal );

  if ( diff > 0  && _rightAlign == false ){
    for ( int i = 0; diff > i; i++ ){
      //std::cout << "\tAdding suffix space #" << i  << std::endl;
      GFXHandler->print(" ");
    }
  }

  // If the printed text isn't the same as the text printed last time, then check the difference
  // in length (to override the text that would otherwise show from the side), and update the 
  // _printerValPrev
  if ( strcmp( _printerVal, _printerValPrev ) != 0 ) {
    _lastChangeMs = millis();

    strcpy(_printerValPrev, _printerVal);
  }

  if ( _allowInterruptPauses == true ) interrupts();

  return this;
}

That TextAnchor::print(void) gets called at the end of any function that updates the data to be displayed. For example:

Milliseconds = SUI.newAnchor(67, 15)->setColor(0xF800)->print("<ms>");
Milliseconds->print(millis());

The print("<ms>"); saves <ms> to the buffer then calls the print(void), which will see that it was the first time it was printed, and just save it to a local variable then update the display. Then when the print(millis()) is called, it will first check what the current value being displayed is (using what was stored in memory), and determine how to properly overwrite it.

So if we go back to the original topic, I'm trying to see if there's a good way to use the Print class to handle converting any data type into a char array so I can handle it. The virtual size_t write(const uint8_t *buffer, size_t size); function (here) looks like it would work well for that, but that's where I run into issues with the buffer variable value.

Hope that helps explain it a bit better.

That's generally left to the user of the library to take of. Take a look at a typical LCD class.

The Print class works the way it works. You're free to collect the individual characters sent to the write() function into an array. However, you have no way of knowing when the item being printed is complete.

You could request the user of the library to always use println() and then print the array when you receive the CR.

Well, I guess I'm the user of the library, and I'm writing a class to handle it..

Its also just a bit of a C++ exercise, since I'm working on learning that more.

Very helpful.. lol.

If needed, I can basically just use a lot of the code from the Print library in my class, but I try to avoid doing that if possible.

If I could just overwrite some of the other methods so I could inject some of my own code into them, then that would be great. Like this method:

size_t Print::print(long n, int base)
{
  if (base == 0) {
    return write(n);
  } else if (base == 10) {
    if (n < 0) {
      int t = print('-');
      n = -n;
      return printNumber(n, 10) + t;
    }
    return printNumber(n, 10);
  } else {
    return printNumber(n, base);
  }
}

I tried overwriting it, but then the problem is that it doesn't get called from methods in the base class (im assuming because its not a virtual method)

If you think you need it, it means that you still do not understand how Print class works. You must not override Print class methods except write one.

Do you mean to copy the Print class code to your library?

Instead of that you can just inherit you library from Print class and then all Print class methods can be used as methods of your library itself.

you can't easily, because these functions are not virtual but you would need a late binding to let them overwritten by your implementation.

in theory you can use the write not only to write to the display but also to append the character to a buffer within your display class.

Then you would need a member function which uses this buffer and delete it if not needed any more.

The more realistic approach is:
you define areas of your display where you want to print something.
Each area has x,y,h,w and its content.
you send new data to an area and the area deletes the old content and then writes the new content to the area.

This is exactly what I'm trying to do, but I would need to be able to execute a method at the end that would take the buffer (which would have all of the characters added to it), then runt he logic to compare it with the old string then print it all at once.

I suppose I could create a separate "display()" method and execute it manually any time I know something needs to be updated (like the SSD1306 module's display() method), but if there's a way to "know" when the full string is written to the buffer then I would prefer to do it that way.
Don't char/strings end in a null/0 character? Maybe I could look to see if the character handed to the write() method is a null terminator, and if so, re-load the display with what was just saved in the buffer?...

Right, that's the issue I was running into last. Not sure what a "late binding" is, ill look into it though. But ideally I won't need to do that. The approach mentioned above would be my prefer route (having some logic execute after the full string is written to the buffer).

I was trying to avoid locking display texts to a specific width/height since the value can be updated several times and the size won't always be the same. But that's kinda similar to what I'm doing, but instead of processing it as a box/boundary (x/y/h/w), I keep track of where the text starts (the cursor), its value, font size, etc, and when it needs to be updated I just write-over it with the background of the text being the same as the background color of the full display.

Not the text itself has an fixed width/height but the AREA where the text can be displayed.

Yeah, that's what I was just trying. And I can overwrite methods just fine, but the base Print class won't execute those functions since they aren't virtual functions.
IE: I can easily create my own MyPrinter::print((unsigned long) n, base) method, but none of the classes in the base Print class will call it. So I would have to create any of the methods that rely on that method to do the final print as well, such as the Print::print(unsigned char b, int base) and
Print::print(unsigned int n, int base).

the way would be ... write just appends characters into a newbuffer
and when you call a .flush() you delete the oldBuffer, write newBuffer, copy newbuffer to oldBuffer.

I did see the virtual flush() method. I would have to call it manually though after I execute any of the print functions though, correct?

everytime you want to "finalize" your print, yes.

Basically what you want to do is a double buffering.

You receive data to show somewhere and you accumulate the representation in some kind of off screen buffer and at some point you want to send (flush) that off screen buffer to the real screen.

The challenge is how do you know the buffer is ready to be sent out.

The flush() method is one way to tell the code that it’s ready but if you had a end of text marker like a new line in the data flow then that would work too. Depending on your circumstances having a time driven flush (n times per second for example) could work too.