DataTerm Lcd Sketch
Software Sketch
Dust Automation System Dust Automation Sketch

I needed something to control my dust collector automation system and this is what happened.   It draws very little current so it runs OK on a 3M long cat-5 cable.   As Neal Okerblocm commented when he saw the system, the terminal looks like the terminal we built at ti back in the 1970s.   The hardware is smple, it's great for controlling a large system or even bread boarding.   As an example, take a look at the Dust Automation Sketch.  
I was thinking about the old VT100 monitor when I did this.   It scrolls like a VT100 (with only 4 rows of chars), and you can use the VT100 escape sequences to clear screen and postion the cursor.   The first row and collumn is zero.   Scrolling happens when printing on row 3 and print a 21st character, or issue a NL, the bottom 3 rows are scrolled up and printing starts on row 3.
When you position the cursor, the terminal does an Erase To End Of Line from the new position.   The Set Cursor Position escape sequence always has 1 character for row and 2 for column (ESC[r;ccf).   If you want to erase a line, position the cursor to the 1st column on the line.

Set Cursor Position"Esc[r;ccf" (Move cursor to row/col)
Clear Screen"Esc[2J"
/***********************************************************************
SCROLLING DATA TERMINAL (SPEAKS AT 9600b 8N1)
Nano, PCF8574 LCD DRIVER CHIP, 4002 LCD Display , PCF8574 I2C I/O EXPANDER
, 4x4 MCU BOAR MATRIX ARRAY SWITCH TACTILE KEYPAD
   KEYPAD 16 KEY             4 BOTTOM KEYS (ROW 3)    READ 8 BIITS FROM KEYPAD
  1  2  3  A  -R0    |   ****   0000     ####     DDDD   | COLS IN LO NIBBLE
  4  5  6  B  -R1    |   ****   0000     ####     DDDD   |  COLS: 01 02 04 08
  7  8  9  C  -R2    |                                   |  ROWS: 10 20 40 80
  *  0  #  D  -R3    |_   1  2  3  4  5  6  7  8  9 10  _| ROWS IN HI NIBBLE
 C0  C1 C2 C3   Conn   | nc C0 C1 C2 C3 R0 R1 R2 R3 nc |  
Nano I2C: Scl: A5 (24), Sda: A4 (23)
WHEN PRINTING TO DATA TERM USE: Serial.print,NOT println, EXTRA CR IS PROBLEM 
 ***********************************************************************/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

/************************** DEFINES ************************************/
#define EXP  0X20                                            // EXPANDER ADDR
#define LCD  0X27                                                 // LCD ADDR
#define CLRDLY   200                              // CLEAR SCREEN DELAY 200MS
#define CURDLY   10                             // CURSOR POSITION DELAY 10MS
#define COLS_HI  0X0F                                     // COLS HI, ROWS LO
#define ROWS_HI  0XF0                                     // ROWS HI, COLS LO
#define IRQ       2                                 // IRQ 0 on DIGITAL PIN 2
#define DBNC_DLY  200                                    // DEBOUNCE DELAY MS
#define ESC     '\\'                                          // BACKSLASH 27
#define NL       10                                                // NEWLINE
#define SPC     ' '
#define SPLAT   '*'
#define BS       8
#define ROWMAX   4
#define COLMAX   20
#define INSTRMAX 100
#define ESCMAX   32
#define BLTMOT 1200000   // BACK LITE TIME OUT: 20 MINS (MS) (20 x 60 x 1000)
#define NORM     0
#define ESCSEQ   1
// #define DEBUG   1                      // TURN ON WHEN DEBUGGGING FROM IDE

/************************** VARIABLES ************************************/
LiquidCrystal_I2C Lcd( LCD, 20, 4 ); //INSTANTIATE LCD, SET ADDRS, COLS, ROWS
char RCChar[9][9] = {          // CONVERT ROW/COL BITS (1,2,4,8) TO ASCII CHR
        0,0,0,0,0,0,0,0,0,
        0,'1','2',0,'3',0,0,0,'A',
        0,'4','5',0,'6',0,0,0,'B',
        0,0,0,0,0,0,0,0,0,
        0,'7','8',0,'9',0,0,0,'C',
        0,0,0,0,0,0,0,0,0,
        0,0,0,0,0,0,0,0,0,
        0,0,0,0,0,0,0,0,0,
        0,'*','0',0,'#',0,0,0,'D'};
bool KeyAvail = false;                            // SET BY IRQ, READ BY LOOP
bool LcdBkOn = true;                                      // LCD BACKLIGHT ON
bool Rs232Avail= false;                 // SET BY serialEvent CLEARED BY LOOP
int32_t KeyTim =0, LstActn =0;    // TIME OF LAST KEY OPER, OR LAST BACKLIGHT
short  KbRow, KbCol;                                         // KBRD ROW, COL
byte Mode = NORM, InNdx =0, EscNdx =0;
char LcdDat[4][21];                          // TERMINAL DATA STORAGE ROW,COL
int  LcdCol =0, LcdRow = 0;                          // TERMINAL ARRY INDEXES
char InStr[INSTRMAX], EscSeq[ESCMAX], *Err;               // ASCII INPUT BUFS

/************************** PROTOTYPES ************************************/
void applyEscSeq();                                          // APPLY ESC SEQ
byte blOff();                                            // LCD BACKLIGHT OFF
byte chkBakLt();                                       // CHECK LCD BACKLIGHT
void chkSeq( char chr );                                // CHECK ESC SEQUENCE
void escRst();
void expWt( char dat );                              // WRITE TO I/O EXPANDER

byte expRd();                                       // READ FROM I/O EXPANDER
void isr0();                              // KEYPAD INTERRUPT SERVICE ROUTING
char rdKbd();                                                // READ KEYPAD
void lcdScroll();                                               // SCROLL LCD
void lcdPnt( char inChr );                                    // PRINT TO LCD
void serialEvent();                       // RUNS EACH TIME (AFTER) LOOP RUNS

/*F******************************************************************
*
**********************************************************************/
void 
setup() 
{
    Wire.begin();                                                // START I2C
    Serial.begin( 9600 );
    pinMode( IRQ, INPUT_PULLUP);          // INIT KEYPAD INT PIN AS IRQ INPUT
    attachInterrupt( digitalPinToInterrupt( IRQ ), isr0, FALLING);  // KP IRQ
    memset( InStr, 0, sizeof( InStr ));                    // CLEAR ASCII BUF
    memset( LcdDat, ' ', sizeof( LcdDat ));             // FILL LCD MEM W SPC
    KeyTim = millis() + DBNC_DLY;
    expWt( COLS_HI );                   // WRITE COLS HI, ROWS LO TO EXPANDER
    Lcd.init();                              // INITIALIZE LCD, CLEARS SCREEN
    Lcd.backlight(); // OR COULD Lcd.setBacklight( HIGH ); //    BACKLIGHT ON
    Lcd.blink();                                         // MAKE CURSOR Blink
    LcdRow = LcdCol =0;
    Lcd.setCursor( LcdCol, LcdRow);                   // GO HOME COL 0, ROW 0
    LstActn = millis();                                       // CAPTURE TIME
}
/*F******************************************************************
*
**********************************************************************/
void 
loop() 
{
    char   key, ndx, chr;

    if( Err )
    {                                        // CLEAN UP & DISPLAY ERROR MSG
        Mode = NORM;
        Lcd.clear();
        lcdPnt( Err );
        Err = NULL;
    }
    if( KeyAvail == true )         // ============= KBD =================
    {                                            // RECEIVED CHAR FROM KEYPAD
        if( (key = rdKbd()) != 0 )                             // READ KEYPAD
              Serial.print( key );                     // TX (SEND) KEY TO HOST
        KeyAvail = false;
          if( chkBakLt() == 1 )
              return;                       // BACK LIGHT WAS OFF, DISCARD CHAR
      } // END OF KEY AVAIL
    if( digitalRead( IRQ ) == LOW )
        expRd();                                 // INCASE BOUNCE LEFT IRQ LO
    if( Rs232Avail == true )        // ============= RS-232 =================
    {                                       // RECEIVED CHAR FROM RS-232 link
        chkBakLt();
        for( ndx =0; (ndx < INSTRMAX); ndx++ )
        {                                  // PRINT Instr, ONE CHAR AT A TIME
            chr = InStr[ndx];
            InStr[ndx] = 0;                          // REPLACE CHR WITH NULL
            if( (chr < 32) && (chr != NL) && (chr != BS))
                continue;
            lcdPnt( chr );                                    // PRINT TO LCD
        }     // END OF FOR
        InNdx = InStr[0] = 0;                                  // CLEAR INSTR
        Rs232Avail= false;
    }      // END OF Rs232
      delay( 100 );
    blOff();
}
/*F******************************************************************
* PRINT 1 CHAR TO LCD
**********************************************************************/
void 
lcdPnt( char inChr )                                          // PRINT TO LCD
{
#ifdef DEBUG
    Serial.print("lcdPnt: inChr ");
    Serial.println( inChr );
#endif
    if( Mode == ESCSEQ )
    {                                      // ACCUMULATING AN ESCAPE SEQUENCE
        if( EscNdx >= ESCMAX )
        {                        // ESCSEQ TOO LONG, MUST HAVE MISSED THE END
            escRst();
            return;
        }
        EscSeq[EscNdx++] = inChr;          // ADD NEW CHAR TO ESCAPE SEQ BUF
        if( (EscSeq[3] == 'j') || (EscSeq[5] == 'f') || (EscSeq[6] == 'f'))
            applyEscSeq();                          // RECOGNIZED ESCAPE SEQ
        return;
    }
    if( (inChr == ESC) || (inChr == '\\')) 
    {                                               // NEW CHAR IS AN ESCAPE
        Mode = ESCSEQ;
        EscNdx =1;
        memset( EscSeq, 0, sizeof( EscSeq ));
        EscSeq[0] = ESC;
#ifdef DEBUG
  Serial.println("lcdPnt: got esc seq ");
#endif
        if( EscNdx > 6 )
          Mode = NORM;
        return;
    }
    if( (LcdRow >= ROWMAX) && (LcdCol >= COLMAX) )
        lcdScroll();                          // AT END OF DISPLAY, SCROLL UP
    if( (inChr == NL) )                                            // NEWLINE
    {
        if( LcdCol >= (COLMAX -1) )
            return;                 // ALREADY AT END OF LINE DISCARD NEWLINE
        LcdRow++;                                // NEWLINE NOT AT END OF ROW
        LcdCol = 0;
        return;
    }
    if( inChr == BS )
    {                                                // EXECUTE A BACK SPACE
        if( --LcdCol < 0 )
        {
            LcdCol = 0;
            LcdRow--;
        }
        Lcd.setCursor( LcdCol, LcdRow ); 
    }
    if( (inChr < 32) && (inChr != NL) && (inChr != BS))
        inChr = SPLAT;                           // AN UNKNOWN CHAR, SPLAT IT
    if( (LcdCol >= COLMAX) && (LcdRow < ROWMAX) )
    {   // END OF ROW, NOT AT BOT OF DISPLAY GO TO NEXT LOWER ROW AND 1st COL
        LcdRow++;
        LcdCol = 0;
    }
    if( LcdRow >= ROWMAX )
        lcdScroll();
    Lcd.setCursor( LcdCol, LcdRow );
    Lcd.print( inChr );                                      // PRINT TO LCD
    LcdDat[LcdRow][LcdCol++] = inChr;         // RECORD WHATS ON LCD ROW,COL
}
/*F******************************************************************
* SCROLL LCD SCREEN
**********************************************************************/
void   // LCD ROW 0 AT TOP, COL 0 ON LEFT
lcdScroll()                                                     // SCROLL LCD
{
    byte     ndxR, ndxC;
    char     chr;  

    Lcd.clear();                                              // CLEAR SCREEN
      delay( CLRDLY );
    for( ndxR = 0; ndxR < ROWMAX;  )
    {                                                            // SCAN COLS
        for( ndxC = 0; ndxC < COLMAX; ndxC++ )
        {                               // COPY 1 ROW OF CHARS TO NEXT ROW UP
            chr = LcdDat[ndxR+1][ndxC];              // GET CHR FROM NEXT ROW
              if( (chr < 32) || (ndxR == 3) )
                  chr = SPC;
            LcdDat[ndxR][ndxC] = chr;    // PLACE IN SAME COL NEXT ROW UP 
            Lcd.print( chr );                                 // PRINT TO LCD
        }           // END OF COL NDX CPY 
        Lcd.setCursor( 0, ++ndxR  );             // COL: 0, Row +1 DOWN 1 ROW
    }    // END OF ROW UP CPY
    if( LcdRow >= ROWMAX )
        LcdRow = 3;
    LcdCol = 0;
    Lcd.setCursor( LcdCol, LcdRow );        // SET CURSOR SCREEN BOTTOM LEFT
}
/*F******************************************************************
* APPLY ESCAPE SEQ
**********************************************************************/
void 
applyEscSeq()                                                // APPLY ESC SEQ
{
    byte  row, col, ndx;

    if( EscSeq[3] == 'j')                                           // ESC[2j 
    {                                                         // CLEAR SCREEN
        Lcd.clear();                                              // CLR SCRN
        LcdCol = LcdRow =0;
        escRst();
    }
    else
    if((EscSeq[5] == 'f') || (EscSeq[6] == 'f'))                 // ESC[r;ccf
    {                                                      // POSITION CURSOR
        row = (EscSeq[2] & 15);
        col = ((EscSeq[4] & 15) *10) +(EscSeq[5] & 15);
       if( (row > ROWMAX) || (col > COLMAX) )
       {
            Lcd.print( "ESC SEQ ERROR");
            return;
       }
        LcdCol = col;
        LcdRow = row;
        Lcd.setCursor( LcdCol, LcdRow  );                       // COL:, ROW:
        for( ndx = LcdCol; ndx < COLMAX; ndx++ )
        {                                             // CLEAR TO END OF LINE
            LcdDat[LcdRow][ndx] = SPC;                // CLEAR SCROLLING BUFF
            Lcd.print( SPC );
        }
        Lcd.setCursor( LcdCol, LcdRow  );                       // COL:, ROW:
        escRst();
    }
}
/*F******************************************************************
* WRITE TO I/O EXPANDER (PCF8574 KEYPAD)
**********************************************************************/
void 
expWt( char dat ) 
{
    Wire.beginTransmission( EXP );
    Wire.write( dat );
    Wire.endTransmission();
}
/*F******************************************************************
* READ FROM I/O EXPANDER (PCF8574 KEYPAD)
**********************************************************************/
byte 
expRd() 
{
    byte    dat = 0;

    Wire.requestFrom( EXP, 1);                     // REQUEST 1 BYTE FROM EXP
    if( Wire.available())                                // IF BYTE AVAILABLE 
        dat = Wire.read();                                     // READ A BYTE
    return( dat );
}
/*F******************************************************************
* Check IF LCD BACK LIGHT IS ON 
**********************************************************************/
byte
chkBakLt() 
{
    byte   rtn =  0;

    if( !LcdBkOn )
    {
        Lcd.backlight();                             // TURN LCD BACKLIGHT ON
        LcdBkOn = true;                              // SET BACKLIGHT MODE ON
        rtn = 1;
    }
    LstActn = millis();                                          // NOTE TIME
    return( rtn );
}
/*F******************************************************************
* CHECK TIME & TURN LCD BACK LIGHT OFF
**********************************************************************/
byte
blOff() 
{
    int32_t  now;

    now = millis();                                   // GRAB CURRENT TIME MS
    if( (now - LstActn) > BLTMOT )
    {
        Lcd.noBacklight();                          // TURN OFF LCD BACKLIGHT
        LstActn = millis();                                      // NOTE TIME
        LcdBkOn = false;
    }
}
/*F******************************************************************
* END ESC SEQ, CLEAR ESC SEQ[], RESET MODE
*********************************************************************/
void
escRst()
{
    EscNdx = 0;
    memset( EscSeq, 0, sizeof( EscSeq ));
    Lcd.setCursor( LcdCol, LcdRow  );
    Mode = NORM;
}
/*F******************************************************************
KEYPAD: 4X4 MATRIX R8 R4 R2 R1 C8 C4 C2 C1, COL BITS IN LOW NIBBLE WEAK PULLUP
WHEN COLS HI, ROWS LO (0X0F), KEY PRESS PULLS DOWN A COL AND INTERRUPTS
READ COL BITS THEN SET ALL 4 COLS HI & ROWS LO, READ FROM ROW
READ VALUE, ISOLATE NIBBLE, INVERT DATA, CONVERT BIN BIT POSITION TO VALUE
**********************************************************************/
char
rdKbd() 
{
    int     cNdx, rNdx, cnt;
    byte    rdCol, rdRow, tmp;
    char    kbChr;

 // DFLT: COLS_HI (OXOF)
    tmp = ~expRd() & 15;     // 1'S COMPLEMENT OF COL IN LO NIBBLE DAT & MSK
    delayMicroseconds( 2000 );        // WAIT FOR SWITCHING NOISE TO SUBSIDE
    rdCol = ~expRd() & 15;   // 1'S COMPLEMENT OF COL IN LO NIBBLE DAT & MSK
    if( !tmp || (tmp != rdCol) )
        return( 0 );                              // DATA NOT Stable FOR 2MS
    expWt( ROWS_HI );                              // WRITE ROWS HI, COLS LO
    delayMicroseconds( 10 );                   // WAIT 10US FOR EXP TO WORK
    tmp = ~expRd();                    // 1'S COMPLEMENT OF ROWS (HI NIBBLE)
    rdRow = (tmp >> 4) & 15;         // MOV HI NIBBLE TO LOW NIBBLE AND MASK
    kbChr = RCChar[rdRow][rdCol];      // CONVERT ROW/COL BITS TO ASCII CHAR
    expWt( COLS_HI );      // BACK TO DEFAULT STATE, WAITING FOR BUTTON PUSH
    delay( 100 );
    if( kbChr !=  0)
    {                                                           // GOOD READ
        KeyAvail = false;                       // TURN OFF KB DATA AVAIL FLAG
        KeyTim = millis() + DBNC_DLY;             // RECORD LAST KEY PUSH TIME
    }
    return( kbChr );
}
/*F******************************************************************
* KeyPad INTERRUPT SERVICE ROUTINE
**********************************************************************/
void 
isr0()
{
    if( millis() > KeyTim )                // THROW AWAY IRQs UNTIL DBNC_DLY
        KeyAvail = true;                          // NEW INTERRRUPT OCCURRED
}
/*F******************************************************************
* RS-232 INTERRUPT SERVICE ROUTINE
**********************************************************************/
void 
serialEvent()  
{                  // RUNS EACH TIME (AFTER) LOOP RUNS
    char  chr;

    while( Serial.available()) // INTERRUPT SERVICE ROUTIND, DON'T TARRY HERE
    {
        chr = (char)Serial.read();                            // GET NEW CHAR
        if( (chr != NL) && (chr != BS) && (chr < 32) )
            continue;         // DISCARD NON PRINtable OR PRINT CONTROL CHAR
        if( InNdx >= INSTRMAX )
        {
            Err = "InStr Oflo";
            InNdx = 0;
            memset( InStr, SPC, sizeof( InStr));
            return;
        }
        InStr[InNdx++] = chr;
        Rs232Avail= true;  // SET FLAG SO MAIN LOOP CAN DO SOMETHING ABOUT IT
        if( InNdx >= INSTRMAX )
        {                                   // IN STR NDX TOO LARGE, RESET IT
            InNdx =0;
            break;
        }
    }
}