Как заменить объекты String массивами символов, продолжая использовать строковые методы

Я запускаю следующий код на своем ESP8266 (AI-Thinker ESP8266MOD).

Я отправляю HTTP-запрос GET, и на выводе устанавливается высокий уровень на 1/2 секунды.

Однако через некоторое время (иногда 1 час, 2 часа, 12 часов, совершенно случайно) он перестает отвечать на HTTP-запросы. Я думаю, это может быть из-за фрагментации кучи из-за объектов String, которые я использую.

Вопросы:

Как заменить requestloop()), s и textGenerateResponse()) объекты с массивами символов? После замены объектов String массивами символов, как я могу использовать метод indexOf() и оператор + для массивов символов?

Поможет ли этот подход с фрагментацией кучи?

Можно ли узнать, является ли проблема фрагментацией кучи?

#include <ESP8266WiFi.h>

const char* ssid = "myWifi";
const char* wifiPassword = "y76ggS";
const char* passwordToOpenDoor = "/81"; //пароль должен начинаться с косой черты

const int doorPin = 5;

WiFiServer server(301); //Выберите любой номер порта, который вам нравится
WiFiClient client;

void setup() {
  Serial.begin(115200);
  delay(10);
  Serial.println(WiFi.localIP());

  pinMode(doorPin, OUTPUT);
  digitalWrite(doorPin, 0);

  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, wifiPassword);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  server.begin();
  Serial.println("Server started. Diagnostics info:");
  Serial.println(WiFi.localIP());
}

void loop() {
  client = server.available();
  if (!client) {    
    return;
  }

  while(!client.available()){
    delay(1);  
  }

  String request = client.readStringUntil('\r'); 
  client.flush();

  Serial.println(request);  
  if (request.indexOf(passwordToOpenDoor) != -1) { //Верный ли пароль?
    GenerateResponse("Password is correct");
    OpenDoor();    
  }
  //Получил запрос GET, и это был не запрос favicon.ico, должно быть, это был неверный пароль:
  else if (request.indexOf("favicon.ico") == -1) {  
    GenerateResponse("Password is incorrect.");
  }  
}

void OpenDoor() {
  digitalWrite(doorPin, 1);
  delay(500);
  digitalWrite(doorPin, 0);
}

void GenerateResponse(String text) {
  Serial.println(text);
  String s = "HTTP/1.1 200 OK\r\n";
  s += "Content-Type: text/html\r\n\r\n";
  s += "<!DOCTYPE HTML>\r\n<html>\r\n";
  s += "<br><h1><b>" + text + "</b></h1>";
  s += "</html>\n";
  client.flush();
  client.print(s);
  delay(1);
}

, 👍0

Обсуждение

В дополнение ко всем ответам, предоставленным до сих пор, при использовании массива char String.indexOf() можно заменить на strstr (str1, str2). Он возвращает номер позиции, когда str2 совпадает с частью str1., @hcheung

Для обучения прочитайте `[<cstring>](https://www.cplusplus.com/reference/cstring/), в котором есть все функции для работы со строками c и массивами, которые вам нужны при работе с массивом char., @hcheung

@hcheung в этой строке "else if (strstr(request, "favicon.ico") == -1) {" Я получаю "ISO C++ запрещает сравнение между указателем и целым числом [-fpermissive]", @David Klempfner

Мне пришлось удалить сравнение с 1 и -1 в обоих операторах if, чтобы заставить его скомпилироваться., @David Klempfner


3 ответа


Лучший ответ:

2

Вы уже получили хороший ответ о некоторых общих идеях для замена объектов String строками C. Здесь я постараюсь добавить немного больше трюков, более конкретно ориентированных на вашу реальную ситуацию.

Я вижу в вашей программе два места, где использование String особенно заметно. его удобство, а замена его строками C потребует некоторого усилие. Во-первых, это использование Stream::readStringUntil(char) для получение первой строки запроса. Здесь Я не вижу лучшего варианта (см. редактирование ниже), чем чтение поток по одному символу за раз и помещая эти символы в массив:

const size_t input_buffer_length = 256;

// Читаем строку текста до первого '\r'.
// Возвращает строку с нулевым завершением без завершающего символа '\r',
// в статически выделенный буфер.
// Предупреждение: это никогда не истекает.
const char *readline(Stream &input)
{
  static char buffer[input_buffer_length];
  size_t pos = 0;  // запись позиции в буфере
  int c;           // текущий символ
  while ((c = input.read()) != '\r') {
    if (c >= 0 && pos < sizeof buffer - 1) {
      buffer[pos++] = c;
    }
  }
  buffer[pos] = '\0';  // завершаем строку
  return buffer;
}

Вы бы использовали это так:

const char *request = readline(client);

Для большей надежности вы можете добавить тайм-аут. Отметим также, что это реализация не является реентерабельной, так как использует статический буфер. Это не проблема для вашего варианта использования.

Обновление: как отметил hcheung в комментарии, вам не нужно реализовывать эту функцию readline(), которую вы можете использовать Stream::readBytesUntil(), который обрабатывает тайм-аут и обеспечивается ядром Arduino:

char buffer[input_buffer_length];
client.readBytesUntil('\r', buffer, input_buffer_length);

Другое место — это использование String::operator+=(const char *) для генерация ответа. Здесь я бы посоветовал вам против использовать strcat() или strncat(): они потребуют дополнительной памяти для хранения конкатенированная строка, которая вам, вероятно, не нужна. Вместо этого вы можете просто print() части один за другим:

void GenerateResponse(const char *text) {
  Serial.println(text);
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println();
  client.println("<!DOCTYPE HTML>");
  client.println("<html>");
  client.print("<br><h1><b>");
  client.print(text);
  client.println("</b></h1></html>");
  client.flush();
  delay(1);
}

В качестве альтернативы вы можете использовать неявную конкатенацию строк литералы, чтобы уменьшить количество вызовов client.print():

void GenerateResponse(const char *text) {
  Serial.println(text);
  client.print(
      "HTTP/1.1 200 OK\r\n"
      "Content-Type: text/html\r\n"
      "\r\n"
      "<!DOCTYPE HTML>\r\n"
      "<html>\r\n"
      "<br><h1><b>"
  );
  client.print(text);
  client.print("</b></h1></html>\r\n");
  client.flush();
  delay(1);
}
,

Вместо создания собственной функции readline(), вероятно, было бы чище использовать client.readBytesUntil('\r', буфер, длина), так как и readStringUntil(), и readBytesUntil() унаследованы от ` Класс потока., @hcheung

Спасибо за Ваш ответ. Он работает уже более 12 часов :) Похоже, проблема была в объектах String. Я дам вам знать, если он все еще работает через пару дней., @David Klempfner


1

Вы спросили: "После того, как я заменю объекты String массивами char, как мне использовать метод indexOf() и оператор + для массивов char?"

Короткий ответ: вы не можете. Если вы хотите использовать строки C (также известные как массивы символов), вам придется использовать строковые функции C.

Методы String работают с объектами String. Здравый смысл состоит в том, чтобы полностью избегать класса String, так как он полагается на память кучи, а платы Arduino слишком сильно нуждаются в памяти, чтобы иметь возможность использовать такой класс, как String, который создает временные объекты в куче.

,

2

Итак, вот код для начала:

myMessageArray [256] = {'\0'}; // Определяем глобальный массив достаточно большим и завершаем нулем

метод + заменяется

strcpy (myMessageArray, "Text to Add"); 
// Инициализирует myMessageArray, начиная с индекса 0
strcat (myMessageArray, "More text to Add"); 
// Добавляется к myMessageArray начиная с текущего индекса

преобразование числовых значений в символы:

uint_16_t myNumberValue  = 31253;
char numBuffer [16] = {'\0'}; // Вспомогательный буфер для конверсий

itoa (myNumberValue,numBuffer,10);  // преобразует целое число в 10 (десятичный) символ
itoa (myNumberValue,numBuffer,2);  // преобразует целое число в двоичный символ с основанием 2
itoa (myNuberValue,numBuffer,16);  // преобразует целое число в 16 (шестнадцатеричный) символ

itoa инициализирует массив символов, поэтому нам нужен вспомогательный массив:

strcat (myMessageArray, numBuffer);    

для преобразования float мы используем

dtostrf(floatVariable, StringLengthIncDecimalPoint, numVarsAfterDecimal, numBuffer);

чтобы преобразовать символы обратно в int, используйте

int16_t myIntVar = atoi(numBuffer);

чтобы преобразовать символы обратно в числа с плавающей точкой, используйте

float myFloatVar = atof(numBuffer, decimalsToShow); // используя только atof(numBuffer)
gives you standard x.XX only 2 decimals

Рабочий IndexOf для массивов символов:

/*************************************************** ***************************************/
/**
\brief Находит индекс заданного значения в массиве, начиная с заданного индекса
\автор codebreaker007
\param [in] targetArray массив для поиска char
\param [in] valueToFind искомое значение
\param [in] startIndex индекс для начала поиска
\вернуть индекс значения в массиве

\details Этот метод возвращает INDEX_NOT_FOUND^(-1) для пустого входного массива.
Отрицательный startIndex обрабатывается как ноль. StartIndex больше, чем массив
длина вернет INDEX_NOT_FOUND(-1)
*/
/********************************************************************************/
int8_t indexOf(char* targetArray, char valueToFind, uint16_t startIndex = 0) {
  if (targetArray == NULL) {
    return INDEX_NOT_FOUND;
  }
  if (startIndex <= 0) {
    startIndex = 0;
  }
  for (uint16_t i = startIndex; i < strlen(targetArray); i++) {
    if (valueToFind == targetArray[i]) {
      return i;
    }
  }
  return INDEX_NOT_FOUND;
}

Чтобы сравнить массивы символов, вы должны изучить функцию strcmp, и если вы хотите проверить, совпадают ли первые n символов, которые вы используете:

if (strncmp(myMessageArray , "POST", 4 ) == 0) { 
// сравнивает первые 4 символа, если они совпадают с = 0, подробности см. в документации по arduino

Надеемся, что это подробное введение в массивы символов поможет вам в ваших текущих и будущих проектах. Как узнать - преобразовать в массивы char и, если он работает стабильно, это было так. До этого преобразования вы никогда не сможете исключить класс String как виновника.

,

Хороший исчерпывающий ответ. (проголосовал). Я обычно использую sprintf() и printf() в C, что слишком тяжело для большинства плат Arduino. Я также не уверен в классах WiFiClient и WiFiServer и в том, есть ли у них методы, которые возвращают массивы символов, а не строки., @Duncan C

Этот способ идеально подходит для переполнения буфера. Я предлагаю использовать функцию snprintf., @SBF

Все стандартные классы на ESP8366/32 используют char (после изучения проблемы кучи String в тяжелых сценариях com). Поскольку snprintf и подобные все используют временные массивы char - посмотрите исходники - это задерживает проблему фрагментации кучи, но не решает ее. @SBF, где вы видите переполнение буфера, я предлагаю глобальные массивы символов, скомпилированные для флэш-памяти - у меня esps работает уже 1,5 года с интенсивным использованием массивов символов, ни одного сброса / сбоя - та же программа с классом String вылетает через 30+ минут, @Codebreaker007

Насчет «_Рабочий IndexOf для массивов символов_»: или просто используйте старый добрый [strchr()](http://man7.org/linux/man-pages/man3/strchr.3.html)., @Edgar Bonet

@ Codebreaker007: Я полностью согласен избегать класса Sting и использовать буфер символов, однако, если ваш буфер слишком мал, когда вы выполняете strcat, у вас происходит переполнение буфера. snprintf() имеет аргумент для ограничения максимального количества байтов, которые будут использоваться в буфере. Сгенерированная строка имеет длину не более n-1, оставляя место для дополнительного завершающего нулевого символа., @SBF

@SBF: snprintf(), вероятно, немного излишен для объединения строк. Альтернативой strcat() с меньшей защитой от переполнения является [strncat()](http://man7.org/linux/man-pages/man3/strcat.3.html)., @Edgar Bonet

Конечно, но он также заменяет весь пушок itoa. С помощью strncat() вам нужно вычислить оставшиеся свободные байты в буфере., @SBF