You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

784 lines
33 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

[![latest](https://img.shields.io/github/v/release/GyverLibs/StringUtils.svg?color=brightgreen)](https://github.com/GyverLibs/StringUtils/releases/latest/download/StringUtils.zip)
[![Foo](https://img.shields.io/badge/Website-AlexGyver.ru-blue.svg?style=flat-square)](https://alexgyver.ru/)
[![Foo](https://img.shields.io/badge/%E2%82%BD$%E2%82%AC%20%D0%9D%D0%B0%20%D0%BF%D0%B8%D0%B2%D0%BE-%D1%81%20%D1%80%D1%8B%D0%B1%D0%BA%D0%BE%D0%B9-orange.svg?style=flat-square)](https://alexgyver.ru/support_alex/)
[![Foo](https://img.shields.io/badge/README-ENGLISH-blueviolet.svg?style=flat-square)](https://github-com.translate.goog/GyverLibs/StringUtils?_x_tr_sl=ru&_x_tr_tl=en)
[![Foo](https://img.shields.io/badge/ПОДПИСАТЬСЯ-НА%20ОБНОВЛЕНИЯ-brightgreen.svg?style=social&logo=telegram&color=blue)](https://t.me/GyverLibs)
# StringUtils
Набор инструментов для работы со строками
- Быстрые функции конвертации
- Парсинг, разбивание по разделителям
- Несколько классов-конвертеров данных в строку и обратно для использования в других библиотеках
- Кодирование и раскодирование base64, url, unicode, йцукен/qwerty-раскладки
### Совместимость
Совместима со всеми Arduino платформами (используются Arduino-функции)
## Содержание
- [Документация](#docs)
- [Версии](#versions)
- [Установка](#install)
- [Баги и обратная связь](#feedback)
<a id="docs"></a>
## Документация
### su::Text
Класс-обёртка для всех типов строк. Может быть создана в конструкторе из:
- `"const char"` - строки
- `char[]` - строки
- `F("f-строки")`
- `PROGMEM` - строки
- `String` - строки
Особенности:
- Хранит тип и длину строки
- Позволяет **печататься**, **конвертироваться** в любой целочисленный формат и **сравниваться** с переменными всех стандартных типов, а также сравниваться с любыми другими строками
- Вывод в подстроки разными способами, поиск и разделение
- **Не может изменять исходную строку**, все операции только "для чтения"
- **Не создаёт копию строки** и работает с оригинальной строкой, т.е. *оригинальная строка должна быть в памяти на время существования Text*
- Если создана из String строки, то оригинальная String строка не должна меняться в процессе работы экземпляра Text
```cpp
// ====== КОНСТРУКТОР ======
su::Text(String& str);
su::Text(const String& str);
su::Text(const __FlashStringHelper* str, int16_t len = 0);
su::Text(const char* str, int16_t len = 0, bool pgm = 0);
// ======== СИСТЕМА ========
bool valid(); // Статус строки, существует или нет
bool pgm(); // Строка из Flash памяти
uint16_t length(); // Длина строки
uint16_t lengthUnicode();// Длина строки с учётом unicode символов
uint16_t readLen(); // посчитать и вернуть длину строки (const)
void calcLen(); // пересчитать и запомнить длину строки (non-const)
Type type(); // Тип строки
const char* str(); // Получить указатель на строку. Всегда вернёт ненулевой указатель
const char* end(); // указатель на конец строки. Всегда вернёт ненулевой указатель
bool terminated(); // строка валидна и оканчивается \0
// ======== ХЭШ ========
size_t hash(); // хэш строки size_t
uint32_t hash32(); // хэш строки 32 бит
// ======== PRINT ========
size_t printTo(Print& p); // Напечатать в Print (c учётом длины)
// ======== СРАВНЕНИЕ И ПОИСК ========
// сравнивается со всеми типами строк через ==
// Сравнить со строкой, начиная с индекса
bool compare(Text s, uint16_t from = 0);
// Сравнить со строкой, начиная с индекса, с указанием количества символов
bool compareN(Text s, uint16_t amount, uint16_t from = 0);
// Найти позицию символа в строке, начиная с индекса
int16_t indexOf(char sym, uint16_t from = 0);
// Найти позицию строки в строке
int16_t indexOf(Text txt, uint16_t from = 0);
// Найти позицию символа в строке с конца
int16_t lastIndexOf(char sym);
// Найти позицию строки в строке с конца
int16_t lastIndexOf(Text txt);
// найти символ и получить указатель на первое вхождение
const char* find(char sym, uint16_t from = 0);
// начинается со строки
bool startsWith(const Text& txt);
// заканчивается строкой
bool endsWith(const Text& txt);
// ======== РАЗДЕЛЕНИЕ И ПАРСИНГ ========
// вернёт новую строку с убранными пробельными символами с начала и конца
Text trim();
// Посчитать количество подстрок, разделённых символом (количество разделителей +1)
uint16_t count(char sym);
// Посчитать количество подстрок, разделённых строками (количество разделителей +1)
uint16_t count(Text txt);
// Разделить по символу-разделителю в массив любого типа
uint16_t split(T* arr, uint16_t len, char div);
// Получить подстроку из списка по индексу
Text getSub(uint16_t idx, char div);
// Получить подстроку из списка по индексу
Text getSub(uint16_t idx, Text div);
// выделить подстроку (начало, конец не включая). Отрицательные индексы работают с конца строки
Text substring(int16_t start, int16_t end = 0);
// Получить символ по индексу
char charAt(uint16_t idx);
// ======== ВЫВОД. СТРОКИ ========
// Получить как String строку
String toString(bool decodeUnicode = false);
// Вывести в String строку. Вернёт false при неудаче
bool toString(String& s, bool decodeUnicode = false);
// Добавить к String строке. Вернёт false при неудаче
bool addString(String& s, bool decodeUnicode = false);
// Вывести в char массив. Вернёт длину строки. terminate - завершить строку нулём
uint16_t toStr(char* buf, int16_t bufsize = -1, bool terminate = true);
// ======== ВЫВОД. B64 ========
// размер данных (байт), если они b64
size_t sizeB64();
// вывести в переменную из b64
bool decodeB64(void* var, size_t size);
// ======== ВЫВОД. ЧИСЛА ========
bool toBool(); // получить значение как bool
int16_t toInt16(); // получить значение как int16
int32_t toInt32(); // получить значение как int32
int64_t toInt64(); // получить значение как int64
uint32_t toInt32HEX(); // получить значение как uint 32 из HEX строки
float toFloat(); // получить значение как float
// также автоматически конвертируется и сравнивается с
bool
char + unsigned
short + unsigned
int + unsigned
long + unsigned
long long + unsigned
float
double
String
// для ручного управления строкой
const char* _str; // указатель на строку
uint16_t _len; // длина
```
#### Пример
```cpp
// конструктор
su::Text v0("-123456");
su::Text v1 = "-123456";
v1 = F("-123456");
String s("abcd");
su::Text v2(s);
v2 = s;
// сравнение
v2 == v1;
v2 == F("text");
v1 == -123456;
// авто конвертация
int v = v0;
String s2 = v2;
// вывод в массив
char buf[20];
v1.toStr(buf);
// парсинг и разделение
su::Text list("abc/123/def");
Serial.println(list.getSub(0, '/')); // abc
Serial.println(list.getSub(2, '/')); // def
Serial.println(list.substring(4, 6)); // 123
su::Text arr[3];
list.split(arr, 3, '/');
Serial.println(arr[0]);
Serial.println(arr[1]);
Serial.println(arr[2]);
// парсить можно в любой тип
int arr2[3]; // float, byte...
list.split(arr2, 3, '/');
Serial.println(arr2[0]);
Serial.println(arr2[1]);
Serial.println(arr2[2]);
// так делать НЕЛЬЗЯ
Text t1(String("123")); // строка будет выгружена из памяти!
// t1.... программа сломается
String s;
Text t1(s);
s += String("123"); // адрес строки изменится!
// t1.... программа сломается
// в то же время вот так - можно
void foo(const Text& text) {
// String существует тут
print(text);
}
foo(String("123"));
```
Встроенный разделитель и хэш-функции позволяют очень просто и эффективно разбирать различные текстовые протоколы. Например пакет вида `key=value`, где `key` может отсылать к переменной в коде. Пакет можно разделить, ключ хешировать и опросить через switch для присвоения н ужной переменной:
```cpp
Text txt("key1=1234");
int val = txt.getSub(1, '='); // значение в int
switch (txt.getSub(0, '=').hash()) { // хэш ключа
case su::SH("key1"):
var1 = val;
break;
case su::SH("key2"):
var2 = val;
break;
case su::SH("key3"):
var2 = val;
break;
}
```
или протокол вида `name/index/value`, где `name` - текстовый ключ, `index` - порядковый номер:
```cpp
Text txt("key/3/1234");
int val = txt.getSub(2, '/');
switch (txt.getSub(0, '/').hash()) {
case su::SH("key"):
switch(txt.getSub(1, '/').toInt16()) {
case 0: break;
case 1: break;
case 2: break;
//.....
}
break;
case su::SH("keykey"):
//...
break;
case su::SH("anotherKey"):
//...
break;
}
```
### su::Value
Добавка к `Text`, поддерживает все остальные стандартные типы данных. Имеет буфер 22 байта, при создании конвертирует число в него:
```cpp
su::Value(bool value);
su::Value(char + unsigned value, uint8_t base = DEC);
su::Value(short + unsigned value, uint8_t base = DEC);
su::Value(int + unsigned value, uint8_t base = DEC);
su::Value(long + unsigned value, uint8_t base = DEC);
su::Value(long long + unsigned value, uint8_t base = DEC);
su::Value(double value, uint8_t dec = 2);
// аналогично с ручным размером буфера
su::ValueT<размер буфера>();
```
#### Пример
```cpp
su::Value v0("-123456"); // все строки также можно
su::Value v1(123);
su::Value v2(3.14);
su::Value v3((uint64_t)12345678987654321);
// конвертируется из числа в текст
v1 = 10;
v1 = 3.14;
v1 = 12345654321ull;
Serial.println(v0); // печатается в Serial
Serial.println(v1 == v2); // сравнивается
// сравнивается с любыми строками
su::Text s("123");
String ss = "123";
Serial.println(s == "123");
Serial.println(s == F("123"));
Serial.println(s == ss);
// конвертируется в любой тип
float f = v2; // f == 3.14
int i = v1; // i = 123
// выводится в String
String S;
v0.toString(s);
// выводится в char[]
char buf[v1.length() + 1]; // +1 для '\0'
v1.toStr(buf);
```
#### Использование в библиотеках
На базе `Text` построены следующие библиотеки:
- [GSON](https://github.com/GyverLibs/GSON)
- [GyverHub](https://github.com/GyverLibs/GyverHub)
- [Pairs](https://github.com/GyverLibs/Pairs)
- [FastBot2](https://github.com/GyverLibs/FastBot2)
##### Передача текста в функцию
- Строки любого типа
- Без аллокаций, что чрезвычайно критично при сборке String
- Без создания десятка перегруженных функций
Например нужна функция, принимающая строку в любом виде. В ванильном фреймворке Arduino можно сделать так:
```cpp
void setText(const String& str) {
// и например прибавить к строке
s += str;
}
```
Такая функция сможет принимать любые строки:
```cpp
setText("const literal");
setText(F("F-string"));
char str[] = "buffer string";
setText(str);
String s = "Arduino String";
setText(s);
```
Но эта строка будет *продублирована* в конструкторе `String`, и самое страшное - в динамической памяти! Таким образом при прибавлении к условно-глобальной String в этой области определения случится переаллокация и фрагментирование памяти. `Text` позволяет полностью этого избежать:
```cpp
void setText(const Text& str) {
// и например прибавить к строке
str.addString(s);
}
```
Теперь эта функция так же умеет принимать строки в любом формате, но **не создаёт их копии**, и например прибавление к строке становится быстрым и безопасным.
##### Вывод текста
Также Text удобен для вывода, например в классе, который хранит буфер и сам наполняет его данными и знает их длину:
```cpp
class MyClass {
public:
su::Text get() {
return su::Text(buffer, len);
}
private:
char buffer[20];
int len;
};
MyClass s;
Serial.println(s.get());
```
Вариант с наследованием:
```cpp
class MyClass : public su::Text {
public:
void foo() {
su::Text::_str = buffer;
su::Text::_len = somelen;
}
private:
char buffer[20];
};
MyClass s;
Serial.println(s);
```
Если вместо `Text` использовать `Value` - функция сможет принимать также любые численные данные.
### su::TextList
Разделитель `Text` списков на `Text` подстроки.
#### Статический
```cpp
TextListT<int16_t cap>(Text list, char div);
TextListT<int16_t cap>(Text list, Text div);
// количество построк
uint16_t length();
// размер буфера
uint16_t capacity();
// получить подстроку под индексом
const Text& get(uint16_t idx);
const Text& operator[](int idx);
```
#### Динамический
```cpp
TextList(Text list, char div);
TextList(Text list, Text div);
// количество построк
uint16_t length();
// получить подстроку под индексом
const Text& get(uint16_t idx);
const Text& operator[](int idx);
```
### Устаревшие парсеры
<details>
<summary>Развернуть</summary>
### su::Parser
Разделение строки на подстроки по разделителю в цикле. **Изменяет** исходную строку, но после завершения возвращает разделители на место.
```cpp
su::Parser p(String& str, char div = ';');
su::Parser p(const char* str, char div = ';');
bool next(); // парсить следующую подстроку. Вернёт false, если парсинг закончен
uint8_t index(); // индекс текущей подстроки
const char* str(); // получить подстроку
Text get(); // получить подстроку как Text
```
#### Пример
```cpp
char buf[] = "123;456;abc";
su::Parser p(buf);
while (p.next()) {
Serial.print(p.index());
Serial.print(": ");
Serial.println(p.get());
}
```
### su::Splitter
Разделение строки на подстроки по разделителю в цикле. **Изменяет** исходную строку! После удаления объекта строка восстанавливается, либо вручную вызвать `restore()`
```cpp
su::SplitterT<макс. подстрок> spl(String& str, char div = ';');
su::SplitterT<макс. подстрок> spl(const char* str, char div = ';');
su::Splitter spl(String& str, char div = ';'); // авто-размер (выделяется в heap)
su::Splitter spl(const char* str, char div = ';'); // авто-размер (выделяется в heap)
void setDiv(char div); // установить разделитель
void restore(); // восстановить строку (вернуть разделители)
uint8_t length(); // количество подстрок
const char* str(uint16_t idx); // получить подстроку по индексу
Text get(uint16_t idx); // получить подстроку по индексу как Text
```
#### Пример
```cpp
char buf[] = "123;456;abc";
su::Splitter spl(buf);
for (uint8_t i = 0; i < spl.length(); i++) {
Serial.print(i);
Serial.print(": ");
Serial.println(spl[i]);
}
spl.restore();
```
### su::list функции
```cpp
// Получить количество подстрок в списке
uint16_t su::list::length(Text list, char div = ';');
// Получить индекс подстроки в списке
int16_t su::list::indexOf(Text list, Text str, char div = ';');
// Проверка содержит ли список подстроку
bool su::list::includes(Text list, Text str, char div = ';');
// Получить подстроку из списка по индексу
Text su::list::get(Text list, uint16_t idx, char div = ';');
// распарсить в массив указанного типа и размера. Вернёт количество записанных подстрок
template <typename T>
uint16_t su::list::parse(Text list, T* buf, uint16_t len, char div = ';');
```
#### Пример
```cpp
Serial.println(su::list::length("123;456;333")); // 3
Serial.println(su::list::includes("123;456;333", "456")); // true
Serial.println(su::list::indexOf("123;456;333", "333")); // 2
Serial.println(su::list::get("123;456;333", 1)); // 456
// распарсить в массив
float arr[3];
su::list::parse(F("3.14;2.54;15.15"), arr, 3);
```
### su::List класс
Получение подстрок по разделителям **без модификации исходной строки**, работает также с PROGMEM строками.
```cpp
List(Text);
// установить разделитель
void setDiv(char div);
// получить размер списка
uint16_t length();
// получить индекс подстроки в списке или -1 если её нет
int16_t indexOf(Text str);
// проверить наличие подстроки в списке
bool includes(Text str);
// получить подстроку под индексом
Text get(uint16_t idx);
// распарсить в массив указанного типа и размера. Вернёт количество записанных подстрок
template <typename T>
uint16_t parse(T* buf, uint16_t len);
```
#### Пример
```cpp
su::List list(F("123;456;333"));
Serial.print("len: ");
Serial.println(list.length()); // 3
Serial.print("index 2: ");
Serial.println(list[2]); // 333
Serial.print("index of '456':");
Serial.println(list.indexOf(F("456"))); // 1
Serial.print("index of '789':");
Serial.println(list.indexOf("789")); // -1
// переписать в массив
int arr[3];
list.parse(arr, 3);
```
</details>
### su::PrintString
```cpp
// строка, в которую можно делать print/println
su::PrintString prs;
prs += "как обычный String";
prs.print(10);
prs.println("hello");
prs.print(F("print!"));
Serial.println(prs);
```
### QWERTY
```cpp
// Изменить раскладку (RU в QWERTY) - String
String su::toQwerty(const String& ru);
// Изменить раскладку (RU в QWERTY) - char* (qw длина как ru + 1, функция добавит '\0')
char* su::toQwerty(const char* ru, char* qw);
```
### Base64
```cpp
// размер закодированных данных по размеру исходных
size_t su::b64::encodedLen(size_t len);
// будущий размер декодированных данных по строке b64 и её длине
size_t su::b64::decodedLen(const char* b64, size_t len);
// закодировать данные в String
void su::b64::encode(String* b64, uint8_t* data, size_t len, bool pgm = false);
// закодировать данные в char[] (библиотека не добавляет '\0' в конец)
void su::b64::encode(char* b64, uint8_t* data, size_t len, bool pgm = false);
// раскодировать данные из строки b64 в буфер data
void su::b64::decode(uint8_t* data, const char* b64, size_t len);
void su::b64::decode(uint8_t* data, const String& b64);
```
### Unicode
Декодер строки, содержащей unicode символы вида `\u0abc`. Также делает unescape символов `\t\r\n`!
```cpp
// декодировать строку.Зарезервировать строку на длину len. Иначе - по длине строки
String su::unicode::decode(const char* str, uint16_t len = 0);
// декодировать строку
String su::unicode::decode(const String& str);
// кодировать unicode символ по его коду. В массиве должно быть 5 ячеек
void su::unicode::encode(char* str, uint32_t c);
// кодировать unicode символ по его коду
String su::unicode::encode(uint32_t code);
```
### URL
```cpp
// символ должен быть urlencoded
bool su::url::needsEncode(char c);
// закодировать в url
void su::url::encode(const char* src, uint16_t len, String& dest);
void su::url::encode(const String& src, String& dest);
String su::url::encode(const String& src);
// раскодировать url
void su::url::decode(const char* src, uint16_t len, String& dest);
void su::url::decode(const String& src, String& dest);
String su::url::decode(const String& src);
```
### Hash
Вместо сравнения строк можно сравнивать хэш этих строк, что делает программу компактнее, легче и в большинстве случаев быстрее. Функции, указанные ниже как "считается компилятором" - считаются компилятором, то есть **строка даже не попадает в код программы** - вместо неё подставляется хэш-число:
```cpp
// считается компилятором
constexpr su::size_t su::SH(const char* str); // (String Hash) размер size_t
constexpr su::size_t SH32(const char* str); // (String Hash) размер 32 бит
// считается в рантайме
size_t su::hash(const char* str, int16_t len = -1); // Размер зависит от платформы и соответствует size_t
uint32_t su::hash32(const char* str, int16_t len = -1); // Размер 32 бит
size_t su::hash_P(PGM_P str, int16_t len = -1); // PROGMEM строка, размер size_t
uint32_t su::hash32_P(PGM_P str, int16_t len = -1); // PROGMEM строка, размер 32 бит
```
> На ESP-платах `SH`, `hash` и `hash_P` по умолчанию являются 32-битными!
По проведённому тесту 32-битная версия хэша имеет 7 коллизий из 234450 английских слов, 16-битная версия - 170723 коллизий (что есть 73% - чисто статистическое количество коллизий из расчёта 16 бит - 65536 значений)
#### Пример
Определить, какая строка была "получена". Классический способ со сравнением строк из PROGMEM:
```cpp
char buf[] = "some_text";
if (!strcmp_P(buf, PSTR("abcdef"))) Serial.println(0);
else if (!strcmp_P(buf, PSTR("12345"))) Serial.println(1);
else if (!strcmp_P(buf, PSTR("wrong text"))) Serial.println(2);
else if (!strcmp_P(buf, PSTR("some text"))) Serial.println(3);
else if (!strcmp_P(buf, PSTR("hello"))) Serial.println(4);
else if (!strcmp_P(buf, PSTR("some_text"))) Serial.println(5);
```
Способ с хэшем строки:
```cpp
using su::SH;
using su::hash;
char buf[] = "some_text";
switch (hash(buf)) {
case su::SH("abcdef"): Serial.println(0); break;
case su::SH("12345"): Serial.println(1); break;
case su::SH("wrong text"): Serial.println(2); break;
case su::SH("some text"): Serial.println(3); break;
case su::SH("hello"): Serial.println(4); break;
case su::SH("some_text"): Serial.println(5); break;
}
```
> Один расчёт хэша занимает чуть большее время, чем сравнение со строкой. Но итоговая конструкция из примера выполняется в 2 раза быстрее (на ESP).
> `SH("строки")` в данном примере вообще не попадают в код программы - вместо них подставляется их хэш
### Прочие утилиты
```cpp
// Длина строки с русскими символами
uint16_t su::strlenRu(const char* str);
// Получить длину целого числа
uint8_t su::intLen(int32_t val);
// Получить длину float числа
uint8_t su::floatLen(double val, uint8_t dec);
// Преобразовать строку в целое число
template <typename T>
T su::strToInt(const char* str, uint8_t len = 0);
// Преобразовать PROGMEM строку в целое число
template <typename T>
T su::strToInt_P(const char* str, uint8_t len = 0);
// Преобразовать float в строку с указанием кол-ва знаков после точки
uint8_t su::floatToStr(double val, char* buf, uint8_t dec = 2);
// Преобразовать HEX строку в целое число. Опционально длина
uint32_t su::strToIntHex(const char* str, int8_t len = -1);
// Длина символа в количестве байт
uint8_t su::charSize(char sym);
// Конвертация числа в char* массив. Пишет от начала массива, добавляет '\0', вернёт длину строки
// для int64 макс. длина буфера - 22 символа, для int32 - 12
uint8_t su::uintToStr(uint32_t n, char* buf, uint8_t base = DEC);
uint8_t su::intToStr(int32_t n, char* buf, uint8_t base = DEC);
uint8_t su::uint64ToStr(uint64_t n, char* buf, uint8_t base = DEC);
uint8_t su::int64ToStr(int64_t n, char* buf, uint8_t base = DEC);
// конвертация из строки во float
float su::strToFloat(const char* s);
// конвертация из PROGEMEM строки во float
float su::strToFloat_P(PGM_P s);
// быстрый целочисленный логарифм 10 (длина числа в кол-ве символов)
uint8_t su::getLog10(uint32_t value);
uint8_t su::getLog10(int32_t value);
// быстрое возведение 10 в степень
uint32_t su::getPow10(uint8_t value);
```
<a id="versions"></a>
## Версии
- v1.0
- v1.1.0 - оптимизация, добавлены фичи, исправлены уязвимости
- v1.2 - добавлены хэш-функции
- v1.2.x - мелкие исправления и улучшения
- v1.3 - оптимизация const, добавлены фичи, сравнения
- v1.3.1 - добавлен substring
- v1.3.2 - поддержка ESP8266 версий 2.x
- v1.3.5 - uintToStr: HEX теперь в нижнем регистре как у си-функций
- v1.3.6 - в Text добавлены toInt32HEX(), count(), split() и getSub(). Добавлен парсер TextList
- v1.3.7 - исправлены варнинги на AVR
- v1.4.0 - AnyText переименован в Text, пространство имён sutil - в su, добавлены функции в Text, результат конвертации и substring приведены к Си и JS функциям
<a id="install"></a>
## Установка
- Библиотеку можно найти по названию **StringUtils** и установить через менеджер библиотек в:
- Arduino IDE
- Arduino IDE v2
- PlatformIO
- [Скачать библиотеку](https://github.com/GyverLibs/StringUtils/archive/refs/heads/main.zip) .zip архивом для ручной установки:
- Распаковать и положить в *C:\Program Files (x86)\Arduino\libraries* (Windows x64)
- Распаковать и положить в *C:\Program Files\Arduino\libraries* (Windows x32)
- Распаковать и положить в *Документы/Arduino/libraries/*
- (Arduino IDE) автоматическая установка из .zip: *Скетч/Подключить библиотеку/Добавить .ZIP библиотеку…* и указать скачанный архив
- Читай более подробную инструкцию по установке библиотек [здесь](https://alexgyver.ru/arduino-first/#%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0_%D0%B1%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA)
### Обновление
- Рекомендую всегда обновлять библиотеку: в новых версиях исправляются ошибки и баги, а также проводится оптимизация и добавляются новые фичи
- Через менеджер библиотек IDE: найти библиотеку как при установке и нажать "Обновить"
- Вручную: **удалить папку со старой версией**, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!
<a id="feedback"></a>
## Баги и обратная связь
При нахождении багов создавайте **Issue**, а лучше сразу пишите на почту [alex@alexgyver.ru](mailto:alex@alexgyver.ru)
Библиотека открыта для доработки и ваших **Pull Request**'ов!
При сообщении о багах или некорректной работе библиотеки нужно обязательно указывать:
- Версия библиотеки
- Какой используется МК
- Версия SDK (для ESP)
- Версия Arduino IDE
- Корректно ли работают ли встроенные примеры, в которых используются функции и конструкции, приводящие к багу в вашем коде
- Какой код загружался, какая работа от него ожидалась и как он работает в реальности
- В идеале приложить минимальный код, в котором наблюдается баг. Не полотно из тысячи строк, а минимальный код