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.
/* 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; } }