NORVI HMI GUI Design for I/O and MODBUS

by NORVI Controllers in Circuits > Electronics

115 Views, 0 Favorites, 0 Comments

NORVI HMI GUI Design for I/O and MODBUS

PZY_2987 cop (2).jpg
PZY_2994 (1).jpg
8.JPG
9.JPG
Product Drawing.jpg

This comprehensive guide provides a step-by-step approach to designing a GUI using SquareLine Studio. The aim is to seamlessly control inputs and outputs, as well as to write the coil values of transistor outputs, specifically for the NORVI ESP32 HMI.

In this guide integrated GUI consisting of three distinct screens:

  • Input Control Screen - This screen allows users to manage and monitor various input signals.
  • Output Control Screen - This screen is dedicated to controlling and observing output signals.
  • MODBUS Output Control Screen - This screen provides functionalities to write and manage the coil values of the transistor outputs with the MODBUS protocol.

Each screen will be designed to offer a user-friendly interface, ensuring ease of use and efficient control.

Installing SquareLine Studio

Start by downloading the software from the official SquareLine Studio website, and selecting the version compatible with the operating system. Once the download is complete, proceed with the installation process by executing the downloaded file. 

After a successful installation, open the SquareLine Studio software. A pop-up window will appear, where to select "Create/Arduino/Arduino with TFT_eSPI" to initiate the Arduino project.

 To customize settings, navigate to the options provided below. When choosing the LVGL version, please refer to the downloaded library within Arduino.

After confirming the LVGL version, select " Create " in the lower right corner, which will unveil the following interface.

There is a series of widgets organized in the left-hand widget column. These selections include basic widgets (ARC, buttons, image, label...), control widgets (calendar, keyboard, slider...), and visualization widgets (bars, charts, and spinner). Additionally, a category dedicated to screens can be found.

Download the example program

Download the LVGL Demo folder for the libraries, and test programs from the link. Find the example programs from the downloaded LVGL Demo/NORVI Squareline demo folder.

  • ESP32_Display_Squareline_Analog_Demo - ESP HMI GUI design for Analog Inputs 
  • ESP32_Display_Squareline_Digi_Demo - ESP HMI GUI design for Digital Inputs 
  • ESP32_Display_Squareline_Modbus io_Demo - Complete example program of this guide.

Adding supported libraries

Download the LVGL Demo libraries folder for the essential libraries.

Copy the libraries from the downloaded LVGL Demo folder into the Arduino/library folder. 

If there is already a downloaded library file, please replace it with this library file.

Create a screen for digital inputs and analog inputs.

Creating a screen with SquareLine Studio allows users to manage and monitor various input signals. The screen has to be configured as,

When the digital inputs are in the OFF state, the GPIO goes HIGH, and when the input is in the ON state, the GPIO goes LOW.

When the analog inputs are given the analog inputs values are changed according to the supply voltage or current.

  • Incorporate the widgets and utilize the Inspector Bar to configure the widget's style settings, tailoring its appearance to suit your design preferences.
  • Rename the widgets.

Create a screen for transistor outputs

To create a screen to seamlessly control the transistor outputs. Activate the LED when the "ON" button is pressed and deactivate it when the "OFF" button is selected. Please refer to the SQUARELINE STUDIOS FOR NORVI ESP32 HMI GUI DESIGN.

Create a screen for write coils (MODBUS protocol)

The process of NORVI ESP32 HMI acting as a MODBUS master and the interaction between the NORVI device (MODBUS Slave) and NORVI ESP32 HMI through the MODBUS protocol, please refer to the NORVI HMI WITH MODBUS PROTOCOL document. This document examines how to write the coil values of the transistor outputs from the NORVI ESP32 HMI and store the values in the NORVI device (MODBUS Slave).

  • In this project these three parts are added as three separate screens for one GUI design.
  • Create a new folder anywhere on your computer and rename it. Let's call it the SQ folder.
  • Select the File/Project Settings and make the changes in the pop-up window.
  • Browse, file export path, and call functions export file type and select the created SQ folder.
  • After making the changes, proceed by selecting "APPLY CHANGES" located in the lower right corner.
  • Then select File/Save to save the project file and Export/Export UI Files to export the project.

Pre-requisites.

Copy the ESP32_Display_Squareline_Demo folder from the downloaded LVGL Demo/NORVI Squareline demo folder and paste it to the location of the created SQ project folder.

Remove all existing files from the ESP32_Display_Squareline_Demo folder in the location of the created SQ project folder, without the ESP32_Display_Squareline_Demo.ino file and the touch.h file.

Copy all the files in the created SQ project folder and paste them to the ESP32_Display_Squareline_Demo folder in the SQ project folder. 

This ESP32_Display_Squareline_Demo.ino file must have some modifications.

For that, open the ESP32_Display_Squareline_Demo.ino file in the SQ project folder. 

Also, open the HMI_DIGITAL.ino, HMI_ANALOG.ino, HMI_OUTPUT.ino and MASTER_TRANS_HMI.ino files in the LVGL Demo/NORVI Squareline demo folder.

  • HMI_DIGITAL - Digital Inputs control code for NORVI HMI.
  • HMI_ANALOG - Analog Inputs control code for NORVI HMI.
  • HMI_OUTPUT - Outputs control code for NORVI HMI.
  • MASTER_TRANS_HMI - MODBUS master code for NORVI HMI.

Without the inside part of the void loop() add all the parts of the HMI_DIGITAL.ino, HMI_ANALOG.ino, HMI_OUTPUT.ino and MASTER_TRANS_HMI.ino files to the ESP32_Display_Squareline_Demo.ino file to the correct places.

Then select the ui_events.c in ESP32_Display_Squareline_Demo.ino file and copy all event functions and paste it in the main code, (before the void setup() function).

Then update that coding part as below. These are for the transistor Outputs and write Modbus commands.

//control the outputs

void OUT1_ON(lv_event_t * e)
{
  writeOutput(GPIO5, 1);
}

void OUT1_OFF(lv_event_t * e)
{
  writeOutput(GPIO5, 0);
}

void OUT3_ON(lv_event_t * e)
{
  writeOutput(GPIO7, 1);
}

void OUT3_OFF(lv_event_t * e)
{
  writeOutput(GPIO7, 0);
}

void OUT2_ON(lv_event_t * e)
{
  writeOutput(GPIO6, 1);
}

void OUT2_OFF(lv_event_t * e)
{
  writeOutput(GPIO6, 0);
}

void OUT4_ON(lv_event_t * e)
{
  writeOutput(GPIO8, 1);
}

void OUT4_OFF(lv_event_t * e)
{
  writeOutput(GPIO8, 0);
}

//write the modbus commands

uint8_t result;
void button1_on(lv_event_t * e) {
  LED1 = 1;
  writeOutput(GPIO5, 1);
  result = node.writeSingleCoil(0x00001, LED1);
}

void button1_off(lv_event_t * e) {
  LED1 = 0;
  writeOutput(GPIO5, 0);
  result = node.writeSingleCoil(0x00001, LED1);
}

void button2_on(lv_event_t * e) {
  LED2 = 1;
  writeOutput(GPIO6, 1);
  result = node.writeSingleCoil(0x00002, LED2);
}

void button2_off(lv_event_t * e) {
  LED2 = 0;
  writeOutput(GPIO6, 0);
  result = node.writeSingleCoil(0x00002, LED2);
}

void button3_on(lv_event_t * e) {
  LED3 = 1;
  writeOutput(GPIO7, 1);
  result = node.writeSingleCoil(0x00003, LED3);
}

void button3_off(lv_event_t * e) {
  LED3 = 0;
  writeOutput(GPIO7, 0);
  result = node.writeSingleCoil(0x00003, LED3);
}

void button4_on(lv_event_t * e) {
  LED4 = 1;
  writeOutput(GPIO8, 1);
  result = node.writeSingleCoil(0x00004, LED4);
}

void button4_off(lv_event_t * e) {
  LED4 = 0;
  writeOutput(GPIO8, 0);
  result = node.writeSingleCoil(0x00004, LED4);
}

Comment the full code of the ui_events.c.

Then update the void loop() function with the below coding part. These are for the Digital Inputs and Analog Inputs.

void loop() {
  uint8_t input = readInput();
  bool inputValues[NUM_INPUT_PINS];
  for (int i = 0; i < NUM_INPUT_PINS; i++) {
    inputValues[i] = bitRead(input, i);
    Serial.print(inputValues[i]);
  }
  Serial.println();

  // Read analog values from ADS1115
  int16_t adc0_2 = ads2.readADC_SingleEnded(0);
  int16_t adc1_2 = ads2.readADC_SingleEnded(1);
  int16_t adc2_2 = ads2.readADC_SingleEnded(2);
  int16_t adc3_2 = ads2.readADC_SingleEnded(3);

  #ifdef USE_UI
    lv_timer_handler();
    delay(5);

    // Assuming you have labels with names Dvalue1, Dvalue2, Dvalue3, and Dvalue4
    lv_label_set_text_fmt(ui_Dvalue1, "%d", inputValues[0]);
    lv_label_set_text_fmt(ui_Dvalue2, "%d", inputValues[1]);
    lv_label_set_text_fmt(ui_Dvalue3, "%d", inputValues[2]);
    lv_label_set_text_fmt(ui_Dvalue4, "%d", inputValues[3]);

    // Assuming you have labels with names Avalue1, Avalue2, Avalue3, and Avalue4
    lv_label_set_text_fmt(ui_Avalue1, "%d", adc0_2);
    lv_label_set_text_fmt(ui_Avalue2, "%d", adc1_2);
    lv_label_set_text_fmt(ui_Avalue3, "%d", adc2_2);
    lv_label_set_text_fmt(ui_Avalue4, "%d", adc3_2);
  #endif

Understanding the Test program

Get the Input values, control Outputs and MODBUS write coil values program for Norvi HMI.

Download the complete example program.

Add the required libraries.

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_ADS1X15.h>
#include <ModbusMaster.h>
#include <Arduino_GFX_Library.h>
#include <lvgl.h>

Defines the I2C data line (SDA) pin as 19 and clock line (SCL) pin as 20.

#define SDA 19
#define SCL 20

Creates an instance of the Adafruit ADS1115 ADC.

Adafruit_ADS1115 ads2;

Definitions for pins such as PCA9538 address, and register addresses.

// I2C address of PCA9538
#define PCA9538_ADDR 0x73

// PCA9538 register addresses
#define PCA9538_INPUT_REG 0x00
#define PCA9538_OUTPUT_REG 0x01
#define PCA9538_POLARITY_REG 0x02
#define PCA9538_CONFIG_REG 0x03

Definitions for FC, RX_PIN, TX_PIN, GPIOs, and the number of output pins.

#define FC    -1
#define RX_PIN 48 
#define TX_PIN 2

Definitions GPIOs for PCA9538 and number of digital input pins as 4.

#define GPIO5 0x10
#define GPIO6 0x20
#define GPIO7 0x40
#define GPIO8 0x80

#define GPIO1 0x01
#define GPIO2 0x02
#define GPIO3 0x04
#define GPIO4 0x08

#define NUM_INPUT_PINS 4

Functions to interact with the PCA9538 GPIO expander chip.

  • setPinMode: Sets the mode (input/output) for a GPIO pin.
  • writeOutput: Writes a value (HIGH/LOW) to a GPIO pin.
  • readInput: Reads the input value from the GPIO expander.
  • readRegister: Reads a register from the PCA9538 chip.
  • writeRegister: Writes a value to a register on the PCA9538 chip.
void setPinMode(uint8_t pin, uint8_t mode) {
  uint8_t config = readRegister(PCA9538_CONFIG_REG);
  if (mode == INPUT) {
    config |= pin;
  }
else {
    config &= ~pin;
  }
  writeRegister(PCA9538_CONFIG_REG, config);
}

void writeOutput(uint8_t pin, uint8_t value) {
  uint8_t output = readRegister(PCA9538_OUTPUT_REG);
  if (value == LOW) {
    output &= ~pin;
  }
else {
    output |= pin;
  }
  writeRegister(PCA9538_OUTPUT_REG, output);
}

uint8_t readInput() {
  return readRegister(PCA9538_INPUT_REG);
}

uint8_t readRegister(uint8_t reg) {
  Wire.beginTransmission(PCA9538_ADDR);
  Wire.write(reg);
  Wire.endTransmission(false);
  Wire.requestFrom(PCA9538_ADDR, 1);
  return Wire.read();
}

void writeRegister(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(PCA9538_ADDR);
  Wire.write(reg);
  Wire.write(value);
  Wire.endTransmission();
}

Variables for storing LED states and a ModbusMaster instance.

ModbusMaster node;
int LED1;
int LED2;
int LED3;
int LED4;

Callback functions for Modbus transmission.

void preTransmission()
{
  digitalWrite(FC, 1);
}

void postTransmission()
{
  digitalWrite(FC, 0);
}

Configures the display panel using the Arduino GFX library.
Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel(
  GFX_NOT_DEFINED /* CS */, GFX_NOT_DEFINED /* SCK */, GFX_NOT_DEFINED /* SDA */,
    4 /* DE */, 5 /* VSYNC */, 6 /* HSYNC */, 7 /* PCLK */,
    1 /* R0 */, 41 /* R1 */, 40 /* R2 */, 38 /* R3 */, 45 /* R4 */,
    47 /* G0 */, 21 /* G1 */, 14 /* G2 */, 9 /* G3 */, 3 /* G4 */, 3 /* G5 */,
    8 /* B0 */, 18 /* B1 */, 17 /* B2 */, 16 /* B3 */, 15 /* B4 */
);

Arduino_RPi_DPI_RGBPanel *lcd = new Arduino_RPi_DPI_RGBPanel(
bus, 800 /* width */, 0 /* hsync_polarity */, 210 /* hsync_front_porch */, 30 /*hsync_pulse_width */,
16 /* hsync_back_porch */,480 /* height */, 0 /* vsync_polarity */, 22 /* vsync_front_porch */,
13 /* vsync_pulse_width */, 10 /* vsync_back_porch */,1 /* pclk_active_neg */, 16000000 /* prefer_speed */,
true /* auto_flush */);

Configures the touch panel and LVGL for the graphical user interface.

#include "touch.h"

#ifdef USE_UI
/* Change to your screen resolution */
static uint32_t screenWidth;
static uint32_t screenHeight;
static lv_disp_draw_buf_t draw_buf;
static lv_color_t disp_draw_buf[800 * 480 / 10];      //5,7inch: lv_color_t disp_draw_buf[800*480/10]            4.3inch: lv_color_t disp_draw_buf[480*272/10]
//static lv_color_t disp_draw_buf;
static lv_disp_drv_t disp_drv;

/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
  uint32_t w = (area->x2 - area->x1 + 1);
  uint32_t h = (area->y2 - area->y1 + 1);

#if (LV_COLOR_16_SWAP != 0)
  lcd->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#else
  lcd->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#endif

  lv_disp_flush_ready(disp);
}

void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
{
  if (touch_has_signal())
  {
    if (touch_touched())
    {
      data->state = LV_INDEV_STATE_PR;
      /*Set the coordinates*/
      data->point.x = touch_last_x;
      data->point.y = touch_last_y;
      Serial.print( "Data x :" );
      Serial.println( touch_last_x );

      Serial.print( "Data y :" );
      Serial.println( touch_last_y );
    }
    else if (touch_released())
    {
      data->state = LV_INDEV_STATE_REL;
    }
  }
  else
  {
    data->state = LV_INDEV_STATE_REL;
  }
  delay(15);
}
#endif

Functions for handling button events and controlling outputs via Modbus communication.

void OUT1_ON(lv_event_t * e)
{
  writeOutput(GPIO5, 1);
}

void OUT1_OFF(lv_event_t * e)
{
  writeOutput(GPIO5, 0);
}

void OUT3_ON(lv_event_t * e)
{
  writeOutput(GPIO7, 1);
}

void OUT3_OFF(lv_event_t * e)
{
  writeOutput(GPIO7, 0);
}

void OUT2_ON(lv_event_t * e)

{
  writeOutput(GPIO6, 1);
}

void OUT2_OFF(lv_event_t * e)
{
  writeOutput(GPIO6, 0);
}

void OUT4_ON(lv_event_t * e)
{
  writeOutput(GPIO8, 1);
}

void OUT4_OFF(lv_event_t * e)
{
  writeOutput(GPIO8, 0);
}

uint8_t result;
void button1_on(lv_event_t * e)
{
  LED1 = 1;
  writeOutput(GPIO5, 1);
  result = node.writeSingleCoil(0x00001, LED1);
}

void button1_off(lv_event_t * e)
{
  LED1 = 0;
  writeOutput(GPIO5, 0);
  result = node.writeSingleCoil(0x00001, LED1);
}

void button2_on(lv_event_t * e)
{
  LED2 = 1;
  writeOutput(GPIO6, 1);
  result = node.writeSingleCoil(0x00002, LED2);
}

void button2_off(lv_event_t * e)
{
  LED2 = 0;
  writeOutput(GPIO6, 0);
  result = node.writeSingleCoil(0x00002, LED2);
}

void button3_on(lv_event_t * e)
{
  LED3 = 1;
  writeOutput(GPIO7, 1);
  result = node.writeSingleCoil(0x00003, LED3);
}

void button3_off(lv_event_t * e)
{
  LED3 = 0;
  writeOutput(GPIO7, 0);
  result = node.writeSingleCoil(0x00003, LED3);
}

void button4_on(lv_event_t * e)

{
  LED4 = 1;
  writeOutput(GPIO8, 1);
  result = node.writeSingleCoil(0x00004, LED4);
}

void button4_off(lv_event_t * e)
{
  LED4 = 0;
  writeOutput(GPIO8, 0);
  result = node.writeSingleCoil(0x00004, LED4);
}

Setup function:

Initializes serial communication, Modbus, display, touch panel, and I2C communication. Sets up pin modes for various GPIO pins and initializes the ADS1115.

void setup()
{
  Serial.begin(9600);
  Serial.println("LVGL Widgets Demo");

  Serial1.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN);
  node.begin(1, Serial1);                           //Slave ID as 1
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);

#if defined(Display_50) || defined(Display_70)
  
//IO Port Pins
  pinMode(38, OUTPUT);
  digitalWrite(38, LOW);
  pinMode(17, OUTPUT);
  digitalWrite(17, LOW);
  pinMode(18, OUTPUT);
  digitalWrite(18, LOW);
  pinMode(42, OUTPUT);
  digitalWrite(42, LOW);

#elif defined(Display_43)
  pinMode(20, OUTPUT);
  digitalWrite(20, LOW);
  pinMode(19, OUTPUT);
  digitalWrite(19, LOW);
  pinMode(35, OUTPUT);
  digitalWrite(35, LOW);
  pinMode(38, OUTPUT);
  digitalWrite(38, LOW);
  pinMode(0, OUTPUT);//TOUCH-CS
#endif
  // Init Display
  lcd->begin();
  lcd->fillScreen(BLACK);
  lcd->setTextSize(2);
  delay(200);

#ifdef USE_UI
  lv_init();
  delay(100);
  touch_init();

  screenWidth = lcd->width();
  screenHeight = lcd->height();

  lv_disp_draw_buf_init(&draw_buf, disp_draw_buf, NULL, screenWidth * screenHeight / 10);
  //  lv_disp_draw_buf_init(&draw_buf, disp_draw_buf, NULL, 480 * 272 / 10);
  /* Initialize the display */
  lv_disp_drv_init(&disp_drv);
  /* Change the following line to your display resolution */
  disp_drv.hor_res = screenWidth;
  disp_drv.ver_res = screenHeight;
  disp_drv.flush_cb = my_disp_flush;
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register(&disp_drv);

  /* Initialize the (dummy) input device driver */
  static lv_indev_drv_t indev_drv;
  lv_indev_drv_init(&indev_drv);
  indev_drv.type = LV_INDEV_TYPE_POINTER;
  indev_drv.read_cb = my_touchpad_read;
  lv_indev_drv_register(&indev_drv);
#endif

#ifdef TFT_BL
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);
#endif

#ifdef USE_UI
  ui_init();//ui from Squareline or GUI Guider
#else
  lcd->fillScreen(RED);
  delay(800);
  lcd->fillScreen(BLUE);
  delay(800);
  lcd->fillScreen(YELLOW);
  delay(800);
  lcd->fillScreen(GREEN);
  delay(800);
#endif
  Serial.println( "Setup done" );

  Wire.begin(SDA, SCL);
  setPinMode(GPIO1, INPUT);
  setPinMode(GPIO2, INPUT);
  setPinMode(GPIO3, INPUT);
  setPinMode(GPIO4, INPUT);

  // Initialize the ADS1115
  if (!ads2.begin(0x49)) {
    Serial.println("Failed to initialize ADS 1 .");
    // while (1);
  }

  // Set the gain for ADS1115
  ads2.setGain(GAIN_ONE);

  setPinMode(GPIO5, OUTPUT);
  setPinMode(GPIO6, OUTPUT);
  setPinMode(GPIO7, OUTPUT);
  setPinMode(GPIO8, OUTPUT);
}

Loop Function:

Reads input values, reads analog values from the ADS1115, and updates the UI accordingly.

void loop() {
  uint8_t input = readInput();
  bool inputValues[NUM_INPUT_PINS];
  for (int i = 0; i < NUM_INPUT_PINS; i++) {
    inputValues[i] = bitRead(input, i);
    Serial.print(inputValues[i]);
  }

  Serial.println();

  // Read analog values from ADS1115
  int16_t adc0_2 = ads2.readADC_SingleEnded(0);
  int16_t adc1_2 = ads2.readADC_SingleEnded(1);
  int16_t adc2_2 = ads2.readADC_SingleEnded(2);
  int16_t adc3_2 = ads2.readADC_SingleEnded(3);

  #ifdef USE_UI
    lv_timer_handler();
    delay(5);

  // Assuming you have labels with names Dvalue1, Dvalue2, Dvalue3, and Dvalue4
    lv_label_set_text_fmt(ui_Dvalue1, "%d", inputValues[0]);
    lv_label_set_text_fmt(ui_Dvalue2, "%d", inputValues[1]);
    lv_label_set_text_fmt(ui_Dvalue3, "%d", inputValues[2]);
    lv_label_set_text_fmt(ui_Dvalue4, "%d", inputValues[3]); 

  // Assuming you have labels with names Avalue1, Avalue2, Avalue3, and Avalue4
    lv_label_set_text_fmt(ui_Avalue1, "%d", adc0_2);
    lv_label_set_text_fmt(ui_Avalue2, "%d", adc1_2);
    lv_label_set_text_fmt(ui_Avalue3, "%d", adc2_2);
    lv_label_set_text_fmt(ui_Avalue4, "%d", adc3_2);

  #endif
}

These lines are responsible for updating the text displayed on the screen, showing the current state of the digital inputs and the current readings of the analog inputs.

// Assuming you have labels with names Dvalue1, Dvalue2, Dvalue3, and Dvalue4
    lv_label_set_text_fmt(ui_Dvalue1, "%d", inputValues[0]);
    lv_label_set_text_fmt(ui_Dvalue2, "%d", inputValues[1]);
    lv_label_set_text_fmt(ui_Dvalue3, "%d", inputValues[2]);
    lv_label_set_text_fmt(ui_Dvalue4, "%d", inputValues[3]);

// Assuming you have labels with names Avalue1, Avalue2, Avalue3, and Avalue4
    lv_label_set_text_fmt(ui_Avalue1, "%d", adc0_2);
    lv_label_set_text_fmt(ui_Avalue2, "%d", adc1_2);
    lv_label_set_text_fmt(ui_Avalue3, "%d", adc2_2);
    lv_label_set_text_fmt(ui_Avalue4, "%d", adc3_2);