The Sunday Blog: Understanding and Customizing HMI components

Part 7: Design a new component from scratch – the bar graph display

Wouldn’t it be nice for several applications to have a bar graph display on our NEXTION HMI? Ideally, it should be scalable, which means that it would have a variable number of data channels (bars), and variable position, height, and width. To make it look still more professional, it should allow setting the background color, the color of the bars, the spacing between the bars, the border width, and the maximum value, so that incoming data to display would be automatically scaled. That’s a long list of specifications and we can’t find such a component in the Nextion Editor. Time to write some code!

The public attributes

Were it a generic component, the properties listed above would appear in the Nextion Editor’s attribute pane and be used for initialization of the component when the containing page is loaded. We emulate this behavior in several steps. First, we declare the attribute variables globally in the program.s file, which will allow us re-using these on multiple pages:

// Define the bargraph's static attributes:
int bg_x,bg_y,bg_w,bg_h // the component's position and dimensions
int bg_bco,bg_pco       // background color and plot color
int bg_ch               // number of channels
int bg_spax             // spacing between the bars
int bg_bdt              // border width
int bg_maxval           // maximum value a bar can take for scaling

The internal static variables

A component needs two types of internal variables. First, a set of pre-calculated values which don’t change when we send new values to display but which can boost the execution speed since they are calculated only once when the page is loaded: We want our component to have a border, so that it looks more clean and professional. Though, we need to get the usable drawing area inside the component’s border because all bar drawing will be referenced to this and we won’t have to re-calculate the “true” x, y, h, and w each time we refresh our component with new data. After that, we have to divide up the drawing space: Displaying n bars means having (n-1) spaces in-between. Subtract these spaces from the usable width and you have the space left for the n bars, which, after division by n, will give us the width of a bar. For drawing all bars in a loop, their x start coordinate will advance in steps, which are logically the width of a bar plus the width of the space. To prevent confusion and errors, we name all these “internals” with a leading underscore:

// Declare the bargraph's internal static variables:
int _bg_dx,_bg_dy,_bg_dw,_bg_dh // the drawing area without the borders
int _bg_sw // the overall space used for spacing inbetween the bars (helper var)
int _bg_bw // the static width of a single bar
int _bg_aw // the static width of a single bar plus its right neighbored spacing

The dynamic drawing variables

What changes during a refresh with new data? The height of each bar, its x and y position, and the loop variable. But that’s not all! What would happen if we overpainted a longer bar  with a shorter one? We would still see the longer one, thus, we have to clear out the space above each bar. Refreshing the whole background would be too time consuming and lead to flicker. Remember, on our Nextion HMIs, the y coordinates go from top to bottom. But we want our bars drawn from bottom to top. Thus, we’ll simply draw two rectangles for each bar, one from the component’s top to the bar’s top using the background color (bco), and then from there to the bottom our bar using the plot color (pco). Thus we need to add a variable for the height of the upper invisible bar:

// Declare the dynamic drawing variables:
int _bg_ci                  // the channel index as loop variable
int _bg_bix,_bg_biy,_bg_bih // the current bar's geometry (the width is static)
int _bg_eih // the height of the empty space above the bar for clearing out

Remember to leave the “page 0” command afterwards in the program.s file!

Coding the init and refresh functions

For the ready-to-use components which come with the Nextion’s firmware, the init process is invisible and called when the containing page is loaded, after retrieving the values set in the attribute pane. We start with the latter and put our attributes setup into the page’s Postinitialize Event. Then, we call the init code which will be hidden in the Touch Release Event code of a very small, almost hidden hotspot.

// Set the bargraph component's attributes:
// trigger its initialization:
click bg_trig,0

And here is the initialization code in the hotspot’s Touch Release Event. It calculates first the x and y coordinates of the drawing area by adding the border width to the original coordinates, then the height and the width by subtracting the border width twice from the original values. Then, the bar width and the step width are calculated as described above, and finally, the background rectangle is drawn.

// Initialize the internal variables
fill bg_x,bg_y,bg_w,bg_h,bg_bco // draw background

Now, we need still a way to tell our component the values of the individual bars. This is simply done by setting a comma separated list of values to a string variable. Since the latter can’t be declared in the program.s file, we put a String variable component with a txt_maxl of 50 on our page and name it bg_data. That way, we can for example send bg_data.txt=”15,35,70,80,45″ from our MCU (Arduino) and we are almost done. Our component has to refresh with the new data afterwards. The code will be stored in the Touch Press Event of our previously used hotspot. Thus, after setting bg_data.txt with the desired value list, we’ll have to send “click bg_trig,1” to update the display.

The spstr function will allow us to extract single values from our comma separated list, but the extracted value will still be a string which we’ll store temporarily in another String variable component named _bg_cxs, with again a leading underscore because it’s internal. From there, we use the covx function to convert it into an integer. The rest is trivial as I like to say: Scaling the integer to the bar height, subtract that from the overall height to get the empty height above, draw a bco rectangle from top downwards and the bar over the remaining height afterwards. All that in a loop to go through all bars and values – ready! The final doevents command will make sure that the drawing is completed before the next data set is processed.

// refresh the component from bg_data;
for(_bg_ci=0;_bg_ci<bg_ch;_bg_ci++)        // for each channel...
spstr bg_data.txt,_bg_cxs.txt,",",_bg_ci   // extract value as text
covx _bg_cxs.txt,_bg_bih,0,0               // convert to integer
_bg_bih=_bg_bih*_bg_dh/bg_maxval           // scale bar height
_bg_eih=_bg_dh-_bg_bih                     // compute remaining height
_bg_bix=_bg_ci*_bg_aw+_bg_dx               // compute drawing x position
_bg_biy=_bg_dy+_bg_eih                     // compute bar's top y position
fill _bg_bix,_bg_dy,_bg_bw,_bg_eih,bg_bco  // draw from top with bgcolor
fill _bg_bix,_bg_biy,_bg_bw,_bg_bih,bg_pco // and then with the plot color

Now, you don’t need to copy/paste all that code. A ready to use example HMI with a 9 channel bar graph and an animated demo hmi file can, as always, be downloaded from the Nextion forums.