Inkplate 6Motion : Sending a UDP frame freezes in an unrecoverable way

Hi,
I have a project which is a wireless remote control for driving a real boat. It is entirely contained in the Inkplate 6Motion board plus 2 potentiometers and 5 pushbuttons. It communicates with the boat system using repeated UDP frames back and forth.
The remote sends a 22 byte binary frame containing commands, to which the boat responds a 44 byte binary frame containing data, and this is repeated about 3 times a second.
I notice freezes that completely stop the traffic in such a way that the only remedy I found is resetting the ESP32 and re-configuring it, but this takes way too long and is not acceptable in the context of driving a boat (I have tested it in real conditions and had a frightening experience!).
The problem occurs in the sending process, implemented in the function

bool WiFiUDP::write(uint8_t *_packet, uint16_t _len)

of the file esp32SpiAtUdp.cpp.

It works a follows (seen from the STM32H7):
{
Send command "AT+CIPSEND=%d\r\n"
Wait for handshake pin for max Timeout1 (200 ms)
Get the response
Check if response is "\r\nOK\r\n\r\n>"
Send the payload part (here, a 22 byte array)
Wait for handshake pin for max Timeout2 (600 ms)
Get the response
Check if response is "\r\nOK\r\n"
}

At any time within a few hours of exchanging 3 messages per second, the system freezes. In one example, it froze after more than 27,000 exchanges. What happens is that the second waiting for handshake takes more than the timeout value. Initially this would put my system in an error state, but I noticed that then the UDP frame was sent anyway. I then decided to just ignore the timeout, so that the operation continues normally. This incident can occur several times (tens of times) without any problem. But eventually, something different happens.
This time, the frame is not sent when the timeout2 occurs. Since the event is ignored, there will be a next time attempting to send a UDP frame, but this time, the process fails at Timeout1. From that time on, not only the ESP32 cannot send frames, but the frames it receives are not detected nor read. So the two-way traffic completely stops, and so far my only choice is to reboot the remote control.
I have measured statistics of the time taken for the handshake pin to be activated. Here is the result. The graph shows the values sorted by increasing values.

One can see that about half the values range from 230 to 300 ms, then about a third of the values are remarkably constant at about 370 ms, then a new step to a value of about 570 ms, and then a few values above, including infinity.
What could be the cause of this problem?
I have analysed the traffic using wireshark, and though it is not very clear, it seems that the freezing incident might occur if an incoming frame arrives right at the time the ESP32 is sending one.
Is there a bug in the ESP32-AT firmware? or an incorrect configuration on my part?

Notes:
I found in the ESP32 forum '( Bot Challenge ) someone seemingly having a similar case, but I found no follow-up of his problem. The reference is :

Re: [ESP32 AT Firmware] Module Stops Responding After Several Hours of Stable Operation
Thanks
Postby amir.shn » Thu Oct 16, 2025 8:22 pm

Hi @jeanmarc_delaplace

Have you tried using the udp-issue branch in the Inkplate Motion library? GitHub - SolderedElectronics/Inkplate_Motion_Arduino_Library at udp-issue · GitHub Does the same issue happen on it?

Thank you for this information. I browsed the code and it seems that this may be the fix I require.

However, I think there is a flaw in the code you suggest me. Please tell me if you agree with me.

In the function:

the lines since line 481 read as follows:

        if (getSimpleAtResponse(\_dataBuffer, INKPLATE_ESP32_AT_CMD_BUFFER_SIZE, 200ULL, &\_respLen))
        {
            // Check of OK. If not found, return error.
            if (strstr(\_dataBuffer, "\\r\\nOK\\r\\n") == NULL)
                \_retValue = false;
        }
    }

>>>> // You got so far. Everything seems to went fine, so return true!
_retValue = true;
}
}

// In needed, copy response len.
if (_len != NULL)
*_len = _respLen;

// Return success/fail flag.
return _retValue;

}

It seems to me that there is a flaw around the line marked with a >>>>.

There is an if statement just before. If the condition is true, the retValue is set to false.

But anyway, just after exiting this block, there is a statement that sets retValue to true. So if the said condition ever occurs, the retValue will never be false.

If you agree, can you submit un update?

regards.

Yeah, the else branch was missing. I updated the udp-issue branch with the fix.

It seems the same thing exists line 446, an else statement missing. However I do not quite understand the code for the initial value of _retValue is false, and there are two places where an if statement, if the condition is true, sets it to false. This is not very tidy. I currently do not have the time to analyse this, so if you could check it and fix whatever seems required, this would be great!

I also found similar situation at line 384. And there was even bigger issue where the READABLE and WRITEABLE statuses were treated as mutually exclusive instead of independent which would cause an issue when the data arrived when the ESP became ready for the next send. Fix is to use bitwise operators instead of checking equalities directly, both fixes were pushed to the branch. I’ve also been running the stress test code you can find in the branch for about 2 hours without issues. There are probably some other logical issues/bugs in the code so let us know if you find any more.

I did some research on the ESP32 forum and I got a hit that race conditions may occur between the readable and writeable states, but did not go any further. You fixed it, great!

Let me add here my findings about this file. Maybe you would like to change the code after my remarks.

1- For the sake of consistency, instead of reading the state of _esp32HandshakePinFlag, use the function getHandshakePinState(). This is also motivated by the next topic of this list.

2- I needed to make the library multitasking-friendly. For so, every time a while loop is used to wait for the handshake pin, a way to provide a call to a yield() function should exist. I implemented as follows:

So I altered the getHandshakePinState() function as follows:



volatile bool WiFiClass::getHandshakePinState()
{
  if ( pYieldHook != nullptr )
    pYieldHook () ;
  return _esp32HandshakePinFlag;
}


with pYieldHook defined as follows:


void (*pYieldHook)(void) = nullptr ; // Pointer to optional function 

// Attach a function call to the idle hook
void WiFiClass::setYieldCallback( void (*Hook)(void) )
{
  pYieldHook = Hook;
}

So, if at initialisation time you call setYieldCallback() with the yield function as an argument, this function will be called repeadtedly during the wait for the handshake pin to come true.

This is the code I am using and after many months of use I did not find any problem.

3- Here is the code of the power function. The changes are:
-I have added delays to allow the ESP32 to power up and down to ensure it is stable when accessed by spi.
-There was a mistake with the setting of the INKPLATE_ESP32_PWR_SWITCH_PIN. The switch off branch did put it High instead of Low.

bool WiFiClass::power(bool _en, bool _resetSettings)
{
	unsigned long _timeoutCounter ;
    if (_en)
    {
        // Enable the power to the ESP32.
        digitalWrite(INKPLATE_ESP32_PWR_SWITCH_PIN, HIGH);

        // Wait a little bit for the ESP32 to boot up.
		_timeoutCounter = millis() + 50 ;
		do
			if ( pYieldHook != nullptr ) pYieldHook () ;		// YIELD !!!!!!!!!!!!!!!!!!!
			while ( millis() < _timeoutCounter ) ;

        // delay(50);		!! original code

        // Wait for the EPS32 to be ready. It will send a handshake to notify master
        // To read the data - "\r\nready\r\n" packet. Since the handshake pin pulled high
        // with the external resistor, we need to wait for the handshake pin to go low first,
        // then wait for the proper handshake event.
        if (!isModemReady())
            return false;

        // Try to ping modem. Return fail if failed.
        if (!modemPing())
            return false;

        // Set ESP32 to its factory settings if needed.
        if (_resetSettings)
        {
            if (!systemRestore())
                return false;
        }

        // Disable echo on command. Return false if failed.
        if (!commandEcho(false))
            return false;

        // Enable default message filters.
        if (!defaultMsgFiltersEn())
            return false;

        // Disable stroing data in NVM. Return false if failed.
        if (!storeSettingsInNVM(false))
            return false;

        // Disable system messages. Can disrupt flow of the library.
        if (!systemMessages(0))
            return false;

        // Initialize WiFi radio.
        if (!wiFiModemInit(true))
            return false;
    }
    else
    {
        // Disable the power to the ESP32.
        digitalWrite(INKPLATE_ESP32_PWR_SWITCH_PIN, LOW);		// Was HIGH, by mistake

        // Wait a little bit for the ESP32 to power down.
		_timeoutCounter = millis() + 50 ;
		do
			if ( pYieldHook != nullptr ) pYieldHook () ;		// YIELD !!!!!!!!!!!!!!!!!!!
			while ( millis() < _timeoutCounter ) ;
        // delay(50);		!! original code
   }

    // Everything went ok? Return true.
    return true;
}

4- There is a function that I needed is that which returns the strength of the wifi field. Currently, it is possible to read this value as of the time the connection was established, but it is not possible to follow in real time the changes in the field strength, which is a very usedful feature on a portable device like the remote control I am developping.

5- Let me remind you the separate post I did on the forum about the function beginPacket which returns false more often than true, which makes the connection process fail altogether.

I hope this long post is clear enough. Thanks again for the fix with the SPI.


I pushed the power pin, handshake pin function call fixes and I raised the timeout time from 100ms to 600ms due to the actual handshake taking longer than 100ms usually. We might add multitasking support in the future but it’s a bit bigger change than these small bug fixes and I’ll discuss it with my team members.

That’s good.

I have two more remarks and an enhancement suggestion.

1- Line 1715:

sprintf(_dataBuffer, "AT+CWINIT=%d\r\n", _status);

_status is defined as a bool. Thus a more correct way to write this is:

sprintf(_dataBuffer, "AT+CWINIT=%d\r\n", _status?1:0);	// %d requires number

2-Line 427:

        if (strstr(_dataBuffer, _expectedResponseAtCmd) == NULL)

strstr as been evaluated line 424, so why not use the value of _responseMatch in the test ?

        if (_responseMatch == NULL)

Suggestions:

-add a function to get the current value of the field strength

-add a function to enable the autoreconnect feature of the ESP32.

For the first point, its not actually a problem as the compiler would automatically cast (promote) bool into 1 or 0.
Second point stands and I applied the fix.
I added those two suggestions along with multitasking for future additions if we decide to implement them.

I have imported in my project the esp32SpiAtUdp.cpp file and the operation is more reliable. However, there are still failures though less frequent. I have tested by running the following actions repetitively:

Power esp32 on

connect

beginpacket

end

unpower esp32

once every 3 or 4 times, the beginpacket still fails.

Specifically, the error occurs line 599 of esp32SpiAt.cpp:

else
{
    // No expected response is found, return false.
    return false;
}
:

By the way, I noticed there are occasional errors during execution of setmode(). It calls disconnect(), which calls sendAtCommandWithResponse() which sometimes return false from line 431 (file esp32SpiAt.cpp), but since setmode() ignores the return value of disconnect(), setmode() returns true and the error is hidden. I could catch it because I added traps in file esp32SpiAt.cpp. I do not know whether this is a problem.