Problem setting up a "data class" for storing POST data

I'm playing around with some ESP8266 (ESP-01) and Arduino IDE. Following some great guides on Random Nerd, I have set up one ESP as a server and two as clients. The idea is for the clients to POST the status of a switch to the server (which will then have a GET endpoint for reading out all the statuses).

I've managed to set up a POST endpoint which takes some input, and stores it in memory (in an array), which I then can GET in a different endpoint. So I thought I'd make a simple data class for these "entries", but then I get problems

Error compiling for board Generic ESP8266 Module.

Maybe I'm setting up the class/array wrong, or maybe I can't do things this way on an ESP8266? Maybe I'm running out of memory?

/*********
https://tttapa.github.io/ESP8266/Chap10%20-%20Simple%20Web%20Server.html
*********/

// Load Wi-Fi library
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <ArduinoJson.h>

#ifndef STASSID
#define STASSID "XXXXXXX"
#define STAPSK  "XXXXXXXXXXXXXX"
#endif

const char* ssid     = STASSID;
const char* password = STAPSK;

// Assign output variables to GPIO pins
const int output5 = 2;
const int output4 = 0;

// Set web server port number to 80
AsyncWebServer server(80);

char * clientID = "n/a";
char * buttonID = "n/a";
char * buttonState = "n/a";

class Entry {
  public:
    char* clientID;
    char* buttonID;
    char* buttonState;
    unsigned long times;

    Entry() {}

    Entry(char* cid, char* bid, char* state) {
      clientID = cid;
      buttonID = bid;
      buttonState = state;
      times = millis();
    }

    String toString() {
      char tmp[100];
      sprintf(tmp, "%u\t%s\t%s\t%s\n", times, clientID, buttonID, buttonState);
      return tmp;
    }
};

static const int MAX_STATES = 20;
Entry states[MAX_STATES];
int statePos = 0;


String processor(const String& var){
  Serial.println(var);
}

// https://stackoverflow.com/questions/68700739/esp8266-crashes-when-sending-json-object-and-resets
// https://arduinojson.org/v6/example/http-server/
String TEMP() {
  String Json;
  StaticJsonDocument<512> doc;
  JsonArray Temp1 = doc.createNestedArray("Log");

  for (int i = 0; i < statePos && i < MAX_STATES; i++) {
    JsonObject doc_0 = doc.createNestedObject();
    doc_0["clientID"] = states[i].clientID;
    doc_0["buttonID"] = states[i].buttonID;
    doc_0["buttonState"] = states[i].buttonState;
    doc_0["times"] = states[i].times;
    Serial.println(states[i].toString());
    Temp1.add(doc_0);
  }
  serializeJson(doc, Json);
  Serial.println(Json);
  
  return Json;
}

void setup() {
  Serial.begin(115200);

  pinMode(output4, OUTPUT);
  pinMode(output5, OUTPUT);

  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    Serial.println("GET /");
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  
    server.on("/buttonstate", HTTP_POST, [](AsyncWebServerRequest *request){
      Serial.println("POST /buttonstate");

      unsigned long currentTime = millis();
      int params = request->params();

      if (!(request->hasParam("ID", true) && request->hasParam("button", true) && request->hasParam("state", true))) {
        request->send(400, "text/plain", "400: Invalid Request");
        return;
      }

      AsyncWebParameter* param1 = request->getParam("ID", true);
      AsyncWebParameter* param2 = request->getParam("state", true);
      AsyncWebParameter* param3 = request->getParam("button", true);
      char *clientID = param1->value();
      char * buttonState = param2->value();
      char * buttonID = param3->value();

      Entry entry(clientID, buttonID, buttonState);
      states[statePos++] = entry; //clientID + ";" + buttonID + ";" + buttonState;
      if (statePos >= MAX_STATES) statePos = 0;
      
      request->send(200, "text/plain", "200: OK");
  });
  // https://stackoverflow.com/questions/47095927/esp8266-json-parameters-in-http-post-request  
  
   server.on("/log", HTTP_GET, [](AsyncWebServerRequest *request){
    Serial.println("GET /log");
    AsyncWebServerResponse *response = request->beginResponse(200, "application/json", TEMP().c_str());
    response->addHeader("Access-Control-Allow-Origin", "*");
    request->send(response);
    //request->send_P(200, "application/json", TEMP().c_str());
  });

  server.begin();
  Serial.println("Ready! v2.22");
}



void loop(){
  digitalWrite(output5, HIGH);
}

I'd be willing to bet the error message says a LOT more than just that...

We need indeed more about the error

You’ll likely also have stale char* pointers and you should implement the destructor in your class and you’ll see probably your local instances going away…

Sorry, yeah, it did say a bit more, but "only" some addresses

Exception 0x8b04488b

I changed the code a bit, changed the char* to String (in the Entry class) and this seems to have fixed the immediate problems :slight_smile: It now works, but I'm still very unconvinced I managed to get this class set up in any decent manner.

Here's the updated parts:

class Entry {
  public:
    String  clientID;
    String  buttonID;
    String  buttonState;
    unsigned long times;

    Entry() {}

    Entry(String cid, String  bid, String state) {
      clientID = cid;
      buttonID = bid;
      buttonState = state;
      times = 1; //millis();
    }

    String toString() {
      char tmp[100];
      sprintf(tmp, "%u\t%s\t%s\t%s\n", times, clientID, buttonID, buttonState);
      return tmp;
    }
};

and


      AsyncWebParameter* param1 = request->getParam("ID", true);
      AsyncWebParameter* param2 = request->getParam("state", true);
      AsyncWebParameter* param3 = request->getParam("button", true);
      String clientID = param1->value();
      String buttonState = param2->value();
      String buttonID = param3->value();

      Entry entry(clientID, buttonID, buttonState);
      states[statePos++] = entry; //clientID + ";" + buttonID + ";" + buttonState;
      if (statePos >= MAX_STATES) statePos = 0;

What happens now, is that the /log endpoint just returns an array of nulls: {"Log":[null,null,null,null]}

^ that's after 4 POST requests received :slight_smile:

Post the new code. You need to ensure you allocate memory to keep your data around

The serial output looks like this:

POST /buttonstate
GET /log
1	11398333	button1	on

1	11398333	button2	on

1	ds	ffe	sfsdf

1	42hg	r3r	wwr

{"Log":[null,null,null,null]}

... so mayyybe I've got something ~OK, but I'm not serializing it (to JSON) correctly?
(btw, the "ds ffe sfsdf" etc is just me posting some arbitrary text from a web form, to test)

You need (at least) a copy constructor probably

But, I mean, the Serial.println (in the serialize for-loop) does output the correct data from the entries objects in the states array... So it seems (to me) that the data is there, and I am able to access it through the member fields (states[i].clientID etc)

... which is why I'm thinking maybe this is mainly a serialization question. However, I'm totally convinced I am lacking something (or several things!) in my Entries class/container :slight_smile:

Post the full code again (reading from iPhone so not easy to see it all)

/*********
https://tttapa.github.io/ESP8266/Chap10%20-%20Simple%20Web%20Server.html
*********/

// Load Wi-Fi library
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <ArduinoJson.h>

#ifndef STASSID
#define STASSID "XXXXX"
#define STAPSK  "XXXXXXXXXXXXX"
#endif

const char* ssid     = STASSID;
const char* password = STAPSK;

// Assign output variables to GPIO pins
const int output5 = 2;
const int output4 = 0;

// Set web server port number to 80
AsyncWebServer server(80);

char * clientID = "n/a";
char * buttonID = "n/a";
char * buttonState = "n/a";

class Entry {
  public:
    String  clientID;
    String  buttonID;
    String  buttonState;
    unsigned long times;

    Entry() {}

    Entry(String cid, String  bid, String state) {
      clientID = cid;
      buttonID = bid;
      buttonState = state;
      times = 1; //millis();
    }

    String toString() {
      char tmp[100];
      sprintf(tmp, "%u\t%s\t%s\t%s\n", times, clientID, buttonID, buttonState);
      return tmp;
    }
};

static const int MAX_STATES = 20;
Entry states[MAX_STATES];
int statePos = 0;



String processor(const String& var){
  Serial.println(var);
}

// https://stackoverflow.com/questions/68700739/esp8266-crashes-when-sending-json-object-and-resets
// https://arduinojson.org/v6/example/http-server/
String TEMP() {
  String Json;
  StaticJsonDocument<512> doc;
  JsonArray Temp1 = doc.createNestedArray("Log");

  for (int i = 0; i < statePos && i < MAX_STATES; i++) {
    JsonObject doc_0 = doc.createNestedObject();
    doc_0["clientID"] = states[i].clientID;
    doc_0["buttonID"] = states[i].buttonID;
    doc_0["buttonState"] = states[i].buttonState;
    doc_0["times"] = states[i].times;
    Serial.println(states[i].toString());
    Temp1.add(doc_0);
  }
  serializeJson(doc, Json);
  Serial.println(Json);
  
  return Json;
}

void setup() {
  Serial.begin(115200);

  pinMode(output4, OUTPUT);
  pinMode(output5, OUTPUT);

  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    Serial.println("GET /");
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  
    server.on("/buttonstate", HTTP_POST, [](AsyncWebServerRequest *request){
      Serial.println("POST /buttonstate");

      unsigned long currentTime = millis();
      int params = request->params();

      if (!(request->hasParam("ID", true) && request->hasParam("button", true) && request->hasParam("state", true))) {
        request->send(400, "text/plain", "400: Invalid Request");
        return;
      }

      AsyncWebParameter* param1 = request->getParam("ID", true);
      AsyncWebParameter* param2 = request->getParam("state", true);
      AsyncWebParameter* param3 = request->getParam("button", true);
      String clientID = param1->value();
      String buttonState = param2->value();
      String buttonID = param3->value();

      Entry entry(clientID, buttonID, buttonState);
      states[statePos++] = entry; //clientID + ";" + buttonID + ";" + buttonState;
      if (statePos >= MAX_STATES) statePos = 0;
      
      request->send(200, "text/plain", "200: OK");
  });
  // https://stackoverflow.com/questions/47095927/esp8266-json-parameters-in-http-post-request  
  
   server.on("/log", HTTP_GET, [](AsyncWebServerRequest *request){
    Serial.println("GET /log");
    AsyncWebServerResponse *response = request->beginResponse(200, "application/json", TEMP().c_str());
    response->addHeader("Access-Control-Allow-Origin", "*");
    request->send(response);
    //request->send_P(200, "application/json", TEMP().c_str());
  });

  server.begin();
  Serial.println("Ready! v2.22");
}

void loop(){
  digitalWrite(output5, HIGH);
}

Maybe this: Why is the output incomplete? | ArduinoJson 6
My JSONDocument is too small? Trying to increase from 512 to 4000 :wink: just to see what happens...
(4000 made the ESP-01 crash and reboot on POST)

However! Storing data like this, in the server, is something I'm just playing around with. Obviously, this is not a sensible thing to do (in a limited memory environment) so I guess the next step is to look into some external/remote backend -- maybe Firebase or something. But it would be nice to understand why this is failing (is it my c++ code, or the memory usage/leak)

The doc states

If you declare a local variable of type StaticJsonDocument, it allocates the memory pool in the stack memory. Beware not to allocate a memory pool too large in the stack because it would cause a stack overflow. Use StaticJsonDocument for small documents (below 1KB) and switch to a DynamicJsonDocument if it’s too large to fit in the stack memory.

Thanks! Looking at that, and I tried switching to DynamicJsonDocument doc(2048); and though it doesn't crash, I'm still only getting an array of null

String TEMP() {
  String Json;
  DynamicJsonDocument doc(2048);
  JsonArray Temp1 = doc.createNestedArray("Log");

  for (int i = 0; i < statePos && i < MAX_STATES; i++) {
    JsonObject doc_0 = doc.createNestedObject();
    doc_0["clientID"] = "bah";  //states[i].clientID;
    doc_0["buttonID"] = "bah";  //states[i].buttonID;
    doc_0["buttonState"] = "bah";  //states[i].buttonState;
    doc_0["times"] = "bah";  //states[i].times;
    Serial.println(states[i].toString());
    Temp1.add(doc_0);
  }
  serializeJson(doc, Json);
  Serial.println(Json);
  
  return Json;
}

Struggling to find documentation on how to serialize an array (of objects) to JSON.

Should I maybe not declare the DynamicJsonDocument within the TEMP() method, which is called (and declared again) on every hit to the /log endpoint?

^ seems this (not reusing document) is a decent way of approaching this: How to reuse a JsonDocument? | ArduinoJson 6

It seems this was the problem: createNestedObject() doesn't like not having a name. If I set it to, say, "entry", then everything seems to work fine!

String TEMP() {
  String Json;
  DynamicJsonDocument doc(2048);
  JsonArray Temp1 = doc.createNestedArray("Log");

  for (int i = 0; i < statePos && i < MAX_STATES; i++) {
    JsonObject doc_0 = doc.createNestedObject("entry");  // some name required (apparently)
    doc_0["clientID"] = states[i].clientID;
    doc_0["buttonID"] = states[i].buttonID;
    doc_0["buttonState"] = states[i].buttonState;
    doc_0["times"] = states[i].times;
    Serial.println(states[i].toString());
    Temp1.add(doc_0);
  } 
  if (doc.overflowed()) {
    Serial.println("DOCUMENT OVERFLOWED!\n");
  } else {  
    serializeJson(doc, Json);
    Serial.println(Json);
  }
  return Json;
}

This now gives me a JSON array of entries. The trailing "entry" field after the "Log" array is something I need to figure out, but the essential stuff seems to be working now :slight_smile:

{
   "Log":[
      {
         "clientID":"zd",
         "buttonID":"www",
         "buttonState":"cw",
         "times":1
      },
      {
         "clientID":"11398333",
         "buttonID":"button1",
         "buttonState":"on",
         "times":1
      },
      {
         "clientID":"11398333",
         "buttonID":"button2",
         "buttonState":"on",
         "times":1
      },
      {
         "clientID":"habba",
         "buttonID":"doo",
         "buttonState":"dabba",
         "times":1
      }
   ],
   "entry":{
      "clientID":"habba",
      "buttonID":"doo",
      "buttonState":"dabba",
      "times":1
   }
}

Thanks for the help!

Ok

Good you solved it

For reference, here's what's happening + how to solve it: ArduinoJson nested array - #2 by wildbill

I found that I was still getting an extra/trailing entry, but now more subtle and hard to find as it was now inside the array... Probably still not doing this 100% correct, as I find the "fix" to this is to not add the last entry (in the loop) to the array -- which really makes little sense, but it works.

If I were to serialize [15, 16, 17], for instance, I'd be getting {"log": [15, 16, 17, 15]} so it's the first element that would be repeated.

Here's the band-aid code. If anyone knows what's really going on, then please let me know!

String exportStates() {
  String Json;
  DynamicJsonDocument doc(2048);
  JsonArray logEntries = doc.createNestedArray("log");
  JsonObject logEntry = logEntries.createNestedObject();

  unsigned long now = millis();

  for (int i = 0; i < statePos && i < MAX_STATES; i++) {
    logEntry["ID"] = states[i].ID;
    logEntry["clientID"] = states[i].clientID;
    logEntry["buttonID"] = states[i].buttonID;
    logEntry["buttonState"] = states[i].buttonState;
    logEntry["times"] = now - states[i].times;
    Serial.println(states[i].toString());
    if (i < statePos -1) logEntries.add(logEntry);   // workaround to prevent trailing element
  } 
  if (doc.overflowed()) {
    Serial.println("ENTRIES DOCUMENT OVERFLOWED!\n");
  } else {  
    serializeJson(doc, Json);
    Serial.println(Json);
  }
  return Json;
}

P.S. It does not work to avoid adding the first element. If I do if (i > 0) then I get the remaining n-1 elements, with the first one repeated at the end :smiley:

The trailing element is there probably because of the acquisition

Could you please elaborate? :slight_smile:
The element is not there in the states array, and the for-loop does iterate the correct number of times.

Post the full code, the iteration is not inventing data. So there is a bug somewhere