Search code examples
esp32arduino-c++arduino-esp32

How to manage a slow callback function in the ESPAsyncWebServer library


I understand that delaying or yielding in the ESPAsyncWebServer library callbacks are a no-no. However, my callback function needs to query another device via the Serial port. This process is slow and will crash the ESP32 as a result.

Here is an example:

void getDeviceConfig(AsyncWebServerRequest *request) {
  AsyncResponseStream *response =
      request->beginResponseStream("application/json");
  StaticJsonDocument<1024> doc;
  JsonArray array = doc.createNestedArray("get");

  for (size_t i = 0; i < request->params(); i++)
    array.add(request->getParam(i)->value());

  serializeJson(doc, Serial);
  /* At this point, the remote device determines what is being asked for 
     and builds a response. This can take fair bit of time depending on 
     what is being asked (>1sec) */

  response->print(Serial.readStringUntil('\n'));
  request->send(response);
}

I looked into building a response callback. However, I would need to know ahead of time how much data the remote device will generate. There's no way for me to know this.

I also looked into using a chunked response. In this case, the library will continuously call my callback function until I return 0 (which indicates that there is no more data). This is a good start - but doesn't quite fit. I can't inform of the caller that there is definitely more data coming, I just haven't received a single byte yet. All I can do here is return 0 which will stop the caller.

Is there an alternative approach I could use here?


Solution

  • The easiest way to do this without major changes to your code is to separate the request and the response and poll periodically for the results.

    Your initial request as you have it written would initiate the work. The callback handler would set global boolean variable indicating there was work to be done, and if there were any parameters for the work, would save them in globals. Then it would return and the client would see the HTTP request complete but wouldn't have an answer.

    In loop() you'd look for the boolean that there was work to be done, do the work, store any results in global variables, set a different global boolean indicating that the work was done, and set the original boolean that indicated work needed to be done to false.

    You'd write a second HTTP request that checked to see if the work was complete, and issue that request periodically until you got an answer. The callback handler for the second request would check the "work was done" boolean and return either the results or an indication that the results weren't available yet.

    Doing it this way would likely be considered hostile on a shared server or public API, but you have 100% of the ESP32 at your disposal so while it's wasteful it doesn't matter that it's wasteful.

    It would also have problems if you ever issued a new request to do work before the first one was complete. If that is a possibility you'd need to move to a queueing system where each request created a queue entry for work, returned an ID for the request, and then the polling request to ask if work was complete would send the ID. That's much more complicated and a lot more work.

    An alternate solution would be to use websockets. ESPAsyncWebServer supports async websockets. A websocket connection stays open indefinitely.

    The server could listen for a websocket connection and then instead of performing a new HTTP request for each query, the client would send an indication over the websocket that it wanted to the server to do the work. The websocket callback would work much the same way as the regular HTTP server callback I wrote about above. But when the work was complete, the code doing it would just write the result back to the client over the websocket.

    Like the polling approach this would get a lot more complicated if you could ever have two or more overlapping requests.