Alpha release of "Font Pal", a new Arduino font manager for HD44780 LCDs

My new Arduino Font Pal is now completely functional, released as alpha version 2.00. Feedback invited. I will try and answer all questions, and will continue to post updates here. --Alastair Roxburgh (kiwiengr)

A late change (compatible with ATmega 2560) moved the font bitmaps to flash memory. A random handful of demo examples enabled in the void loop() section shows a significant reduction in overall RAM use, from around 4400 bytes to 2100 bytes. This is more than a 50% reduction, so is well worth doing!

I have had to change some font flag characters (long story) which unfortunately seems less intuitive (e.g., b for bold caps, B for bold lowercase). To assist you with this, I have included a handy table of codes at the end of the ReadMe.ino

If you want to use this software on a non-ATmega Arduino (say Renesas RA4M1), here is what you need to change to move the font bitmaps back to RAM:

  1. Remove #include <avr/pgmspace.h> at the start of the main program.

  2. In the font declarations, revert

    const byte bargraphfont[][8] PROGMEM = {

back to

  byte bargraphfont[][8] {          //This is the first of 26 instances to change 
  1. Also in the main program, revert the struct from:

    typedef struct {
    char fontFlag;
    const byte (*fontBits)[8]; // Pointer to the CC bitmap arrays.
    String fontChars;
    } CustomFont;

back to:

typedef struct {
   char fontFlag;
   byte (*fontBits)[8];  // Pointer to the CC bitmap arrays.
   String fontChars;
} CustomFont;

Note: If you don't revert to the struct with the const, Font Pal will still compile and run, but you'll get a bunch of compiler warnings (one for each font): E.g., invalid conversion from 'const byte ()[8] {aka const unsigned char ()[8]}' to 'byte ()[8] {aka unsigned char ()[8]}' [-fpermissive]

  1. Also in the main program, replace this:
    uint8_t* getBitmap(char myfontChar, char myfontFlag) {
    for (const CustomFont& font : fonts) {
    if (font.fontFlag == myfontFlag) {
    int charIndex = font.fontChars.indexOf(myfontChar);
    if (charIndex != -1) {
    static uint8_t bitmap[8];
    for (int i = 0; i < 8; i++) {
    bitmap[i] = pgm_read_byte_near(font.fontBits[charIndex] + I);**
    }
    return bitmap;
    }
    }
    }
    return nullptr; // Font or Character not found
    }

with this:
uint8_t* getBitmap(char myfontChar, char myfontFlag) {
for (const CustomFont& font : fonts) {
if (font.fontFlag == myfontFlag) {
int charIndex = font.fontChars.indexOf(myfontChar);
if (charIndex != -1) return font.fontBits[charIndex];
}
}
return nullptr; // Font or Character not found
}

If you decide to comment-out some of the font data, make sure to also comment-out the relevant items in lines 600-638 below the font bitmaps.

Here is the full Font Pal alpha 2.00 code:

// #############################################################################
// ##################     A r d u i n o - F o n t - P a l     ##################
// ##################       ( F o n t   P a l o o z a )       ##################
// ##################  F O R   H I T A C H I   H D 4 4 7 8 0  ##################
// ##################      L C D   C O N T R O L L E R S      ##################
// #############################################################################
//  
//  Copyright (c) 2024 Alastair Roxburgh
//  (Font Pal recreates the functionality of an original 68HC11 stack-frame
//  assembly language program named SCREENS, devised in 1997 by Alastair
//  Roxburgh & Sunil Rawal for the EOS series of TheaterMaster digital home theater
//  audio processors. Written in Arduino C++, Font Pal makes heavy use of the String
//  class and arrays of struct to create a custom font manager for 2x20 LCD displays
//  based on the Hitachi HD44780 chip.)
//
//  Note:
//    While Font Pal, which was developed expressly for the Arduino Mega platform,
//    reproduces most of the functionality of its 1977 68HC11 predecessor its
//    internal algorithms are only remotely similar.
//
//  Font Pal is free software: you can redistribute it and/or modify it
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation version 3 of the License.
//
//  Font Pal is distributed in the hope that it will be useful, but WITHOUT
//  WITHOUT ANY WARRANTY; without even the implied warranty of MECHANTABILITY
//  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Public License for more
//  details.
//
//  You should have received a copy of the GNU General Public License along
//  with Font Pal. If not, see <http://www.gnu.org/licenses/>.
//
// See the license.txt file for further licensing & copyright details.
//
// -----------------------------------------------------------------------
// History
//
// 2024.09.04 Alastair Roxburgh (kiwiengr) - initial creation 
// 2024.09.13 Alastair Roxburgh (kiwiengr) - Updated Example 1: 'LCD scene'
// 2024.09.15 Alastair Roxburgh (kiwiengr) - Updated Documentation and ReadMe 
// 2024.09.20 Alastair Roxburgh (kiwiengr) - Release of alpha version 2.00
//
//
//
// ---To Do---
// Add more error checking:
// If the lcdString is inconsistently formatted, display "*" flag on the LCD.
//   Rules: One '|' => lcdString.length() - 2 * marker_idx = 1
//          Two '|' => lcdString.length() - 4 * marker_idx = 2
// Add an LCD size option: 16x1, 20x1, 16x2, 20x2, 20x4.
// Add an interface option: 4-bit parallel, I2C.
// Study the feasibility of configuring Font Pal as an Arduino library. 
//
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

#include <Arduino.h>
#include <LiquidCrystal.h>
#include <avr/pgmspace.h>

// Initialize the library with the numbers of the interface pins
LiquidCrystal lcd(7, 6, 8, 9, 10, 11, 12);  // RS, RW, E, D0, D1, D2, D3. 4-bit transfer with R/W


// %%%%%%%%%%% GLOBALS:
String screenImage = ""; // Holds 40 fontChars (0-39) and 40 fontFlags in an interleaved format (80 characters numbered 0-79).
String lcdString = "";
int16_t startPos = 0; // Position in screenImage that will be overwritten with a new font-formatted String.
String CC_id_table[8]; // e.g., {"gL","dL","",...}
int8_t CGRAM_ptr; // Has values 0-7 to address the CC_id_table, but we add 8 (giving 8-f) to address CGRAM!
byte blankBitmap[8] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; // All pixels off.
bool in_CC_id_table = false;

/*----------------------------------------------*/
/*     A Collection of Useful CC Strings        */
/*----------------------------------------------*/
String daisy[] = { "242|CCC" 
                   "1d1|CCC",          // 3/2/0 daisy 0  (daisies 0-6 are without LFE channel)

                   "2c2|CCC" 
                   "a5a|CCC",          // 2/P/0 daisy 1

                   "2c2|CCC" 
                   "ada|CCC",          // 2/0/0 daisy 2

                   "242|CCC" 
                   "a5a|CCC",          // 3/1/0 daisy 3

                   "242|CCC" 
                   "ada|CCC",          // 3/0/0 daisy 4

                   "2c2|CCC" 
                   "1d1|CCC",          // 2/2/0 daisy 5

                   "b4b|CCC" 
                   "ada|CCC",          // 1/0/0 daisy 6

                   "bcb|CCC" 
                   "ada|CCC",          // 0/0/0 daisy 7 (no lock pattern)

                   "232|CCC" 
                   "1e1|CCC",          // 3/2/.1 daisy 8 (daisy 8-14 are with LFE channel)

                   "2f2|CCC" 
                   "a6a|CCC",          // 2/P/.1 daisy 9 

                   "2f2|CCC" 
                   "aea|CCC",          // 2/0/.1 daisy 10

                   "232|CCC" 
                   "a6a|CCC",          // 3/1/.1 daisy 11

                   "232|CCC"  
                   "a6a|CCC",          // 3/0/.1 daisy 12

                   "2f2|CCC" 
                   "1e1|CCC",          // 2/2/.1 daisy 13

                   "b3b|CCC"
                   "aea|CCC",          // 1/0/.1 daisy 14
                   
                   "242|CCC"           // 3/3/0  daisy 15 
                   "151|CCC",

                   "232|CCC"            // 3/3/.1 daisy 16 
                   "161|CCC",

                   "   |   "           // 0/0/0 daisy xx (for no lock mute) 
                   "   |   " 
                  };

String normalSpeakers[] = { 
                            "LF|cc",  // 0. E.g., Display a selectable menu of speakers (all are in small caps): LF  CR  RF  
                            "RF|cc",  // 1.                                                                      LS  SB  RS
                            "Ls|c ",  // 2.
                            "Rs|c ",  // 3.
                            "cR| c",  // 4.
                            "sB| c"   // 5.
                          };

String revSpeakers[ ] = {         
                          "LF|VV",  // 0. Example of use: indicate a speaker selection for adjustment (all are in reverse small caps). 
                          "RF|VV",  // 1.
                          "Ls|VV",  // 2.
                          "Rs|VV",  // 3.
                          "cR|VV",  // 4.
                          "sB|VV"   // 5.
                        };

String HorizontalSPLmeter = {"SPL:  Ref!      . dB|"  // Boiler plate for horizontal SPL meter. Ref "!|G" (down arrow) marks a reference level.
                             "         G          "}; // However, the actual bar graph is constructed on the bottom LCD row (not shown)

String invArray[][2] = {
                         { "a a a a a|I I I I I", "a a a a a|O O O O O" }, // 0
                         { "b b b b b|I I I I I", "b b b b b|O O O O O" }, // 1
                         { "c c c c c|I I I I I", "c c c c c|O O O O O" }, // 2
                         { "d d d d d|I I I I I", "d d d d d|O O O O O" }  // 3
};

/*----------------------------------------------*/
/*            Custom Character Fonts            */
/*----------------------------------------------*/
// @@@@@@@@@@ G - BARGRAPH FONT @@@@@@@@@@
const byte bargraphfont[][8] PROGMEM = {  // "0abcLR!87654321"
// Horizontal Bar Graph:
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '0' no bars
  { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 }, // 'a' one bar
  { 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14 }, // 'b' two bars
  { 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15 }, // 'c' three bars
  { 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18 }, // 'L' L-bar
  { 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03 }, // 'R' R-bar
  { 0x00, 0x1c, 0x04, 0x04, 0x04, 0x15, 0x0e, 0x04 }, // '!' bent down-arrow
// Multi-Channel VU Meter Bar Graph (6 dB per step):
  { 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b }, // '8'
  { 0x00, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b }, // '7'
  { 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b }, // '6'
  { 0x00, 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b }, // '5'
  { 0x00, 0x00, 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x1b }, // '4'
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x1b, 0x1b }, // '3'
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x1b }, // '2'
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b }  // '1'
};

// @@@@@@@@@@ 0 to 9 - BIGNUMS FONTS @@@@@@@@@@
const byte big0font[][8] PROGMEM = {  // "ABCD"
// '0' = 4*0 + 0,1,2,3
  { 0x03, 0x07, 0x0f, 0x0e, 0x1c, 0x1c, 0x18, 0x18 }, // 'A'
  { 0x18, 0x1c, 0x1e, 0x0e, 0x07, 0x07, 0x03, 0x03 }, // 'B'
  { 0x18, 0x18, 0x1c, 0x1c, 0x0e, 0x0f, 0x07, 0x03 }, // 'C'
  { 0x03, 0x03, 0x07, 0x07, 0x0e, 0x1e, 0x1c, 0x18 }  // 'D'
};
const byte big1font[][8] PROGMEM = {  // "ABCD"
// '1' = 4*1 + 0,1,2,3
  { 0x01, 0x07, 0x0f, 0x0f, 0x01, 0x01, 0x01, 0x01 }, // 'A'
  { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 }, // 'B'
  { 0x01, 0x01, 0x01, 0x01, 0x01, 0x0f, 0x0f, 0x0f }, // 'C'
  { 0x10, 0x10, 0x10, 0x10, 0x10, 0x1e, 0x1e, 0x1e }  // 'D'
};  
const byte big2font[][8] PROGMEM = {  // "ABCD"
// '2' = 4*2 + 1
  { 0x01, 0x07, 0x0f, 0x1e, 0x18, 0x00, 0x00, 0x00 }, // 'A'
  { 0x18, 0x1c, 0x1e, 0x07, 0x03, 0x03, 0x07, 0x0e }, // 'B'
  { 0x00, 0x01, 0x03, 0x07, 0x0e, 0x1f, 0x1f, 0x1f }, // 'C'
  { 0x1c, 0x18, 0x10, 0x00, 0x03, 0x1f, 0x1f, 0x1f }  // 'D'
};  
const byte big3font[][8] PROGMEM = {  // "ABCD"
// '3'
  { 0x03, 0x0f, 0x1f, 0x18, 0x00, 0x00, 0x00, 0x03 }, // 'A'
  { 0x18, 0x1c, 0x1e, 0x06, 0x06, 0x06, 0x0e, 0x1c }, // 'B'
  { 0x03, 0x00, 0x00, 0x10, 0x18, 0x1f, 0x0f, 0x07 }, // 'C'
  { 0x1e, 0x0f, 0x07, 0x07, 0x07, 0x1e, 0x1c, 0x18 }  // 'D'
};  
const byte big4font[][8] PROGMEM = {  // "ABCD"
// '4'
  { 0x00, 0x00, 0x01, 0x01, 0x03, 0x03, 0x07, 0x0e }, // 'A'
  { 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x0c, 0x0c, 0x0c }, // 'B'
  { 0x1f, 0x1f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 'C'
  { 0x1f, 0x1f, 0x1f, 0x0c, 0x0c, 0x1e, 0x1e, 0x1e }  // 'D'
};  
const byte big5font[][8] PROGMEM = {  // "ABCD"
// '5'
  { 0x0f, 0x0f, 0x0f, 0x0c, 0x0c, 0x0c, 0x0f, 0x0f }, // 'A'
  { 0x1f, 0x1f, 0x1f, 0x00, 0x00, 0x00, 0x1c, 0x1e }, // 'B'
  { 0x00, 0x00, 0x00, 0x08, 0x0c, 0x0f, 0x07, 0x03 }, // 'C'
  { 0x07, 0x03, 0x03, 0x03, 0x07, 0x1e, 0x1c, 0x18 }  // 'D'
};  
const byte big6font[][8] PROGMEM = {  // "ABCD"
// '6'
  { 0x03, 0x07, 0x0e, 0x1c, 0x18, 0x18, 0x1b, 0x1f }, // 'A'
  { 0x1c, 0x1e, 0x07, 0x03, 0x01, 0x00, 0x1c, 0x1e }, // 'B'
  { 0x1c, 0x18, 0x18, 0x1c, 0x0e, 0x0f, 0x07, 0x03 }, // 'C'
  { 0x07, 0x03, 0x03, 0x03, 0x07, 0x1e, 0x1c, 0x18 }  // 'D'
};  
const byte big7font[][8] PROGMEM = {  // "ABCD"
// '7'
  { 0x0f, 0x0f, 0x0f, 0x0c, 0x00, 0x00, 0x00, 0x00 }, // 'A'
  { 0x1f, 0x1f, 0x1f, 0x07, 0x07, 0x0e, 0x0e, 0x0e }, // 'B'
  { 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01 }, // 'C'
  { 0x1c, 0x18, 0x18, 0x18, 0x18, 0x10, 0x10, 0x10 }  // 'D'
};  
const byte big8font[][8] PROGMEM = {  // "ABCD"
// '8'
  { 0x03, 0x07, 0x0f, 0x1c, 0x1c, 0x1c, 0x0e, 0x07 }, // 'A'
  { 0x18, 0x1c, 0x1e, 0x07, 0x07, 0x07, 0x0e, 0x1c }, // 'B'
  { 0x0f, 0x1c, 0x1c, 0x1c, 0x1c, 0x0f, 0x07, 0x03 }, // 'C'
  { 0x1e, 0x07, 0x07, 0x07, 0x07, 0x1e, 0x1c, 0x18 }  // 'D'
};  
const byte big9font[][8] PROGMEM = {  // "ABCD"
// '9'
  { 0x03, 0x07, 0x0f, 0x1c, 0x18, 0x18, 0x18, 0x1c }, // 'A'
  { 0x18, 0x1c, 0x1e, 0x0e, 0x07, 0x03, 0x03, 0x07 }, // 'B'
  { 0x0f, 0x07, 0x00, 0x10, 0x18, 0x1c, 0x0f, 0x07 }, // 'C'
  { 0x1f, 0x1b, 0x03, 0x03, 0x07, 0x0e, 0x1c, 0x18 }  // 'D'
};

// @@@@@@@@@@ X - SYMBOL FONT @@@@@@@@@@
const byte symbolfont[][8] PROGMEM = {  // "DSTMH><^vuds_1234KkLQnNbCF"
  { 0x04, 0x04, 0x0a, 0x0a, 0x11, 0x11, 0x1f, 0x00 }, // 'D'   Greek Delta 
  { 0x1f, 0x10, 0x08, 0x04, 0x08, 0x10, 0x1f, 0x00 }, // 'S'   Greek Sigma 
  { 0x1f, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00 }, // 'T'   Trademark symbol, use "TM|SS"
  { 0x11, 0x1b, 0x15, 0x11, 0x00, 0x00, 0x00, 0x00 }, // 'M' 
  { 0x1b, 0x1f, 0x1b, 0x00, 0x1f, 0x18, 0x1e, 0x18 }, // 'H'   single HF character symbol, use "H|S"
  { 0x10, 0x18, 0x1c, 0x1e, 0x1c, 0x18, 0x10, 0x00 }, // '>'   Right Arrow symbol
  { 0x01, 0x03, 0x07, 0x0f, 0x07, 0x03, 0x01, 0x00 }, // '<'   Left Arrow symbol
  { 0x04, 0x04, 0x0e, 0x0e, 0x1f, 0x1f, 0x00, 0x00 }, // '^'   Up Arrow symbol
  { 0x00, 0x00, 0x1f, 0x1f, 0x0e, 0x0e, 0x04, 0x04 }, // 'v'   Down Arrow symbol
  { 0x04, 0x0e, 0x1f, 0x00, 0x04, 0x0e, 0x1f, 0x00 }, // 'u'   Up double arrow
  { 0x1f, 0x0e, 0x04, 0x00, 0x1f, 0x0e, 0x04, 0x00 }, // 'd'   Down double arrow
  { 0x00, 0x00, 0x00, 0x0e, 0x10, 0x0c, 0x02, 0x1c }, // 's'   Subscript 's' 
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f }, // '_'   underline char (in the cursor row)
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 }, // '1'   Centered 4-dot screen-saver pattern:
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00 }, // '2'
  { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '3'
  { 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '4'
  { 0x00, 0x01, 0x03, 0x1f, 0x1f, 0x1f, 0x03, 0x01 }, // 'K'   Big Speaker (Kicker)
  { 0x00, 0x00, 0x01, 0x03, 0x0f, 0x03, 0x01, 0x00 }, // 'k'   small speaker (small-k kicker)
  { 0x10, 0x18, 0x1f, 0x1f, 0x1f, 0x18, 0x10, 0x00 }, // 'L'   Big Speaker reversed
  { 0x06, 0x08, 0x08, 0x1c, 0x08, 0x09, 0x16, 0x00 }, // 'Q'   Pound Sterling £ "Quid" (ALT-163)
  { 0x04, 0x06, 0x05, 0x05, 0x0c, 0x1c, 0x18, 0x00 }, // 'n'   Musical note
  { 0x06, 0x05, 0x07, 0x05, 0x05, 0x1d, 0x1b, 0x03 }, // 'N'   Two musical notes
  { 0x04, 0x0e, 0x0e, 0x0e, 0x1f, 0x04, 0x00, 0x00 }, // 'b'   Bell
  { 0x18, 0x18, 0x00, 0x07, 0x08, 0x08, 0x08, 0x07 }, // 'C'   Celsius degrees 
  { 0x18, 0x18, 0x00, 0x0f, 0x08, 0x0e, 0x08, 0x08 }  // 'F'   Fahrenheit degrees 
};

// @@@@@@@@@@ b - BOLD Uppercase FONT @@@@@@@@@@
const byte Boldfont[][8] PROGMEM = { // "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  { 0x0e, 0x19, 0x19, 0x19, 0x1f, 0x19, 0x19, 0x00 }, // 'A'
  { 0x1e, 0x19, 0x19, 0x1e, 0x19, 0x19, 0x1e, 0x00 }, // 'B'
  { 0x0e, 0x19, 0x18, 0x18, 0x18, 0x19, 0x0e, 0x00 }, // 'C'
  { 0x1c, 0x1a, 0x19, 0x19, 0x19, 0x1a, 0x1c, 0x00 }, // 'D'
  { 0x1f, 0x18, 0x18, 0x1e, 0x18, 0x18, 0x1f, 0x00 }, // 'E'
  { 0x1f, 0x18, 0x18, 0x1e, 0x18, 0x18, 0x18, 0x00 }, // 'F' 
  { 0x0e, 0x19, 0x18, 0x1b, 0x19, 0x19, 0x0f, 0x00 }, // 'G'
  { 0x19, 0x19, 0x19, 0x1f, 0x19, 0x19, 0x19, 0x00 }, // 'H'
  { 0x0f, 0x06, 0x06, 0x06, 0x06, 0x06, 0x0f, 0x00 }, // 'I'
  { 0x0f, 0x06, 0x06, 0x06, 0x06, 0x16, 0x0c, 0x00 }, // 'J'
  { 0x1b, 0x1a, 0x1c, 0x18, 0x1c, 0x1a, 0x1b, 0x00 }, // 'K'
  { 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1f, 0x00 }, // 'L'
  { 0x11, 0x1b, 0x1f, 0x1f, 0x1b, 0x1b, 0x1b, 0x00 }, // 'M'
  { 0x19, 0x19, 0x1d, 0x1f, 0x1b, 0x19, 0x19, 0x00 }, // 'N'
  { 0x0e, 0x19, 0x19, 0x19, 0x19, 0x19, 0x0e, 0x00 }, // 'O'
  { 0x1e, 0x19, 0x19, 0x19, 0x1e, 0x18, 0x18, 0x00 }, // 'P'
  { 0x0e, 0x19, 0x19, 0x19, 0x19, 0x1a, 0x0d, 0x00 }, // 'Q'
  { 0x1e, 0x13, 0x13, 0x1e, 0x1c, 0x16, 0x13, 0x00 }, // 'R' rhs bold
  { 0x0e, 0x19, 0x1c, 0x0e, 0x07, 0x13, 0x0e, 0x00 }, // 'S'
  { 0x0f, 0x0f, 0x06, 0x06, 0x06, 0x06, 0x06, 0x00 }, // 'T'
  { 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x0e, 0x00 }, // 'U'
  { 0x19, 0x19, 0x19, 0x19, 0x19, 0x0a, 0x04, 0x00 }, // 'V'
  { 0x11, 0x11, 0x11, 0x15, 0x1f, 0x1f, 0x0a, 0x00 }, // 'W'
  { 0x19, 0x19, 0x0e, 0x04, 0x0e, 0x13, 0x13, 0x00 }, // 'X'
  { 0x19, 0x19, 0x19, 0x0e, 0x04, 0x04, 0x0e, 0x00 }, // 'Y'
  { 0x1f, 0x03, 0x06, 0x0c, 0x18, 0x10, 0x1f, 0x00 }, // 'Z'
  { 0x0e, 0x19, 0x19, 0x19, 0x19, 0x19, 0x0e, 0x00 }, // '0'
  { 0x06, 0x0e, 0x16, 0x06, 0x06, 0x06, 0x1f, 0x00 }, // '1'
  { 0x0e, 0x13, 0x03, 0x06, 0x0c, 0x18, 0x1f, 0x00 }, // '2'
  { 0x1f, 0x03, 0x06, 0x0e, 0x03, 0x13, 0x0e, 0x00 }, // '3'
  { 0x06, 0x0e, 0x16, 0x16, 0x1f, 0x06, 0x06, 0x00 }, // '4'
  { 0x1f, 0x18, 0x18, 0x1e, 0x01, 0x11, 0x1e, 0x00 }, // '5'
  { 0x07, 0x0c, 0x18, 0x1e, 0x19, 0x19, 0x0e, 0x00 }, // '6'
  { 0x1f, 0x13, 0x03, 0x06, 0x0c, 0x0c, 0x0c, 0x00 }, // '7'
  { 0x0e, 0x19, 0x19, 0x0e, 0x19, 0x19, 0x0e, 0x00 }, // '8'
  { 0x0e, 0x19, 0x19, 0x0f, 0x01, 0x02, 0x0c, 0x00 }  // '9'
};

// @@@@@@@@@@ B - bold Lowercase Font @@@@@@@@@@
const byte boldlcfont[][8] PROGMEM = { // "abcdefghijklmnopqrstuvwxyz"
  { 0x00, 0x00, 0x0e, 0x03, 0x0f, 0x1b, 0x0f, 0x00 }, // 'a'
  { 0x18, 0x18, 0x1e, 0x1b, 0x1b, 0x1b, 0x1e, 0x00 }, // 'b'
  { 0x00, 0x00, 0x0e, 0x1b, 0x18, 0x1b, 0x0e, 0x00 }, // 'c' 
  { 0x03, 0x03, 0x0f, 0x1b, 0x1b, 0x1b, 0x0f, 0x00 }, // 'd'
  { 0x00, 0x00, 0x0e, 0x1b, 0x1f, 0x18, 0x0e, 0x00 }, // 'e'
  { 0x06, 0x0d, 0x0c, 0x1e, 0x0c, 0x0c, 0x0c, 0x00 }, // 'f'
  { 0x00, 0x00, 0x0f, 0x1b, 0x1b, 0x0f, 0x03, 0x0e }, // 'g'
  { 0x18, 0x18, 0x1e, 0x1b, 0x1b, 0x1b, 0x1b, 0x00 }, // 'h'
  { 0x06, 0x00, 0x0e, 0x06, 0x06, 0x06, 0x0f, 0x00 }, // 'i'
  { 0x03, 0x00, 0x07, 0x03, 0x03, 0x03, 0x13, 0x0e }, // 'j'
  { 0x18, 0x18, 0x1b, 0x1e, 0x1c, 0x1a, 0x1b, 0x00 }, // 'k'
  { 0x0e, 0x06, 0x06, 0x06, 0x06, 0x06, 0x0f, 0x00 }, // 'l'
  { 0x00, 0x00, 0x1a, 0x1f, 0x15, 0x15, 0x15, 0x00 }, // 'm'
  { 0x00, 0x00, 0x16, 0x1b, 0x1b, 0x1b, 0x1b, 0x00 }, // 'n'
  { 0x00, 0x00, 0x0e, 0x1b, 0x1b, 0x1b, 0x0e, 0x00 }, // 'o'
  { 0x00, 0x00, 0x1e, 0x1b, 0x1b, 0x1e, 0x18, 0x18 }, // 'p'
  { 0x00, 0x00, 0x0d, 0x13, 0x13, 0x0f, 0x03, 0x03 }, // 'q'
  { 0x00, 0x00, 0x16, 0x19, 0x18, 0x18, 0x18, 0x00 }, // 'r'
  { 0x00, 0x00, 0x0f, 0x1c, 0x0e, 0x07, 0x1e, 0x00 }, // 's'
  { 0x0c, 0x0c, 0x1e, 0x0c, 0x0c, 0x0d, 0x06, 0x00 }, // 't'
  { 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x1b, 0x0d, 0x00 }, // 'u'
  { 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x0a, 0x04, 0x00 }, // 'v'
  { 0x00, 0x00, 0x11, 0x15, 0x1f, 0x1b, 0x11, 0x00 }, // 'w'
  { 0x00, 0x00, 0x11, 0x1b, 0x0e, 0x1b, 0x11, 0x00 }, // 'x'
  { 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x0f, 0x03, 0x0e }, // 'y'
  { 0x00, 0x00, 0x1f, 0x03, 0x06, 0x0c, 0x1f, 0x00 }  // 'z'
};

// %%%%%%%%%% L - LOWERCASE FONT %%%%%%%%%%
const byte lowercasefont[][8] PROGMEM = { // "gjpqym"
  { 0x00, 0x00, 0x0f, 0x11, 0x11, 0x0f, 0x01, 0x0e }, // 'g' 
  { 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x12, 0x0c }, // 'j'
  { 0x00, 0x00, 0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10 }, // 'p'
  { 0x00, 0x00, 0x0e, 0x12, 0x12, 0x0e, 0x02, 0x03 }, // 'q'
  { 0x00, 0x00, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e }, // 'y'
  { 0x00, 0x00, 0x1a, 0x15, 0x15, 0x15, 0x15, 0x00 }  // 'm'  An alternate to the ROM 'm'
};

// @@@@@@@@@@ c - SMALL CAPS FONT @@@@@@@@@@
const byte smallcapsfont[][8] PROGMEM = { // "ABDEFGHIJKLMNPQRTUY"
  { 0x00, 0x00, 0x0e, 0x11, 0x1f, 0x11, 0x11, 0x00 }, // 'A'
  { 0x00, 0x00, 0x1e, 0x11, 0x1e, 0x11, 0x1e, 0x00 }, // 'B'
// Use c
  { 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x00 }, // 'D'
  { 0x00, 0x00, 0x1f, 0x10, 0x1c, 0x10, 0x1f, 0x00 }, // 'E'
  { 0x00, 0x00, 0x1f, 0x10, 0x1c, 0x10, 0x10, 0x00 }, // 'F'
  { 0x00, 0x00, 0x0f, 0x10, 0x17, 0x11, 0x0f, 0x00 }, // 'G'
  { 0x00, 0x00, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x00 }, // 'H'
  { 0x00, 0x00, 0x0e, 0x04, 0x04, 0x04, 0x0e, 0x00 }, // 'I'
  { 0x00, 0x00, 0x01, 0x01, 0x01, 0x11, 0x0e, 0x00 }, // 'J'
  { 0x00, 0x00, 0x12, 0x14, 0x18, 0x14, 0x12, 0x00 }, // 'K'
  { 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x00 }, // 'L'
  { 0x00, 0x00, 0x11, 0x1b, 0x15, 0x11, 0x11, 0x00 }, // 'M'
  { 0x00, 0x00, 0x11, 0x19, 0x15, 0x13, 0x11, 0x00 }, // 'N'
// Use o
  { 0x00, 0x00, 0x1e, 0x11, 0x1e, 0x10, 0x10, 0x00 }, // 'P'
  { 0x00, 0x00, 0x0e, 0x11, 0x11, 0x15, 0x0e, 0x01 }, // 'Q'
  { 0x00, 0x00, 0x1e, 0x11, 0x1e, 0x14, 0x12, 0x00 }, // 'R'
// Use s
  { 0x00, 0x00, 0x1f, 0x04, 0x04, 0x04, 0x04, 0x00 }, // 'T'
  { 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00 }, // 'U'
// Use v
// Use w
// Use x
  { 0x00, 0x00, 0x11, 0x11, 0x0a, 0x04, 0x04, 0x00 }  // 'Y'
// Use z
};

// @@@@@@@@@@ F - SMALL FREQUENCY FONT @@@@@@@@@@
const byte smlfrequfont[][8] PROGMEM = { // "kHz"
  { 0x00, 0x00, 0x08, 0x0a, 0x0c, 0x0a, 0x09, 0x00 }, // 'k' as in kHz
  { 0x00, 0x00, 0x09, 0x09, 0x0f, 0x09, 0x09, 0x00 }, // 'H' as in Hz
  { 0x00, 0x00, 0x00, 0x1e, 0x04, 0x08, 0x1e, 0x00 }  // 'z' as in Hz
};

// @@@@@@@@@@ V - SMALL REVERSE CAPS FONT @@@@@@@@@@
const byte smlrevcapsfont[][8] PROGMEM = { // "BCcFLSsTR"
  { 0x1f, 0x1f, 0x13, 0x15, 0x13, 0x15, 0x13, 0x1f }, // 'B'
  { 0x1f, 0x1f, 0x19, 0x17, 0x17, 0x17, 0x19, 0x1f }, // 'C'
  { 0x1f, 0x1f, 0x19, 0x17, 0x17, 0x17, 0x19, 0x1f }, // 'c'
  { 0x1f, 0x1f, 0x11, 0x17, 0x11, 0x17, 0x17, 0x1f }, // 'F'
  { 0x1f, 0x1f, 0x17, 0x17, 0x17, 0x17, 0x11, 0x1f }, // 'L'
  { 0x1f, 0x1f, 0x19, 0x17, 0x1b, 0x1d, 0x13, 0x1f }, // 'S'
  { 0x1f, 0x1f, 0x19, 0x17, 0x1b, 0x1d, 0x13, 0x1f }, // 's'
  { 0x1f, 0x1f, 0x11, 0x1b, 0x1b, 0x1b, 0x1b, 0x1f }, // 'T'
  { 0x1f, 0x1f, 0x13, 0x15, 0x13, 0x15, 0x15, 0x1f }  // 'R'
};

// @@@@@@@@@@ v - REVERSE CAPS FONT @@@@@@@@@@
const byte revcapsfont[][8] PROGMEM = { // "ABCDEFGHIKLOPRSTUVXYZ0123456789=:"
  { 0x1f, 0x1b, 0x15, 0x15, 0x11, 0x15, 0x15, 0x1f }, // 'A'
  { 0x1f, 0x13, 0x15, 0x13, 0x15, 0x15, 0x13, 0x1f }, // 'B'
  { 0x1f, 0x19, 0x17, 0x17, 0x17, 0x17, 0x19, 0x1f }, // 'C'
  { 0x1f, 0x13, 0x15, 0x15, 0x15, 0x15, 0x13, 0x1f }, // 'D'
  { 0x1f, 0x11, 0x17, 0x11, 0x17, 0x17, 0x11, 0x1f }, // 'E'
  { 0x1f, 0x11, 0x17, 0x13, 0x17, 0x17, 0x17, 0x1f }, // 'F'
  { 0x1f, 0x11, 0x17, 0x17, 0x15, 0x15, 0x11, 0x1f }, // 'G'
  { 0x1f, 0x15, 0x15, 0x11, 0x15, 0x15, 0x15, 0x1f }, // 'H'
  { 0x1f, 0x11, 0x1b, 0x1b, 0x1b, 0x1b, 0x11, 0x1f }, // 'I'
// No J 
  { 0x1f, 0x15, 0x13, 0x17, 0x13, 0x15, 0x15, 0x1f }, // 'K'
  { 0x1f, 0x17, 0x17, 0x17, 0x17, 0x17, 0x11, 0x1f }, // 'L'
// No M,N
  { 0x1f, 0x11, 0x15, 0x15, 0x15, 0x15, 0x11, 0x1f }, // 'O'
  { 0x1f, 0x13, 0x15, 0x15, 0x13, 0x17, 0x17, 0x1f }, // 'P'
// No Q
  { 0x1f, 0x11, 0x15, 0x11, 0x13, 0x15, 0x15, 0x1f }, // 'R'
  { 0x1f, 0x19, 0x17, 0x1b, 0x1d, 0x1d, 0x13, 0x1f }, // 'S'
  { 0x1f, 0x11, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1f }, // 'T'
  { 0x1f, 0x15, 0x15, 0x15, 0x15, 0x15, 0x11, 0x1f }, // 'U'
  { 0x1f, 0x15, 0x15, 0x15, 0x15, 0x15, 0x1b, 0x1f }, // 'V'
// No W
  { 0x1f, 0x15, 0x15, 0x1b, 0x1b, 0x15, 0x15, 0x1f }, // 'X'
  { 0x1f, 0x15, 0x15, 0x1b, 0x1b, 0x1b, 0x1b, 0x1f }, // 'Y'
  { 0x1f, 0x11, 0x1d, 0x1b, 0x1b, 0x17, 0x11, 0x1f }, // 'Z'
  { 0x1f, 0x1b, 0x15, 0x15, 0x15, 0x15, 0x1b, 0x1f }, // '0'
  { 0x1f, 0x1b, 0x13, 0x1b, 0x1b, 0x1b, 0x11, 0x1f }, // '1'
  { 0x1f, 0x1b, 0x15, 0x1d, 0x1b, 0x17, 0x11, 0x1f }, // '2'
  { 0x1f, 0x11, 0x1d, 0x1b, 0x1d, 0x15, 0x1b, 0x1f }, // '3'
  { 0x1f, 0x17, 0x15, 0x15, 0x11, 0x1d, 0x1d, 0x1f }, // '4'
  { 0x1f, 0x11, 0x17, 0x1b, 0x1d, 0x15, 0x1b, 0x1f }, // '5'
  { 0x1f, 0x1b, 0x15, 0x17, 0x13, 0x15, 0x1b, 0x1f }, // '6'
  { 0x1f, 0x11, 0x15, 0x1d, 0x1b, 0x17, 0x17, 0x1f }, // '7'
  { 0x1f, 0x11, 0x15, 0x1b, 0x15, 0x15, 0x11, 0x1f }, // '8' thin waist, 1b is rounded top and bottom
  { 0x1f, 0x1b, 0x15, 0x19, 0x1d, 0x15, 0x1b, 0x1f }, // '9'
  { 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f }, // '=' space (5x8 black pixels)
  { 0x1f, 0x1f, 0x1f, 0x1b, 0x1f, 0x1b, 0x1f, 0x1f }  // ':' colon
};

// @@@@@@@@@@ C - CHANNEL-DAISY FONT @@@@@@@@@@
const byte channelfont[][8] PROGMEM = { // "241dca5b3ef6xyzuvwjkl"
                           
  { 0x00, 0x00, 0x00, 0x0e, 0x1f, 0x1f, 0x1f, 0x0e }, // '2'
  { 0x0e, 0x1f, 0x1f, 0x1f, 0x0e, 0x00, 0x0e, 0x11 }, // '4'
  { 0x0e, 0x1f, 0x1f, 0x1f, 0x0e, 0x00, 0x00, 0x00 }, // '1'
  { 0x11, 0x0e, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e }, // 'd'
  //=================================================
  { 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x0e, 0x11 }, // 'c'
  { 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, 0x00 }, // 'a'
  { 0x11, 0x0e, 0x00, 0x0e, 0x1f, 0x1f, 0x1f, 0x0e }, // '5'
  //=================================================
  { 0x00, 0x00, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e }, // 'b'
  //=================================================
  { 0x0e, 0x1f, 0x1f, 0x1f, 0x0e, 0x00, 0x0e, 0x1f }, // '3'
  { 0x1f, 0x0e, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e }, // 'e'
  //=================================================
  { 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x0e, 0x1f }, // 'f'
  { 0x1f, 0x0e, 0x00, 0x0e, 0x1f, 0x1f, 0x1f, 0x0e }, // '6'
  //=================================================
  { 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x00 }, // 'x' Analog pass-thru lower-half of daisy (vertical triangle) "xyz|CCC"
  { 0x04, 0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x00 }, // 'y'
  { 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x14, 0x00 }, // 'z'
  //=================================================
  { 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x07, 0x00 }, // 'u' Analog pass-thru 'Mute' daisy (hor. triangle) "uvw|CCC" (alternate with xyz|CCC)
  { 0x04, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00 }, // 'v'
  { 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x1c, 0x00 }, // 'w'
  //=================================================
  { 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x07, 0x00 }, // 'j' Solid triangle symbol (same shape as the vert,. and hor. triangles) "jkl|CCC"
  { 0x04, 0x0e, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x00 }, // 'k'
  { 0x00, 0x00, 0x00, 0x00, 0x10, 0x18, 0x1c, 0x00 }  // 'l'

};

// @@@@@@@@@@ l - LOGOS FONT @@@@@@@@@@
const byte logofont[][8] PROGMEM = { // "LRdtsHDC0123456 789EQ"
  { 0x1f, 0x13, 0x11, 0x11, 0x11, 0x13, 0x1f, 0x00 }, // 'L'    (Dolby Labs logo, use "LR|LL")
  { 0x1f, 0x19, 0x11, 0x11, 0x11, 0x19, 0x1f, 0x00 }, // 'R'
//
  { 0x03, 0x03, 0x03, 0x0f, 0x1b, 0x1b, 0x0f, 0x00 }, // 'd'    (Digital Theater Systems logo. use "dts|LLL")
  { 0x04, 0x0c, 0x1f, 0x0c, 0x0c, 0x0c, 0x07, 0x00 }, // 't'
  { 0x00, 0x00, 0x0f, 0x1c, 0x0e, 0x07, 0x1c, 0x00 }, // 's'
//
  { 0x00, 0x00, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x00 }, // 'H'    ('HDCD' small caps logo, use "HDCD|LLLL")
  { 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x00 }, // 'D'
  { 0x00, 0x00, 0x0f, 0x10, 0x10, 0x10, 0x0f, 0x00 }, // 'C'
//
// Standard version of CinEQ: Use "0123456"
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00 }, // '0' _  ('-cinEQ--' EAD CinEQ logo, use "0123456|LLLLLLL")
  { 0x03, 0x04, 0x04, 0x03, 0x00, 0x1f, 0x00, 0x00 }, // '1' C
  { 0x0e, 0x04, 0x04, 0x0e, 0x00, 0x1f, 0x00, 0x00 }, // '2' I
  { 0x0c, 0x12, 0x12, 0x12, 0x02, 0x12, 0x08, 0x07 }, // '3' N
  { 0x1e, 0x10, 0x10, 0x1e, 0x10, 0x1e, 0x00, 0x1f }, // '4' E
  { 0x0e, 0x11, 0x11, 0x11, 0x15, 0x0e, 0x04, 0x1f }, // '5' Q
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e }  // '6' +  (underbar)
};

// @@@@@@@@@@ O,I,P,E,s,U INVADERS FONTS @@@@@@@@@@
// O - Outvader (Invader legs out) 
const byte outvaderfont[][8] PROGMEM = { // "abcd"
  { 0x0e, 0x15, 0x0e, 0x11, 0x00, 0x00, 0x00, 0x00 }, // 'a'  /oo\ row 1
  { 0x00, 0x0e, 0x15, 0x0e, 0x11, 0x00, 0x00, 0x00 }, // 'b'  /oo\ row 2
  { 0x00, 0x00, 0x0e, 0x15, 0x0e, 0x11, 0x00, 0x00 }, // 'c'  /oo\ row 3
  { 0x00, 0x00, 0x00, 0x0e, 0x15, 0x0e, 0x11, 0x00 }  // 'd'  /oo\ row 4
};

// I - Invader  (Invader legs in)
const byte invaderfont[][8] PROGMEM = { // "abcd"
  { 0x0e, 0x15, 0x0e, 0x0a, 0x00, 0x00, 0x00, 0x00 }, // 'a'  \oo/ row 1
  { 0x00, 0x0e, 0x15, 0x0e, 0x0a, 0x00, 0x00, 0x00 }, // 'b'  \oo/ row 2
  { 0x00, 0x00, 0x0e, 0x15, 0x0e, 0x0a, 0x00, 0x00 }, // 'c'  \oo/ row 3
  { 0x00, 0x00, 0x00, 0x0e, 0x15, 0x0e, 0x0a, 0x00 }  // 'd'  \oo/ row 4
};

// P - Laser
const byte laserfont[][8] PROGMEM = { // "12345_"
  { 0x00, 0x00, 0x00, 0x00, 0x04, 0x0e, 0x1f, 0x1f }, // '1'  frame 1
  { 0x00, 0x00, 0x04, 0x04, 0x04, 0x0e, 0x1f, 0x1f }, // '2'  frame 2
  { 0x04, 0x04, 0x04, 0x00, 0x04, 0x0e, 0x1f, 0x1f }, // '3'  frame 3
  { 0x04, 0x04, 0x00, 0x00, 0x04, 0x0e, 0x1f, 0x1f }, // '4'  frame 4
  { 0x04, 0x00, 0x00, 0x00, 0x04, 0x0e, 0x1f, 0x1f }, // '5'  frame 5
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f }  // '_'  Empty laser storage
};

// E - Explosion
const byte kapowfont[][8] PROGMEM = { // "12345"
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x0e, 0x00 }, // '1'  frame 1
  { 0x00, 0x00, 0x00, 0x00, 0x15, 0x0e, 0x0e, 0x15 }, // '2'  frame 2
  { 0x00, 0x00, 0x00, 0x00, 0x11, 0x04, 0x04, 0x11 }, // '3'  frame 3
  { 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x11 }, // '4'  frame 4
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }  // '5'  frame 5
};

// s - Shield
const byte shieldfont[][8] PROGMEM = { // "LR"
  { 0x0f, 0xff, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 'L'  LH half
  { 0x1e, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00 }  // 'R'  RH half
};

// U - Mothership/UFO
// Six frames of 4-char animation. Gameplay stops and
// the mothership flies across the LCD screen.
const byte UFOfont[][8] PROGMEM = { // "ABCDIJKLQRSTabcdijklqrst" 
  { 0x00, 0x03, 0x07, 0x0d, 0x1f, 0x07, 0x02, 0x00 }, // 'A'
  { 0x1e, 0x1f, 0x1f, 0x0d, 0x1f, 0x0c, 0x00, 0x00 }, // 'B'
  { 0x00, 0x10, 0x18, 0x0c, 0x1e, 0x18, 0x10, 0x00 }, // 'C'
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 'D'
//  
  { 0x00, 0x01, 0x03, 0x05, 0x0f, 0x03, 0x01, 0x00 }, // 'I' 
  { 0x0f, 0x1f, 0x1f, 0x0d, 0x1f, 0x06, 0x00, 0x00 }, // 'J'
  { 0x00, 0x18, 0x1c, 0x0e, 0x1f, 0x1c, 0x08, 0x00 }, // 'K' 
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 'L' 
//  
  { 0x00, 0x00, 0x01, 0x03, 0x07, 0x01, 0x00, 0x00 }, // 'Q' 
  { 0x07, 0x1f, 0x1f, 0x0d, 0x1f, 0x13, 0x00, 0x00 }, // 'R'
  { 0x00, 0x1c, 0x1e, 0x0d, 0x1f, 0x0e, 0x04, 0x00 }, // 'S' 
  { 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00 }, // 'T'
//
  { 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00 }, // 'a'
  { 0x03, 0x0f, 0x1f, 0x0d, 0x1f, 0x19, 0x10, 0x00 }, // 'b'
  { 0x10, 0x1e, 0x1f, 0x0d, 0x1f, 0x07, 0x02, 0x00 }, // 'c'
  { 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00 }, // 'd'
//
  { 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00 }, // 'i'
  { 0x01, 0x0f, 0x1f, 0x0d, 0x1f, 0x1c, 0x08, 0x00 }, // 'j'
  { 0x18, 0x1f, 0x1f, 0x0d, 0x1f, 0x13, 0x01, 0x00 }, // 'k'
  { 0x00, 0x00, 0x00, 0x10, 0x18, 0x00, 0x00, 0x00 }, // 'l'
//
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 'q'
  { 0x00, 0x07, 0x0f, 0x1d, 0x1f, 0x0e, 0x04, 0x00 }, // 'r'
  { 0x1c, 0x1f, 0x1f, 0x0d, 0x1f, 0x19, 0x00, 0x00 }, // 's' 
  { 0x00, 0x00, 0x10, 0x18, 0x1c, 0x10, 0x00, 0x00 }  // 't' 
};

/*----------------------------------------------*/
/*  Define the CustomFont struct using typedef  */
/*----------------------------------------------*/
typedef struct {
  char fontFlag;
  const byte (*fontBits)[8];  // Pointer to the CC bitmap arrays.
  String fontChars;
} CustomFont;

/*----------------------------------------------*/
/* Create instances of the CustomFont struct,   */
/*  and assemble into a CustomFont array of     */
/*        struct, one array per font            */
/*----------------------------------------------*/
// Order of fields: fontFlag, fontBits,  fontChars
CustomFont font0  = { 'G', bargraphfont, "0abcLR!87654321" }; // 15
CustomFont font1  = { '0', big0font, "ABCD" }; // 4      Location of the parts forming two bignums on-screen: 
CustomFont font2  = { '1', big1font, "ABCD" }; // 4                AB  A'B'
CustomFont font3  = { '2', big2font, "ABCD" }; // 4                CD  C'D'
CustomFont font4  = { '3', big3font, "ABCD" }; // 4       A maximum of two bignums digits!
CustomFont font5  = { '4', big4font, "ABCD" }; // 4
CustomFont font6  = { '5', big5font, "ABCD" }; // 4
CustomFont font7  = { '6', big6font, "ABCD" }; // 4
CustomFont font8  = { '7', big7font, "ABCD" }; // 4
CustomFont font9  = { '8', big8font, "ABCD" }; // 4
CustomFont font10 = { '9', big9font, "ABCD" }; // 4
CustomFont font11 = { 'X', symbolfont, "DSTMH><^vuds_1234KkLQnNbCF" };  // 26
CustomFont font12 = { 'b', Boldfont, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" }; // 36
CustomFont font13 = { 'B', boldlcfont, "abcdefghijklmnopqrstuvwxyz" }; // 26
CustomFont font14 = { 'L', lowercasefont, "gjpqym" }; // 6
CustomFont font15 = { 'c', smallcapsfont, "ABDEFGHIJKLMNPQRTUY" }; // 19
CustomFont font16 = { 'F', smlfrequfont, "kHz" }; // 3
CustomFont font17 = { 'V', smlrevcapsfont, "BCcFLSsTR" }; // 9  
CustomFont font18 = { 'v', revcapsfont, "ABCDEFGHIKLOPRSTUVXYZ0123456789=:" }; // 33
CustomFont font19 = { 'C', channelfont, "241dca5b3ef6xyzuvwjkl" }; // 21
CustomFont font20 = { 'l', logofont, "LRdtsHDC0123456" }; // 15
CustomFont font21 = { 'O', outvaderfont, "abcd" }; // 4
CustomFont font22 = { 'I', invaderfont, "abcd" }; // 4
CustomFont font23 = { 'P', laserfont, "12345_" }; // 6
CustomFont font24 = { 'E', kapowfont, "12345" }; // 5
CustomFont font25 = { 's', shieldfont, "LR" }; // 2
CustomFont font26 = { 'U', UFOfont, "ABCDIJKLQRSTabcdijklqrst" }; // 24

CustomFont fonts[] = { font0,  font1,  font2,  font3,  font4,  font5,  font6,
                       font7,  font8,  font9,  font10, font11, font12, font13,
                       font14, font15, font16, font17, font18, font19, font20,
                       font21, font22, font23, font24, font25, font26 };


/*----------------------------------------------*/
/* getBitmap() - FETCH CUSTOM CHARACTER BITMAPS */
/*----------------------------------------------*/
// This function returns the bitmap for a fontFlag, fontChar pair.
// NOTES: The foreach loop (a.k.a "range-based for loop"), introduced with C++11,
// simplifies traversal over an iterable data set. It does this by eliminating the
// initialization process and traversing over each and every element rather than an iterator.
// uint8_t* getBitmap(char myfontChar, char myfontFlag) {
//   for (const CustomFont& font : fonts) {
//     if (font.fontFlag == myfontFlag) {
//       int charIndex = font.fontChars.indexOf(myfontChar);
//       if (charIndex != -1) return font.fontBits[charIndex];
//     } 
//   }
//   return nullptr; //  Font or Character not found
// }

uint8_t* getBitmap(char myfontChar, char myfontFlag) {
  for (const CustomFont& font : fonts) {
    if (font.fontFlag == myfontFlag) {
      int charIndex = font.fontChars.indexOf(myfontChar);
      if (charIndex != -1) {
        static uint8_t bitmap[8];
        for (int i = 0; i < 8; i++) {
          bitmap[i] = pgm_read_byte_near(font.fontBits[charIndex] + i);
        }
        return bitmap;
      }
    }
  }
  return nullptr; // Font or Character not found
}

/*********************************************************/
/* setupRow2() - Set up Row 2 for Double-row CC graphics */
/*********************************************************/
void setupRow2() {
  int8_t startPos2 = startPos + 20; 
  startPos = startPos2; // Update startPos for row2
  lcdString = lcdString.substring((lcdString.indexOf('|') << 1) +1 );
  return;
}


/*----------------------------------------------------*/
/* clearTheStage() - Clears the LCD 'stage', making   */
/* it ready for a new LCD 'scene' composed of one or  */
/* several lcdStrings.                                */
/* It clears the LCD and various relevant variables:  */
/* the CC_id_table[], the LCD CGRAM, and screenImage. */
/*----------------------------------------------------*/
void clearTheStage() {
  for (CGRAM_ptr = 0; CGRAM_ptr < 8; CGRAM_ptr++) {
    CC_id_table[CGRAM_ptr] = "";  // Clears out CC_id_table [0 to 7].
    lcd.createChar(CGRAM_ptr, blankBitmap); delay(1); // Blank out all 8 CGRAM locations.
  }
  screenImage = ""; for (int i = 0; i < 80; i++) screenImage += ' '; // Fill the stage with 80 spaces
  lcd.setCursor(0,0); lcd.print("                    "); // 20 spaces
  lcd.setCursor(0,1); lcd.print("                    ");
  // ---This is the equivalent of: startPos = 0; lcdString = " 80 spaces "; LCDfontManager(); 
  return;
}

/*---------------------------------*/
/*    LCDsplash() - LCD SIGN-ON    */
/* (Does not use the font manager) */
/*---------------------------------*/
// Uses CGRAM locations 8,9,10,11,12.
void LCDsplash() {
byte my_F[8]    = { 0x1f, 0x18, 0x18, 0x1e, 0x18, 0x18, 0x18, 0x00 }; // Bold F
byte my_P[8]    = { 0x1e, 0x19, 0x19, 0x19, 0x1e, 0x18, 0x18, 0x00 }; // Bold P
byte myBar8[8]  = { 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b }; // Bar graph = 8
byte myBar6[8]  = { 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b }; // Bar graph = 6
byte myBar4[8]  = { 0x00, 0x00, 0x00, 0x00, 0x1b, 0x1b, 0x1b, 0x1b }; // Bar graph = 4
byte my_star[8] = { 0x04, 0x15, 0x0e, 0x1f, 0x0e, 0x15, 0x04, 0x00 }; // custom '*' char 1 
lcd.createChar(8,my_F);
lcd.createChar(9,my_P);
lcd.createChar(10,myBar4);
lcd.createChar(11,myBar6);
lcd.createChar(12,myBar8);
lcd.createChar(13,my_star);
lcd.setCursor(0,0);
// Very important: DO NOT embed byte(0) in Strings, and don't try to print it to the LCD!!! (it's equivalent to \0, the
// end-of-c-string null character). The issue arises because Hitachi HD44870 LCD controllers use byte(0) as a CGRAM address.
// Fortunately, the CGRAM 0x00-0x07 addresses are mirrored from 0x08-0x0f. Those embed in Strings without problems!
lcd.write(byte(13));lcd.print("-----");
lcd.write(byte(8));lcd.print("ont ");
lcd.write(byte(9));lcd.print("al------"); 
lcd.setCursor(0,1);
for(uint8_t i = 0; i < 3; i++) lcd.write(byte(10)); //4
lcd.print("2");
for(uint8_t i = 0; i < 3; i++) lcd.write(byte(11)); //6
lcd.print(".");
for(uint8_t i = 0; i < 4; i++) lcd.write(byte(12)); //8
lcd.print("0");
for(uint8_t i = 0; i < 3; i++) lcd.write(byte(11)); //6
lcd.print("0");
for(uint8_t i = 0; i < 3; i++) lcd.write(byte(10)); //4
delay(1000);
} 

/*---------------------------------*/
/*         printStringHex()        */
/*---------------------------------*/
void printStringHex(const String& myString) 
  {
    String Interp = ""; 
    Serial.print(" ");
    for (size_t i = 0; i < myString.length(); ++i) 
    {
      char c = myString.charAt(i);
      Interp = Interp + String(c); // + "   ";
      if (c > 15) Serial.print("0x"); 
      else Serial.print("0x0");
      Serial.print(c, HEX); // Print the character as a hexadecimal value
      Serial.print(" ");    // Add inter-character space
    }
    Serial.print(Interp);
    //Serial.println(); // Print a newline at the end
  }

/*---------------------------------------------*/
/* ++++++++++++++++ S E T U P ++++++++++++++++ */
/* ++++++++++++++++ S E T U P ++++++++++++++++ */
/* ++++++++++++++++ S E T U P ++++++++++++++++ */
/*---------------------------------------------*/
void setup() {
  lcd.begin(20,2);
  lcd.clear(); 
  //Serial.begin(9600);
  //Serial.print("\n");myFontsPrinter(); // FOR DEBUG & MAINTENANCE ONLY
  LCDsplash();
  delay(1000); 
  // The following six or seven lines are an example of how you might set up
  // a device spash screen, in place of Font Pal's own LCDsplash() screen,
  // and can be run as part of Example 1, below, or commented out.
  // 
}


/*---------------------------------------------*/
/* ++++++++++++++++  L O O P  ++++++++++++++++ */
/* ++++++++++++++++  L O O P  ++++++++++++++++ */
/* ++++++++++++++++  L O O P  ++++++++++++++++ */
/*---------------------------------------------*/
void loop() {

// - - - - - - - - - - - - - - - S P E C I A L  T E S T - - - - - - - - - - - - - - - - - - - - - 
// The following pair of tests show that when constructing a sceene (in this example, from two parts), font-
// formatted text written into a second field will not affect font-formatted text written to the LCD previously.
// In both cases (which change the order of writing), the result of writing ten bold "I" characters is correctly
// shown as "ABCDEFGH ##########T", and "##########T ABCDEFGH", respectively.
//
// -----SCENE 1:
// clearTheStage();
// startPos = 0;
// lcdString = "ABCDEFGH Plain Text!|"
//             "bbbbbbbb            ";
// LCDfontManager();
// delay(1000);

// startPos = 9;
// lcdString = "IIIIIIIIII |"  
//             "bbbbbbbbbb "; 
// LCDfontManager();

// -----SCENE 2:
// clearTheStage();
// startPos = 0;
// lcdString = "Plain Text! ABCDEFGH|"
//             "            bbbbbbbb";
// LCDfontManager();
// delay(1000);

// startPos = 0;
// lcdString = "IIIIIIIIII |" 
//             "bbbbbbbbbb "; 
// LCDfontManager();
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

//while(true){};

// clearTheStage();
// startPos = 0;
// lcdString = "B AB BAB Plain Text!|" // Row 0 should be: [B AB BAB Plain Text!] (correct!)
//             "b bb bbb            "  // 
//             "B AA BAA BAEbHIJKXYZ|" // Row 1 should be: [B AA BAA BAE@HIJKX##] (correct!) 
//             "b bb bbb bbbbbbbbbbb"; 
// LCDfontManager();
// setupRow2();
// LCDfontManager();
// delay(3000);


// clearTheStage();
// startPos = 0;
// lcdString = "B A BBBBBAADEFGHIJKZ|" // Row 0 should be: [B A BBBBBAADEFGHI###] (correct)
//             "b b bbbbbbbbbbbbbbbb"  
//             "B AB B sB BAB AAAZ s|" // Row 1 should be: [B AB B @B BAB AAA# @] (correct)
//             "b bb b bb bbb bbbb b"; 
// LCDfontManager();
// setupRow2();
// LCDfontManager();
// delay(3000);


//   clearTheStage();
//   startPos = 0;   
//   lcdString = "ABCDABCDABCDABCDABCD|" //(correct)
//               "bbbbbbbbbbbbbbbbbbbb"; 
//   LCDfontManager();
//   delay(3000); 


//   clearTheStage(); // WORKS!
//   startPos = 0;   
//   lcdString = "DTD DDDDD TTTTT DTDT|" //(correct)
//               "bbb bbbbb bbbbb bbbb"; 
//   LCDfontManager(); 
//   delay(3000);


//   clearTheStage(); 
//   startPos = 0;   
//   lcdString = "D T D DDDDD TTTTT DT|" //(correct)
//               "b b b bbbbb bbbbb bb"; 
//   LCDfontManager(); 
//   delay(3000);


  // clearTheStage(); 
  // startPos = 0;   
  // lcdString = "B ZBBBAB ZB T DBB|" //(correct)
  //             "b bbbbbb bb b bbb"; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Works!";
  // LCDfontManager(); 
  // delay(3000);


  // clearTheStage(); 
  // startPos = 0;   
  // lcdString = "B AB BBBB AAAA AB |" //(correct)
  //             "b bb bbbb bbbb bb "; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Works!";
  // LCDfontManager(); 
  // delay(3000);


  // clearTheStage();
  // startPos = 0;   
  // lcdString = "B A B BBBB AAAA A B |" //(Correct)
  //             "b b b bbbb bbbb b b "; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Space AB & it Works!";
  // LCDfontManager(); 
  // delay(3000);
  

  // clearTheStage(); 
  // startPos = 0;   
  // lcdString = "C AC CCCCC AAAAA AC|" //(Correct) 
  //             "b bb bbbbb bbbbb bb"; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Swap B <--> C Works!";
  // LCDfontManager(); 
  // delay(3000);


  // clearTheStage(); 
  // startPos = 0;   
  // lcdString = "B|" //(Correct)
  //             "b"; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Works!";
  // LCDfontManager(); 
  // delay(3000);
 

  // clearTheStage(); 
  // startPos = 0;   
  // lcdString = "BA|"  //(Correct)
  //             "bb"; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Works!";
  // LCDfontManager(); 
  // delay(3000);

 
  // clearTheStage(); 
  // startPos = 0;   
  // lcdString = "B A|" //(Correct)
  //             "b b"; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Works!";
  // LCDfontManager(); 
  // delay(3000);


  // clearTheStage();
  // startPos = 0;   
  // lcdString = "B AB|" //(Correct) 
  //             "b bb"; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Works!";
  // LCDfontManager(); 
  // delay(3000);


  // clearTheStage();
  // startPos = 0;   
  // lcdString = "B A B|" //(Correct) 
  //             "b b b"; 
  // LCDfontManager(); 
  // startPos = 20;
  // lcdString = "Works!";
  // LCDfontManager(); 
  // delay(3000);


// =========
// EXAMPLE 1: LCD 'scene construction' emulating the display of a home theater digital surround sound processor:
// =========
// ---------------------------------------------------------------------------
// TEST THE CONSTRUCTION OF AN APPLICATION SCREEN FROM SEVERAL SMALLER STRINGS:
//
// (i) clearTheStage() is generally used once only, prior to any LCD loop. One
// exception to this would occur if we jump out of that loop to display a bignums
// volume up/dpwm adjustment screen (which also uses all 8 CCs). Then before
// starting the type of loop shown in this example, clearTheStage() would be called.
// Therefore, because this test loops through various status readouts, which change
// by emulating the user changing inputs, I have moved clearThe Stage() to the end
// of void Setup().
//
// (ii) Perhaps not so obvious, is that the daisy[n] strings have two rows, and
// therefore require a call to setupRow2() and a second call to LCDfontManager().
//
// (iii) Perhaps even less obvious is that lcdStrings written to the LCD must be
// padded with spaces, not only to prevent fragments of prior lcdStrings from being
// visible, but also because this is how Font Pal purges any CCs referenced by the
// overwritten String from the CC_id_table, thereby freeing up those table locations
// for new lcdStrings and their CC references. This is why, generally, we don't need
// to have a line of code like the following to free up CC_id_table locations:
//      startPos = 4; lcdString = "                "; LCDfontManager();
// The only case I can think of where this line might be needed is if a field is
// purposefully blanked. A non-font-formatted string of plain-text spaces is 
// sufficient for this purpose.
 
  clearTheStage(); // Clear LCD image and CCs prior to 'building an LCD Scene'
  startPos = 0; 
  lcdString = "   TheaterMaster         SIGNATURE      |" 
              "   bBBBBBBbBBBBB                        ";
  LCDfontManager();
  delay(3000);

//while(true){};

  clearTheStage(); // Clear LCD image and CCs for an 'LCD Scene' (Already done at the end of Void Setup(), 
  startPos = 32;   //  above, but may be needed here if other exampes are run immediately before this.
  lcdString = "-24.5 dB"; // 0 CCs
  LCDfontManager();
  delay(100);

  startPos = 24;
  lcdString = "D3:DVD";  // 0 CCs   An example of a plain-text string.
  LCDfontManager();
  delay(100);
  
  startPos = 4;
  lcdString = "No Signal       |"  // 1 CC. String is padded with spaces to the end of the field.
              "     L          ";
  LCDfontManager();
  delay(100);
  
  startPos = 0;
  lcdString = daisy[7]; // 0/0/0 channel configuration not yet determined (4 CCs)
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  delay(1000);

  startPos = 24;
  lcdString = "D1:Sat";  // 0 CCs   An example of a non font-encoded string.
  LCDfontManager();
  delay(100);

  startPos = 4; 
  lcdString = "LR Digital 5.1  |" // 3 CCs. String is padded with spaces to the end of the field.
              "ll   L          ";
  LCDfontManager();
  delay(100);

  startPos = 0;
  lcdString = daisy[8]; // 3/2/0.1 ('5.1') channel configuration (4 CCs)
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  delay(1000);

  startPos = 24;
  lcdString = "D2:Blu";  // 0 CCs   An example of a non font-encoded string.
  LCDfontManager();
  delay(100);
  
  startPos = 4; 
  lcdString = "dts ES 6.1      |" // 4 CCs. Padded out with spaces to match the length of the field. 
              "lll             ";
  LCDfontManager();
  delay(100);

 startPos = 0;
  lcdString = daisy[16];  // 3/3/0.1 ('6.1') channel configuration (4 CCs)
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  delay(1000);

//while(true){}; // uncomment to stop the loop

//----------------------------------------------------------------------------------------
//  Other Examples of font-formatted 'blurb' Strings:

  // clearTheStage();
  // startPos = 5;   
  // lcdString = "ABABABAB   |88880000   "   // Double-row Strings must
  //             "CDCDCDCDPro|88880000   ";  // be of equal length! Correct: 8800Pro
  // LCDfontManager();
  // setupRow2();
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 0;   
  // lcdString = "LR RL Hey Baby Bluejay LLLLLLLLLL RRRRR|"  \\(Correct) Correctly shows ##
  //             "ll ll b L b  L bBBBLBL llllllllll lllll";   
  // LCDfontManager(); 
  // delay(3000);


  // clearTheStage();
  // startPos = 0;   
  // lcdString = "SDS   B AB   S DS  v|"  // (correct)
  //             "XXX   b bb   X XX  X"  
  //             "SDS   B AB   S DS  ^|"
  //             "XXX   b bb   X XX  X";
  // LCDfontManager(); 
  // setupRow2();
  // LCDfontManager();
  // delay(2000);


  // clearTheStage();
  // startPos = 0; 
  // lcdString = "All Bold A A A A A A A A A A A A A A A B|" //
  //             "    b    b b b b b b b b b b b b b b b b";
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 0; 
  // lcdString = "ABABABABABABABABABABABABABABABABABABABAB|" //
  //             "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 0;   
  // lcdString = "All Bold AAAAAAAAABA|"  // (Correct)
  //             "    b    bbbbbbbbbbb"   
  //             "AAAAAAAAAAAAAAAAAAAB|"  
  //             "bbbbbbbbbbbbbbbbbbbb";
  // LCDfontManager();
  // setupRow2();
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 0;   
                  
  // lcdString = "WWW b wwwwwwwwwwwwwb|"  // (Correct)
  //             "    B BBBBBBBBBBBBBB"  
  //             "AAAAAAAAAAAAAAAAAAAA|" 
  //             "bbbbbbbbbbbbbbbbbbbb"; 
  // LCDfontManager();
  // setupRow2();
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 0; 
  // lcdString = "SSSSSSSSSSSSSSSSSSSTSSSSSSSSSSSSSSSSSSST|" // (Correct)
  //             "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
  // LCDfontManager();
  // delay(3000);

//while(true){}

/*
// INVADER SPLASH SCREEN  (7 CCs, 7 unique)
clearTheStage();
delay(500);
startPos = 0;
lcdString = "   Space Invaders   |"
            "   bL    b          "
            "   b-     ABC-      |"
            "   I      UUU       ";
LCDfontManager();
setupRow2();
LCDfontManager();
delay(800);
startPos = 25;
lcdString = "2";  // Add points.
LCDfontManager();
delay(200);
startPos = 26;
lcdString = "5";
LCDfontManager();
delay(400);
startPos = 34;
lcdString = "1";
LCDfontManager();
delay(200);
startPos = 35;
lcdString = "0";
LCDfontManager();
delay(200);
startPos = 36;
lcdString = "0";
LCDfontManager();
delay(1500); 

// INVADER GET READY! SCREEN  (3 CCs, 2 unique)
clearTheStage();
startPos = 0;
lcdString = "       GET       000"
            "      READY!        ";
LCDfontManager();
delay(1000);
startPos = 37;
lcdString = "111|PPP"; // Stock up on lasers.
LCDfontManager();
delay(1000);
startPos + 37;
lcdString = "_|P"; // Show one laser used/in use: '_'
LCDfontManager();
delay(500);

//while(true) {}

// INVADER GAME SCREEN  (cannot work properly until section clear wipes out CCs from CC_id_table)
clearTheStage();
startPos = 0;
lcdString = "                 000|"
            "                    "
            "  LR LR 3 LR LR  _11|"
            "  ss ss P ss ss  PPP";
LCDfontManager();
setupRow2();
LCDfontManager();
for (int8_t row=0; row<4; row++){   // 0 to 3
  for (int8_t col=0; col<7; col++){ // 0 to 8
    startPos = 0;
    lcdString = "                "; // 16 " ". Update to section clear later
    LCDfontManager();
    if (col % 2) lcdString = invArray[row][0]; else lcdString = invArray[row][1];
    if (row % 2) startPos = 7 - col; else startPos = 1 + col; 
    LCDfontManager();
    delay(600);
  }
}
delay(2000);
// MOTHERSHIP FLY-BY

// INVADER OUTRO SCREEN   (9 CCs, 8 unique, but with animation we have 10 CCs, 9 unique)
  clearTheStage(); 
  startPos = 0;
  lcdString = "Hi-Scdrers:  AJR SCR|" 
              "bB bBOBBBB          "
              "             AR  JSH|"
              "                    "; 
  LCDfontManager();
  setupRow2();
  LCDfontManager();
// Now continue with a brief animation of one of the CCs:
  startPos = 5;
  for (uint8_t k=0; k < 8; k++) {
    lcdString = "d|I"; // Space Invaders alien, legs in. 
    LCDfontManager();
    delay(500);
    lcdString = "d|O"; // Space Invaders alien, legs out.
    LCDfontManager();
    delay(500);
    }

while(true){};
*/


  // clearTheStage();
  // startPos = 0; 
  // lcdString = "Space is big.|" // 15 CCs, 12 unique, which should trigger CGRAM overflow --> # # # #  (correct)
  //             "bBBBB BB BBB "
  //             "         Really big.|"
  //             "         b    L BBB ";
  // LCDfontManager();
  // setupRow2();
  // LCDfontManager();
  // delay(3000);


  // startPos = 0;
  // clearTheStage();
  // lcdString = "You just won't      believe how vastly,|" // 8 CCs, 8 unique. One @ due to bad fontFlag  (correct)
  //             "    L    L                      BBBBBB "; 
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // lcdString = "hugely, mind-bogglingly big it is.|" // 13 CCs, 8 unique. (Correct)
  //             "  L  L  BBBB   LL   L L BBB       ";
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();  
  // lcdString = "THE UNIVERSE IS HUGE|" // 17 CCs, 10 unique, which should trigger CGRAM overflow --> # # # (correct)
  //             "bbb bbbbbbbb bb bbbb"
  //             "(Douglas Adams HHGG)";
  // LCDfontManager();
  // setupRow2();
  // LCDfontManager();
  // delay(3000);


  // clearTheStage(); 
  // startPos = 0; 
  // lcdString = "A bright LCD displayis very easy to read|" // Example with exactly 40 screen characters. Requires 82 bytes of buffer memory. (Correct)
  //             "     L   bbb    L  L      L    L        ";
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 8;
  // lcdString = "Hello World!"; // Example of plain text (i.e., no CCs) (Correct)
  // LCDfontManager();
  // delay(3000);

//while(true){};

  // clearTheStage();
  // startPos = 0; 
  // lcdString = "   TheaterMaster         SIGNATURE      |" 
  //             "   bBBBBBBbBBBBB                        ";
  // LCDfontManager();
  // delay(3000);

  // clearTheStage(); 
  // startPos = 0;
  // lcdString = "   TheaterMaster          Ovation       |" 
  //             "                          bBBBBBB       ";
  // LCDfontManager();
  // delay(3000);

  // clearTheStage();
  // startPos = 0; 
  // lcdString = "   TheaterMaster          Encore        |" 
  //             "                          bBBBBB        ";
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 0;
  // lcdString = "CPU:6.9o Z1:00302004DIP:FF-3 Z2:00302004|" // 10 CCs, 8 unique.
  //             "vvv      vv         vvv      vv         ";
  // LCDfontManager();
  // delay(3000);


  // clearTheStage();
  // startPos = 0; 
  // lcdString = " LF cR RF  Ls sB Rs |"
  //             " cc  c cc  c   c c  "  
  //             " 55 77 55  77 55 77 |" // The small caps channel labels use 4 CCs. These bars need 1 CC.  
  //             " GG GG GG  GG GG GG "; // This will always work.
  // LCDfontManager();
  // setupRow2();
  // LCDfontManager();
  // delay(3000);

//while(true) {};

// Test many selection variations of a 5.1-channel surround sound speaker setup.
// Note the use of "s" and "c" lowercase characters as stand-ins for small caps.
// Without this work-around most of the following screen would be impossible.
// The best way to avoid a messy substitution (of "C" for "c" and "S" for "s")
// would be to extend the Reverse Caps font with "c" and "s", using the same
// bitmaps as the ordinary Reverse Caps (Update: Already implmented).
// Another approach would be to have reversed version of the big and small
// speakers, and to indicate selection with those.


  // clearTheStage();
  // startPos = 0;
  // lcdString = " LF:K   cR:kx  RF:K  Ls:kx  sB:K   Rs:kx|" //  14 CCs, 6 unique. 3 speakers full range, 3 limited range.
  //             " VV X    c X   cc X  c  X    c X   c  X "; // The 'x' indicates that a particular small speaker has a cross-over. 
  // LCDfontManager();      
  // delay(4000); 
    

  //clearTheStage();
  // startPos = 0;
  // lcdString = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF|" //  40 CCs comprising 2 unique.
  //             "bvbvbvbvbvbvbvbvbvbvbvbvbvbvbvbvbvbvbvbv"; 
  // LCDfontManager();
  // delay(1000); 

//while(true){};

 
// Test of channel daisies in a home theater controller:
// WHEN IT GET THE CC_id_table CLEARING WORKING, THE WHOLE OF THIS MUST BE in ONE LOOP:
/*
  clearTheStage();
  startPos = 4; 
  lcdString = "LR Digital      |" // 3 CCs
              "ll   L          ";
  LCDfontManager();
  
  startPos = 0;
  lcdString = daisy[0];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/2/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[1];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "2/P/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[2];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "2/0/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[3];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/1/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[4];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/0/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[5];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "2/2/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[6];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "1/0/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  clearTheStage(); // Makes sure the LCD is cleared of previous modes.
  startPos = 4; 
  lcdString = "No Lock"; // input PLL unable to lock (caused by input signal fault)
  LCDfontManager();
  startPos = 0;
  lcdString = daisy[7];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "0/0/0  ";  // Plain text
  LCDfontManager();
  delay(1000);

  //clearTheStage();
  startPos = 4; 
  lcdString = "LR Digital      |" // 3 CCs
              "ll   L          ";
  LCDfontManager();
  startPos = 0;
  lcdString = daisy[8];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/2/0.1";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[9];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "2/P/0.1";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[10];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "2/0/0.1";  // Plain text
  LCDfontManager();
  delay(500);
  
  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[11];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/1/0.1";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[12];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/0/0.1";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[13];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "2/2/0.1";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "LR Digital      |" // 3 CCs
  //            "ll   L          ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[14];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "1/0/0.1";  // Plain text
  LCDfontManager();
  delay(500);

  clearTheStage();
  startPos = 4; 
  lcdString = "dts ES          |" // 3 CCs
              "lll             ";
  LCDfontManager();
  startPos = 0;
  lcdString = daisy[15];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/3/0  ";  // Plain text
  LCDfontManager();
  delay(500);

  //clearTheStage();
  //startPos = 4; 
  //lcdString = "dts ES          |" // 3 CCs
  //            "lll             ";
  //LCDfontManager();
  startPos = 0;
  lcdString = daisy[16];
  LCDfontManager();
  setupRow2();
  LCDfontManager();
  startPos = 24;
  lcdString = "3/3/0.1";  // Plain text
  LCDfontManager();
  delay(500);
  */

} // ------END------

LCDfontManager.ino

/*-------------------------------------------------------*/
/* LCDfontManager() - Liquid Crstal Display Font Manager */
/*-------------------------------------------------------*/
void LCDfontManager() {
  //Serial.begin(9600);
  bool fontEncoded = true; 
  String merged_lcdString= "";

  // BEGIN FONT PROCESSING...
  // ==[1]==================== 
  // Scan lcdString to detect '|' markers... 
  int8_t marker_idx = lcdString.indexOf('|');
  if (marker_idx == -1) fontEncoded = false; // If lcdString is plain text, print it more-or-less directly...
  
  // ==[2]==================== 
  // Perform an interleave merge of fontChar & fontFlag sections of lcdString, excluding the '|' marker
  // (e.g.,  "LR DoobyoD|ll BBB LBB" --> "LlRl  DBoBoBb yLoBDB"), and also processes the top row of double-row
  // symbols (e.g., "Test|B   CASE|BBBB|" --> "TBe s t "):
  if (fontEncoded == true) {
    merged_lcdString = "";
    for (uint8_t i = 0; i < marker_idx; i++) {
      merged_lcdString += lcdString.charAt(i);
      merged_lcdString += lcdString.charAt(marker_idx + 1 + i);
    } 
  } 
  else { // if lcdString isn't font-encoded, treat as plain text...
    merged_lcdString = "";
    for (uint8_t i = 0; i < lcdString.length(); i++) merged_lcdString += (lcdString.charAt(i) + String(" "));
    in_CC_id_table = false; // Is this line needed? 
  }

  // ==[3]==================== 
  // TO MAXIMIZE THE EIGHT-CC CGRAM RESOURCE, WE NOW NEED TO CLEAN UP THE CC_id_table,
  // removing any entries that will be overwritten by CCs in the new lcdString. 
  int16_t mystartPos; 
  mystartPos = startPos << 1;
  uint8_t scrn_ptr = mystartPos;
  while (scrn_ptr < (mystartPos + merged_lcdString.length())) {
    uint8_t fontByte = screenImage.charAt(scrn_ptr); 
    if ((fontByte >= 0x08) && (fontByte <= 0x0f)) CC_id_table[fontByte - 8] = ""; 
    scrn_ptr += 2; 
  }
  screenImage = screenImage.substring(0, mystartPos) + merged_lcdString + screenImage.substring((mystartPos) + merged_lcdString.length());
  
  // ==[4]==================== 
  // BEGIN A LONG LOOP, PROCESSING THE MERGED LCDSTRING INTO SCREENIMAGE AT STARTPOS, LOADING CC 
  // BITMAPS INTO THE CGRAM, FLAGGING FONT ERRORS AND ANY CGRAM OVERFLOW (@ and # errors, respectively).
  // Parse screenImage for CC flags, loading the CC bitmaps to the LCD chip, also replacing fontChars
  // in screenImage with CGRAM_ptrs. Processed CCs have the fontChar part replaced with a byte between 0x08
  // and 0x0f, and the fontFlag part with " ". Subtracting 8 from the 0x08-0x0f byte gives the 0-7 location
  // of the CC in the CC_id_table. This offset of 8 avoids problems caused by ASCII null characters (0x00),
  // related to the use of \0 as an end-of-string marker in C and C++.
   
  scrn_ptr = startPos;
  while (scrn_ptr < 40) {
    uint16_t even_scrn_ptr = scrn_ptr << 1;
    uint16_t odd_scrn_ptr = even_scrn_ptr + 1;
    String current_fontChar = String(screenImage.charAt(even_scrn_ptr)); // e.g., "q"  font char  IF FONTCHAR IS 8-15, ignore
    String current_fontFlag = String(screenImage.charAt(odd_scrn_ptr));  // e.g., "l"  bold caps font-flag

    // %%%%% If the current fontChar has a non-blank fontFlag, we assume that it must be an unprocessed CC...
    //       (There are two types of CCs: Those that are in the cc_id_table and those that are not.)
    if (current_fontFlag != " ") {
      uint8_t* gBitmap = getBitmap(current_fontChar.c_str()[0], current_fontFlag.c_str()[0]); 
      // - where uint8_t* is a pointer into the CC bitmap array of struct. Example: Get the 'g' bitmap from the 'L' font. [0] ensures 1 byte
      String current_CC_id = current_fontChar + current_fontFlag;  // e.g., "dL" ( = "d" + "L") 

      if (gBitmap) {
        // Type (A) --- Those that already have a bitmap in CGRAM and are in the CC_id_table: 
        // For these we replace all instances of it in screenImage with the index of the current_CC_id in the CC_id_table (0x08-0x0f + " ")...
        in_CC_id_table = false; // initial assumption is that current_CC_id is not in CC_id_table
        for (CGRAM_ptr = 0; CGRAM_ptr < 8; CGRAM_ptr++)
        if (CC_id_table[CGRAM_ptr] == current_CC_id) {  // E.g., if CC_id_table[0-7] == "ql", then...
          in_CC_id_table = true; // ...set in_CC_id_table to true, and replace all instances of current_CC_id:
          screenImage.replace(current_CC_id, String(char(CGRAM_ptr + 0x08)) + String(" "));
        } 
             
        // Type (B) --- Those that have no bitmap and are not in the CC_id_table: 
        // IF the current_CC_id is not already in the CC_id_table, save it to a free location in the table, and then replace
        // all instances of it in screenImage with the index of the current_CC_id in the CC_id_table (0x08-0x0f + " " )...   
        if (in_CC_id_table == false) { 
          // Locate 1st free location in CC_id_table:  
          CGRAM_ptr = 0; while ((CGRAM_ptr < 8) && (CC_id_table[CGRAM_ptr] != "")) CGRAM_ptr++; 
          
          // If CC_id_table has a free location, save the current_CC_id there,
          // load the bitmap into CGRAM, and replace all instances in screenImage with (CGRAM_ptr + 0x08) + " ":
          if (CGRAM_ptr < 8) {
            CC_id_table[CGRAM_ptr] = current_CC_id;   // CC_id format is, e.g., "ql"
            lcd.createChar((CGRAM_ptr + 0x08), gBitmap); // Load bitmap into the LCD chip's CGRAM
            in_CC_id_table = true; // Set in == true, and then replace all instances of current_CC_id in...
            screenImage.replace(current_CC_id, String(char(CGRAM_ptr + 0x08)) + String(" ")); // ...screenImage with (CGRAM_ptr + 0x08) + " " 
          }

          // If the CC_id_table is full (due to all eight CCGRAM locations being in use),
          // signal CGRAM overflow by replacing all instances of the current_CC_id in screenImage with "# ": 
          if (CGRAM_ptr == 8) screenImage.replace(current_CC_id, String("# ")); 
        }  
      } 

       // IF the current bitmap is not found (due to the char/font combination being missing or incorrect), 
       // signal a Char/Font Error by replacing all instances of the current_CC_id in screenImage with "@ ":  
      else screenImage.replace(current_CC_id, String("@ "));
    }
    // %%%%% CC_ids ignored by the above font processing include the following:
    //       Plain-text characters in font-formatted Strings, e.g., "a ";
    //       Characters in plain-text Strings e.g., "Plain Text!";
    //       Processed CCs, e.g. String(char(0x0a) + String(" "); 
    //       The "@ " (bad char/font), and "# " (CGRAM overflow) error flags.
    // 
    scrn_ptr += 1; // Loop back to beginning to process the next CC_id in screenImage...   
  } 

  // ==[5]==================== 
  // Finally, print all EVEN characters in screenImage 0,2,4,6,..., to the LCD display:
  lcd.setCursor(0,0);
  String final_screenImage = "";
  for (scrn_ptr = 0; scrn_ptr < screenImage.length(); scrn_ptr += 2) final_screenImage = final_screenImage + screenImage.charAt(scrn_ptr);
  lcd.setCursor(0,0); // row 0
  lcd.print(final_screenImage.substring(0, 20));
  lcd.setCursor(0,1); // row 1
  lcd.print(final_screenImage.substring(20, 40));
  final_screenImage = "";
} 

ReadMe.ino

/*
#############################################################################
##################     A r d u i n o - F o n t - P a l     ##################
##################       ( F o n t   P a l o o z a )       ##################
##################  F O R   H I T A C H I   H D 4 4 7 8 0  ##################
##################      L C D   C O N T R O L L E R S      ##################
#############################################################################

Copyright (c) 2024 Alastair Roxburgh

(Abstract: Font Pal recreates the functionality of an original 68HC11 stack-
frame assembly language program named SCREENS, devised in 1997 by Alastair
Roxburgh & Sunil Rawal for the EOS series of TheaterMaster digital home theater
audio processors. Written in Arduino C++, Font Pal makes heavy use of the String
class and arrays of struct to create a custom font manager for 2x20 LCD displays
based on the Hitachi HD44780 chip.)

Note:
  While Font Pal, which was developed expressly for the Arduino Mega platform,
  reproduces most of the functionality of its 1977 68HC11 predecessor its
  internal algorithms are only remotely similar.

Font Pal is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation version 3 of the License.

Font Pal is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MECHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Public License for more details.

You should have received a copy of the GNU General Public License along with
Font Pal. If not, see <http://www.gnu.org/licenses/>.

See the license.txt file for further licensing & copyright details.
-----------------------------------------------------------------------

-----------------------------------------------------------------------
History

2024.09.04 Alastair Roxburgh (kiwiengr) - initial creation 
2024.09.13 Alastair Roxburgh (kiwiengr) - Updated Example 1: 'LCD scene'
2024.09.19 Alastair Roxburgh (kiwiengr) - Alpha release. Note: Font letter name changes.

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

---To Do---
Once everything is working, add more error chekcing:
a) Check that the length of lcdString <=40.
b) If there is one '|' in lcdString, check that lcdString.length() - 2 * marker_idx = 1.
c) If there are two '|' in lcdString, check that lcdString.length() - 4 * marker_idx = 2.
d) Consider how to select LCD size: 16x1, 20x1, 16x2, or 20x4.
e) Determine if there a sensible way to configure Font Pal as an Arduino library.


SUMMARY
=======

Font Pal is a font manager for Arduino and Hitachi HD44780-based LCDs, so-named
as a more easily pronounced version of Font-Palooza, a handle inspired by (a more
challenging to pronounce) Lollapalooza, the famous recurring music festival first
held in 1991. Since then, the word "palooza" has been used so often that it has
entered the mainstream language as a stand-alone word, a neologism. Indeed, following
in Lollapalooza's footsteps, "palooza" is now defined as an event or thing that aspires
to be "an extraordinary or unusual thing, person, or event; an exceptional example or
instance" (dictionary.com). Some of my favorite examples are Hula Palooza, Datapalooza,
law-LA-palooza, St. Lou-a-palooza, and MINE-a-palooza (sometimes hyphenated, sometimes
not; sometimes preceded by the "a" from Lolla). OED Online traces Lollapalooza in its
various spellings back to 1904 but, strangely, makes no mention of Lala Palooza, the
name of the female protagonist in Pulitzer prize winner Rube Goldberg's 1936-1939 comic
strip of the same name.

Hence, with apologies to Douglas Adams, Font Pal hopes it, too, will be seen as
impressive and wholly remarkable, even if not as wildly popular and in-your-face
as the asteroid named Arthurdent (asteroid 25924).  

Written in Arduino C++ and making heavy use of the String type and arrays of struct,
Font Pal recreates the functionality of an original 68HC11 stack-frame assembly
language program that was devised in 1997 by Alastair Roxburgh & Sunil Rawal for
the ground-breaking EOS series of TheaterMaster digital home theater audio processors.

Although this new implementation mirrors many of the capabilities and structure
of its 68HC11-based predecessor, apart from using a similar character bitmap
database, many of the algorithmic details are different. Interestingly, whereas
the 68HV11E1 microcontroller used in EOS TheaterMasters is an 8-/16-bit 2 MHz
CISC-based design, the Arduino's 16 MHz 8-bit RISC-based ATmega2560 runs at a
similar speed overall, the bottleneck being the modest speed of the Hitachi
HD44780 controller chip in typical LCDs, not to mention the relatively slow
response of typical Okaya STN Transflective LCDs (which can take 150 ms to go
from clear to full black when a typical character is displayed and 300 ms to
fade to clear when a space is displayed.)

TABLE OF CONTENTS:
=================

INTRODUCTION

USING FONT PAL
	FONT FOMATTING OF LCD DISPLAY STRINGS
	DISPLAYING PLAIN TEXT
	MORE HINTS ON HOW TO USE FONT PAL
	THE FOLLOWING METHOD IS NOT RECOMMENDED FOR DOUBLE ROW CC GRAPHICS
	ERROR REPORTING
	GENERAL POINTS ABOUT HITACHI HD44780-BASED LCD DISPLAYS
	NOTES:
 	   A SUMMARY OF FONT PAL FORMATS AND COMMANDS
     LCDfontManager() Implemtation Notes


INTRODUCTION
============

Font Pal for the Arduino Mega platform allows the C++ app designer to bypass
much of the frequently complex chore of custom character (CC) and font manage-
ment for typical Hitachi HD44780 character-based LCDs. In its essentials, Font
Pal automates the management of CGRAM, a scarce memory resource internal to the
HD44780 LCD controller chip, that stores the bit-maps of up to eight CCs. Font
Pal reduces some of the barriers to programming complex and varied sequences of
CCs. Working behind the scenes, the HD44780 loads and unloads CC bitmaps in real
time, according to the moment-by-moment graphics needs of a particular software
application, be it a simple hobby video game, or something more serious such as
an embedded controller in a home theater receiver or a home control system. Font
Pal's value proposition is that it separates the chore of font management from
the application code, giving the application programmer more time to develop
application features not bogged down by the tedious task of manually managing
CCs and CGRAM memory. 

When Font Pal loads a CC into CGRAM, it stores a tag to that CC in every data
display memory (DDRAM) location that will display the CC. When parts of the
screen are updated with new CCs, Font Pal first erases the relevant locations
in CGRAM. If this happens to erase a CC that is still in use in a different
part of the screen, and as long as it will not exceed the limit of eight CCs, 
the CC will be reloaded into CGRAM.  

Consider how much of an all-around improvement Font Pal can produce: 

  1) Reduced application development time,
  2) Improved application quality, 
  3) Nicer-looking and more legible displays, 
  4) Better LCD utilization through optimized CC automation.

Such improvements will not necessarily be subtle, and may also defer,
indefinitely, the need to move to a bit-mapped display and faster
microprocessor.

Several examples of Font Pal use follow:

EXAMPLE (1):
Font Pal can quickly turn a 2x20 LCD into a 2- to 10-channel VU meter,
simultaneously displaying up to 16 levels per channel at 6 dB per step.
If we reserve the bottom bar for zero signal and ignore the 6 dB gap
between the two LCD rows, we obtain a total range on each channel of
15*6 dB = 90 dB, which can be updated several times a second. Indeed,
by using the two LCD rows separately, we can display even 20 channels
with half the vertical resolution. In these extreme cases of power
through repetition of bitmaps, eight static bitmaps stored in the
CGRAM are stretched to as many as twenty simultaneously displayed
locations on the LCD, as in the following examples of symmetrical
VU mater layouts:

                     xxxxxxx  xxxxxxx      2 channels
                   xxxxxx xxxxxx xxxxxx    3 channels      
                    xxxx xxxx xxxx xxxx    4 channels
                   xxx xxx xxxx xxx xxx    5 channels
                   xx xx xxx  xxx xx xx    6 channels
                   xx xx xx xx xx xx xx    8 channels
                    x x x x xx x x x x     9 channels
                   x x x x x  x x x x x    10 channels

where 'x' represents a single bar-font character. The following diagram
shows a VU meter for ten audio channels constructed using a 2x20 char LCD.
For 6 dB steps, converting from linear amplitude to logarithmic (dB)
amplitude is straightforward: the audio DSP averages the observed position
of the highest 1-bit in arithmetically positive audio samples (typically
16- to 24-bit words). It does not matter if the number representation is
integer or fractional. If the VU meter processes five values per second,
an appropriate averaging period is about 200 ms or 10,000 audio samples.
If you require a peak-reading VU meter, display the position of the highest
bit observed during the same period rather than the average. You can also
slide the dB scale up or down in 6 dB steps to set the 0 dB wherever you
want. The diagram (below) shows amplitudes of 0, 0, 60 dB, 66 dB, 18 dB,
78 dB, 30 dB, 36 dB, 96 dB, and 0, where '0' (not defined on a logarithmic
scale) represents zero signal. This example illustrates how you can stretch
the eight CGRAM locations to create a VU meter for up to 10 channels on a
2x20 LCD:
                   +--------------------+
                   |     2 3   5     8  |
                   | 1 1 8 8 4 8 6 7 8 1|
                   +--------------------+
where the numeral signifies the height in pixels of a block-shaped CC. 
A '1' alone in a channel (i.e., the bottom row of pixels only) represents
zero signal, leaving a range of 16 x 6 dB = 96 dB (including the 1-pixel-
height inter-row gap of 6 dB). You can find an example of this type of bar
graph in the optional Font Pal splash screen. Also included in Font Pal is
a utility function, printFonts(), which can help you verify the correctness
of the font bitmap tables after you've made changes.

In the vertical bargraphs just described, the entire resource of eight CCs
is dedicated to this one use. Nevertheless, we are free to choose the number
of channels and bar widths because these take advantage of the freedom to
repeat CCs to build bars of any suitable widths. This way, we can create 
displays that compel the eye of the beholder, creating the impression that
there are more than eight individually programmable CCs. I need to point
out that the Font Pal LCD manager avoids the method of tricking the eye
through CC animation, which can lead to less-than-satisfactory results due
to the relatively slow response time of STN (super-twisted nematic) trans-
flective LCDs. Font Pal's bar graph characters have a central gap to hide
the distracting presence of the inter-character gap when the bar width is
greater than one character. This gap also allows for flexibility in setting
the width of bars and using mixed bar widths while maintaining the same look
of the bars. For example, if we construct three bars having widths of one,
two, and three characters, respectively, with heights of 1, 3, and 2 pixels,
they will look something like:  

                              XX XX XX XX      XX XX XX XX XX XX
                              XX XX XX XX      XX XX XX XX XX XX
             XX XX XX XX      XX XX XX XX      XX XX XX XX XX XX
rather than:
                                               XXXXX XXXXX XXXXX
                              XXXXX XXXXX      XXXXX XXXXX XXXXX
             XXXXX XXXXX      XXXXX XXXXX      XXXXX XXXXX XXXXX

The split bar design of the top example makes the bars look more uniform.

EXAMPLE (2):
A home theater processor's LCD screen might look like this:
                   +--------------------+
                   |vOv LR Digital 5.1  |
                   |^Q^ D1:Blu  X -15 dB|
                   +--------------------+
where the eight unique CCs are marked as vO^QLRXg, for a total of ten CCs
on screen. The vOv and ^Q^ represent a "channel daisy" that indicates which
channels are in use with the current signal source, requiring four unique
CCs. The "LR" is two CCs forming a Dolby "Double-Dee" logo; the "g" is an
improved lowercase g with a true descender, and the "X" is an equalization
indicator, a single CC in the shape of 'E/Q'.

The Channel ("C") font produces 6.1 channel layouts similar to the following:

      *                                  C            C   =  Center front
  *       *                         LF       RF       LF  =  Left Front 
      *       correspond channels:      SUB           RF  =  Right Front
  *       *                         LS       RS       LS  =  Left Surround
      *                                  ES           RS  =  Right Surround
                                                      SUB =  Subwoofer

EXAMPLE (3):
A micro-space-invaders game layout might look like this:
                   +--------------------+
                   | x x x x x       000|
                   |  /\ /\ ! /\ /\  _!!|
                   +--------------------+
where the five on screen CCs are marked x/\!, representing an invader (repeated
five times), the "/\" is a pair of CCs that has the shape of a shield, while "!"
is a CC in the shape of a laser. The invaders use ten different CCs (five with
legs in and 5 with legs out) that load in sequence as the waves of aliens descend
ever closer. Arduino C++ code for constructing the above screen (I have added
comments about the various items in this code fragment):

startPos = 0; // The position (0-39) on the screen where the String will be written.
lcdString = "a a a a          000|" // No ';' "|" marks the end of a font-formatted String
            "I I I I             "  // No ';'
            " LR LR 3 LR LR   _11|" // No ';' Another "|" because this is a 2-row screen-write.
            " ss ss P ss ss   PPP"; // Yes ';' marks end of C++ statement.
LCDfontManager(); // This writes the first LCD row.
setupRow2(); // Housekeeping function needed...
LCDfontManager(); // ahead of writing the second row of the 2-row String.


EXAMPLE (4):
A screen example that might look at home in a home theater audio processor:
                   +--------------------+
                   |LF:O   CR:ox  LR:O  |
                   |LS:ox  SB:O   RS:ox |
                   +--------------------+
where the LF, CR, LR, LS, SB, and RS, use a mixture of lowercase ('c' and 's')
and small-caps CCs (L,F,R,B) to render a larger set of small-caps using only four
CCs. Together with speaker-shaped CCs (large and small, denoted here as 'O' and
'o'), and a regular lowercase 'x' indicating channels with small speakers that
cross-over the low frequencies to the subwoofer channel, a readout that helps to
simplify the speaker and basemanagement setup for a home theater processor.
This screen uses fourteen CCs, Six of these are unique, but, as a result of 
repetition, gives the impression that there are more than that. 

                  
USING FONT PAL
==============

FONT FOMATTING OF LCD DISPLAY STRINGS:
-------------------------------------
This routine parses font-encoded Strings, N characters at a time, loading the
relevant custom characters before sending the processed String to a Hitachi
HD44780-compatible LCD module for display.

Example String (where "_" represents an ASCII space 0x20, but you would type
an actual space character):

               clearTheStage();
               startPos = 0; 
               lcdString = "Large LCD Display|"
                           "   l  BBB    l  l";
               LCDfontManager(); 

There can be up to 40 display characters before the '|' marker character,
with the corresponding font flags in the following row, which is convenient
when constructing a display String on the fly. The VU meter capability is
discussed later. In the meantime, here is a more normal string that fills
all 40 locations on a 2x20 LCD:

clearTheStage();
startPos = 0
lcdString = "This long String fills two lines exactly|"  // 2 CCs, 2 unique.
            "        l      l                       l";
LCDfontManager(); 

The number of characters after the "|" must equal the number before it. 
Counting is zero-relative, so the marker character is at position 40. Thus,
the number of fontChars before the "|" is 40, and there are 40 fontFlags
after the "|". A space in the second line indicates that the LCD's default
ROM-based characters will be used. The total number of characters in this
string is 81, of which only the first 40 get displayed (after CC substitutions).

Important Note: The Font Pal LCD font manager expects ALL font-formatted
Strings to have the second part marked by "|" and containing the fontFlags.

DISPLAYING PLAIN TEXT:
---------------------
In addition to displaying font-formatted strings, Font Pal also supports
un-font-formatted Strings, a.k.a. plain text. Or it can display a mixture
of the two. However, if plain text suffices for a particular LCD field, and
is 100% plain text (i.e., no font flags or "|" font marker character), the
text is displayed more quickly. Refer to the examples below for more guidance
on using this feature. 

MORE HINTS ON HOW TO USE FONT PAL:
------------------------------------
You can copy and paste the following examples of Font Pal into your Arduino
IDE, or you can uncomments the examples in the void loop() section of the 
main .ino file.

(a) An example of how to display a plain text String at position 25 on the
LCD, euivalent to placing the text starting at position 5 in the second row.
Note: If our intention is to add a new String to an an existing LCD 'scene',
we would omit clearTheStage().

               clearTheStage(); // This line may not be needed (see note)
               startPos = 25; // Allowable range is 0-39
               lcdString = "D2:DVD"; // Plaintext example (no font encoding)         
               LCDfontManager();

The variable named startPos is used to select the position on the LCD where
you want to display the text. Strings that are positioned so that they extend
beyond the end of row 0 of the LCD, wrap around onto row 1. Strings that extend
past 40 characters (i.e., the size of the screen) are truncated. The variable
startPos has a default value of 0, and can go as high as 39. 

(b) The best way to blank out only part of the LCD is to write a blank plain-
text String: 
               // Write six spaces to the LCD, starting at position 8:   
               startPos = 8; 
               lcdString = "      "; // <-- Note: no '|' marker. 
               LCDfontManager(); 

(c) An example of displaying a String at position 4 that has mixed font-formatted
text and plain text: 

               clearTheStage();
               startPos = 4; 
               lcdString = "LR Dolby digital|" // <-- Note the '|' marker, and no ';'
                           "LL     l   l    "; // Need the ';' here, though!
               LCDfontManager(); 


(d) To make sure that a new character string written to the LCD completely overwrites
any previous write to that field, it is a simple matter to pad an item with spaces, 
illustrated by the extra space in the following bold-formatted "ON" String. Without
this extra space, the last "F" in "OFF would still be on the LCD, giving "ONF":

               "ON |BB ";    and    "OFF|BBB";  

(e) Whenever beginning a new LCD 'scene', the best way to clear the LCD and all
CCs stored in the CGRAM,of the LCD is to 'clear the stage':

              clearTheStage(); // Do this before building a new 'scene'

On the other hand, if we just need to clear all or part of the LCD, without
clearing the relevant CCs out of CGRAM, we can write some plain-text spaces.
The following writes 8 plain-text spaces beginning at screen position 10:

               startPos = 10; 
               lcdString = "        "; // 8 plain-text spaces beginning at 10.

Note: This is simpler and takes much less time than the following alternative
approach of using a font-formated string of spaces to write over the same field:

               startPos = 10;
               lcdString = "        |" // 8 spaces beginning at 10.
                           "        ";

or:
               startPos = 10;
               lcdString = "        |        "; // 8 spaces beginning at 10.


(f) Most useful for shapes that take up two lines (e.g., the channel 'daisy'
    CC patterns defined in the fint library) is the ability to break up the
    pattern into two short Strings of equal length, rather than overwrite the
    rest of the display with spaces. These lcdStrings differ from the standard
    lcdString in the use of two "|" markers, one for each row:

               startPos = 0;
               lcdString = "232|CCC"
                           "1e1|CCC";

If you prefer, this may be written in the following alternative (equivalent)
form: 

      startPos = 0;
      lcdString = "232|" // Make sure all four Strings have the same length.
                  "CCC"
                  "1e1|"
                  "CCC";

However, to make either of these double-row examples work, you must use two
calls to LCDfontManager(). I have omitted the optional call to clearTheStage()
because if the LCD already displayed a different 'daisy', this new one would
rewrite it, leaving any other text on the LCD unchanged. It is important to 
note that the 'daisy' examples all use four unique CCs, leaving only four
for any other lcsStrings that are part of this 'scene'. 

Either:
               // Recommended for formatting double row CC graphics:
               clearTheStage();
               startPos = 0;  
               lcdString = "232|CCC" 
                           "1e1|CCC";
               LCDfontManager();
               setupRow2();
               LCDfontManager();

Or:
               // Recommended for formatting double row CC graphics:
               clearTheStage();
               startPos = 0;
               lcdString = "232|"
                           "CCC"
                           "1e1|"
                           "CCC";
               LCDfontManager();
               setupRow2();
               LCDfontManager();

Without the setupRow2() function and the second call to LCDfontManager(),
the second row ("1e1|CCC") would be ignored and not get printed.

THE FOLLOWING METHOD IS NOT RECOMMENDED FOR DOUBLE ROW CC GRAPHICS:
------------------------------------------------------------------
The method of combining both rows of characters into a single string that 
wraps onto the second row of the LCD fails to be as useful because not only
does it fail to mimic the LCD layout, but it cannot avoid overwriting other
items on the LCD. Although it bypasses the need for the setupRow2() helper
function and a second call to the LCD font manager, it leaves you with the 
need to pack out lcdString with enough spaces to push the second row characters
("1e1|CCC" in this example) past the end of the first row and onto the second
row, which (depending on the value of startPos) may write over parts of LCD
screen you wish to keep. However, unless that is your intention, the two "|"
marker format shown above largely avoids the use of space characters to 
correctly format double-row graphics, and is the preferred method.

               // The following is not recommended for double-row CC graphics:
               startPos = 0;
               lcdString = "232                 1e1|CCC                 CCC";
               LCDfontManager();


ERROR REPORTING:
---------------
Various error conditions are indicated by the appearance of the following special
characters on the LCD (which the controller displays in place of every requested,
but unavailable custom character):

   "#"   ---   CGRAM overflow (more than 8 unique custom characters)
   "@"   ---   Bad font-char or bad font-flag



GENERAL POINTS ABOUT HITACHI HD44780-BASED LCD DISPLAYS:
-------------------------------------------------------
The LCD is used write-only, with all screen editing taking place in the screenImage
RAM buffer that is copied to the LCD display by calls to the LCDfontManager() function.
A major part of any digital controller design that uses a Hitachi HD44780-based LCD display
is consideration of the font-formatting design; juggling the timing of *what* is shown on
the LCD, and *when*, given the scarcity of CGRAM locations in the HD44780. This controller
chip can display only eight unique custom characters (CCs), mitigated by the ease with which
any CC can be repeated on-screen up to the limit of 40 characters in a 20x2 LCD display. 

With due care during the design phase of a project (such as the EAD EOS TheaterMasters), the
limitation on the number of unique CCs can be made to look like many more. Esay to overlook,
but useful in this regard are the following points:

(i)   Lowercase ROM characters scouvwxz are sufficiently shape-similar to the custom smallcaps
      font in this package, that they can do the work of up to eight more CCs. 

(ii)  By carefully choosing your onscreen words and names, Font Pal can aid you in minimizing
      your use of custom characters versus the use of built-in ROM characters, especially in
      the all-important 'splash screen' that uniquely identifies a product or system: 
      
      Splash screen example 1: TheaterMaster SIGNATURE (8 unique CCs, 9 unique ROM chars)
                 clearTheStage(); startPos = 3;   
                 lcdString = "TheaterMaster|BbbbbbbBbbbbb"; // Bold and lowercase bold
                 LCDfontManager(); startPos = 25; 
                 lcdString = "SIGNATURE"; // Plain text
                 LCDfontManager(); 
                 delay(3000);
      or
                 clearTheStage(); startPos = 3; 
                 lcdString = "TheaterMaster         SIGNATURE|" 
                             "BbbbbbbBbbbbb                  "; 
                 LCDfontManager();
                 delay(3000);
                      
      Splash screen example 2: TheaterMaster Ovation (7 unique CCs, 8 unique ROM chars)
                 clearTheStage(); startPos = 3;   
                 lcdString = "TheaterMaster          Ovation|"
                             "                       Bbbbbbb";
                 LCDfontManager(); 
                 delay(3000);

      Splash screen example 3: TheaterMaster Encore (6 unique CCs, 8 unique ROM chars)
                 clearTheStage(); startPos = 3;   
                 lcdString = "TheaterMaster          Encore|"
                             "                       Bbbbbb";
                 LCDfontManager(); 
                 delay(3000);

      Splash screen example 4: 8800Pro (8 unique bignum CCs, 3 unique ROM characters)
                      clearTheStage(); startPos = 5;   
                      lcdString = "ABABABAB   |88880000   "   // Double-row Strings must
                                  "CDCDCDCDPro|88880000   ";  // be of equal length!
                      LCDfontManager(); setupRow2(); LCDfontManager();
                      delay(3000);

      Splash/Feature screen example 5: (6 unique CCs, 9 unique ROM characters)
                      clearTheStage(); startPos = 1;   
                      lcdString = "Reference CinemaTM    & Cinema 7.1TM|"
                                  "B         B     SS      B      B BSS";
                      LCDfontManager(); 
                      delay(3000);

(iii) Letters in-common between font-enhanced words come at no additional CGRAM cost.    

(iv)  Although CGRAM limits unique custom characters (CCs) to eight, they can be repeated
      up to the size of the display (a 2x20 LCD can display as many as 40 CCs).

(v)   Eight CCs is sufficient to code multi-channel vertical bargraphs that have 16 steps, 
      and horizontal bargraphs that have up to 100 steps.

(vi)  Sometimes it's feasible to design multi-character CCs in a way that hides the one-pixel
      gap between LCD characters and LCD rows (with reference to Okaya and similar 2x20 LCDs).
      Examples of this are provided in the bargraph font. 

(vii) Use the "#" and "@" error flags to help you design screen layouts free from CGRAM
      overflow and font errors.


NOTES:
During this development, I discovered that Arduino variables of type String, and built-in
functions that process them, often have a problem with bytes or characters with the value
0x00, i.e., byte(0). This is a carry-over from C (and C++'s) use of the \0 null character
(0x00) the end-of-string marker in traditional C-strings, and even though the relatively
new String type does not use such markers, my attempts to print Strings containing 0x00
sometimes had problems. Perhaps the Hitachi HD44780 designers already anticipated this 
problem, because, to their credit, they arranged for a set of shadow addresses, allowing
us to access to those same eight CC bitmap locations in the chip's CGRAM at 0x00-0x07 or
0x08-0x0f (BTW, that OR is an inclusive-OR). Therefore, where it matters in this program,
we number the HD44780 LCD controller's CGRAM locations from 0x08 to 0x0f (8 to 15).
However, because no such duplicate addressing exists for the various arrays, all of which
start at index 0, make sure to add 8 to all zero-relative custom character (CC) CGRAM
pointer values when assembling a 'scene' (a name used here to describe an LCD display
that is made up of two or more small Strings that are assembled into the screenImage
String being written to the LCD.

Example of Arduino C++ code for displaying two aligned 20-character Strings:

             clearTheStage();
             startPos = 0;
             lcdString = "Hi-Sccrers:  AJR SCR|" // Begins at startPos on the LCD.
                         "bb bbObbbb          "
                         "             AR  JSH|" // Begins at startPos + 20 on LCD.
                         "                    ";
             LCDfontManager();
             setupRow2();
             LCDfontManager();

Note that all such two-row strings have two '|' font markers. While the same
display may be constructed using a single String, the lack of inherent screen
layout makes verification more difficult, despite fewer function calls: 

               clearTheStage();
               startPos = 0;
               lcdString = "Hi-Scorers:  AJR SCR             AR  JSH|"
                           "bB bBOBBBB                              ";
               LCDfontManager();

Behind the scenes, after custom font processing, lcdString overwrites screenImage,
which, in turn, is written to the LCD. Whichever way you edit your Strings, the
resulting LCD screen layout will be similar to the following:

                   +--------------------+
                   |Hi-Scorers:  AJR SCR|
                   |             AR  JSH|
                   +--------------------+ 
*/
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 
/*
     -----A SUMMARY OF FONT PAL FORMATS AND COMMANDS-----
     -----A SUMMARY OF FONT PAL FORMATS AND COMMANDS-----
     -----A SUMMARY OF FONT PAL FORMATS AND COMMANDS-----

1) Plain text (1-40 chars) starting at position 0-39. Example:
startPos = 5; lcdString = "Hello World!"; LCDfontManager();

2) Font-formatted text and CCs (1-40 chars) starting at position 0-39.
Formatted text must end with '|'. The fontChar and fontFlag parts of
lcdString must match. Using two lines as shown allows you to visually
check the font formatting. Example:

               startPos = 24; lcdString = "Nice day for it!|"
                                          "b      L        ";
               LCDfontManager();

3) Optional command to clear font formatting prior to overwriting
the LCD with a new font-formatted string. This is useful when creating
an LCD 'scene' from multiple lcdStrings:

               clearTheStage();  

4) Font-formatting of double-height aligned blocks of text and CCs.
This is particularly useful when creating 'big nums' or other CC groupings.
Every double-height lcdString must have exactly two '|' marker characters.
The string lengths must match. Due to the HD44780 limit of eight CCs,
repetition of CCs is your friend, because, for example, a single CC can
be repeated 40 times to fill the display, at no cost. Bar graphs, in 
particular can take full advantage of this. Example:

               clearTheStage(); 
               startPos = 0;
               lcdString = "232|"
                           "CCC"
                           "1e1|"
                           "CCC"; 
               LCDfontManager();
               setupRow2();
               LCDfontManager();  

5) If your LCD is the STN (super-twisted nematic) transflective type, and 
your display data changes several times a second, you may need to add delays
to ensure full visibility. Although providing excellent sunlight visibility
at low cost, STN LCDs can take 150 ms to go from clear to full black, and
300 ms to fade to clear. To make your display easy to read under all light
conditions, you may need to wait out these times by adding delays of 100 ms
or more. Experimentation is your best guide. 

6) Dynamic displays such as bar graphs require lcdString to be constructed
on the fly,. e.g. using C++ built-in String functions.

// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

LCDfontManager() - Liquid Crstal Display Font Manager Implemtation Notes

(1) Scan lcdString to detect '|' markers... 
  
(2a) Perform an interleave merge of fontChar & fontFlag sections of lcdString, excluding the '|' marker,
e.g.,  "LR DoobyoD|ll BBB LBB" --> "LlRl  DBoBoBb yLoBDB"), and also processes the top row of double-row
symbols (e.g., "Test|B   CASE|BBBB|" --> "TBe s t "):

(3) We are nearly ready to copy the merged lcdString into screenImage starting at startPos, but first
we need to clean up the CC_id_table, removing any entries that will be overwritten by the current
lcdString, necessary to maximize the tiny eight CC CGRAM resource. 

DETAILS: Parse the even characters in merged_screenImage for CC codes that will be overwritten by a new
merged lcdString, beginning at startPos, and ending at startPos + lcdString.length() -1. As we proceed to
discover these CCs in the relevant section of screenImage, we will delete them from the CC_id_table. And 
because CCs in screenImage are codes in the range 0x08-0x0f, that is the range we will look for. The tricky
part of this parsing is to take into account that just like merged_lcsString, screenImage also uses merged
format of screenImage, wherein the relevent fontchars are at even locations, 0, 2, 4, 6, ... We can obtain 
even addresses by using a simple arithmetic left-shift (<< 1). However, we don't do this for CC_id_table
(a table that contains a list of the CCs loaded into CGRAM of the LCD's controller chip). Given that C++
arrays are 0-relative, the CC_id_table addressing is 0x00 to 0x07, but because 0x00 (\nul) has special
use in C and C++ for marking the end of c-char character strings, we cannot cannot use 0x00 as a CC code
value in String function and char processing. Luckily, the HD44780 LCD controller provides a set of shadow
addresses for the CGRAM: 0x08-0x0f, obtained by adding 8 to the 0x00-0x07 addresses. This avoids having to
treat the 0x00 CC code value as a special case. However, since as mentioned above, C and C++ arrays start
at 0 and proceed up from there, this program makes use of both ranges, 0x00-0x07 and 0x08-0x0f as and when
necessary. Typcal contents of CC_id_table may look like this: 

     CC_id_table[0] = "2C"; // ==> CC code = 0x08 = 0 + 8 = 8      Table Format: "2C" = fonchar,fontflag   
     CC_id_table[1] = "3C"; // ==> CC code = 0x09 = 1 + 8 = 9
     CC_id_table[2] = "1C"; // ==> CC code = 0x0a = 2 + 8 = 10
     CC_id_table[3] = "eC"; // ==> CC code = 0x0b = 3 + 8 = 11
     CC_id_table[4] = "dL"; // ==> CC code = 0x0c = 4 + 8 = 12
     CC_id_table[5] = "tL"; // ==> CC code = 0x0d = 5 + 8 = 13
     CC_id_table[6] = "gl"; // ==> CC code = 0x0e = 6 + 8 = 14     lowercase g with true descender.
     CC_id_table[7] = "sL"; // ==> CC code = 0x0f = 7 + 8 = 15

If our parse finds, e.g., a CC code of 6 in the merged_screenImage overwrite section, this is a reference
to the gl entry in the table, currently stored at CC_id_table[6]. We delete it from the table with  ???????????????????



Then, after lcdString is overwritten on at its startPos in the merged_screenImage, any new CCs in lcdString are
saved into free locations in CC_id_table. A condition called CGRAM overflow can occur if there are more than 
eight unique custom characters (CCs). This cindition is signalled with one or more "#" flags on the LCD.   

%%%%% Continuing, we now parse the to-be-overwritten section of merged_screenImage, and delete any priorCCs found:
       (Prior CCs have a fontChar between 0x08 and 0x0f, with fontFlag == " ")       

(4) Now parse screenImage for CC flags, load the CC bitmaps to the LCD chip, and place CGRAM_ptrs in //    screenImage, in place of fontChars. Use CC_id_table to keep track of the limit of only 8 CCs!
%%%%%%%%%%%%%% BEGIN PROCESSING THE MERGED LCDSTRING INTO SCREENIMAGE AT STARTPOS, INSERTING CCs AND ERROR FLAGS AS NEEDED...

(5) Now print all EVEN characters, 0,2,4,6,..., in screen_image to the LCD display...

Note: STN transflective LCDs typically transition relatively slowly (150 ms transparent to 90% black, and 300 ms black
to 90% transparent), requiring a delay of about 200 ms before the next LCD screen write, a time best determined by experiment. 

A List of Font Letter Names and Font Characters:
===============================================

Font    Font Name         Characters                               Size
 G  =   bargraphfont      0abcLR!87654321                          15 
 0  =   big0font          ABCD                                      4 
 1  =   big1font          ABCD                                      4   AB A'B' 
 2  =   big2font          ABCD                                      4   CD C'D' 
 3  =   big3font          ABCD                                      4   A maximum of two bignums digits! 
 4  =   big4font          ABCD                                      4 
 5  =   big5font          ABCD                                      4 
 6  =   big6font          ABCD                                      4 
 7  =   big7font          ABCD                                      4 
 8  =   big8font          ABCD                                      4 
 9  =   big9font          ABCD                                      4 
 X  =   symbolfont        DSTMH><^vuds_1234KkLQnNbCF               26 
 b  =   Boldfont          ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789     36 
 B  =   boldlcfont        abcdefghijklmnopqrstuvwxyz               26 
 L  =   lowercasefont     gjpqym                                    6 
 c  =   smallcapsfont     ABDEFGHIJKLMNPQRTUY                      19 
 F  =   smlfrequfont      kHz                                       3 
 V  =   smlrevcapsfont    BCcFLSsTR                                 9 
 v  =   revcapsfont       ABCDEFGHIKLOPRSTUVXYZ0123456789=:        33 
 C  =   channelfont       241dca5b3ef6xyzuvwjkl                    21 
 l  =   logofont          LRdtsHDC0123456                          15 
 O  =   outvaderfont      abcd                                      4 
 I  =   invaderfont       abcd                                      4 
 P  =   laserfont         12345_                                    6 
 E  =   kapowfont         12345                                     5 
 s  =   shieldfont        LR                                        2 
 U  =   UFOfont           ABCDIJKLQRSTabcdijklqrst                 24

*/
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

printFonts.ino

/*------------------------------------------------------------------------*/
/* myFontsPrinter() - PRINT ALL FONT TABLES (USE FOR DEBUG PURPOSES ONLY) */
/*------------------------------------------------------------------------*/
void myFontsPrinter() {  
Serial.begin(9600);
Serial.print("\n");Serial.print("----- FONT TABLES -----\n");
  for (const CustomFont &font : fonts) {  // Use a foreach statement for a compact printout of the structs.
    Serial.print("\nFont Flag: ");
    Serial.print(font.fontFlag);
    //Serial.print("\nFont Name: ");
    //Serial.print(font.fontName);
    Serial.print("\nFont Characters: ");
    Serial.print(font.fontChars);
    Serial.print("\nFont Bitmaps for Each Custom Character:");
    for (uint8_t i = 0; i < font.fontChars.length(); i++) {
      Serial.print("\nCharacter '");
      Serial.print(font.fontChars[i]);
      Serial.print("':");
      for (int j = 0; j < 8; j++) {
        Serial.print("0x");
        if (font.fontBits[i][j] < 0x10) Serial.print("0");
        Serial.print(font.fontBits[i][j], HEX);
        Serial.print(" ");
      }
      Serial.print("\n");
    }
    Serial.print("\n");
  }
  return;
}

Possible problem in the LCDfontManager() function.

This code removes the custom character entries for characters that are being overwritten, but does not check if those custom characters are still in use elsewhere on the display.


  // ==[3]====================
  // TO MAXIMIZE THE EIGHT-CC CGRAM RESOURCE, WE NOW NEED TO CLEAN UP THE CC_id_table,
  // removing any entries that will be overwritten by CCs in the new lcdString.
  int16_t mystartPos;
  mystartPos = startPos << 1;
  uint8_t scrn_ptr = mystartPos;
  while (scrn_ptr < (mystartPos + merged_lcdString.length())) {
    uint8_t fontByte = screenImage.charAt(scrn_ptr);
    if ((fontByte >= 0x08) && (fontByte <= 0x0f)) CC_id_table[fontByte - 8] = "";
    scrn_ptr += 2;
  }

Executing the following code will corrupt the "Theater" portion of "TheaterMaster":

  clearTheStage(); // Clear LCD image and CCs prior to 'building an LCD Scene'
  startPos = 0;
  lcdString = "   TheaterMaster         SIGNATURE      |"
              "   bBBBBBBbBBBBB                        ";
  LCDfontManager();
  delay(3000);
  
  startPos = 10;
  lcdString = "Master|"
              "bBBBBB";
  LCDfontManager();
  delay(10000);

David: Thank you for your insight. None of my test cases revealed this one. But since your test represents something that any user would expect to work, the logic in section ==[3] ===== must be improved to address it. As it stands, this section cleans up the CC_id_table, removing any entries that will be overwritten by CCs in the new lcdString. I'll examine whether it will be sufficient to add the additional condition: BUT BUT NOT FOR CCs THAT EXIST ELSEWHERE IN screenImage.

It is almost a theorem that if we write any characters (but especially CCs) to the LCD and rewrite any part with the same characters, the display should remain the same.

This is an egregious error caused by a lapse of logic. I appreciate that you reported it to the forum. This evening, I rechecked all other tests in the last part of the main .ino file. All passed, suggesting that the alpha 2.00 code I posted on the forum is clean.

On some related news, I have tested Font Pal 2.00 alpha on UNO R4 with a 4x20 I2C LCD (I removed the ATmega 'fontmaps-in-flash' patches from Font Pal beforehand), and obtained these numbers: program 67168 bytes, data 13056 bytes (from which we can infer that RA4M1 code is stored less densely). I'm very new to Renesas, so I will need further study to understand if those numbers are typical. It is probably a different C compiler. Does anyone know if the Renesas supports the old ARM byte code known for its memory efficiency?

Subject to further testing, I think the following should take care of the problem:

// ==[3]==================== 
  // TO MAXIMIZE THE EIGHT-CC CGRAM RESOURCE, WE NOW NEED TO CLEAN UP THE CC_id_table,
  // removing any entries that will be overwritten by CCs in the new lcdString. BUT DON'T EXIST ELSEWHERE IN screenImage... 
  int16_t mystartPos; 
  mystartPos = startPos << 1;
  uint8_t scrn_ptr = mystartPos;
  while (scrn_ptr < (mystartPos + merged_lcdString.length())) {
    uint8_t fontByte = screenImage.charAt(scrn_ptr); 
//Change the following line:    
if ((fontByte >= 0x08) && (fontByte <= 0x0f) && (screenImage.indexOf(fontByte) == -1)) CC_id_table[fontByte - 8] = "";
    scrn_ptr += 2; 
  }
  screenImage = screenImage.substring(0, mystartPos) + merged_lcdString + screenImage.substring((mystartPos) + merged_lcdString.length());

The change is one line only, marked with a comment, above.

The additional if clause, && (screenImage.indexOf(fontByte) == -1), to fix david_2018's bug, has broken the Space Invaders demo included in the main file of Font Pal. The fault was CGRAM overflow ('#'). Therefore, standby for another correction.

A similar thing happened to the home theater receiver LCD scene-building test (lines 1000-1095), causing some of the 'daisy' CCs to overflow the CGRAM (indicated by '#'). In this case, I solved the problem (without consequences) by adding clearTheStage(); each time, I simulated a change of signal source).

david_2018: FYI, here's what was going on with your CC test:
The string "TheaterMaster" (bold) uses exactly 8 CCs ('T', 'h', 'e', 'a', 't', 'r', 'M', 's').
These are allocated to the CC_id_table in the order they are found in lcdString, i.e.,
0 'T', 1 'h', 2 'e', 3 'a', 4 't', 5 'r', 6 'M', 7 's'.

With your test (writing bold Master over bold TheaterMaster starting at the 'M'), clears the CC_id_table, as follows:
0 'T', 1 'h', 2 "", 3 "", 4 "", 5 "", 6 "", 7 "".

Writing bold Master at startPos 10 updates the table, in order, giving:
0 'T', 1 'h', 2 ('M'), 3 ('a'), 4 ('s'), 5 ('t'), 6 ('e'), 7 ('r') (where the () indicate the new entries).
Since the CC indices in CC_id_table, for the bold "Theater" part stay the same, some characters will change because the indices 2 through 7 look up a bitmap in the HD44780 that is different from the one placed there the first time around.

The bold "Theater" in screenImage and the LCD, because it is completely made of CCs, looks like a sequence of numbers between 0x08 - 0xff (which have 8 subtracted to give the actual CC_id_table indices (this complication avoids a valid CGRAM address, 0x00, from acting as a C end-of-string NUL).

table indices:  0x00, 0x01, 0x02, 0x03, 0x04, 0x02, 0x05, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07
LCD CC:         0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0a, 0x0d, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
LCD:             T     h     M     a     s     M     t     M     a     s     t     e      r

Changing the if statement at line 40 to
if ((fontByte >= 0x08) && (fontByte <= 0x0f) && (screenImage.indexOf(fontByte) == -1)) CC_id_table[fontByte - 8] = ""; 
 
We observe the correct result:
LCD:            T     h     e     a     t     e     r     M     a     s     t     e      r

  while (scrn_ptr < (mystartPos + merged_lcdString.length())) {
    uint8_t fontByte = screenImage.charAt(scrn_ptr); 
    //Change the following line:    
    if ((fontByte >= 0x08) && (fontByte <= 0x0f) && (screenImage.indexOf(fontByte) == -1))
      CC_id_table[fontByte - 8] = ""; 

screenImage.indexOf(fontByte) will never be -1, fontByte will always exist at the location you just read it from. What I've done is replace the character in screenImage with a space before searching to see if it exists elsewhere. Not sure how easy that is to do with String, I prefer working with char arrays.

David: Sorry, I did not see your reply. The index will be -1 whenever screenImage does not have the same fontByte anywhere. The reason is that lcdString (which contains the fontByte in question) has not yet overwritten screenImage.

If indexOf returns any value other than -1, we leave the CC_id_table entry alone, which protects the "Theater" part of "TheaterMaster" in the example.

The logic required to control the HD44780 is challenging; sometimes I have to walk away from it, and try again later.

Try patching the if statement and let me know how it went.

Here is your test, slightly modified so that I can tell that the second write actually happened:

// David_2018's CC text
  clearTheStage(); // Prepare the font manager for a new 'LCD scene'.
  startPos = 0;
  lcdString = "   TheaterMaster         SIGNATURE      |"
              "   bBBBBBBbBBBBB                        ";
  LCDfontManager();
  delay(3000);
  
  startPos = 10;
  lcdString = "Master OK|"    // The 'OK' is to let you know that something happened.
              "bBBBBB   ";
  LCDfontManager();
  delay(1000);

//while(true){}; 

But there is still a gap in the program logic, as revealed by the Space Invaders test, which gets '#' flags. Your writing a space to screenImage sounds like an early part of my strategy, but I seem to have left it out of this version. I had planned on clearing out the part of screenImage that would get overwritten, which sounds a bit like your workaround.

I will take another look at this tomorrow. Either way, I think this thing is close to fully working. Perhaps we can devise some more tests to test not just edge cases but more combinations of edge cases. In an earlier version, I had a problem that seemed to be affected by adding a space here or there. Version 1.20 Alpha is the result of me diving in while everything was fresh, and doing a clean rewrite (of LCDfontManager.ino).

The fontByte in question was read from screenImage, not lcdString, so it will always be found when searching screenImage for fontByte. Overwriting the character with a space will remove it from screenImage and allow you to check if fontByte exists anywhere else in screenImage. Overwriting the character will not cause any other effects in the code, since the character location is going to be overwritten by lcdString.

  // ==[3]====================
  // TO MAXIMIZE THE EIGHT-CC CGRAM RESOURCE, WE NOW NEED TO CLEAN UP THE CC_id_table,
  // removing any entries that will be overwritten by CCs in the new lcdString.
  int16_t mystartPos;
  mystartPos = startPos << 1;
  uint8_t scrn_ptr = mystartPos;
  while (scrn_ptr < (mystartPos + merged_lcdString.length())) {
    uint8_t fontByte = screenImage.charAt(scrn_ptr);
    //overwrite the character before checking if it exists elsewhere in screenImage
    screenImage.setCharAt(scrn_ptr, ' ');
    if ((fontByte >= 0x08) && (fontByte <= 0x0f) && (screenImage.indexOf(fontByte) == -1)) CC_id_table[fontByte - 8] = "";
    scrn_ptr += 2;
  }

David that one space overwrite was the icing on the cake. Thank you for your input.
I took some time this morning to run Font Pal through every test I have, and it passed 100%.

I will tidy up the help file and some of the comments and post this UNO R4 version when I am done. I'll also post the Mega version, which uses PROGMEM to store the font bitmaps in Flash.

I am working on a multi-format version (adding 2x16 and 4x20). For the 4x20, I'm trying to decide whether to extend the existing format to another two rows (characters numbered 0-79) or more like two separate 2x20 displays (of course, with the limitation that there are still only 8 CCs).

Font bitmaps stored in flash are also possible for the UNO... we'll have to wait and see about that one.

I'll begin a version/change table in the ReadMe file.

That's all for now, but before I sign off, thank you for helping make my 'jump in at the deep end' introduction to Arduino a great experience.

Why don't you add some #ifdef lines and conditionally compile based on whatever board is being used?

1 Like

There really should not be a need to make any changes for the code to compile on the Renesas RA4M1 (as used in the UNO R4), if you are using the Arduino IDE. The boards package for the Renesas based boards will handle the special PROGMEM code properly, basically ignoring it. This is done with most if not all of the board packages used with the Arduino IDE, so that code is compatible across the various boards.

There is also no reason to remove the const keyword when moving from the Mega to the Renesas RA4M1, leaving the data as const lets the compiler know the data can be stored in flash memory instead of ram.

< edit >
I tried your code on both a Mega and an UNO R4 WiFi, same code compiles and runs on both. Only change that I've made is to use LiquidCrystal_I2C instead of LiquidCrystal because of the LCD display I'm using.

1 Like

Yeah this seems very odd.
Would seem better if B could be for upper case and b for lower case.

Do you have github project for this set up yet?

Odd indeed. I, like you, would like to have font mnemonics that are, where possible, 'mnemonic'. I am in the middle of addressing this. There should be no behavioral tendency quirks that depend on font naming. I discovered early that the worst offenders were font-enhanced characters matching the font name. E.g., "B" in the B-font (bold caps) was one. So, on a whim, I changed the font mnemonics to characters not in the affected fonts. This is how I arrived at B for bold-lowercase and (seemingly illogically) b for bold-uppercase. This worked 100% with at least 100 different lcdString tests until your comment, which provided enough impetus to find out what's going on or to provably avoid the problem.

I soon discovered several more complex contexts and character combinations (probably even less likely to be used by anyone than my original misbehaving cases). However, today, after rewriting the String to char conversion, I seem to have shown that these problems (which typically give @ flags where there should be none and sometimes cause a character adjacent to the bogus @ to go blank) are caused by that sticky C conversion-thing. I use the word bogus for these @ flags because they do not appear because I mistakenly used the wrong font mnemonic or character without a bit map.

The weird thing about this @ error, if I can call it that, is that by swapping font mnemonics or swapping the Bs or bs for Cs or cs, I can make it come and go, which supports the conversion problem theory.

Here are some notes from yesterday, which I don't have time to re-edit right now, but which will still give some idea of the behaviour. Based on what I'm seeing with the new conversion code today, I'm confident that the @ problem will be fixed soon.

And regarding Github, yes, I will probably do that.

Here are the notes (not guaranteed to be up-to-date):

Here's a snip from my CustomFont fonts[] array:
// Order of fields: fontFlag, fontBits,  fontChars
CustomFont font12 = { 'b', Boldfont, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" }; // 36
CustomFont font13 = { 'B', boldlcfont, "abcdefghijklmnopqrstuvwxyz" }; // 26

Note: for this discussion, ASCII space 0x20 is represented by _ ) 
Notice that in both font12 and font13, the fontFlag byte does not exist in the fontChars String.
When I specify the following string 
"B_AB_BAB|"
"b_b__bbb";
the result on the LCD is correct: B_AB_BAB (showing as bold B, _, bold A, B_, bold BAB)

If I swap the 'b' and 'B' fontFlags, to the more intuitive B-for-boldcaps, b-for-boldlowercase, i.e.,
"B_AB_BAB|"
"B_B__BBB";
the result is incorrect: B_@__B@@ (where the two Bs show as bold).

Playing around with variations, I have found that there is nothing peculiar about B or b. The pattern seems to be that if the fontFlag character is the same as one of the fontChars, when that fontChar is displayed, in combination with spaces and/or some other bold or plain-text char, that the getBitmap() function returns -1 for those positions, also messing up the printing of the adjacent plain-text.

Here are some of the patterns that fail (I have formatted them onto one line for brevity):
"b|b"         @_..._ (20 characters)
"b_|b_"       @_...._
"_b_|_b_"     _@_..._

These @ are bogus in the sense that I did not request a character that's not in the CC bitmaps. 
To get a "genuine" @ we can, e.g., do this:
"b|B"         bold b            @   (correct, because there is no b in the B font) or
"B|b"         @   (correct, because there is no B in the b font) 
"B|B"         @   (bug)
"b|b"         b   (bug)

Are you at least using version control?
git is one of if not the the best, and if using git, github is an obvious fit.
Also, if using github you can make your project an Arduino library that users could install from the IDE GUI using the library manager.

I've been doing projects for 40 yrs and have ALWAYS used version control.
I can't imagine doing any project without it. Modern tools like git are so much more powerful than what was available decades ago.

--- bill

1 Like

This is a rather hard one to find.

"B_AB_BAB|"
"B_B__BBB";

merged this will be:
"BB__ABB___BBABBB"

Fine so far, but when you look up the first custom character, Bold B, you do a global replace in the screenImage, replacing all occurrences of "BB" with the custom character and a space.

"BB__ABB___BBABBB"
 ^^   ^^   ^^ ^^ 
"x___Ax____x_Ax_B" resulting screenImage, x represents the custom character

This corrupts subsequent character/font pairs in screenImage, resulting in the erroneous output.

Thanks, David! Great detective work!! I think you're onto something. The replacements risk being invalid unless they are on an even double-byte boundary. This is probably why we observe the '@' problem, particularly when there is a plain-text character after the current CC_id (e.g., "B|B" merging to "BB", which should be replaced only if the pair is meaningful... the even-pair thing), and explains why the next character can be blanked out (and why sometimes another plain-text character can clear the effect during further traversals of the main loop.) I noticed that the CC_id pattern that accompanied this problem looked like it had the fontChar and fontFlag halves swapped, but the resulting pattern was not exactly a swap (e.g., 0x41, 0x20, rather than something like a correct CC pattern, such as 0x01 0x20), but was unable to see the true cause.

---To-Do List---
(1) I will consolidate all w.i.p. into a new upload to the Arduino forum, including a more refined/robust font bitmap lookup for LCDfontManager.ino lines 51-78 (part ---[4]-------------); I will (possibly with help) specify a method to ensure that all CC replacements in screenImage operate on even byte boundaries (to avoid the 'bogus @' problem (this may need a custom version of the Arduino C++ .myString.replace() method); I will also provide some new test examples and an updated ReadMe file which, among other things, will describe how some of the Font Pal text input formats are more flexible than as currently described.

(2) Once Font Pal is free of the odd byte boundary problem, I will revert the font letter names to their more easily remembered previous forms (e.g., B for bold caps, b for bold lowercase).

(3) Set up a GitHub project for Font Pal (modern version control!)

(4) Future work wish list:
4-line version with the ability to have one 2x4 or 2x3 vertically-oriented composite CC (in addition to the current 2-line CCs).
#ifdefs for: LCD format (1x20, 2x20, 1x16, 2x16, 4x20);
Processor dependencies such as font bitmaps in flash.

Here is the latest version of LCDfontManager:

/*-------------------------------------------------------*/
/* LCDfontManager() - Liquid Crstal Display Font Manager */
/*-------------------------------------------------------*/
void LCDfontManager() {
  //Serial.begin(9600);
  bool fontEncoded = true; 
  String merged_lcdString= "";
  char CC_char_id[3] = {"  "};

  // BEGIN FONT PROCESSING...
  // ==[1]==================== 
  // SCAN lcdString TO DETECT PRESENCE OF '|' FONT-ENCODING MARKERS... 
  int8_t marker_idx = lcdString.indexOf('|');
  if (marker_idx == -1) fontEncoded = false; // If lcdString is plain text, print it more-or-less directly...
  
  // ==[2]==================== 
  // PERFORM AN INTERLEAVE MERGE OF fontChar & fontFlag SECTIONS OF lcdString, EXCLUDING THE '|' MARKER.
  // (e.g.,  "LR DoobyoD| ll bBB LBb" --> "LlRl  DboBoBb yLoBDb"). This also processes the top row of
  // double-row symbols (e.g., "Test|b   CASE|bbbb|" --> "Tbe s t "):
  // %%%%% The setupRow2() function allows the second row of a double-row
  //       symbol to be processed by LCDfontManager as a pseudo-first row.
  //       The font-encoding test cases give several examples of this.
  if (fontEncoded == true) {
    merged_lcdString = "";
    for (uint8_t i = 0; i < marker_idx; i++) {
      merged_lcdString += lcdString.charAt(i);
      merged_lcdString += lcdString.charAt(marker_idx + 1 + i);
    } 
  } 
  else { // if lcdString isn't font-encoded, treat as plain text...
    merged_lcdString = "";
    for (uint8_t i = 0; i < lcdString.length(); i++) merged_lcdString += (lcdString.charAt(i) + String(" "));
    in_CC_id_table = false;
  }

  // ==[3]==================== 
  // TO MAXIMIZE THE EIGHT-CC CGRAM RESOURCE, WE NOW NEED TO CLEAN UP THE CC_id_table, REMOVING ANY TABLE
  // ENTRIES THAT WILL BE OVERWRITTEN BY CCs IN THE NEW lcdString, BUT DON'T EXIST ELSEWHERE IN screenImage... 
  int16_t mystartPos; 
  mystartPos = startPos << 1;
  uint8_t scrn_ptr = mystartPos;
  while (scrn_ptr < (mystartPos + merged_lcdString.length())) {
    uint8_t fontByte = screenImage.charAt(scrn_ptr);
    // Overwrite the character before checking if it exists elsewhere in screenImage:
    screenImage.setCharAt(scrn_ptr, ' '); // (david_2018 patch 9/26/2024) 
    if ((fontByte >= 0x08) && (fontByte <= 0x0f) && (screenImage.indexOf(fontByte) == -1)) CC_id_table[fontByte - 8] = ""; 
    scrn_ptr += 2; 
  }
  screenImage = screenImage.substring(0, mystartPos) + merged_lcdString + screenImage.substring((mystartPos) + merged_lcdString.length());
  
  // ==[4]==================== 
  // BEGIN A LONG LOOP, PROCESSING THE MERGED LCDSTRING INTO SCREENIMAGE AT STARTPOS, LOADING CC 
  // BITMAPS INTO THE CGRAM, FLAGGING FONT ERRORS AND ANY CGRAM OVERFLOW (@ and # errors, respectively).
  // Parse screenImage for CC flags, loading the CC bitmaps to the LCD chip, also replacing fontChars
  // in screenImage with CGRAM_ptrs. Processed CCs have the fontChar part replaced with a byte between 0x08
  // and 0x0f, and the fontFlag part with " ". Subtracting 8 from the 0x08-0x0f byte gives the 0-7 location
  // of the CC in the CC_id_table. This offset of 8 avoids problems caused by ASCII null characters (0x00),
  // related to the use of \0 as an end-of-string marker in C and C++.
   
  scrn_ptr = startPos; // Lines 60-71 rewritten. Also see comment on line 79.
  while (scrn_ptr < 40) {
    uint16_t even_scrn_ptr = scrn_ptr << 1;
    String current_CC_id = screenImage.substring(even_scrn_ptr,even_scrn_ptr+2);
    current_CC_id.toCharArray(CC_char_id,3);
    const char fontChar = CC_char_id[0];
    const char fontFlag = CC_char_id[1];

    // %%%%% If the current fontChar has a non-blank fontFlag, we assume that it must be an unprocessed CC...
    //       (There are two types of CCs: Those that are in the cc_id_table and those that are not.)
    if (CC_char_id[1] != ' ') {
      uint8_t* gBitmap = getBitmap(fontChar, fontFlag); //revised
      if (gBitmap) {
        // Type (A) --- Those that already have a bitmap in CGRAM and are in the CC_id_table: 
        // For these we replace all instances of it in screenImage with the index of the current_CC_id in the CC_id_table (0x08-0x0f + " ")...
        in_CC_id_table = false; // initial assumption is that current_CC_id is not in CC_id_table
        for (CGRAM_ptr = 0; CGRAM_ptr < 8; CGRAM_ptr++)
        if (CC_id_table[CGRAM_ptr] == current_CC_id) {  // E.g., if CC_id_table[0-7] == "ql", then...
          in_CC_id_table = true; // ...set in_CC_id_table to true, and replace all instances of current_CC_id: 
          screenImage.replace(current_CC_id, String(char(CGRAM_ptr + 0x08)) + String(" ")); // We need to rewrite replace to use only even byte boundaries!
        } 
             
        // Type (B) --- Those that have no bitmap and are not in the CC_id_table: 
        // IF the current_CC_id is not already in the CC_id_table, save it to a free location in the table, and then replace
        // all instances of it in screenImage with the index of the current_CC_id in the CC_id_table (0x08-0x0f + " " )...   
        if (in_CC_id_table == false) { 
          // Locate 1st free location in CC_id_table:  
          CGRAM_ptr = 0; while ((CGRAM_ptr < 8) && (CC_id_table[CGRAM_ptr] != "")) CGRAM_ptr++; 
          
          // If CC_id_table has a free location, save the current_CC_id there,
          // load the bitmap into CGRAM, and replace all instances in screenImage with (CGRAM_ptr + 0x08) + " ":
          if (CGRAM_ptr < 8) {
            CC_id_table[CGRAM_ptr] = current_CC_id;   // CC_id format is, e.g., "ql"
            lcd.createChar((CGRAM_ptr + 0x08), gBitmap); // Load bitmap into the LCD chip's CGRAM
            in_CC_id_table = true; // Set in == true, and then replace all instances of current_CC_id in...
            screenImage.replace(current_CC_id, String(char(CGRAM_ptr + 0x08)) + String(" ")); // ...screenImage with (CGRAM_ptr + 0x08) + " " 
          }

          // If the CC_id_table is full (due to all eight CCGRAM locations being in use),
          // signal CGRAM overflow by replacing all instances of the current_CC_id in screenImage with "# ": 
          if (CGRAM_ptr == 8) screenImage.replace(current_CC_id, String("# ")); 
        }  
      } 

       // IF the current bitmap is not found (due to the char/font combination being missing or incorrect), 
       // signal a Char/Font Error by replacing all instances of the current_CC_id in screenImage with "@ ":  
      else screenImage.replace(current_CC_id, String("@ "));
    }
    // %%%%% CC_ids ignored by the above font processing include the following:
    //       Plain-text characters in font-formatted Strings, e.g., "a ";
    //       Characters in plain-text Strings e.g., "Plain Text!";
    //       Processed CCs, e.g. String(char(0x0a) + String(" "); 
    //       The "@ " (bad char/font), and "# " (CGRAM overflow) error flags.
    // 
    scrn_ptr += 1; // Loop back to beginning to process the next CC_id in screenImage...   
  } 

  // ==[5]==================== 
  // FINALLY, PRINT ALL EVEN CHARACTERS IN screenImage 0,2,4,6,..., TO THE LCD:
  lcd.setCursor(0,0);
  String final_screenImage = "";
  for (scrn_ptr = 0; scrn_ptr < screenImage.length(); scrn_ptr += 2) final_screenImage = final_screenImage + screenImage.charAt(scrn_ptr);
  lcd.setCursor(0,0); // row 0
  lcd.print(final_screenImage.substring(0, 20));
  lcd.setCursor(0,1); // row 1
  lcd.print(final_screenImage.substring(20, 40));
  final_screenImage = "";
} 

Hint of the day:

Post your master (or main as the case may be) branch to github. It is ONLY for working releasable code. Make a second branch called dev or something to do your work on and only merge to master when the sauce is ready.

You can post your dev branch too if you want, but make sure you keep a working version on master. Your followers will thank you for that. And it will allow you to also have the current working version alongside your development version in the same repo at home while you work. If you get stuck on something that stops working, you can checkout master or diff against master and see what has changed.

1 Like

Delta_G: Thank you for the tips and advice. Can you point me towards an excellent GitHub tutorial? The reason I am asking is that after creating a project and posting the README file to GitHub, and making some adjustments in the rich editor, I realized it would be less work to do most of the edits with a more complete editor, I deleted the project I and started again. The only problem is that, perhaps due to a mode change resulting from my unfamiliarity with GitHub, I no longer have access to the rich editor (I want the README to use rich text interspersed with code snippets, but the editor I got on my second attempt is code format only, which in the normal view/preview is just a block of text minus formatting. Some things on GitHub are well signposted or obvious, and many are not, so I think I need to go to 'school' again on this one. :wink: