Перейти к содержимому

Работа с динамическими строками без класса String

Метки:

Добрый день, уважаемый читатель! Практически в каждой прошивке приходится в том или ином виде работать со строковой информацией. Это могут быть топики, уведомления, отладочные сообщения (логи) и т.д. В данной статье я расскажу про свой способ работы с динамическими строками.

Для начала чуть-чуть теории. Строка в 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() требует для своей работы буфер (блок памяти) необходимого размера. Решение данной проблемы довольно простое: вызываем 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“, hms);

Пример формирования GET-запроса с вставкой данных с сенсора

В ответ вы получите указатель на участок памяти, где размещена готовая к употреблению строка (массив символов).

⚠️ Не забывайте после использования смывать за собой удалить строку из кучи с помощью 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“, topicsubtopic);

 

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-канале!


Пожалуйста, оцените статью:
[ 0 из 5, всего 0 оценок ]

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *