Добрый день, уважаемый читатель!
Практически в каждой прошивке приходится в том или ином виде использовать переменные, содержащие строковую информацию. Это могут быть топики, уведомления, отладочные сообщения (логи) и т.д. В данной статье я расскажу про свой способ работы с динамическими строками.
Для начала чуть-чуть теории. Строка в C/С++ (си) – это всегда массив символов, который по-хорошему всегда должен заканчиваться символом конца строки – ‘\0’ (целочисленное значение которого равно 0). Это хорошо иллюстрирует картинка, найденная мной на просторах интернета:
Объявить строку в программе можно двумя (по большому счёту) способами – статически и динамически:
- К статическим строкам можно отнести все строковые константы типа
const char*
,#define "строка"
,char str[]="текст"
или"просто любой текст, заключенный в двойные кавычки"
. Необходимая область памяти под статическую строку будет выделена автоматически при компиляции программы и программисту не требуется об этом заботиться, поэтому работать с ними исключительно просто. Но у них есть существенный недостаток – они не могут быть изменены во время работы программы. - Динамические строки размещаются в общей оперативной памяти (heap, куча) и программист сам должен позаботиться о выделении необходимой памяти под строку и удалении её после того, как строка стала более не нужна. В том числе перераспределять выделенную память и при изменениях строки – например при изменениях её длины. Работа с динамическими строками требует от программиста внимательности и аккуратности, но взамен предоставляется возможность изменения строк.
- Существует ещё “промежуточный” вариант работы с “изменяемыми” строками с использованием заранее выделенного (при компиляции) буфера:
char[длина_буфера]
. Тут вроде бы и память под буфер выделяется статически, но и есть возможность изменения строки в run time. Но, как всегда, ничего не дается даром, поэтому приходится платить либо перерасходом оперативной памяти, либо упираться в длину выделенного буфера (а чаще бывает и то и другое).
Возможно, приведенная выше классификация не соответствует классификации “по учебникам”, но, на мой взгляд так будет понятнее.
При переходе с Arduino на ESP-IDF наибольшие проблемы вызывают как раз динамические строки и работа с ними. В Arduino для работы со строками есть замечательная библиотека-класс String, которая предоставляет множество удобный методов для работы с динамическими строками. Отказавшись от Arduino, мы автоматически теряем доступ и к этой библиотеке.
Впрочем, описываемая ниже библиотечка по большей части вполне может приходится и для других платформ и микроконтроллеров, так как в ней используются “стандартные” библиотечные функции. В том числе и для Arduino.
Библиотека rStrings
Исходя из вышесказанного, пришлось написать свою библиотечку, которая мне очень помогает с динамическими строками. Но в ней применен несколько иной подход (в отличие от String) – через использование семейства библиотечных функций vprintf(). Мне очень нравится работать с форматными строками, с их помощью гораздо удобнее выполнять преобразование чисел в строки. Например для преобразования числа в строку вместо String(10.3)
необходимо записать vprintf("%f", 10.3)
, что поначалу наверняка покажется сложнее и не удобнее. Но взамен мы получаем возможность сразу же добавить “окружающий” текст и причесать число в нужный вид – добавить лидирующие нули или округлить до необходимого количества знаков после запятой. Если вы ещё не знакомы с этими функциями, вы можете узнать про них более подробно из справочной системы:
Но vprintf() требует для своей работы буфер (блок памяти) необходимого размера. Решение данной проблемы довольно простое: вызываем vprintf() два раза, причем в первый раз передаем ей NULL вместо указателя на буфер. В ответ vprintf() вернет длину результирующей строки, то есть фактически это и есть размер необходимого буфера. Добавляем к полученному значению 1 (под завершающий символ “\0”) и выделяем необходимую память.
char * malloc_stringf(const char *format, ...) { char *ret = nullptr; if (format != nullptr) { // get the list of arguments va_list args1, args2; va_start(args1, format); va_copy(args2, args1); // calculate length of resulting string int len = vsnprintf(nullptr, 0, format, args1); va_end(args1); // allocate memory for string if (len > 0) { ret = (char*)malloc(len+1); if (ret != nullptr) { memset(ret, 0, len+1); vsnprintf(ret, len+1, format, args2); } else { ESP_LOGE(tagHEAP, "Failed to format string: out of memory!"); }; }; va_end(args2); }; return ret; }
Про особенности работы с памятью вы можете почитать из следующей статьи: Использование памяти в ESP32
Теперь мы может рассмотреть функции библиотеки подробнее…
char * malloc_stringf(const char *format, …)
Форматирование строки с автоматическим выделением блока памяти нужного размера. Использовать можно например так:
- char* _str = malloc_stringf(“Вывод другой строки в этой: %s“, str1);
- char* _strtime = malloc_stringf(“Текущее время: %.2d:%.2d:%.2d“, h, m, s);
В ответ вы получите указатель на участок памяти, где размещена готовая к употреблению строка (массив символов).
⚠️ Не забывайте после использования смывать за собой удалить строку из кучи с помощью free().
uint16_t format_string(char* buffer, uint16_t buffer_size, const char *format, …)
Иногда всё-таки требуется отформатировать строку, используя буфер известного размера. Эта версия проверяет размер предоставленного буфера и выдаст ошибку, если предоставленный буфер слишком мал.
char * malloc_string(const char *source)
А что если нужно просто “снять копию” с уже существующей строки? Ну в принципе есть библиотечная _strdup(). Но я когда то написал свою “версию” – malloc_string() и пользуюсь ей. Пользоваться ей очень просто – передаете указатель на исходную строку (любого типа) и в ответ получаете указатель но новую строку.
Если требуется клонировать строку строго заданной длины – есть malloc_stringl().
Конкатенация (объединение) строк с перераспределением памяти
Иногда требуется склеить две строки в одну, а исходные строки удалить. В принципе, это несложно сделать с помощью указанной выше format_string(), но я создал парочку готовых функций дабы не повторять один и тот же код постоянно:
char * concat_strings(char * part1, char * part2)
Простое склеивание двух строк с уничтожением “исходников”. Если какая-то из частей отсутствует (NULL), то вернется указатель на другую часть. Аналог String+ для Arduino.
char * concat_strings_div(char * part1, char * part2, const char* divider)
Склеивание двух строк с участием заданного разделителя, например через запятую или точку с запятой. Очень удобно использоваться для формирования JSON или HTTPS-запросов с параметрами:
char * omValues = nullptr; // Улица if (sensorOutdoor.getStatus() == SENSOR_STATUS_OK) { omValues = concat_strings_div(omValues, malloc_stringf("p1=%.3f&p2=%.2f", sensorOutdoor.getValue2(false).filteredValue, sensorOutdoor.getValue1(false).filteredValue), "&"); }; // Комната if (sensorIndoor.getStatus() == SENSOR_STATUS_OK) { omValues = concat_strings_div(omValues, malloc_stringf("p3=%.3f&p4=%.2f", sensorIndoor.getValue2(false).filteredValue, sensorIndoor.getValue1(false).filteredValue), "&"); }; // Котёл if (sensorBoiler.getStatus() == SENSOR_STATUS_OK) { omValues = concat_strings_div(omValues, malloc_stringf("p5=%.3f", sensorBoiler.getValue(false).filteredValue), "&"); }; // Отправляем данные if (omValues) { dsSend(EDS_OPENMON, CONFIG_OPENMON_CTR01_ID, omValues, false); free(omValues); };
Функции для преобразования даты и времени в строку
Очень часто требуется преобразовать дату, время или интервал времени в строку. В принципе в этом нет ничего сложного, но я написал несколько функций-оберток просто для удобства использования.
char * malloc_timestr(const char *format, time_t value)
Выделяет необходимый блок памяти, а затем помещает в него дату или время в соответствии с заданным форматом. Здесь используется другой принцип форматирования строк, применяемый только для даты и времени: strftime, wcsftime, _strftime_l, _wcsftime_l
На самом деле внутри всё происходит с точностью до наоборот, вначале строка форматируется в статический буфер, а потом с буфера делается “слепок” в динамической памяти.
char * malloc_timestr_empty(const char *format, time_t value)
Вариант предыдущей функции, но если value = 0, функция вернет строку “—” (которая настраивается макросом CONFIG_FORMAT_EMPTY_DATETIME).
size_t time2str(const char *format, time_t value, char* buffer, size_t buffer_size)
Данная функция работает аналогично malloc_timestr(), но только с предоставленным буфером заданного размера. Иногда бывает нужно. По сути это простая обертка вокруг strftime().
size_t time2str_empty(const char *format, time_t value, char* buffer, size_t buffer_size)
Вариант предыдущей функции, но если value = 0, функция поместит в буфер символы “—” (CONFIG_FORMAT_EMPTY_DATETIME).
char * malloc_timespan_hms(time_t value)
Форматирует интервал времени в фиксированном виде ЧЧ:ММ:СС, например 08:00:01, то есть 8 часов 0 минут и 1 секунда. При превышении часов 24 будет число часов может быть больше, например 77:18:00.
char * malloc_timespan_dhms(time_t value)
Форматирует интервал времени в фиксированном виде Д.ЧЧ:ММ:СС, например 9.08:00:01, то есть 9 дней, 8 часов, 0 минут и 1 секунда.
Функции для генерации MQTT топиков по определенным правилам
Библиотека rStrings также включает в себя несколько функций для генерации MQTT-топиков по определенным правилам. Как я уже писал в предыдущих статях, я формирую топики устройства по определенным правилам:
<PREFIX><LOCATION>/<DEVICE>/...
Где <PREFIX>
– необязательный параметр, которой зависит только от брокера и вашего желания, <LOCATION>
– расположение устройства, <DEVICE>
– название устройства. Данные “составные части” каждого топика указываются в файле параметров проекта в разделе настроек MQTT-брокеров. Подробнее про это можно прочитать в следующих статьях, если не читали, рекомендую ознакомиться:
Термостат на ESP32 с удаленным управлением. Часть 3. Телеметрия
Термостат на ESP32 с удаленным управлением. Часть 4. MQTT-топики
Указанные ниже функции как раз и формируют топики по описанным правилам.
char * mqttGetSubTopic(const char *topic, const char *subtopic)
Склеивает две составных части топика воедино через стандартный для MQTT разделитель /. Внутри это просто malloc_stringf(“%s/%s“, topic, subtopic);
char * mqttGetTopicDevice(const bool primary, const bool local, const char *topic1, const char *topic2, const char *topic3);
Семейство функций для генерации “стандартного” топика устройства по правилу: <PREFIX><LOCATION>/<DEVICE>/topic1/topic2/topic3
. Если нужен более короткий топик (например только <PREFIX><LOCATION>/<DEVICE>/topic1
, передайте NULL в качестве соответствующих атрибутов функции), либо можно использовать соответствующую “номерную” функцию.
mqttPublish( mqttGetTopicDevice1(true, false, "water_level"), // Получаем топик malloc_stringf("{\"status\":%d}", getWaterLevel(), // Формируем JSON 1, true, // QOS и retained true, // Удалить топик из кучи после отправки true // Удалить JSON из кучи после отправки );
Атрибут primary
указывает на то, что генерируется топик для основного сервера, атрибут local
указывает на то, что используется локальный брокер. Подробнее о настройках брокеров см. статью “Термостат, часть 3“, раздел “Настройка MQTT брокеров”.
char * mqttGetTopicLocation(const bool primary, const bool local, const char *topic1, const char *topic2, const char *topic3);
Похожий набор функций, но формирующий топик, который могут использовать все приборы локации (то есть без <DEVICE>): <PREFIX><LOCATION>/topic1/topic2/topic3
. Всё остальное аналогично предыдущему варианту.
char * mqttGetTopicSpecial(const bool primary, const bool local, const char *special, const char *topic1, const char *topic2, const char *topic3);
Набор функций, очень похожий на mqttGetTopicDevice(), но здесь <DEVICE>
заменен на произвольную часть special, что позволяет напрямую подписываться на топики других устройств для простого и надежного обмена данными через локальный брокер (через публичный брокер тоже можно, но при отсутствии доступа в интернет обмен данными прекратится).
// SHT31: Температура и влажность воздуха (основной) topicLocdataAir1Temp = mqttGetTopicSpecial3(primary, true, CONFIG_SENSOR_LOCAL_EXCHANGE, CONFIG_MQTT1_LOC_DEVICE, SENSOR_AIR1_TOPIC, CONFIG_SENSOR_TEMP_NAME); if (topicLocdataAir1Temp) { rlog_i(logTAG, "Generated local topic for air 1 temperature: [ %s ]", topicLocdataAir1Temp); }; topicLocdataAir1Hum = mqttGetTopicSpecial3(primary, true, CONFIG_SENSOR_LOCAL_EXCHANGE, CONFIG_MQTT1_LOC_DEVICE, SENSOR_AIR1_TOPIC, CONFIG_SENSOR_HUMIDITY_NAME); if (topicLocdataAir1Hum) { rlog_i(logTAG, "Generated local topic for air 1 humidity: [ %s ]", topicLocdataAir1Hum); };
Функции генерации топиков “завязаны” на мой проект прошивки и вам могут оказаться бесполезны. В таком случае создайте fork библиотеки и удалите их (или измените под свои нужды).
На этом пока всё, благодарю за внимание. До встречи на сайте и на dzen-канале!
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью:
Thanks!