The Sunday Blog: Advanced programming

Parsing files from SD card

Sometimes, we’d like to change static content of our Nextion HMI, be it text or colors on the screen, be it to initialize variables or to control variants in our program. And all – preferably – without compiling and uploading a new tft file. Why not use the SD card, save a configuration file on it, let the Nextion parse it and adapt its program at the next startup? That’s possible with the FileStream component which exists for the Intelligent (P) series Nextion HMI displays!

A common use case

You have for sure already seen .ini files. These are text files where each line consists of a optionName:value pair. Our goal today is to let the Nextion HMI read and parse such a file, adding dynamically all option names in a ComboBox. Afterwards, when we select an option with it, we want to display the corresponding value for demo purposes. After having understood how that works, you’ll be able to extend this demo for your own purposes, using the optionName:value pairs to initialize variables, set screen text and colors – yes, localization can also be done this way -, or use the ini data as parameters to control other peripherals over the serial port. The strategy is to split the file into single lines by using the “\r” line ending as a separator, and then each line, this time with the “:” colon to separate the option name from the corresponding value.

Many ways lead to Rome

The FileStream Component is something like a simplified file object as we know it for example from PHP. Simplified – with respect to the embedded MCU – means that we can use the open(), close(), read(), write(), and find() methods with the actual r/w pointer in the .val attribute of the component. You’d perhaps think that a way for our task is to open the file, to find the first “:”, to read everything before after having calculated the length in bytes, to store it as the first option name, then find the first line ending “\r”, to read everything between the colon and the line ending after – again – having calculated the length in bytes, store this as the first value, and so on. Sounds complicated since it involves using find() which will – when successful – position the read pointer behind the content we are looking for, so that we have to calculate the number of bytes to read by subtracting the byte address of the last find, minus 1 byte when it was a colon and 2 bytes when it was a line ending, from the current pointer value. Now that we have the number of bytes to read, we have to calculate the start address for our read, which is our actual pointer address minus the calculated length. Are you still following? This approach is probably prone to giving you a headache and some endless debug sessions…

Fortunately, there is another way, thanks to the fact that the Intelligent series has plenty of memory. So, we might in a first step use a Text Variable component fb (for “file buffer”) with its txt_maxl attribute set to enough characters (1024 in today’s demo project) to read the file sequentially byte by byte into this text variable. Here, parsing the text is much easier: With incrementing the line counter starting from zero, we can extract line by line into another Text Variable lb (for “line buffer”): spstr fb.txt,lb.txt,”\r”,lcount. Now, we need only to check if the text length in lb is > 0, which means we got something. If the returned length is zero, the line is empty which we’ll need to stop our iteration. At this moment, the numeric lcount variable contains exactly the number of (valid) lines. Inside the loop, where we see only the current line, we can use spstr lb.txt,sopt.txt,”:”,0 to retrieve the option name, and spstr lb.txt,sval.txt,”:”,1.

The demo project

A single page project, as usual: An empty ComboBox cb0 will be populated with the option names, a Text component t1 will then display the corresponding value. Another Text component t0 serves for status or error messages, and that’s it. To populate the ComboBox, we simply need to concatenate all the option names in the order of reading with “\r”s to a multiline string olist and set it as the .path attribute of the ComboBox. When we’ll select for example the third option from it at runtime, its .val attribute will hold the ordered number of the selected option (2, since counting starts at zero). Now, we’ve been smart and we thought of concatenating all parsed values in a similar way with “\r”s so a multiline string vlist, which allows us – with spstr vlist.txt,t1.txt,”\r”,cb0.val – to display the corresponding value with this one-liner in the ComboBox’s TouchRelease event. Hint: That’s an elegant way to work with array-like structures, not only in this context!

All numeric variables have been defined in program.s:

int fsize,lcount,ccount
page 0                       //Power on start page 0

The text variables are defined as components on page 0. The main task as described above happens in the page’s PostInitialize event:

if(fs0.open("sd0/inidemo.txt")==1)
{
  // read the file sequentially, byte for byte, into file buffer
  while(fs0.read(fb.txt,fs0.val,1)==1)
  {
  }
  fs0.close()
  // count the length in bytes and format the output
  btlen fb.txt,fsize
  covx fsize,lb.txt,0,0
  lb.txt+=" bytes in "
  t0.txt=lb.txt
  // initialize the line counter, extract the first line and find its length
  lcount=0
  spstr fb.txt,lb.txt,"\r",lcount
  btlen lb.txt,ccount
  // continue while the line length is > 0
  while(ccount>0)
  {
    // extract the option name
    spstr lb.txt,sopt.txt,":",0
    // extract the value
    spstr lb.txt,sval.txt,":",1
    // concatenate option names and values separately
    if(lcount>0)
    {
      olist.txt+="\r"
      vlist.txt+="\r"
    }
    olist.txt+=sopt.txt
    vlist.txt+=sval.txt
    lcount++
    spstr fb.txt,lb.txt,"\r",lcount
    btlen lb.txt,ccount
  }
  // take the number of lines and format the output
  covx lcount,lb.txt,0,0
  lb.txt+=" lines read\r"
  t0.txt+=lb.txt
  // populate the combo box
  cb0.path=olist.txt
}else
{
  t0.txt="Could not open the file!"
}

Now, there only remains the one-liner in cb0::TouchRelease to display the corresponding option:

spstr vlist.txt,t1.txt,"\r",cb0.val

… and we are almost done! We just need still the inidemo.txt file containing a network configuration example to copy onto the SD card:

ip:192.168.1.23
mask:255.255.255.0
router:192.168.1.1
dns1:8.8.8.8
dns2:8.8.4.4

As usual, you don’t have to rewrite or copy/paste everything, the zip archive containing the hmi file and the example txt file for the SD card is here: SD_file_demo

I’ve always an open ear for your questions, critics and comments. Talk to me in the Nextion forums (registration required)!

Happy Nextioning!