The Nextion MEGA IO project – Part 3

As at the beginning of every big project, things seem to advance slowly at the beginning. But if everything is well thought in advance and all the foundations well set, the speed of progress will increase afterwards as you’ll see soon! After we defined a very compact an efficient protocol in Part 1 and started implementing it in a Nextion Demo project in Part 2, we need now the counterpart on the Arduino Mega side. That’s our today’s topic.

To keep our Arduino sketch as “clean” as possible, I decided to pack everything in a single .h file, so that it can be included like a library. Although only one single instance will be needed on each MCU, even when we’ll extend this to up to 8 Arduino Megas in the future, I packed everything in a C++ class, to avoid confusion and conflicts with other parts of your code which you probably want to tinker around our universal IO control. And it will make it easier to extend it in the future.

When going over the Nextion demo project from last week, I found a few omissions. The baud rate to communicate with the Mega was too slow (9600 bauds set in program.s – should be 115200), and the default value of all sliders was 128 which was nice for taking pictures, but they have to start at 0. Thus, please fix that or download the updated .hmi file at the end of this article!

The Arduino Code

The NexMegaIOdemo.ino file shows how easy to use our “Nextion IO driver” is:

#include "NexMegaIO.h"

// Declare a NexMegaIO object with default parameters:
// Nextion on Serial1 (pins 18 and 19), 115200 baud
NexMegaIO NexIO;

void setup() {
  // put your setup code here, to run once:
  NexIO.begin();  // Start the NexMegaIO object
}

void loop() {
  // put your main code here, to run repeatedly:
  while (NexIO.NexPort.available()) {
    // Hand all received bytes over to the object for processing:
    NexIO.process(NexIO.NexPort.read());
  }
}

All the magic happens in the NexMegaIO.h file. There, our NexMegaIO object is defined with a default and a specialized constructor, in case you would use a different Serial port or speed. Then, there are the public methods .begin() to start up the object and .process() to handle all the incoming bytes which are handed over by our code snippet in void loop(). The (finally) used upstream port NexPort to communicate with the Nextion is a public object member, too, so that it can be used in the .ino file. All other data variables are kept isolated in the private storage area of the object since these are only needed for internal purposes. That allows to keep everything extremely compact and clean.

Handling the command byte

The heavy work is done in the .process(uint8_t abyte) function. First of all, the incoming byte is checked for its type – is it a command byte or a data byte? If it’s a command byte, a further distinction happens – is it a read or a write command? If it’s read, we know that only one data byte will follow, while if it’s write, two data bytes are to be expected before the command sequence is complete and can be processed. Afterwards, all the data handling pointer is reinitialized to make sure that even if the previous command was incomplete for whatever reason, this new command can be correctly processed. Finally, the network address (which we won’t use for the moment) and the data type bits are extracted from the identified command byte and stored in the corresponding variables.

Handling the data byte(s)

If an incoming byte is identified as a data byte, the first thing is to check if we are still expecting a data byte, depending on the previous command byte. If not, we’ll simply discard it for the sake of stability. But if yes, it will be stored. Then, there is a check if all the expected data bytes have arrived (1 for a read command, 2 for a write command). If this is not the case, we’ll do nothing but wait for the next data byte. But if yes, we can (finally) process our command.

Processing the command sequence

When all expected data bytes have arrived, we extract the channel bits because these are needed for read and write operations. Then, in both cases (read and write) a switch(_type) allows us to implement individual handlers for the different data types. Today, we have only the PWM parts, but the others will be added consecutively.

The whole thing with explaining comments in the code is here:

#ifndef NexMegaIO_h
#define NexMegaIO_h

class NexMegaIO {
public:
  // Full constructor :
  NexMegaIO(HardwareSerial& uart, uint32_t bauds)
    : _bauds(bauds), NexPort(uart) {}
  // Default constructor :
  NexMegaIO()
    : NexMegaIO(Serial1, 115200) {}
  // To prevent early initialisation conflicts :
  void begin() {
    NexPort.begin(_bauds);
  }
  // Handling incoming bytes
  void process(uint8_t abyte) {
    if (abyte & 0x80) {    // it's a command byte
      if (abyte & 0x40) {  // it's a read command
        _readflag = true;
        _wait4data = 1;  // set the number of expected data bytes following
      } else {           // it's a write command
        _readflag = false;
        _wait4data = 2;  // set the number of expected data bytes following
      }
      _recdata = 0;                    // initialise the data pointer
      _netadr = (abyte & 0x38) >> 3;   // save the network address (actually unused, thus don't care)
      _type = abyte & 0x07;            // save the data type
    } else {                           // it's a data byte
      if (_wait4data > 0) {            // are we expecting data bytes ? If not, don't care
        _rawdata[_recdata++] = abyte;  // save the data byte and increment the pointer afterwards
        _wait4data--;                  // decrement the number of expected data bytes following
        if (_wait4data == 0) {         // we have received the expected number of data bytes and may now process
          _channel = (_rawdata[0] & 0x78) >> 3;
          if (_readflag) {  // handle the read request
            switch (_type) {
              case 0x02:                                                                 // PWM
                NexPort.write(0x82);                                                     // Command byte for write PWM
                NexPort.write((_channel << 3) + (_pwmValues[_channel] & 0x80 ? 1 : 0));  // First data byte
                NexPort.write(_pwmValues[_channel] & 0x7F);                              // Second data byte
                break;
              default:  // do nothing
                break;
            }
          } else {  // handle the write request
            switch (_type) {
              case 0x02:                                                                // PWM
                _pwmValues[_channel] = _rawdata[1] + ((_rawdata[0] & 0x01) ? 128 : 0);  // save the data for further read
                analogWrite(_channel + 2, _pwmValues[_channel]);                        // update the PWM pin
                break;
              default:  // do nothing
                break;
            }
          }
        }
      }
    }
  }

  HardwareSerial& NexPort;
private:
  uint32_t _bauds;
  uint8_t _rawdata[2];
  uint8_t _wait4data = 0;
  uint8_t _recdata = 0;
  uint8_t _netadr;
  uint8_t _type;
  uint8_t _channel;
  uint8_t _ddata;
  uint16_t _adata;
  uint8_t _pwmValues[12] = { 0 };
  bool _readflag;
};
#endif

Here everything which saves you from re-typing everything. First, the updated and fixed .hmi file: mega_io_ext.HMI 2 and second, the .ino and the .h files for the Arduino: NexMegaIOdemo

For today, thanks for reading and happy Nextioning!

Questions, comments, critics, suggestions? Just send me an email to thierry (at) itead (dot) cc! 🙂

And, by the way, if you like what I write, and you are about to order Nextion stuff with Itead, please use my referral link! To you, it won’t make a change for your order. But it will pay me the one or the other beer or coffee. And that will motivate me to write still more interesting blogs 😉