Искажение больших строковых переменных на ESP8266
У меня возникает проблема с приёмом большого JSON с данными об участниках беседы ВКонтакте, используя метод API messages.getConversationMembers. В беседе, где 27 человек, я получаю большой JSON размером примерно 5 килобайт, вытягиваю его функцией readString в локальную переменную типа String, и очень часто после этого появляются искажения в виде нескольких пропущенных символов (а иногда вместо них может появиться отзеркаленный вопросительный знак). Фрагмент принятого с искажениями JSON выглядит вот так:
В результате этого я постоянно ловлю ошибки InvalidInput (для работы с JSON использую библиотеку ArduinoJson). Заметил, что после включения ESP8266 и первого приёма большого JSON всё нормально, но на последующие разы появляются вот такие искажения. Хотя спустя несколько попыток данные все же удается вытянуть нормально. В небольшой тестовой беседе, где 4 человека и где данных приходит немного, такой проблемы нет. Вот моя функция, которую я использую для запросов к API ВКонтакте:
WiFiClientSecure *req;
String apiRequest(const char* link, const char* connection, String request) {
if (!clientCreated) { // если клиент не создан,
req = new WiFiClientSecure; // создаем его
req->setFingerprint(vkapi_fp); // настраиваем отпечаток HTTPS SHA-1
clientCreated = true;
Serial.println(String("req created, heap: ") + ESP.getFreeHeap()); // выведем в терминал информацию о свободной HEAP после создания клиента.
}
if (!req->connected()) { // если подключение не установлено,
Serial.print("connect...");
if (!req->connect(vkapi, 443)) { // соединяемся по 443 порту и сразу же проверяем, если вдруг подключение неудачно
Serial.println("fail!");
return ""; // прервем функцию, вернув пустую строку
} else Serial.println("ok");
}
Serial.print("send...");
req->print(String("POST ") + link + " HTTP/1.1\r\nHost: " + vkapi + "\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " + request.length() + "\r\nConnection: " + connection + "\r\n\r\n" + request); // формируем и отправляем POST запрос
Serial.println("done");
delay(500); // задержка 500 мс на всякий случай, чтобы успели придти данные
while (req->connected()) { // пока подключение установлено,
if (req->readStringUntil('\n') == "\r") { // вычитываем заголовки HTTP до тех пор, пока не дойдём до конца, чтобы от них избавиться (взято из примеров библиотеки ESP8266)
Serial.println(F("headers received"));
break;
}
}
req->setTimeout(100); // выставим таймаут на ожидание следующих данных - 100 мс. Если этого не сделать, то readString() вызовет большую задержку
String ret = req->readString(); // принимаем строку и сохраняем в переменную типа String
if (connection == "close" && clientCreated) { // если было указано, что соединение должно быть закрыто и при этом клиент создан
delete req; clientCreated = false; // уничтожим клиент для освобождения HEAP-памяти
Serial.println(String("req deleted, heap: ") + ESP.getFreeHeap()); // выведем в терминал информацию о свободной HEAP памяти
}
if (debug) Serial.println("\nRET = " + ret); // если стоит режим отладки, то выведем полученную строку в терминал.
return ret; // вернем полученную строку из функции.
}
Если вместо всех readString'ов воспользоваться костылём по посимвольному приёму JSON с отрезанием всего ненужного в начале:
String ret; bool found = false;
while (req->available()) {
char c = req->read();
if (debug) Serial.print(c);
if (c == '{') found = true;
if (found) ret += c;
}
То в данных, которые были выведены в терминал посимвольно с помощью Serial.print(c), не появляется никаких искажений. В полученной строке ret всё тоже самое - тоже появляются пропуски символов. Можно ли это как-то исправить? Спасибо заранее.
Ответы (1 шт):
Проблема решилась переходом на массив char'ов вместо String, и соответственно использованием посимвольного чтения буфера, которое работало отлично.
char er[] = "error";
char* apiRequest(const char* link, const char* connection, String request) {
if (!clientCreated) {
req = new WiFiClientSecure;
req->setFingerprint(vkapi_fp);
clientCreated = true;
Serial.println(String("req created, heap: ") + ESP.getFreeHeap());
}
if (!req->connected()) { // если подключение не установлено
Serial.print("connect...");
if (!req->connect(vkapi, 443)) { // подключаемся, и при неудачном подключении
Serial.println("fail!");
return er; // вернем указатель на массив char'ов, где записано слово "error"
} else Serial.println("ok");
}
Serial.print("send...");
req->print(String("POST ") + link + " HTTP/1.1\r\nHost: " + vkapi + "\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " + request.length() + "\r\nConnection: " + connection + "\r\n\r\n" + request); // высылаем запрос
Serial.println("done");
while (!req->available()) yield(); // ждём получения данных в буфер
uint16_t contentLength = 0; // объявляем переменную для хранения Content-Length
while (req->available()) { // пока в буфере есть данные
String line = req->readStringUntil('\n'); // читаем строку из буфера
if (line.startsWith("Content-Length")) { // если это Content-Length, то
contentLength = line.substring(16).toInt(); // читаем то, что стоит после фразы Content-Length, и сразу же преобразовываем в Int
if (debug) Serial.printf("Length extracted: %u\n", contentLength); // если стоит режим отладки, выведем значение Content-Length в терминал.
}
else if (line == "\r") { // при окончании заголовков (детектируем пустую строку)
if (debug) Serial.println(F("headers cleaned")); // при включенном режиме отладки выводим в терминал сообщение о том, что заголовки очищены
break; // прерываем цикл чтения буфера - он нужен был только для очистки заголовков и извлечения content-length
}
}
if (contentLength == 0) { // если переменная Content-Length осталась равной нулю
Serial.println(F("Content-Length not found!")); // выведем сообщение об этом в терминал (макрос F означает, что строка будет читаться прямиком из flash-памяти esp8266, и не будет лежать в оперативной памяти)
return er; // вернем указатель на массив char'ов, где записано слово "error"
}
char *reply = new char[contentLength]; // создадим в "куче" массив char'ов, размер которого равен значению Content-Length
for (uint16_t i = 0; i < contentLength; i++) { // символ за символом читаем буфер в массив char'ов
while (!req->available()) yield(); // смысл этой строки объяснил ниже
reply[i] = req->read(); // читаем символ из буфера в индекс i массива
}
if (connection == "close" && clientCreated) {
delete req; clientCreated = false;
Serial.println(String("\nreq deleted, heap: ") + ESP.getFreeHeap());
}
if (debug) Serial.printf("\nRET = %s\n", reply); // если стоит режим отладки, выведем ответ от сервера в терминал
return reply; // вернем ответ от сервера в виде указателя на созданный массив char'ов.
}
После вызова функции и обработки полученного с сервера ответа, обязательно нужно удалить массив char'ов из кучи, с помощью функции delete. В функции для отправки сообщения это реализовано так:
bool msgSend(unsigned long peer_id, String msg, String attachment = "", bool disable_mentions = false, unsigned int forward = 0) {
msg.replace(" ", "+"); // заменим все пробелы в запросе на плюсики, иначе сервер ВК не сможет обработать запрос
msg.replace("\n", "%0D%0A"); // заменим символы переноса строки на HTML-символы переноса строки и возврата каретки
unsigned long random_id = random(4294967295), random_id_secondary = random(4294967295); // создадим random_id
String request = "peer_id=" + String(peer_id) + "&message=" + msg + "&random_id=" + String(random_id) + String(random_id_secondary); // добавляем к запросу peer_id получателя сообщения, текст сообщения, и два склеенных рандомных числа
if (attachment != "") request += "&attachment=" + attachment; // если задано вложение, добавляем его к запросу
if (forward) request += "&forward_messages=" + String(forward); // если задан ID сообщения, которое нужно переслать, добавим и его
if (disable_mentions || debug) request += "&disable_mentions=1"; // если указан режим отключения упоминаний, либо включен режим отладки, добавим к запросу флаг disable_mentions (функция VK API для отключения уведомлений об упоминаниях)
request += String("&access_token=") + token + "&v=" + api_ver; // добавим access_token и используемую версию API.
bool msgSendOk = false;
char* reply = apiRequest("/method/messages.send", "Keep-Alive", request); // делаем запрос
if (reply != "error" && reply[2] == 'r') msgSendOk = true; // если функция запроса не вернула ошибку и при этом ответ начался с {"response" (проверяем это только по третьему знаку, если он равен r), ставим флаг, что всё ОК
delete [] reply; // удаляем ответ сервера из кучи
return msgSendOk;
}
В процессе реализации было замечено, что буфер при чтении в массив периодически обрывался (видимо данные с сервера читались блоками по несколько байт), в результате чего в массиве появлялась пачка отзеркаленных вопросительных знаков. Строчка while (!req->available()) yield(); внутри цикла по чтению в массив решила эту проблему - она приостанавливает чтение буфера в массив, пока буфер пуст.
