Добрый день, уважаемый читатель! Практически в каждой прошивке приходится в том или ином виде работать со строковой информацией. Это могут быть топики, уведомления, отладочные сообщения (логи) и т.д. В данной статье я расскажу про свой способ работы с динамическими строками.
Для начала чуть-чуть теории. Строка в 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)
, что поначалу наверняка покажется сложнее и не удобнее. Но взамен мы получаем возможность сразу же добавить “окружающий” текст и причесать число в нужный вид – добавить лидирующие нули или округлить до необходимого количества знаков после запятой. Если вы ещё не знакомы с этими функциями, вы можете узнать про них более подробно из справочной системы:
Синтаксис спецификации формата: функции printf и wprintf
Но vprintf() требует для своей работы буфер (блок памяти) необходимого размера. Решение данной проблемы довольно простое: вызываем vprintf() два раза, причем в первый раз передаем ей NULL вместо указателя на буфер. В ответ vprintf() вернет длину результирующей строки, то есть фактически это и есть размер необходимого буфера. Добавляем к полученному значению 1 (под завершающий символ “\0”) и выделяем необходимую память.
Про особенности работы с памятью вы можете почитать из следующей статьи: Использование памяти в 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)
Склеивание двух строк с участием заданного разделителя, например через запятую или точку с запятой. Очень удобно использоваться для формирования https-запросов с параметрами:
Функции для преобразования даты и времени в строку
Очень часто требуется преобразовать дату, время или интервал времени в строку. В принципе в этом нет ничего сложного, но я написал несколько функций-оберток просто для удобства использования.
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 в качестве соответствующих атрибутов функции), либо можно использовать соответствующую “номерную” функцию.
Атрибут 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, что позволяет напрямую подписываться на топики других устройств для простого и надежного обмена данными через локальный брокер (через публичный брокер тоже можно, но при отсутствии доступа в интернет обмен данными прекратится).
Функции генерации топиков “завязаны” на мой проект прошивки и вам могут оказаться бесполезны. В таком случае создайте fork библиотеки и удалите их (или измените под свои нужды).
На этом пока всё, благодарю за внимание. До встречи на сайте и на dzen-канале!
Пожалуйста, оцените статью: