Displaying Text on the Adafruit CharlieWing

I bought my Adafruit Feather HUZZAH ESP8266 and CharlieWing from Little Bird Electronics to use as a display for the temperature and humidity sensors I built in my previous post: Example Ruby on Rails Arduino Project. This post describes the other half of the project where the HUZZAH connects to the Rails server and pulls temperature and humidity data to scroll on the CharlieWing’s display.

Checking the Display

First we make sure everything works as expected before we forget about the hardware and focus on the code.

Prerequisites:

  • A Feather HUZZAH set up as per the demo with pin headers installed.
  • A Feather CharlieWing set up as per the demo with pin headers installed.
  • Feather HUZZAH and CharlieWing coupled correctly

Ensure you have both Adafruit_IS31FL3731 and Adafruit_GFX libraries installed in your Arduino libraries folder. Mine on my Mac are in ~/Documents/Arduino/libraries. I’m going to use the main sketch from the Swirl Demo, but the version I fixed previously.

In the Arduino IDE:

  1. File > New
  2. Copy and paste fixed code from Swirl Demo
  3. File > Save “CharlieWingScroller”
  4. Verify. Upload.
  5. Ensure the Swirl Demo code runs on your CharlieWing without locking up

Displaying Static Text

Now we just want to be able to display a text character on the CharlieWing. But how do we do that?

The code provided with the CharlieWing doesn’t give an example of displaying text. However I found a folder ~/Documents/Arduino/libraries/Adafruit_GFX/Fonts/ which has an amazing set of fonts for use on dot matrix displays. Having hand coded a font like this myself I can see the value in these!

I’ve spent some time online looking for a good demo of how this all works. The most useful tidbit I’ve found is in Adafruit’s GitHub repository where they say:

‘Fonts’ folder contains bitmap fonts for use with recent (1.1 and later) Adafruit_GFX. To use a font in your Arduino sketch, #include the corresponding .h file and pass address of GFXfont struct to setFont(). Pass NULL to revert to ‘classic’ fixed-space bitmap font.

Yeah? I don’t know what to make of that. So I’m going to start digging through code.

Wanting to know more about the Adafruit_IS31FL3731_Wing class I went looking for .h and .cpp files with that name, but no luck. I did find header and implementation files for Adafruit_IS31FL3731 in ~/Documents/Arduino/libraries/, so I opened them up.

Near the bottom of Adafruit_IS31FL3731.h you can see the Adafruit_IS31FL3731_Wing class inherits the Adafruit_IS31FL3731 class.

class Adafruit_IS31FL3731_Wing : public Adafruit_IS31FL3731 {

Higher up in the file we see Adafruit_IS31FL3731 inherits Adafruit_GFX.

class Adafruit_IS31FL3731 : public Adafruit_GFX {

Looking at Adafruit_GFX.h we see Adafruit_GFX inherits Print which is the same one we use when we do a Serial.print().

  class Adafruit_GFX : public Print {

Hmm. Can we just do ledmatrix.print(…)? I tried this:

  1. Comment out all the code in the loop() function
  2. In setup(), add the following line to the bottom of the function
    ledmatrix.print('?');
  3. Verify, Upload
  4. And I have a question mark on my CharlieWing!

Let’s get a little greedy and see if can handle a string:

  1. Amend last line of setup() to: 
    ledmatrix.print("Hello");
  2. Verify, Upload
  3. And now I have “He” on the CharlieWing.

That’s not exactly what I wanted, but it shows a string can be displayed – at least partially. But why do we get just “He”? I’d guess that, since the default font is a fixed-width font, the next letter ‘l’ requires a 5 pixel-wide area to be displayed fully. Since there are only three columns of LEDs left it would be truncated, so it’s not displayed at all. Can we change fonts as was indicated above?

Changing Fonts

Back into ~/Documents/Arduino/libraries/Adafruit_GFX/Fonts to find an alternative font file to use. Alphabetically the first fonts listed are all ‘mono’ (mono-space) fonts, so the first non-mono-space font is FreeSans9pt7b.h. Let’s give that a go!

  1. At the top of the main sketch, include the new font:
    #include <Fonts/FreeSans9pt7b.h>
  2. Then call setFont() just before we print(). We have to call it giving the address of the GFXfont struct.
    NOTE: To see where the struct is defined, open FreeSans9pt7b.h and scroll to the bottom.
    ledmatrix.setFont(&FreeSans9pt7b);
    ledmatrix.print("Hello");
  3. Verify, Upload
  4. I now have what looks like the bottom half of the letter ‘H’ on the CharlieWing.

Hmm. I wonder can we set the size? Looking through Adafruit_GFX.h there is a signature there for a function called setTextSize(uint8_t s). Which might be what we want except that the parameter is a uint8_t (unsigned 8-bit integer type). So we can’t give it a decimal value. I’d like to cut the font size in half, but 0.5 won’t work as a size parameter for this function.

I added another line to the bottom of my setup() so now I have these three:

  ledmatrix.setFont(&FreeSans9pt7b);
  ledmatrix.setTextSize(1);
  ledmatrix.print("Hello");

The value ‘1’ for setTextSize() is my attempt to make sure it’s the minimum size it could be. That produced the same result as before. I also tried setTextSize(0) for giggles and it did the same. Damn.

So print() displays the default monospace font just fine but truncates the string at the first character that can’t be completely displayed.

Reading some more of Adafruit_GFX.h I can only find these function signatures:

drawChar(int16_t x, int16_t y, unsigned char c, uint16_t color, uint16_t bg, uint8_t size),
setCursor(int16_t x, int16_t y),
setTextColor(uint16_t c),
setTextColor(uint16_t c, uint16_t bg),
setTextSize(uint8_t s),
setTextWrap(boolean w),
cp437(boolean x=true),
setFont(const GFXfont *f = NULL),
getTextBounds(char *string, int16_t x, int16_t y, int16_t *x1, int16_t *y1, uint16_t *w, uint16_t *h),
getTextBounds(const __FlashStringHelper *s, int16_t x, int16_t y, int16_t *x1, int16_t *y1, uint16_t *w, uint16_t *h);

Some combination of these functions will do what we want. And if not, we implement a few functions of our own. What if we use drawChar() to print a character at a time on the display? Try this:

//  ledmatrix.setFont(&FreeSans9pt7b);
//  ledmatrix.setTextSize(0);
//  ledmatrix.print("Hello");
  ledmatrix.drawChar(0, 0, 'H', 100, 0, 1);
  ledmatrix.drawChar(6, 0, 'e', 100, 0, 1);
  ledmatrix.drawChar(12, 0, 'l', 100, 0, 1);

Since we know only three characters will fit on the CharlieWing using the default font, why bother displaying the whole word “Hello”? Let’s just get the first three up there! I played with the color parameter and found it increases the brightness of the LEDs up to values around 1000 and then has little effect. 100 is plenty bright enough.

And whaddya know? The three characters are rendered just fine, including the ‘l’, which has its rightmost columns overflowing off the end of the display.

Scrolling Fixed Text

Having verified the best we can do is to display only three characters we simply must be able to scroll so a longer message is readable. Scrolling left is the only logical option as all Latin alphabet languages read left to right. (How much harder must it be to implement Arabic or any of the other non-Latin-similar alphabets?)

We’ve already verified that writing a letter off the right end of the display works. If we’re going to implement scrolling, we’ll need to be able to start a string before the x = 0 cursor position. Does that work?

  ledmatrix.drawChar(-2, 0, 'H', 100, 0, 1);
  ledmatrix.drawChar(4, 0, 'e', 100, 0, 1);
  ledmatrix.drawChar(10, 0, 'l', 100, 0, 1);

I’ve decremented the x-axis positions by 2. Yes it works! My next question is: will it always be a maximum of 3 characters displayed at a time, or can even the edges of four characters be displayed?

When we used ledmatrix.print("Hello"); it seemed each character’s glyph included a blank column on the right edge. That is, when we draw a string at (0, 0), the left part of the first characters is displayed in the first (x = 0) column of LEDs. The second character is displayed after the five pixel wide glyph and after a blank column of pixels. So each glyph appears to be six pixels wide, by definition.

If the display is showing the right-most column of the first character on screen (which will be blank) then we still have 14 columns left on our 15 x 7 LED CharlieWing. Two more characters will require 12 pixels leaving 2 columns of LEDs in which to display the first two columns of a fourth character. Therefore we need to write four characters to the screen to account for the partial rendering of the first and fourth characters on screen.

So let’s make the word “Hello” scroll on the screen! If we just write all five characters, because we can, and adjust the x position by -1 each time we write to the display, we should get a complete scroll. Since we’re going to be writing to the screen repeatedly we need to move our code out of setup() and into loop(). Here’s the bottom of setup() and all of `loop()’:

//  ledmatrix.drawChar(-2, 0, 'H', 100, 0, 1);
//  ledmatrix.drawChar(4, 0, 'e', 100, 0, 1);
//  ledmatrix.drawChar(10, 0, 'l', 100, 0, 1);
}  

void loop() {

  static int x = 16; // Starting point off the display to the right

  ledmatrix.drawChar(x, 0, 'H', 100, 0, 1);
  ledmatrix.drawChar(x+6, 0, 'e', 100, 0, 1);
  ledmatrix.drawChar(x+12, 0, 'l', 100, 0, 1);
  ledmatrix.drawChar(x+18, 0, 'l', 100, 0, 1);
  ledmatrix.drawChar(x+24, 0, 'o', 100, 0, 1);

  if (--x < -30) x = 16;  // decrement the x position and reset when 5 characters are off screen to the left.

  delay(50);
}

Now, obviously this is very simple code that only works for the message “Hello”. But it works!

It’s interesting; if you pay close attention to the display you can see the scroll speed is inversely proportional to the number of characters on screen. The ‘H’ enters fast and slows down as the subsequent characters are added to the screen. Likewise, the ‘o’ accelerates as it leaves the screen. We might want to control timing differently so the display doesn’t seem to slow down and speed up so obviously.

One of the basic tutorials covers the changes we need: Blink Without Delay. While I’m doing that I’ll add a variable to the code to handle the brightness of the display. Here’s a full dump of the code at this point:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_IS31FL3731.h>

Adafruit_IS31FL3731_Wing ledmatrix = Adafruit_IS31FL3731_Wing(); // NOTE: Uncommented  

const long interval = 50;
unsigned long previousMillis = 0;
unsigned long brightness = 100;

void setup() {  

  Serial.begin(250000); // NOTE: Baud rate changed from 9600  

  if (! ledmatrix.begin()) {  
   Serial.println("IS31 not found");  
   while (1);  
  }  
  Serial.println("IS31 found!");  
}  

void loop() {

  static int x = 15; // Starting point off the display to the right
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {

    previousMillis = currentMillis;

    ledmatrix.drawChar(x, 0, 'H', brightness, 0, 1);
    ledmatrix.drawChar(x+6, 0, 'e', brightness, 0, 1);
    ledmatrix.drawChar(x+12, 0, 'l', brightness, 0, 1);
    ledmatrix.drawChar(x+18, 0, 'l', brightness, 0, 1);
    ledmatrix.drawChar(x+24, 0, 'o', brightness, 0, 1);

    if (--x < -30) x = 16;  // decrement the x position and reset when 5 characters are off screen to the left.
  }
}

Scrolling Strings

The ultimate goal here is to be able to create a string on the fly and have that displayed. I want to be able to display temperature and humidity data from my Rails server on the screen. But for now the issue is that only the string “Hello” can be displayed. We need to be able to define the message string and manage to display it, whatever it’s length.

While reading through Adafruit_GFX.h I found a function to give the height and width of text as it would appear on the screen. The function getTextBounds() comes in two varieties. The first deals with C-strings, the second with String objects stored in flash memory.

getTextBounds(char *string, int16_t x, int16_t y, int16_t *x1, int16_t *y1, uint16_t *w, uint16_t *h),

getTextBounds(const __FlashStringHelper *s, int16_t x, int16_t y, int16_t *x1, int16_t *y1, uint16_t *w, uint16_t *h);

Because my Arduino programming is a little rusty, I’m looking at C-style string, String object and String Constructor.

If I declare a String object, like String message = "Hello world!" I can’t send that straight to the getTextBounds() function as it’s neither a C-string nor a C-string in flash memory. A C-string would be declared as char thing[] = "Hello World!" and a flash memory string would be declared using PROGMEM, like this: const char thing[] PROGMEM = "Hello World!".

Since we’d like to make up our message string on the fly, it’s best to use the String data type which allows concatenation and other ‘normal’ string functions. When we need to pass it to the display, we simply convert the String object to a C-string first. Simply using message.c_str() doesn’t work as c_str() returns a const char * — a pointer to an unchanging character. Since getTextBounds() wants a normal pointer to character we cast it to that type with (char*)message.c_str().

String message;
message = "Hello";
message += " World!";
Serial.println((char*)message.c_str());

In this case we don’t have to use a c_str because println() has no trouble dealing with the String type. But when the message variable has to be handled by the Adafruit_GFX class, it must be a C-string.

So here’s my test. I commented out all the code in loop(), used the following setup() function and added a function called getTextWidth().

void setup() {  
  Serial.begin(250000); 
  Serial.println("Message scrolling test");

  if (! ledmatrix.begin()) {
   Serial.println("IS31 not found");  
   while (1);  
  }  
  Serial.println("IS31 found!");  

  String message;
  message = "Hello";
  message += " World!";
  int width = getTextWidth(message);

  Serial.println();
  Serial.print("Message Width: ");
  Serial.println(width):
}  

int getTextWidth(char *message) {

  int16_t x1, y1;
  uint16_t w, h;

  ledmatrix.getTextBounds(message, 0, 0, &x1, &y1, &w, &h);

  Serial.println();
  Serial.print("Display H x W: ");
  Serial.print(ledmatrix.height());
  Serial.print(" x ");
  Serial.println(ledmatrix.width());

  Serial.print("Message H x W: ");
  Serial.print(h);
  Serial.print(" x ");
  Serial.println(w);

  return w;
}

Output:

Display H x W: 7 x 15
Message H x W: 48 x 12
Message Width: 12

So it should be as simple as that, right? Put in any message I want, get out the width in pixels of that message. Ah, no. I expected 12 characters * 6 pixels width = 72 pixels. Maybe the display is rotated 90°? I added two lines into getTextWidth():

  Serial.print("Rotation: ");
  Serial.println(ledmatrix.getRotation());

Zero. No, it’s not rotated. What could it be? I found the wrap private member variable in the Adafruit_GFX class, but I can’t print that to debug it.

  boolean wrap, // If set, 'wrap' text at right edge of display

So I guess I just set it and see. Back in setup() after “IS31 found!” I added the line:

  ledmatrix.setTextWrap(false);

Lo, and behold! We have a sensible number for the width of the message now. Output:

Display H x W: 7 x 15
Message H x W: 8 x 72
Message Width: 72

To scroll a custom message, and therefore a message of unknown length, we start displaying it at x = 15 (16th column of LEDs) so the whole message is off the right edge of the screen. We then decrement the x-position until x < -(lengthOfMessage). Let’s try that!

… time passes …

I’ve got the code working and done some tidy up. So here’s an updated complete listing.

#include <Wire.h>  
#include <Adafruit_GFX.h>  
#include <Adafruit_IS31FL3731.h>

Adafruit_IS31FL3731_Wing ledmatrix = Adafruit_IS31FL3731_Wing();

const int START_X = ledmatrix.width();
const long DISPLAY_UPDATE_INTERVAL = 50; // milliseconds
const unsigned long BRIGHTNESS = 100; // 0-1000 effective range

String message;  // Can change while running!

void setup() {

  Serial.begin(250000);
  Serial.println("Message scrolling test");

  if (! ledmatrix.begin()) {
   Serial.println("IS31 not found");  
   while (1);  
  }  
  Serial.println("IS31 found!");  

  ledmatrix.setTextWrap(false);

  message = "Hello";
  message += " World!";

  Serial.print("Message Width: ");
  Serial.println(getTextWidth((char *)message.c_str()));
}  

void loop() {

  changeMessage();
  updateDisplay();
}

void changeMessage() {

  // Does nothing yet!
}

void updateDisplay() {

  static unsigned long previousMillis = 0;
  unsigned long currentMillis = millis();

  static int x = 15; // Starting point off right edge of display
  static char* messageCstr = (char*)message.c_str();
  static int length = getTextWidth(messageCstr);

  if (currentMillis - previousMillis >= DISPLAY_UPDATE_INTERVAL) {

    previousMillis = currentMillis;

    Serial.print("Displaying at x= ");
    Serial.print(x);
    Serial.print(": ");

    char* messageCharacter = messageCstr;
    int cursor = x;

    while (*messageCharacter) {

      Serial.print(*messageCharacter);

      ledmatrix.drawChar(cursor, 0, *messageCharacter++, BRIGHTNESS, 0, 1);
      cursor += 6;      
    }
    Serial.println();

    if (--x < -length) {
      x = START_X;
      messageCstr = (char*)message.c_str();
      length = getTextWidth(messageCstr);
    }
  }
}

int getTextWidth(char *message) {

  int16_t x1, y1;
  uint16_t w, h;

  ledmatrix.getTextBounds(message, 0, 0, &x1, &y1, &w, &h);
  return w;
}

And that works!

Keying String Updates to Display Timing

I want to be able to update the message to be displayed at ANY time. So the changeMessage() function can fire any time it wants to and the updateDisplay() function should continue scrolling the current message until that message scrolls right off the screen. Only when the display is blank should it grab the updated message and use that.

Now that loop() calls changeMessage() and updateDisplay(), these are free to do their work any time they choose so long as they don’t use delay() or otherwise occupy the processor. For now, that should not be a problem. However, I’m later going to pull JSON from a HTTP server and that might inject delays that aren’t so easily allowed for. In that case I might need to flip the logic: when the display is blank changeMessage() is called so there is no obvious hiccup in the message scrolling speed.

For now I need to do a little experimental code to work out how String maintains its c_str data. And here it is:

void setup() {

  Serial.begin(250000);

  String message1 = "Hello";
  char* message1char = (char *)message1.c_str();

  Serial.println();

  Serial.print("message1         @: ");
  Serial.print((int)&message1);
  Serial.print(' ');
  Serial.println(message1);

  Serial.print("message1.c_str() @: ");
  Serial.print((int)message1.c_str());
  Serial.print(' ');
  Serial.println(message1.c_str());

  Serial.print("message1char     @: ");
  Serial.print((int)message1char);
  Serial.print(' ');
  Serial.println(message1char);

  String rubbish = "This is just taking up valuable RAM!";

  message1 += " world!";

  Serial.println();

  Serial.print("message1         @: ");
  Serial.print((int)&message1);
  Serial.print(' ');
  Serial.println(message1);

  Serial.print("message1.c_str() @: ");
  Serial.print((int)message1.c_str());
  Serial.print(' ');
  Serial.println(message1.c_str());

  Serial.print("message1char     @: ");
  Serial.print((int)message1char);
  Serial.print(' ');
  Serial.println(message1char);
}

void loop() {
}

So what does this do? Well, it creates only one String object, called message1, and a character pointer to the c_str() belonging to it, called message1char.

Ignoring the initial Serial.println(), the first block of four Serial.print lines prints the name of the String variable, its memory address and its content. The second block does the same for the c_str belonging to message1 — note that message1 owns this pointer and can change it at will. The third block prints the pointer message1char which is frozen in time! This pointer always points to where c_str() was when the pointer was created, because it’s never updated.

We deliberately create the String rubbish so the next memory address after message1 is used up. Without this line, when we append ” world!” all the variables and pointers stay where they were. That’s because the memory locations after message1 are available, so message1 just extends the string from where it was originally located. The starting positions (pointers) don’t move. Because rubbish has been created, when we append to message1 the entire string has to be written at a new location; it can’t fit where it is. The output shows this:

message1         @: 1073672956 Hello
message1.c_str() @: 1073677044 Hello
message1char     @: 1073677044 Hello

message1         @: 1073672956 Hello world!
message1.c_str() @: 1073677044 Hello world!
message1char     @: 1073677060 �

Note that in the second block message1 remains at the same memory location. message1 is a reference to an object of type String. While its content might change, the reference remains the same size and so does not need to move. message1.c_str() does move to a new location because, as discussed, there is no space to extend the array of characters where it is. The pointer message1char still points to the original location of c_str but, since the array has now moved, that memory has been freed and may have already been reused. So while we still have a valid pointer to character what it points to might have changed.

So what have we learned from this? Any time we modify a string, its c_str might move in memory, invalidating anything pointing to the old location. So if we need to have two copies of a string, we need to ensure they reside in seperate memory locations! I’ve just checked that initialising one string from another works.

void setup() {

  Serial.begin(250000);

  String word1 = "Hello";
  String word2 = String(word1);

  Serial.println();

  Serial.println((int)word1.c_str());
  Serial.println((int)word2.c_str());
}

void loop() {
}

Output:

1073676932
1073676956

So back to our original program! When the display is blank, we need to copy the message and only update that copy when the display again is blank. This will be my final code block. This blog post is waaaay too long. If you’ve read all this: I’m sorry. AND WELL DONE!

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_IS31FL3731.h>

Adafruit_IS31FL3731_Wing ledmatrix = Adafruit_IS31FL3731_Wing();

const int START_X = ledmatrix.width();
const long DISPLAY_UPDATE_INTERVAL = 100; // milliseconds
const long MESSAGE_UPDATE_INTERVAL = 10000;
const unsigned long BRIGHTNESS = 100; // 0-1000 effective range

String message;  // Can change while running!

void setup() {

  Serial.begin(250000);
  Serial.println("Message scrolling test");

  if (! ledmatrix.begin()) {
   Serial.println("IS31 not found");  
   while (1);  
  }  
  Serial.println("IS31 found!");  

  ledmatrix.setTextWrap(false);

  message = "Hello";
  message += " World!";
}  

void loop() {

  changeMessage();
  updateDisplay();
}

void changeMessage() {

  static unsigned long previousMillis = 0;
  unsigned long currentMillis = millis();

    if (currentMillis - previousMillis >= MESSAGE_UPDATE_INTERVAL) {

      previousMillis = currentMillis;

      message = "CPU Time:";
      message += currentMillis;

      Serial.print("New message: ");
      Serial.println(message);
    }
}

void updateDisplay() {

  static unsigned long previousMillis = 0;
  unsigned long currentMillis = millis();

  static int x = 15; // Starting point off right edge of display
  static String currentMessage = String(message);
  static int length = getTextWidth((char *)currentMessage.c_str());

  if (currentMillis - previousMillis >= DISPLAY_UPDATE_INTERVAL) {

    previousMillis = currentMillis;

    Serial.print("Displaying at x= ");
    Serial.print(x);
    Serial.print(": ");

    char* messageCharacter = (char *)currentMessage.c_str();
    int cursor = x;

    while (*messageCharacter) {

      Serial.print(*messageCharacter);

      ledmatrix.drawChar(cursor, 0, *messageCharacter++, BRIGHTNESS, 0, 1);
      cursor += 6;      
    }
    Serial.println();

    if (--x < -length) {
      x = START_X;
      currentMessage = String(message);
      length = getTextWidth((char *)currentMessage.c_str());
    }
  }
}

int getTextWidth(char *message) {

  int16_t x1, y1;
  uint16_t w, h;

  ledmatrix.getTextBounds(message, 0, 0, &x1, &y1, &w, &h);
  return w;
}

That is all!