Nextion LCD / Touch Screen
revised d. bodnar 9-07-2016

Introduction
I have been experimenting with various throttle designs for DCC++ and have had some success.  (See:
http://www.trainelectronics.com/DCC_Arduino/DCC++/Throttle/index.htm )

I recently began experimenting with a very capable touch screen LCD display from Itead Studios. 

 

While there is a good bit of information on using this unit on the Internet most of it is scattered around and not well presented. 

This web page is just a set of notes that may help me to organize what I am trying to do with the Nextion Display.

Implementing a Numeric Keyboard
I started by setting up a set of 10 number keys (1--9 + zero) and a DONE and BACK button.  This set of keys should allow users to enter a 4 digit DCC address onto the screen.

The keyboard was designed using the Nextion Editor, a program that allows you to easily design screens.  The first draft is shown here and can be downloaded here:  nextion keypad project-2.HMI

NOTE:  This code works properly using the Nextion library that is found here:
https://github.com/bborncr/nextion

My second set of screens was done in CorelDraw and transferred to the Nextion editor.  It looks much nicer (at least to me!).  The editor files are here:  nextion keypad project-6.HMI

The code is maturing and currently connects to the throttle and button which changes direction and updates the throttle scale at the top of the display.  It also allows you to choose one of three DCC addresses.  Pressing one of the three DCC buttons (in this case 67, 555 and 3311) highlights the active address. 

The throttle is accessed via the knob at the bottom which is attached to a rotary encoder.  The selected speed is displayed as both a moving bar and numerically.  Direction is changed by pressing the knob on the encoder in and is shown by ">>>" or "<<<" to the side of the numeric speed.  If the throttle know is moved slowly the speed increases slowly.  If it moved more rapidly the speed increases much more quickly.

Pressing the "Change DCC Address" button (screen above) brings up this screen where a new address can be entered.  You can enter all four digits or you can press Done for a shorter address.  Pressing Abort will return to the first screen with no change made to the highlighted address.

The 10 "f" buttons at the bottom of the main screen are used to activate functions F1-->F9.  The current state of these functions is shown (in binary) below the buttons.  The number displayed, 0100 00010, shows that function 2 (the 2nd "1" from the right) is active along with functions 8.  Pressing f0 will  return them all to zero.

The Arduino code that reacts to the button pushes on the display is here.  Note that it is under development.
DCC_Throttle_Nextion_REncoder-v3-12.ino

While the program below works it still has a way to go before being ready for distribution - bugs still need to be tracked down & squished!

This is an updated version of the Nextion screens: nextion keypad project-12.HMI 
for SD card install use:
nextion keypad project-12.tft
note: on my PC the tft files are located in C:\Users\David\AppData\Roaming\Nextion Editor\bianyi

Changes in this version:

  • If you press the top of the Throttle Screen (see the yellow box in the image below) all trains will stop  and power will be removed from the track
  • If you briefly tap the throttle button knob the direction will change (as it did before)
  • If you HOLD the throttle button knob for 1 second the current loco speed will go to zero - note that the throttle graph goes up from left to right as you hold the knob down - if you release it before it gets to the end the speed will not change.

 

/*
  d. bodnar  revised 9-06-2016  V3.12
  Steve's mod


*/
#include "Arduino.h"
#include <SoftwareSerial.h>
#include <Nextion.h>
#include<EEPROM.h>
SoftwareSerial nextion(4, 5);// Nextion TX to pin 4 and RX to pin 5 of Arduino

Nextion myNextion(nextion, 9600); //create a Nextion object named myNextion using the nextion serial port @ 9600bps
String message;
int debug = 1; // set to 1 to show debug info on serial port - may cause issues with DCC++ depending on what is sent
int ActiveAddress = 0; // make address1 active
int counter = 0;
byte Key;
char key ;
unsigned long currentTime;
unsigned long lastTime;
unsigned long DCCtime;
unsigned long DCCcurrentTime;
const int buttonPin = 8;     // the number of the pushbutton pin on encoder
const int ledPin =  13;      // the number of the LED pin
boolean encA;
boolean encB;
boolean lastA = false;
unsigned long number = 0;
int lowest = 0;
unsigned long highest = 120;//126;
long changeamnt = 1;
int jumpamnt = 10;
int scaled = 0;
int encoderChange = 0; // flag to show encoder changed
byte Fx = 0;
int maxLocos = 3;// number of loco addresses
int LocoDirection[3] = {1, 1, 1};
int LocoSpeed[3] = {0, 0, 0};
byte LocoFN0to4[3] = {128, 128, 128};
byte LocoFN5to8[3] = {176, 176, 176};
byte Locofn9to12[4];// 9-12 not yet implemented
int xxxxx = 0;
int old_speed = 0;
int ZeroSpeedFlag = 0;
int z = 0;
int powerTemp = 0;
int i = 0;
char VersionNum[] = "3.12";
int DCCflag = 0;
boolean ledPin_state;
unsigned long previousMillis = 0;        // will store last time LED was updated
unsigned long currentMillis = millis();
const long interval = 300;
int saveAddress = 0;
int LocoAddress[4] = {000, 1830, 3, 456};
int FNbutton = 0;
volatile byte aFlag = 0;
volatile byte bFlag = 0;
volatile byte encoderPos = 0;
volatile byte oldEncPos = 0;
volatile byte reading = 0;
int old_pos = encoderPos;
int dir = 0; // direction
int buttonState = 0;
unsigned long interruptTime;
int FwdRev = 0; // 0 for backwards, 1 for forwards
static int pinA = 2; // Our first hardware interrupt pin is digital pin 2
static int pinB = 3; // Our second hardware interrupt pin is digital pin 3
int directionFlag = 0;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(pinA, INPUT_PULLUP); // set pinA as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)
  pinMode(pinB, INPUT_PULLUP); // set pinB as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)
  pinMode(buttonPin, INPUT_PULLUP);
  attachInterrupt(0, PinA, RISING); //interrupt on PinA, rising edge signal & executing the "PinA" Interrupt Service Routine (below)
  attachInterrupt(1, PinB, RISING); // same for B
  pinMode(ledPin, OUTPUT);
  Serial.begin (115200);
  Serial.print("9-05-2016  version ");
  for (int i = 0; i < 4; i++) {
    Serial.print(VersionNum[i]);
  }
  Serial.print("<0>");// power off to DCC++ unit
  digitalWrite(ledPin, HIGH);           // Turn the LED on.
  ledPin_state = digitalRead(ledPin);   // Store initial LED state. HIGH when LED is on.
  myNextion.init(); // send the initialization commands for Page 0
  currentTime = millis();
  lastTime = currentTime;
  myNextion.sendCommand("page 0" );
  myNextion.setComponentText("Version", "3.12");
  Serial.println("Version 3.12");
  delay(2000); //show title screen for minimum of 2 seconds
  myNextion.sendCommand("page 2" );
  dir = 1; //forward
  //myNextion.setComponentText("forward", ">>>>");//Now done as default on display
  //myNextion.setComponentText("reverse", "  ");
  getAddresses(); // read from EEPROM
  //myNextion.sendCommand("n0.font=1");//Now done as default on display
  //myNextion.sendCommand("n0.bco=65504");// change background color yellow
  //myNextion.sendCommand("n1.font=0");
  //myNextion.sendCommand("n1.bco=65535");// change background color white
  //myNextion.sendCommand("n2.font=0");
  //myNextion.sendCommand("n2.bco=65535");// change background color white
  updateDCCaddresses();
  makeFWD();
}  // END SETUP

void loop() {
  encoderInterrupt(); // check rotary encoder
  DCCcurrentTime = millis(); //send DCC every 30000 ms
  if (DCCcurrentTime - DCCtime >= 30000 | DCCflag == 1) {
    DCCflag = 0;
    DCCtime = DCCcurrentTime;
    doDCC();
  }
  checkButton();  // check for change in direction
  getActiveAddress();  // check for DCC address change
}   // *** END LOOP ***

void encoderInterrupt() {
  currentTime = millis();
  if (oldEncPos != encoderPos) {
    if ((currentTime - interruptTime) >= 100) {
    }
    else {  // if turned rapidly increment another 4 (total of 5 per turn)
      if (FwdRev == 0) {
        if (encoderPos >= 4)     {
          encoderPos = encoderPos - 4;
        }
      }
      else {
        if (encoderPos <= 115) {
          encoderPos = encoderPos + 4;
        }
      }
    }
    interruptTime = millis();
    int x = old_pos - encoderPos;
    //Serial.print(encoderPos);
    //Serial.print("\t");
    //Serial.println(FwdRev);
    oldEncPos = encoderPos;
    scaled = encoderPos * 100;
    scaled = scaled / highest;
    myNextion.setComponentValue("throttle", scaled);
    myNextion.setComponentValue("throttleNum", encoderPos);
    LocoSpeed[ActiveAddress] = encoderPos;
    doDCC();
  }
}

void checkButton() {  // Checks button on encoder to change direction
  buttonState = digitalRead(buttonPin);
  directionFlag = 0;
  currentMillis = millis();
  if (buttonState == LOW) {
    do {
      if (millis() - currentMillis > 200) {
        myNextion.setComponentValue("throttle", (millis() - currentMillis) / 10);
      }
      buttonState = digitalRead(buttonPin);
      if (millis() - currentMillis >= 1000) {
  //      Serial.println("MORE THAN 1000 MILLIS");
  //      Serial.println("button held");
        LocoSpeed[ActiveAddress] = 0;
        encoderPos = 0;
        myNextion.setComponentValue("throttle", 0);
        myNextion.setComponentValue("throttleNum", 0);
        directionFlag = 1;
      }
    }
    while (buttonState == LOW);
    if (directionFlag == 0) {
      directionFlag = 0;
      dir = !dir;
      if (dir) {
        digitalWrite(ledPin, HIGH);
        makeFWD();
      }
      else {
        digitalWrite(ledPin, LOW);
        makeBKW();
      }
      currentMillis = millis();
      LocoDirection[ActiveAddress] = dir;
      doDCC();
    }
  }
}

void getActiveAddress() {
  message = myNextion.listen(); //check for message
  if (message != "") {
    char mostSignificantDigit = message.charAt(5);// for function numbers
    String myString;
    myString = mostSignificantDigit;
    FNbutton = myString.toInt();
    FNbutton = FNbutton - 1;
    if (FNbutton == -1) {
      if (myString == "a") FNbutton = 9;
      if (myString == "b") FNbutton = 0;
    }
    if (FNbutton >= 0 && FNbutton <= 9) doFunction();
    if (message.startsWith("65 2 e")) { // move to page 1
      getLocoAddress();  // get new DCC address #
    }

    if (message.startsWith("AllStop")) { // DCC1 button page 2
      Serial.println("STOP STOP  STOP"); //Selected DCC address #1 on page 2
      Serial.print("<0>");// power off to DCC++ unit

      LocoSpeed[0] = 0;
      LocoSpeed[1] = 0;
      LocoSpeed[2] = 0;
      encoderPos = 0;
      myNextion.setComponentValue("throttle", 0);
      myNextion.setComponentValue("throttleNum", 0);
      dir = 1; //forward
      makeFWD();
    }
    if (message.startsWith("l1")) { // DCC1 button page 2
      Serial.println("one"); //Selected DCC address #1 on page 2
      encoderPos = LocoSpeed[0];
      updateDCC1();
    }
    if (message.startsWith("l2")) { // DCC1 button page 2
      Serial.println("two");//Selected DCC address #2 on page 2
      encoderPos = LocoSpeed[1];
      updateDCC2();
    }
    if (message.startsWith("l3")) { // DCC1 button page 2
      Serial.println("three");//Selected DCC address #3 on page 2
      encoderPos = LocoSpeed[2];
      updateDCC3();
    }
  }
}

//START DO FUNCTION BUTTONS
int doFunction() {
  key = FNbutton - 1; // convert from ascii value
  if (key <= 4) {
    if (bitRead(LocoFN0to4[ActiveAddress], key) == 0 ) {
      bitWrite(LocoFN0to4[ActiveAddress], key, 1);
    }
    else {
      //if (bitRead(LocoFN0to4[ActiveAddress], key) == 1 ) {
      bitWrite(LocoFN0to4[ActiveAddress], key, 0);
      //}
    }
    doDCCfunction04();
  }
  if (key >= 5 && key <= 8) {
    key = key - 5;
    if (bitRead(LocoFN5to8[ActiveAddress], key) == 0 ) {
      bitWrite(LocoFN5to8[ActiveAddress], key, 1);
    }
    else {
      //if (bitRead(LocoFN5to8[ActiveAddress], key) == 1 ) {
      bitWrite(LocoFN5to8[ActiveAddress], key, 0);
      //}
    }
    doDCCfunction58();
  }
  if (key == -1)
  {
    key = 0;
    LocoFN0to4[ActiveAddress] = B10000000; //clear variables for which functions are set
    LocoFN5to8[ActiveAddress] = B10110000;
    doDCCfunction04();
    doDCCfunction58();
    key = 0;
  }
  key = 0;
  String temp = "0000" + String(LocoFN0to4[ActiveAddress], BIN);  // pad with leading zeros
  int tlen = temp.length() - 5;
  Serial.println("");
  Serial.println(temp.substring(tlen));
  String FN1 = temp.substring(tlen);
  temp = "000" + String(LocoFN5to8[ActiveAddress], BIN);
  tlen = temp.length() - 4;
  FN1 = temp.substring(tlen) + " " + FN1;
  myNextion.setComponentText("FunctionBinary", FN1);
}//END DO FUNCTION BUTTONS

void doDCC() {
  Serial.print("<1>");
  Serial.print("<t1 ");
  Serial.print(LocoAddress[ActiveAddress] );//locoID);
  Serial.print(" ");
  Serial.print(LocoSpeed[ActiveAddress] );
  Serial.print(" ");
  Serial.print(LocoDirection[ActiveAddress] );
  Serial.println(">");
  number = LocoSpeed[ActiveAddress];
  scaled = number * 100;
  scaled = scaled / highest;
  myNextion.setComponentValue("throttle", scaled);
  myNextion.setComponentValue("throttleNum", number);
  if (dir) {
    makeFWD();
  }
  else {
    makeBKW();
  }
}

void doDCCfunction04() {
  Serial.write("<f ");
  Serial.print(LocoAddress[ActiveAddress] );
  Serial.print(" ");
  //int fx = LocoFN0to4[ActiveAddress] + 128;
  Serial.print(LocoFN0to4[ActiveAddress]);
  Serial.print(" >");
}
void doDCCfunction58() {
  Serial.write("<f ");
  Serial.print(LocoAddress[ActiveAddress] );
  Serial.print(" ");
  //int fx = LocoFN5to8[ActiveAddress] + 176;
  Serial.print(LocoFN5to8[ActiveAddress]);
  Serial.print(" >");
}

void getLocoAddress() {// get new DCC address # on Nextion page #1
  myNextion.sendCommand("page 1" );
  myNextion.setComponentValue("AddrNew", ActiveAddress + 1);
  myNextion.setComponentValue("DCCnew", 0);
  saveAddress =   LocoAddress[ActiveAddress];
  int total = 0;
  counter = 0;
  do {
    do {
      message = myNextion.listen(); //check for message
      if (message != "") {
      }
    }
    while (message == "");
    getNumber();
    if (key == 98 ) { //"done" button hit - less than 4 digits
      break;// exit routine if # button pressed - ABORT new address
    }
    if (key == 99) {
      break;
    }
    counter++;
    int number =  key;
    total = total * 10 + number;
    if (total == 0) {   // print multiple zeros for leading zero number
      for (int tempx = 1; tempx <= counter; tempx++) {
      }
    }
    myNextion.setComponentValue("DCCnew", total);
    myNextion.setComponentValue("j0", total);
  }
  while (counter <= 3); //  collect exactly 4 digits
  LocoAddress[ActiveAddress] = total;
  total = 0;
  counter = 0;
  myNextion.sendCommand("page 2" );
  if (key == 99) {
    LocoAddress[ActiveAddress] = saveAddress;
  }
  switch (ActiveAddress) {
    case 0:
      updateDCC1();
      break;
    case 1:
      updateDCC2();
      break;
    default:
      updateDCC3();
  }
  saveAddresses();
}

void getAddresses() {  // from EEPROM
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos - 1; xyz++) {
    LocoAddress[xyz] = EEPROM.read(xyz * 2) * 256;
    LocoAddress[xyz] = LocoAddress[xyz] + EEPROM.read(xyz * 2 + 1);
    if (LocoAddress[xyz] >= 10000) LocoAddress[xyz] = 3;
  }
  maxLocos = EEPROM.read(20);
  if (maxLocos >= 4) maxLocos = 4;
}


void saveAddresses() { // TO EEPROM
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos ; xyz++) {
    xxx = LocoAddress[xyz] / 256;
    EEPROM.write(xyz * 2, xxx);
    xxx = LocoAddress[xyz] - (xxx * 256);
    EEPROM.write(xyz * 2 + 1, xxx);
  }
  EEPROM.write(20, maxLocos);
}

// FROM NEXTION CODE
void getNumber() {   // note that the 2nd digit (after the 65) was changed from page 0 to page 1
  if (message.startsWith("65 1 a")) {
    key = 0;
  }
  if (message.startsWith("65 1 1")) {
    key = 1;
  }
  if (message.startsWith("65 1 2")) {
    key = 2;
  }
  if (message.startsWith("65 1 3")) {
    key = 3;
  }
  if (message.startsWith("65 1 4")) {
    key = 4;
  }
  if (message.startsWith("65 1 5")) {
    key = 5;
  }
  if (message.startsWith("65 1 6")) {
    //Serial.println("six");
    key = 6;
  }
  if (message.startsWith("65 1 7")) {
    key = 7;
  }
  if (message.startsWith("65 1 8")) {
    key = 8;
  }
  if (message.startsWith("65 1 9")) {
    key = 9;
  }
  if (message.startsWith("65 1 b")) {
    key = 98;
  }
  if (message.startsWith("65 1 c")) {
    key = 99;
  }
}

void makeFWD() {
  myNextion.setComponentText("forward", ">>>>");
  myNextion.setComponentText("reverse", "  ");
  myNextion.setComponentValue("throttle", scaled);
  myNextion.setComponentValue("throttleNum", number);
}

void makeBKW() {
  myNextion.setComponentText("forward", "  ");
  myNextion.setComponentText("reverse", "<<<<");
  myNextion.setComponentValue("throttle", scaled);
  myNextion.setComponentValue("throttleNum", number);
}

void updateDCCaddresses() {
  String stringOne =  String(LocoAddress[0]);
  String stringTwo =  String(LocoAddress[1]);
  String stringThree =  String(LocoAddress[2]);
  myNextion.setComponentText("bt0", stringOne);//myNextion.setComponentValue("n0", LocoAddress[0]); Hope this converts
  myNextion.setComponentText("bt1", stringTwo);//myNextion.setComponentValue("n1", LocoAddress[1]);
  myNextion.setComponentText("bt2", stringThree);//myNextion.setComponentValue("n2", LocoAddress[2]);
  DCCflag = 1;
}

void updateDCC1() {
  ActiveAddress = 0;
  number =  LocoSpeed[ActiveAddress];
  dir = LocoDirection[ActiveAddress];
  //myNextion.sendCommand("n0.font=1");//Done on display
  //myNextion.sendCommand("n0.bco=65504");// change background color yellow
  //myNextion.sendCommand("n1.font=0");
  //myNextion.sendCommand("n1.bco=65535");// change background color white
  //myNextion.sendCommand("n2.font=0");
  //myNextion.sendCommand("n2.bco=65535");// change background color white
  updateDCCaddresses();
}

void updateDCC2() {
  ActiveAddress = 1;
  number =  LocoSpeed[ActiveAddress];
  dir = LocoDirection[ActiveAddress];
  //myNextion.sendCommand("n1.font=1");//Done on display       // myNextion.setComponentValue("n1", LocoAddress[2]);
  //myNextion.sendCommand("n1.bco=65504");// change background color yellow
  //myNextion.sendCommand("n0.font=0");
  //myNextion.sendCommand("n0.bco=65535");// change background color white
  //myNextion.sendCommand("n2.font=0");
  //myNextion.sendCommand("n2.bco=65535");// change background color white
  updateDCCaddresses();
}

void updateDCC3() {
  ActiveAddress = 2;
  number =  LocoSpeed[ActiveAddress];
  dir = LocoDirection[ActiveAddress];
  //myNextion.sendCommand("n2.font=1");//Done on display
  //myNextion.sendCommand("n2.bco=65504");// change background color yellow
  //myNextion.sendCommand("n1.font=0");
  //myNextion.sendCommand("n1.bco=65535");// change background color white
  //myNextion.sendCommand("n0.font=0");
  //myNextion.sendCommand("n0.bco=65535");// change background color white
  updateDCCaddresses();
}

void PinA() {
  cli(); //stop interrupts happening before we read pin values
  reading = PIND & 0xC; // read all eight pin values then strip away all but pinA and pinB's values
  if (reading == B00001100 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
    if (encoderPos >= 1) {
      encoderPos --;
    }//decrement the encoder's position count
    FwdRev = 0;
    bFlag = 0; //reset flags for the next turn
    aFlag = 0; //reset flags for the next turn
  }
  else if (reading == B00000100) bFlag = 1; //signal that we're expecting pinB to signal the transition to detent from free rotation
  sei(); //restart interrupts
}

void PinB() {
  cli(); //stop interrupts happening before we read pin values
  reading = PIND & 0xC; //read all eight pin values then strip away all but pinA and pinB's values
  if (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
    if (encoderPos <= 119) {
      encoderPos ++;
    } //increment the encoder's position count
    FwdRev = 1;
    bFlag = 0; //reset flags for the next turn
    aFlag = 0; //reset flags for the next turn
  }
  else if (reading == B00001000) aFlag = 1; //signal that we're expecting pinA to signal the transition to detent from free rotation
  sei(); //restart interrupts
}
Build Photos
The Arduino Pro Mini is attached to the back of the Nextion screen with double sided tape.  Note that the connecting pins on the Arduino are right angle pins so that the connections come out the side - this helps to keep down the thickness of the case.

Here the circuitry is shown inside of the case.

Notes:
To change page from the Arduino:
void loop() {
myNextion.sendCommand("page 1" );
Serial.println("Id=1 ");
delay (1000);
myNextion.sendCommand("page 0" );
Serial.println("Id=0 ");
delay (1000);
}