Dropped Events while using library

I started using Jyberg’s Enhanced-Nextion-Library in the hopes that it would make development a little easier, and it works pretty well. I made a few changes on my personal fork to use int32s for numbers, and remove a chunk of code I thought was causing the below the issue.

However, I have a bunch of numeric/xfloat fields I need to keep in sync with the MCU, so I will periodically send the new value to them. As I added more fields, I started to drop events. A button would do the press/release animation on the screen but the MCU wouldn’t always respond to it. This is extra worrisome because my spec has one button doing different things depending on how long its pressed (including a quick safety stop if released before all the events finish).

I tried to reduce the frequency of field updates (only send values when they change), but that doesn’t fully fix the issue. I looked on my logic analyzer, and the delay between sending a new value and the ACK from the Nextion can be upwards of 20 ms, and I think the code is blocking, which is messing with some PID loops, if not also detecting all the events.

My current plan is to write something that doesn’t wait in a loop for the display to respond, and if it’s reading a value, use a callback to trigger those necessary actions. I’m unsure if this is overkill, if another method exists for this, or if there’s another way to avoid these dropped events.

If it helps, developing on a Teensy 3.6 in PIO, but can’t share the code

You should look at putting code on the nextion display then your not waiting for the serial bottle neck. The external MCU only needs to watch for incoming commands from the nextion I just went through this with " Stuck on how to code an intervelometer" sending back and forth to the external MCU for many things is unnecessary.

I don’t think we can cut out our external MCU. We’re using the majority of the pins, and running 3 PID loops (with up to 5 more planned in the near future). While the display does have some PWM capable pins, not having native float support makes it a lot harder. There’s also a lot of sunken development time, and my boss is giving me a 1-2 week hard stop on this project.

I plugged in a logic analyzer and I can pinpoint where the library is going wrong though.

The above shows the button event getting sent (and processed with the second channel’s blip). An update (new value written to Number Component) is then sent and the code waits until the ACK (0x01) is recieved 20 ms later.

The above shows one of the problems, where the button event happens in between the update and the ACK. The library reads the event as a failed ACK and then returns an error and drops the event.

I’m pretty close to a refactor of the library that removes the dependency on the next recieved command being an ACK, just need to test it. When looking at a lot of the code samples out there, there aren’t plans for true asynchronous communication, decoupling the events and the ACKs. This could be partially fixed with bkcmd=0, but that’s only a bandaid for receiving field values back from the display.

The fix involves queueing up sent commands so the MCU can see responds in a FIFO order, and using callbacks to avoid blocking code. I think this will help my application run a lot faster (not waiting for responses) and in general be able to facilitate bi-direction communication for more complex projects more easily.

Not suggesting you cut out your external mcu. The button has two events touch press & touch release you can set up a timer that gets activated by the touch press this timer increments a variable the touch release stops the timer and sends the value of the variable to your external mcu then resets the the variable ready for next time.

Using the nextion’s own commands enables much more complex projects eg my one talks to my off grid inverters to do this it has to put a command together do a xmodem crc on it then send that to the inverter , then listen for a reply from the inverter which also has a xmodem crc then it has to parse the data sent back and display that in text boxes , add some data to get totals , activate different animation based on some of the data.

here is code to test (it works)
Touch Press Event
tm1.en=1 //enable timer 1

Timer 1
va3.val+=1 //I set the timer to 500 ms so every 500 ms it increments va3 by 1

Touch Release Event
tm1.en=0 //disable timer
n1.val=va3.val //put value in number box
va4.val=va3.val //transfer value to another variable needs to be done due to the way nextion works
tm2.en=1 //enable another timer to print result needs to be done due to the way nextion works
va3.val=0 // clear variable

timer 2 I set it to 10ms
print va4.val
tm2.en=0

Hi and welcome @dotdash32!

Your observation is pretty much spot on I think. Nextion does have a non-negligible delay. On top of that the delay is somewhat variable depending on what the display is doing, so as soon as you’re putting a decent load on the serial bus you need to work with acknowledges (which Nextion thankfully supports) and - ideally - non-blocking communication.

The acknowledge part is easy, and apparently already in use judging by your LA screenshot. The non-blocking part may be more tricky depending on your requirements. F.ex. if you need to know if a specific command executed correctly or failed, you need to keep track of the outgoing commands and associate Nextions return replies with them and communicate this back to the code that originally emitted the command. Using an RTOS may provide some tools that simplify the task significantly (some allow you to write code in a “blocking” way but without actually blocking. In case you don’t know what I mean, if you have a waiting loop, you add a function call to the loop that will tell the task scheduler ‘hey, I’m currently waiting, check other pending tasks and come back to me afterwards’ - on Arduino - if available - this function is called yield() I think).
If however a 2 priority system is all you need (time critical stuff like PID loops vs everything else), I’d suggest running the time critical stuff by one or more timer interrupts, and keep everything else blocking as it is. Hardware interrupts are not blocked by loops so your time critical stuff is guaranteed to work properly. Especially when there’s no experience with any RTOS and/or a lot of code that would have to be adapted this can be a simple solution.
Again, note for readers who have less experience with hardware interrupts: since they block everything else, you should be extra careful to keep the stuff they do as short and fast as possible. If they take too long, you’ll see “weird” issues like millis() or Serial.read/print/write not working properly (they all depend on interrupts that’ll be blocked/delayed by yours if it’s too long).

This aside, I also agree with @paulvk’s point. In my personal experience it is often - not always - a good idea to cross the “serial line” as rarely as possible. This helps keeping the UI separate from the “actual” software, reducing code complexity, dependency and delays where one processor is waiting for the other one to receive or respond.
I said most of the time because there are cases where Nextion’s so limited that it makes more sense to send stuff to the microcontroller, get it processed there, and send the result back (the lack of math functions would be one example).
In all other cases however, especially when Nextion does have good support for what you need, I strongly suggest to use it. Button press measurement can be done with literally five lines on Nextion:

// Touch Press Event
// Reset counter variable and start timer to measure button 
// press duration.
timeMS.val=0
btDlyTimer.en=1

// Touch Release Event
// Stop timer, read elapsed time from the counter variable.
// on Nextion. 
btDlyTimer.en=0
// Do stuff based on timeMS.val

// Timer Event
// NOTE: you could hardcode the resolution, or have a counter
// that only increases by 1 each time, but the following code 
// is much cleaner IMO and flexible: you can change the timer
// resolution without changing anything else and you don't have
// to think about converting between "ticks" and milliseconds 
// anywhere else. 
// Note that I personally like to add the unit to the name of 
// variables - especially useful if you have different units 
// across your code. Just one of those little habits that can
// prevent stupid errors in the future. 
timeMS.val+=btDlyTimer.tim

This works for any number of buttons because Nextion doesn’t have multi-touch support. Note that the code above only measures the delay, it can’t do anything until the user actually releases the button (since only the release event decides what action shall be taken).
This can easily be changed by adding the “release” code to the timer event. Again, since there can only be one button pressed at the same time it’s perfectly fine to have many timers, for each button action one (even if you have 20 timers, they’re not going to run simultaneously, so no risk of overloading).

This was an extensive example, but there are more things you can do:

  • I found it much more efficient to not use the Nextion Instruction Set for sending data from Nextion to the MCU. This allows me to send multiple values together in a single serial packet.
  • Also, personally, I prefer Nextion sending stuff to the MCU whenever it changes instead of the MCU polling for changes. Considering the overhead of sending a request to Nextion and waiting for a reply, this can save you a lot of time.
  • Getting rid of the NIS for communication towards Nextion is possible (recmod / protocol reparse mode), but a pain in the ass because you have to run it off of timers, basically polling the serial buffer continuously - and copy that code to every page because there’s no option for global code. There are cases where it makes sense but you do lose a lot of comfort. If you go for it, it makes probably more sense to have fixed, periodic updates of variables. Put a bunch of them together in one packet, such that you have as little different packets as possible to distinguish and parse on the Nextion side. Send that packet 1-3 times per second and check for it every 100ms on the Nextion side (rough values I’d start with). The lower the update rate, the stupider the format can be.
  • The MCU doesn’t need to know about every button press and release. Often enough, certain buttons are only there to navigate through the UI without actually having any effect on the device they control. UI navigation can be done rather easily on Nextion, so there’s potential here to get rid of some serial messages.

Hope this helps!
Max

@paulvk While timers would be useful, MCU needs to start a command when button pressed and stop it (execute a soft-stop) when the button is released. It’s not ideal, but that was the spec I was given, and I don’t think a timer will work since the action is on the order of 10-20 seconds, so the MCU needs to start doing the action as soon as it reads the button pressed.

@Max I do think an RTOS would be a good choice, and have been looking into it for the next iteration of the design.

The current plan is to use a queue to order events, storing the “ideal” return code, a function callback for failure, a function callback for value return, and a timeout. As the MCU recieves commands, it can check against the return code to either process the Serial event as an asynchronous button press or response. The callbacks should approximate interrupts without as much overhead, i.e. non-blocking code that still associates specific actions with sent commands.

The Nextion display is functionining primarily as a display – it shows internal process variables (like motor position, heater temperature/power), which have to be calculated on the MCU and pushed to the display, I don’t think I can do any processing of the numbers on the Nextion itself because things will start to get out of sync.

I do want to cross the Serial line as little as possible, and any page navigation is done on the Nextion itself. Right now, most of the messages are just fixed update-rate value updates and button presses. I think the issue is that I have so many values to update that a high frequency causes the command misalignment I found with the LA. I did make a set of code to only actually send the update if it changed value, but that’s only a band-aid over the flawed code that doesn’t distinguish between actual return codes and true button events.

I think you should use the reparse mode that way you can send the data in one transmission and the nextion then puts the values into the places needed no back and forth with extra bytes sent , your in control of the serial and what is sent for a button press. I think to many are use to dumb displays that need to be controlled and are not treating the nextion as a microcontroller.

In this case it might indeed be the better solution. Especially with fixed update rates it could be possible to omit the acknowledge part because you know the buffer won’t overflow and Nextion will have finished processing the previous one. Nextions serial buffer is 1024 bytes; that should be enough to update all (or at least most) values.
Even if you don’t omit ACK (which may be good design practice even in this case), you don’t have 100 individual ACKs but only one for the big package.

Kind regards,
Max

First off, if you’re working on a serious or moderately sized project then I highly recommend ditching the canned Nextion libraries and using your own custom implementation instead. It gives you so much more control and versatility.

I might suggest increasing your baud rate to 921k to reduce any serial bottlenecks and looking into Protocol Reparse mode to have more control over the MCU to Nextion interactions.

I ended up going through the library refactor, and at this point, I feel like I basically re-wrote most of the actual Serial interface. The advantage of the library is that it keeps a lot of initialization work and class inheritance stuff. So sort of rebuilding the foundation while keeping the roof in place. (worked out better than it sounds).

I kept the baud at 115200 because that was as fast as I could get it to run on my logic analzyer, will likely boost it up on production after I get more process verification done.

It has lead to a somewhat weird implementation for the blocking code - there is a “sent command” queue that will stores the values and expects the correct return ACK, and will use a secondary psuedo-queue to copy the RX buffer for the specific blocking function. But it works pretty well, and doesn’t involve any breaking API changes.

The big thing I was able to do was build in non-blocking code with a bunch of callbacks. When a command is sent (or data requested), just attach a success/failure callback function that can execute like an interrupt, but is called from a serial polling routine. For me, this seems to work really well, in that it keeps a nice abstraction layer and allows non-blocking code.

My code is up on my GitHub. I’ll try to PR it, but I get the sense it might more limited in its usefulness than I hope. Also need to update documentation.

Moving to non-blocking data setters (and keeping the keypad readers exactly as they were) fixed the motor stuttering I was seeing on the PID loops. The other thing is because there is only a single place that reads the Serial buffer, there is a much lower chance of dropping button events.

1 Like