NEXTION HMI with Industruino

Touch screen over Serial1 on IDC (IND.I/O and PROTO)

Tom

NEXTION is a popular HMI (human machine interface) for the Arduino environment, as it is relatively good value, easy to connect over Serial, and comes with a decent GUI editor. However, Nextion (a branch of Itead) also receives a lot of criticism, so please note this blog post is by no means an endorsement by Industruino, we just want to show you how to connect a popular HMI to our platform.

HMI are typically LED displays with touch screen functionality, so the user can interact by pressing buttons on a screen, navigating between different pages to read&control settings. In this example we use a 3.2'' Nextion display with resistive touch, which sells for around USD 22. It needs 5V with a recommended 500mA PSU, and the interface is Serial TTL. There are many getting started tutorials for Nextion, e.g. this one is well made.

In this blog post, we will illustrate the connection over standard Serial TTL, which should be exactly the same as any Arduino. In an earlier blog post, we have also shown in more detail how to use the INDIO's RS485 port to connect this Nextion HMI, which has the advantage to allow longer cabling, and also keeps the IDC port free for an Ethernet or GSM module.

Connecting the Nextion HMI to an Industruino unit, IND.I/O or PROTO, is very straight forward if you go for the standard Serial TTL communication. The Nextion has 4 wires that can be connected directly to the Industruino's IDC port:

  • 2 power wires, red and black, to 5V and GND. Note that the Industruino's DC/DC converter (V+/V- to 5V) is 2W, so can supply 400mA max on the 5V line. The topboard needs around 50mA, depending on the LCD backlight, so 350mA is available on the IDC 5V. My 3.2'' Nextion typically draws 85mA according to its datasheet so we can use the Industruino's 5V pin (Nextion recommends a 500mA power supply)
  • RX and TX which can be connected directly to pins D10/D5 (Serial1). On my unit, it is yellow (RX) to D5 and blue (TX) to D10. The Nextion is a 5V device, and the Industruino's GPIO is 3.3V but the Nextion datasheet says it detects HIGH from 3V.
At this point, we are ready to test the communication, using this simple sketch to send commands over Serial1 via the Arduino IDE's Serial Monitor, and display the replies. We do not need any library yet to do this, we can directly use so-called Nextion Instruction Set e.g. 'rest' for reset should return 0 0 0 FF FF FF 88 FF FF FF indicating 'Nextion Startup' and 'Nextion Ready'.
/*
   Industruino demo   NEXTION HMI over Serial1 on IDC expansion port

   NEXTION commands:
   rest       reset
   bkcmd=3    always get feedback (default is 2: only on failure)
   page 0     go to page 0
*/

void setup() {
  Serial1.begin(9600);    // D10/D5 on IDC
  SerialUSB.begin(115200);
  delay(1000);
  SerialUSB.println("Serial1 receiver and sender");
}

void loop() {
  while (Serial1.available()) {
    byte byte_received = Serial1.read();
    SerialUSB.println(byte_received, HEX);
  }
  if (SerialUSB.available()) {
    String cmd = SerialUSB.readStringUntil('\n');
    SerialUSB.print("sending cmd= ");
    SerialUSB.println(cmd);
    //delay(10);
    Serial1.print(cmd);
    Serial1.write(0xFF);
    Serial1.write(0xFF);
    Serial1.write(0xFF);
  }
}

Now we are ready to use a library to make our life easier. The official Nextion library has not been updated for over 3 years. This is quite strange as Nextion seems to be releasing new products, and new versions of its GUI Editor. If you insist on using this library, it seems to work with a few modifications: in NexConfig.h change dbSerial to SerialUSB and nexSerial to Serial1, and in NexUpload.cpp comment out the line //#include <SoftwareSerial.h> - but note that below example uses a different library!

It may well be that most people are writing their own low level interfaces to the Nextion displays, using the instruction set documentation. Also there seems to be an active user community around this unofficial forum

Luckily there is also the 'Enhanced-Nextion-library' which we will use here as a starting point.

It allows configuration for different platforms (Arduino, ESP8266 etc): hardware or software serial, debug serial. The default works on Arduino UNO. We have to modify just one file:

In NexConfig.h:

find this block, uncomment this first line, and modify the last:

//#define NEX_SOFTWARE_SERIAL
#ifndef NEX_SOFTWARE_SERIAL
// hardware Serial port
#define nexSerial Serial1

Now the library is configured to use Serial1 on the D10/D5 pins.

You can now try our below test sketch, it is based on our Nextion RS485 example, and uses the identical Nextion code .tft and .hmi that you can find in this zip. See the other blog post for details on how this sketch works. Only the .ino is slightly different, as below.

/*
   Industruino demo with NEXTION HMI on IDC Serial1 D10/D5

   Nextion library: https://github.com/jyberg/Enhanced-Nextion-Library
   with changes to:
   > NexHardware.cpp  update line #define nexSerial Serial1 // for IDC D10/D5

   Nextion files:
   > nextion4024indio1.HMI
   > upload nextion4024indio.tft to the display by SD card

   Functionality:
   > digital CH1-4 are outputs: control by buttons on display
   > digital CH5-8 are inputs: show state on display
   > analog output CH1-2: control by slider on display (0-100%)
   > analog input CH1-4: show value and bar (0-100%)

   Note: all analog channels are in V10_p mode
   
   Tom Tobback for Industruino, Jan 2020
   
   *******************************************************************************************************************************************************
    MIT LICENSE
    Copyright (c) 2019-2020 Cassiopeia Ltd
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    IN THE SOFTWARE.
   *******************************************************************************************************************************************************
*/

#include <UC1701.h>
static UC1701 lcd;

#include <Indio.h>
#include <Wire.h>

// Indio variables
boolean dig_input_state [9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};   // to track changes in dig input channels, by index 5-8
int ana_input_state [5] = {101, 101, 101, 101, 101};     // to track changed in ana input channels, by index 1-4  (percentage, as integer), set to 101 to make sure it is updated on first reading

#include "Nextion.h"
// Declare your Nextion objects - Example (page id = 0, component id = 1, component name = "b0")
NexDSButton btCH1 = NexDSButton(1, 4, "btCH1");
NexDSButton btCH2 = NexDSButton(1, 6, "btCH2");
NexDSButton btCH3 = NexDSButton(1, 7, "btCH3");
NexDSButton btCH4 = NexDSButton(1, 8, "btCH4");
NexText tCH5 = NexText(1, 10, "tCH5");
NexText tCH6 = NexText(1, 11, "tCH6");
NexText tCH7 = NexText(1, 12, "tCH7");
NexText tCH8 = NexText(1, 13, "tCH8");
NexSlider hCH1 = NexSlider(2, 14, "hCH1");
NexSlider hCH2 = NexSlider(2, 15, "hCH2");
NexText tCH1out = NexText(2, 16, "tCH1out");
NexText tCH2out = NexText(2, 17, "tCH2out");
NexProgressBar jCH1 = NexProgressBar(2, 6, "jCH1");
NexProgressBar jCH2 = NexProgressBar(2, 11, "jCH2");
NexProgressBar jCH3 = NexProgressBar(2, 12, "jCH3");
NexProgressBar jCH4 = NexProgressBar(2, 13, "jCH4");
NexText tCH1 = NexText(2, 7, "tCH1");
NexText tCH2 = NexText(2, 8, "tCH2");
NexText tCH3 = NexText(2, 9, "tCH3");
NexText tCH4 = NexText(2, 10, "tCH4");
NexPage page0 = NexPage(0, 0, "page0");
NexPage page1 = NexPage(1, 0, "page1");
NexPage page2 = NexPage(2, 0, "page2");

// Register a button object to the touch event list.
NexTouch *nex_listen_list[] = {
  &btCH1,
  &btCH2,
  &btCH3,
  &btCH4,
  &hCH1,
  &hCH2,
  NULL
};

void btCH1callback(void *ptr) {
  uint32_t state;  // needs this type for getValue function
  btCH1.getValue(&state);
  SerialUSB.print("[button action] digital output CH1: ");
  SerialUSB.println((bool)state);
  Indio.digitalWrite(1, (bool)state);
}

void btCH2callback(void *ptr) {
  uint32_t state;  // needs this type for getValue function
  btCH2.getValue(&state);
  SerialUSB.print("[button action] digital output CH2: ");
  SerialUSB.println((bool)state);
  Indio.digitalWrite(2, (bool)state);
}

void btCH3callback(void *ptr) {
  uint32_t state;  // needs this type for getValue function
  btCH3.getValue(&state);
  SerialUSB.print("[button action] digital output CH3: ");
  SerialUSB.println((bool)state);
  Indio.digitalWrite(3, (bool)state);
}

void btCH4callback(void *ptr) {
  uint32_t state;  // needs this type for getValue function
  btCH4.getValue(&state);
  SerialUSB.print("[button action] digital output CH4: ");
  SerialUSB.println((bool)state);
  Indio.digitalWrite(4, (bool)state);
}

void hCH1callback(void *ptr) {
  uint32_t number = 0;
  hCH1.getValue(&number);
  number = constrain(number, 0, 100); // percentage
  SerialUSB.print("[slider action] analog output CH1: ");
  SerialUSB.print(number);
  SerialUSB.println("%");
  Indio.analogWrite(1, number, false);  // false: do not store value in eeprom
  // update value on display
  while(!sendCmd("page2.tCH1out.txt=\"" + String(number) + "%\""));
  lcd.setCursor(80, 4);
  lcd.print("CH1:");
  lcd.print(number);
  lcd.print("%  ");
}

void hCH2callback(void *ptr) {
  uint32_t number = 0;
  hCH2.getValue(&number);
  number = constrain(number, 0, 100); // percentage
  SerialUSB.print("[slider action] analog output CH2: ");
  SerialUSB.print(number);
  SerialUSB.println("%");
  Indio.analogWrite(2, number, false);  // false: do not store value in eeprom
  while(!sendCmd("page2.tCH2out.txt=\"" + String(number) + "%\""));
  lcd.setCursor(80, 5);
  lcd.print("CH2:");
  lcd.print(number);
  lcd.print("%  ");
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

void setup() {

  // INDIO channel config
  // all digital as output to clear any high signals
  for (int i = 1; i <= 8; i++) {
    Indio.digitalMode(i, OUTPUT);
    Indio.digitalWrite(i, LOW);
  }
  // leave digital 1-4 as output, and set 5-8 as input
  for (int i = 5; i <= 8; i++) {
    Indio.digitalMode(i, INPUT);
  }
  // analog output
  Indio.analogWriteMode(1, V10_p); // percentage
  Indio.analogWriteMode(2, V10_p); // percentage
  Indio.analogWrite(1, 0, false); // false: do not store value in eeprom
  Indio.analogWrite(2, 0, false); // false: do not store value in eeprom
  // analog input
  Indio.setADCResolution(12); // Set the ADC resolution. Choices are 12bit@240SPS, 14bit@60SPS, 16bit@15SPS and 18bit@3.75SPS.
  for (int i = 1; i <= 4; i++) {
    Indio.analogReadMode(i, V10_p);
  }

  // Industruino LCD
  pinMode(26, OUTPUT);
  digitalWrite(26, HIGH);   // LCD backlight on d21g
  lcd.begin();
  lcd.clear();
  lcd.print("Industruino Nextion");

  // Industruino Serial1 on D10/D5
  Serial1.begin(9600);       

  // Serial Monitor
  SerialUSB.begin(115200);  // Serial Monitor
  delay(1000);
  SerialUSB.println("========================");
  SerialUSB.println("Industruino Nextion test");
  SerialUSB.println("========================");

  // NEXTION HMI
  nexInit();
  // Register the pop event callback function of the components
  btCH1.attachPop(btCH1callback, &btCH1);  // dual state button for digital output
  btCH2.attachPop(btCH2callback, &btCH2);  // dual state button for digital output
  btCH3.attachPop(btCH3callback, &btCH3);  // dual state button for digital output
  btCH4.attachPop(btCH4callback, &btCH4);  // dual state button for digital output
  hCH1.attachPop(hCH1callback); // slider for analog output
  hCH2.attachPop(hCH2callback); // slider for analog output

  while (Serial1.available()) Serial1.read(); // flush any input on Serial1

  SerialUSB.println("Setting bkcmd=3 so we get feedback ACK in all cases, success and failure");
  lcd.setCursor(0, 1);
  lcd.print("Enabling ACK..");
  while (!sendCmd("bkcmd=3"));
  SerialUSB.println("Feedback ACK enabled, we can continue");
  lcd.print(" OK");

  SerialUSB.println("Setting digital output channels to 0");
  while(!sendCmd("page1.btCH1.val=0"));  // reset output button to OFF
  while(!sendCmd("page1.btCH2.val=0"));  // reset output button to OFF
  while(!sendCmd("page1.btCH3.val=0"));  // reset output button to OFF
  while(!sendCmd("page1.btCH4.val=0"));  // reset output button to OFF
  SerialUSB.println("Setting analog output channels to 0");
  while(!sendCmd("page2.tCH1out.txt=\"0%\"")); 
  while(!sendCmd("page2.tCH2out.txt=\"0%\""));
  while(!sendCmd("page2.hCH1.val=0")); 
  while(!sendCmd("page2.hCH2.val=0")); 
   
  // go to page 0
  SerialUSB.println("Display page 0: welcome");
  //  page0.show(); // included in nexInit

  // stay on welcome page for a little while
  delay(3000);
  SerialUSB.println("Display page 1: digital channels");
  SerialUSB.println("Press button to move to page 2: analog channels");
  page1.show();
  SerialUSB.println("===============================================");

  lcd.setCursor(0, 1);
  lcd.print("                              ");
  lcd.setCursor(80, 4);
  lcd.print("CH1:0%");
  lcd.setCursor(80, 5);
  lcd.print("CH2:0%");
} 

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

void loop() {

  nexLoop(nex_listen_list);

  // DIGITAL CH1-4 outputs: switch to inputs briefly to read, then switch back
  lcd.setCursor(0, 2);
  lcd.print("DIG IO: ");
  for (int i = 1; i <= 4; i++) {
    Indio.digitalMode(i, INPUT);
    lcd.print(Indio.digitalRead(i));
    Indio.digitalMode(i, OUTPUT);
  }

  // DIGITAL CH5-8 inputs: if state has changed, send command to display
  for (int i = 5; i <= 8; i++) {
    bool state = Indio.digitalRead(i);
    lcd.print(state);
    if (state != dig_input_state[i]) {
      if (i == 5) {
        if (state) {
          while (!sendCmd("page1.tCH5.bco=1024"));
          while (!sendCmd("page1.tCH5.pco=65535"));
        } else {
          while (!sendCmd("page1.tCH5.bco=50712"));
          while (!sendCmd("page1.tCH5.pco=0"));
        }
      }
      if (i == 6) {
        if (state) {
          while (!sendCmd("page1.tCH6.bco=1024"));
          while (!sendCmd("page1.tCH6.pco=65535"));
        } else {
          while (!sendCmd("page1.tCH6.bco=50712"));
          while (!sendCmd("page1.tCH6.pco=0"));
        }
      }
      if (i == 7) {
        if (state) {
          while (!sendCmd("page1.tCH7.bco=1024"));
          while (!sendCmd("page1.tCH7.pco=65535"));
        } else {
          while (!sendCmd("page1.tCH7.bco=50712"));
          while (!sendCmd("page1.tCH7.pco=0"));
        }
      }
      if (i == 8) {
        if (state) {
          while (!sendCmd("page1.tCH8.bco=1024"));
          while (!sendCmd("page1.tCH8.pco=65535"));
        } else {
          while (!sendCmd("page1.tCH8.bco=50712"));
          while (!sendCmd("page1.tCH8.pco=0"));
        }
      }
      dig_input_state[i] = state;
    }
  }

  // ANALOG CH1-4 inputs: if state has changed, send command to display
  for (int i = 1; i <= 4; i++) {
    int reading = Indio.analogRead(i);  // returns percentage, cast to integer
    reading = constrain(reading, 0, 100);  // percentage
    lcd.setCursor(0, 3 + i);
    lcd.print("ANA CH");
    lcd.print(i);
    lcd.print(":");
    lcd.print(reading);
    lcd.print("% ");
    if (reading != ana_input_state[i]) {
      if (i == 1) {
        while(!sendCmd("page2.jCH1.val=" + String(reading)));
        while(!sendCmd("page2.tCH1.txt=\"" + String(reading) + "%\""));
      }
      if (i == 2) {
        while(!sendCmd("page2.jCH2.val=" + String(reading)));
        while(!sendCmd("page2.tCH2.txt=\"" + String(reading) + "%\""));
      }
      if (i == 3) {
        while(!sendCmd("page2.jCH3.val=" + String(reading)));
        while(!sendCmd("page2.tCH3.txt=\"" + String(reading) + "%\""));
      }
      if (i == 4) {
        while(!sendCmd("page2.jCH4.val=" + String(reading)));
        while(!sendCmd("page2.tCH4.txt=\"" + String(reading) + "%\""));
      }
      ana_input_state[i] = reading;
    }
  }
  delay(100);
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

// this is similar to the sendCommand() function in the library, but that returns void
// this function returns true if the display sends success ACK (0x01 0xFF 0xFF 0xFF)
boolean sendCmd(String cmd) {

  while (Serial1.available()) Serial1.read(); // clean out input buffer

  SerialUSB.print(">>> ");
  SerialUSB.print(cmd);
  Serial1.print(cmd);
  Serial1.write(0xFF);
  Serial1.write(0xFF);
  Serial1.write(0xFF);

  byte reply[4] = {0};
  int i = 0;
  unsigned long ts = millis();
  while (Serial1.available() || millis() - ts < 100) {
    if (Serial1.available()) {
      reply[i] = Serial1.read();
      //SerialUSB.print(reply[i], HEX);
      //SerialUSB.print(" ");
      i++;
    }
    if (i == 4) {
      //SerialUSB.println();
      break;
    }
  }
  if (reply[0] == 1 && reply[1] == 0xFF && reply[2] == 0xFF && reply[3] == 0xFF) {
    SerialUSB.println(" OK");
    return true;
  }
  else {
    SerialUSB.println(" fail");
    return false;
  }
}