Добрый день, уважаемый читатель!
Сервис open-monitoring.online позволяет накапливать и хранить данные с контроллеров, а затем отображать их в виде графиков и гистограмм. Создан сервис компанией “НСК Электро” для мониторинга параметров солнечных электростанций, которые они продают. Но никто не запрещает пользоваться данным сервисом, так сказать, “в личных целях”, и причем абсолютно бесплатно. Чем мы и воспользуемся. В данной статье рассмотрим процесс регистрации на данном сервисе, создание контроллера для хранения данных, а так же отправку данных с устройств на базе Arduino и ESP32 (ESP-IDF).
Необходимое предупреждение! Данная статья – не рекламный материал. Я никак не связан с администрацией сервиса и не переписывался с ними. Все выводы и данные, приведенные в статье, получены на основе лично моего опыта работы с данным сервисом. Цель данной статьи – помочь Вам начать работу с сервисом, если Вы хотите научиться строить online-графики без создания личного сервера и базы данных.
Для кого и чего это нужно?
Если Вы стоите систему домашней автоматизации на базе какого-нибудь “умного дома” – Home Assistant или подобного, то Open Monitoring, скорее всего, Вам и не нужен. Если же Ваше устройство(а) работает автономно (как у меня), и умеет публиковать текущие данные на MQTT брокер, то Вы, наверное, задумывались о том, как бы было хорошо иметь возможность просмотреть историю изменения каких-либо значений по времени. Ну например: температуры в доме за сутки, неделю, год… Что ж, это довольно просто организовать с помощью рассматриваемого сервиса. Достаточно регулярно отправлять на него данные простым http-запросом, а сервис сам построит на основе этих данных графики. Если Вы уже знакомы с похожим сервисом ThingSpeak.com, то принцип работы здесь в чем-то похож, только open-monitoring более прост и удобен. Здесь Вам не потребуется утомительно подбирать множество параметров для вывода графиков, а сами графики легко накладываются друг на друга без извращений в виде MathLab. Процесс смены интервалов происходит в один или несколько кликов, система сама подбирает параметры усреднения данных.
Кроме собственно накопления и отображения данных, на сервисе имеется возможность настройки уведомлений о выходе параметра за пределы допустимого диапазона или потере связи с устройством. Это позволит Вам создать простую схему мониторинга состояния объекта без дополнительных усилий по программированию самого устройства, отправляющего данные. Уведомления будут записаны в журнал контроллера, а также есть возможность отправки сообщения на электронную почту.
Итак, что необходимо для того, чтобы начать пользоваться Open Monitoring:
- Желание наблюдать изменения параметров устройства во времени
- Устройство на базе Arduino (с каким-либо сетевым интерфейсом), ESP8266, ESP32, Raspberry Pi или подобное. Впрочем, этот список не является окончательным. Отправлять данные на сервер можно откуда угодно – например с компьютера, через браузер или любым другим удобным способом. К примеру, можно отправлять на сервис данные о загрузке процессора, свободной памяти и т.д. с какого-либо сервера. Можно придумать ещё много сценариев использования – всё зависит от Вашей фантазии.
- Постоянное подключение к сети интернет. Я специально дополнительно отметил этот пункт, так как с “классических” плат Arduino без сетевого адаптера отправить данные на сервер будет невозможно.
Ограничения и проблемы
Прежде чем начать пользоваться данным сервисом, давайте вначале рассмотрим имеющиеся ограничения и проблемы. Вдруг что-то кому-то из читателей покажется существенным, тогда и читать дальше не стоит.
При самостоятельной регистрации на сайте создается бесплатный аккаунт. Платный (PRO), насколько я понимаю, Вы можете получить, купив в компании солнечную электростанцию. Аккаунт PRO дает возможность добавлять на сцену элементы управления контроллерами. Я не являюсь клиентом компании, поэтому имею, как и все, бесплатный аккаунт. Ограничения для бесплатного режима:
- Количество хранимых (фактических) значений на один контроллер: 30. Дополнительно можно создать вычисляемые поля.
- Минимальный интервал отправки на сервер пакета значений для каждого контроллера: 60 секунд
- Максимальное количество контроллеров: либо не ограничено, либо я ещё не достиг ограничения. На текущий момент на моем аккаунте создано 10 контроллеров и есть возможность добавления ещё как минимум одного.
Как видим, условия вполне щедрые. Но есть еще пару отрицательных моментов, лично для меня не существенных, но всё-же стоит упомянуть о них:
- Сайт на текущий момент работает только по протоколу HTTP. Как просмотр в браузере, так и отправка данных. Впрочем, отправка данных из-за этого на устройстве несколько проще.
- Сервис иногда “отваливается” на непродолжительное время. Вероятно это бывает из-за проблем на сервере или технического обслуживания. Следовательно, программа отправки данных на устройстве должна учитывать этот момент, а не “зависать” на неопределенное время.
- Контроллер можно сделать “публичным”. Но это, увы, не значит, что графики с него можно просматривать “со стороны”. Доступным он станет только для других пользователей сервиса.
Если Вас не отпугнула данная информация, то приступаем к регистрации аккаунта. Если же у Вас аккаунт уже имеется, пропустите следующий раздел.
Регистрация аккаунта
Открываем open-monitoring.online, сайт автоматически перенаправит Вас на форму входа в аккаунт. Если аккаунта у Вас нет, нажмите ссылку “Регистрация”. Для регистрации Вам потребуется только адрес Вашей электронной почты, чтобы подтвердить аккаунт. Если у Вас нет электронной почты, то придется его вначале зарегистрировать на любом удобном почтовом сервисе.
Заполняем все предложенные поля, нажимаем кнопку “Регистрация”. Сайт перенаправит Вас обратно на форму входа. Но входить ещё рано – проверьте указанную Вами почту на предмет письма с активацией аккаунта. Если письмо долго не приходит – проверьте папку “Спам”, письмо наверняка находится там. В полученном письме нужно нажать ссылку “Активировать ваш аккаунт”, как на рисунке ниже:
После этого Вас перекидывает обратно на форму входа, где мы вводим адрес электронной почты и пароль, которые указали на форме регистрации. Если всё введено верно, то мы попадаем в личный кабинет. Здесь у нас у нас пусто. Можно создавать первый контроллер.
Создаем контроллер
Контроллер в терминах сайта – это набор данных, получаемых с одного устройства. Как я уже упоминал, в одном контроллере можно хранить до 30 значений (параметров). Впрочем, ничего не мешает отправлять с одного физического устройства данные на несколько виртуальных контроллеров. Например, я так поступаю, когда нужно хранить совершенно разные данные: например климат (температура / влажность / давление / iaq) и системную информацию (пинг, свободная память в куче, rssi и т.д.).
Каждое поле (“параметр” в терминах сайта, я же привык выражаться “программисткими” терминами, применимыми к таблицам баз данных, так как много лет ими занимаюсь) может хранить либо целые числа, либо дробные числа, либо строки (правда, я не представляю, как из них строить графики).
В левом меню нажмите “Новый контроллер” > “Создать“:
Допустим, к нашему устройству подключено три сенсора AHT10 для измерения температуры и влажности воздуха в различных помещениях и один DS18B20 в гильзе для измерения температуры теплоносителя в системе отопления. Поэтому нам понадобится 2*3+1=7 полей для данных. Если Вы намереваетесь использовать фильтрацию данных (например медианный фильтр или среднее значение за период), то имеет смысл хранить отдельно RAW-значения (полученные непосредственно с сенсоров) и обработанные фильтрами значения, то есть увеличить количество полей вдвое.
Тут стоит отметить одну особенность сервиса: после того, как Вы создадите контроллер, можно переименовать поля (параметры), но вот добавить или удалить их уже будет нельзя. Поэтому если в будущем понадобится добавить ещё несколько сенсоров к устройству, то придется удалить контроллер (потеряв накопленные данные), а затем создать его заново. Поэтому я заранее закладываю несколько параметров разного типа “в резерв”, чтобы потом в случае необходимости их просто переименовать и начать заполнять данными. Отправлять в них “нули” совершенно не обязательно.
Поочередно вводим названия полей (в названиях можно и нужно использовать кириллицу, но, к сожалению, нельзя использовать различные служебные знаки – скобки и т.д.). Не забываем выбирать правильный тип – для каждого нового поля он по умолчанию установлен как “целое”:
Когда все параметры введены – проверьте ещё раз типы данных. Названия полей Вы потом сможете исправить в случае ошибок, а вот типы полей – нет. Если всё введено правильно, нажимайте синюю кнопку снизу “Создать” для завершения создания контроллера. После этого Вам будут показаны свойства только что созданного контроллера:
По умолчанию не заполнено название контроллера и включены для отображения на графике все параметры. Поэтому надо немного подкорректировать только что созданный контроллер. Для этого выберите “Контроллер 1” > “Настройки“. На этой странице Вы можете задать удобное название для контроллера (например “Дача”), а также выбрать тип – “Приватный” или “Публичный”. К сожалению, для просмотра публичных контроллеров необходимо иметь аккаунт на сервисе, “со стороны” не посмотришь, поэтому особого смысла для меня в публичных контроллерах нет. На следующей части настроек можно выбрать отображать графики на главной сцене контроллера или нет (о сцене ниже). Еще ниже можно включить или отключить отображение графиков и гистограмм для части параметров “по умолчанию”. Просмотреть любой из параметров на графике можно в любой момент, просто нажав на соответствующий маркер под графиком, но вот чтобы при открытии графика не выводить сразу всё и сразу, стоит половину отключить. В самом конце формы Вы можете добавить вычисляемые параметры, о них тоже более подробно поговорим ниже. Для отправки изменений на сайт не забудьте нажать кнопку “Применить изменения”
Добавление вычисляемых параметров
На основе реальных данных, полученных “извне”, можно “на лету” вычислять новые значения. Например: по значениям двух или более датчиков вычислить среднее значение. Операций в списке не так много: сложение, вычитание, деление, умножение и сумма за выбранный период. Для одного вычисляемого параметра можно применить только одну операцию, поэтому среднее значение по нескольким датчикам придется за N “проходов” (вычисляемых полей): вначале посчитать сумму первого и второго физического параметра. Получим первое вычисляемое поле. Потом к этой сумме добавить ещё одно значение, получим второе вычисляемое поле. В конце разделить полученную сумму на количество датчиков (константу). Для двух сенсоров это выглядит так:
При создании вычисляемых параметров будьте внимательны и аккуратны: однажды добавив вычисляемый параметр, изменить или удалить его будет уже нельзя! На момент написания этой статьи сервис не предусматривает такой возможности.
Настройка сцены
Сцена предназначена для отображения актуальных (последних) данных с контроллера (сейчас это часто называют модным словом dashboard, хотя русское название мне лично больше нравится). Там же можно отображать графики и гистограммы, если установить соответствующие опции в настройках контроллера.
Для редактирования сцены нажмите символ ? в верхнем правом углу сцены, и Вы попадете на форму редактирования главной сцены. Редактор не самый удобный, но что есть, то есть.
Справа от макета сцены находится основная панель, на которой можно задать размер сцены и выбрать фоновое изображение. Хорошо подумайте, прежде чем загружать фоновое изображение – однажды добавив его, удалить изображение уже не реально. Картинка не дублируется, а масштабируется под заданный размер сцены, и причем только пропорционально. Не угадал с размером – остались белые полосы по краю. Поэтому вначале лучше всего расположить все элементы, определиться с размером, а потом в графическом редакторе подбирать картинку под размер сцены.
Чтобы добавить новый элемент с текстом, нажмите кнопку “Добавить элемент“, чтобы отредактировать уже существующий – выделите его. Если задать в элементе заголовок и параметр одновременно – будет добавлен двухстрочный текст, но для такого текста невозможно задать раздельные атрибуты. Чтобы иметь возможность сделать заголовок тонким шрифтом, а значение – жирным, а так же разместить их “в линию”, приходится добавлять два элемента на каждый параметр. Не особо сложно, но придется немного повозиться. Для каждого элемента можно задать фон – цвет или картинку, размер шрифта и его атрибуты.
События
Служба событий позволяет отправлять уведомления на заданную электронную почту при наступлении того или иного события. Вам не потребуется программировать устройство, чтобы оно отправляло электронные письма, это можно легко сделать через Open Monitoring. На сервисе можно создать три типа событий: внешние, по значению параметра и по таймеру. Для доступа к событиям необходимо открыть меню “Контроллер” – “Журнал событий“. На этой вкладке можно просмотреть журнал событий, то есть когда они произошли:
Для создания или редактирования событий необходимо нажать символ ? в верхнем правом углу окна, после чего Вы попадете в настройку событий:
Внешние события. Для того, чтобы внешнее событие сработало и отправило уведомление (в журнал и на почту), необходимо выполнить с устройства специальный HTTP GET-запрос, аналогично отправке данных в контроллер (более подробно процесс отправки данных будет рассмотрен ниже, поэтому здесь физической стороны процесса я касаться не буду). Текст запроса Вы сможете увидеть в окне редактирования события. По сути, данный тип события представляет собой удобный способ отправки сообщений электронной почты с устройства по шаблону.
Для того, чтобы при выполнении запроса сервер смог отправить сообщение, необходимо заполнить соответствующие поля. Можно настроить только запись в журнал, а можно настроить дополнительно еще и отправку на указанный адрес электронной почты. Для подстановки полученного в запросе значения используйте [value].
Как это можно использовать? Допустим у Вас есть устройство, управляющее бойлером. И Вы имеете желание получать уведомления о том, когда он включен или выключен. Создайте внешнее событие с текстом “Бойлер [value]“. Тогда при отправке GET-запроса http://open-monitoring.online/get/event?cid=9999&key=xXxXxX&eid=1&value=включен, Open Monitoring отправит Вам сообщение “Бойлер включен“. Конечно, это же самое можно сделать и непосредственно с устройства, но иногда этот способ может быть проще или удобнее.
По значению параметра. Для данного типа ничего программировать не придется – ведь Вы в любом случае отправляете данные с устройства на сервис. Дополнительно ничего и не потребуется. При получении очередных данных сервис сам проконтролирует, соответствует выбранный параметр заданному диапазону, и при необходимости сгенерирует соответствующее уведомление. Например: у меня на даче под чистовым полом (но поверх утеплителя) проложены трубы водоснабжения. Чтобы исключить риск, что холод таки проберется через двадцати пяти сантиметровый слой утеплителя и “прихватит” трубы, я засунул под чистовой пол к трубам датчик AHT10, который постоянно мониторит температуру и влажность. А Open Monitoring мне пришлет тревожный сигнал, если “что-то пойдет не так”.
По таймауту. Этот тип событий позволяет легко и непринужденно контролировать работоспособность устройств. Если Ваше устройство в течение заданного интервала не прислало ни одного пакета данных, то Вам будет выслано соответствующее уведомление. Это может случиться из-за отсутствия электропитания, доступа в интернет и т.д.
Для любого типа событий запись в журнал производится в любом случае, поэтому шаблоны сообщений для журнала необходимо заполнять обязательно. А вот отправка на почту – по желанию, поэтому адрес и шаблоны можно не заполнять. Шаблоны записи в журнал и отправки на почту могут различаться. Если Вы не получаете никаких уведомлений на электронную почту от сервиса – проверьте папку “Спам”, Ваши письма наверняка там. И включите адрес отправителя в “белый” список.
Отправка данных в контроллер
Отправка данных в контроллер осуществляется с помощью простого HTTP GET-запроса. Прототип для отправки данных GET-запросом можно посмотреть на странице “Контроллер” > “О контроллере“. В моем случае это выгладит примерно так:
http://open-monitoring.online/get?cid=9999&key=xXxXxX&p1=ЗНАЧ1&p2=ЗНАЧ2&p3=ЗНАЧ3&p4=ЗНАЧ4&p5=ЗНАЧ5&p6=ЗНАЧ6&p7=ЗНАЧ7&p8=ЗНАЧ8&p9=ЗНАЧ9&p10=ЗНАЧ10&p11=ЗНАЧ11&p12=ЗНАЧ12&p13=ЗНАЧ13&p14=ЗНАЧ14&p15=ЗНАЧ15&p16=ЗНАЧ16&p17=ЗНАЧ17&p18=ЗНАЧ18&p19=ЗНАЧ19&p20=ЗНАЧ20&p21=ЗНАЧ21&p22=ЗНАЧ22&p23=ЗНАЧ23&p24=ЗНАЧ24&p25=ЗНАЧ25
Не обязательно отправлять одним запросом сразу все данные, можно успешно отправить только одно или несколько значений. Вы можете сразу потестировать отправку данных, поставив в текст запроса тестовые данные и выполнив его через любой браузер с компьютера. Только следует помнить, что интервал между запросами не может быть меньше 1 минуты, если запрос придет на сервер раньше – он будет просто игнорирован. Ну например:
http://open-monitoring.online/get?cid=9999&key=xXxXxX&p1=14.0&p2=14.1&p3=40.5&p4=41.5
Разумеется, Вы должны подставить этот пример запроса реальные cid и key. После успешного выполнения запроса в вкладке “Данные” должны появиться строки с отправленными Вами данными. А после того, как строк будет не меньше 2, то можно посмотреть и графики.
Теперь рассмотрим, как можно отправлять данные непосредственно с микроконтроллеров. Поскольку для успешной отправки данных на сервис абсолютно необходимым условием является доступ в интернет, обычная плата типа Arduino Uno или Nano мало подходит для таких целей, так как не имеет сетевых интерфейсов на борту. Конечно, можно прицепить к ней ESP8266 в качестве модема, но зачем “городить огород”, если сам ESP8266 обладает большими вычислительными мощностями, и его можно также легко программировать из того-же ArduinoIDE и подключать к нему датчики. Так что смело отправляем Arduino в ящик, а весь код пишем в ESP8266 или ESP32.
Отправка данных из ESP8266 c помощью фреймворка Arduino
Я не буду здесь рассматривать как подключить микроконтроллер к Вашей WiFi сети – примеров множество и они достаточно простые. Пример такого устройства Вы можете найти и на моем сайте. Остановлюсь лишь на процедуре непосредственной отправки данных, когда ESP8266 уже подключена к WiFi-роутеру и имеет свободный доступ в сеть интернет. Приведенный здесь пример не является единственно верным, Вы можете использовать его в качестве основы, переработав по своему. В данном примере использован String для хранения и передачи строки с данными.
Функция отправки данных на сервер:
// Параметры OpenMon const char* omApiServer = "open-monitoring.online"; const char* omApiPost = "GET /get?%s HTTP/1.1\r\n"; const char* omApiHost = "Host: %s\r\n"; const char* omApiId = "cid="; const char* omApiKey = "&key="; const char* omApiField = "&p"; bool omPublish(const String request) { bool result = false; // Отправляем HTTP-запрос if (WiFi.connect(omApiServer, 80)) { WiFi.printf(omApiPost, request.c_str()); WiFi.printf(omApiHost, omApiServer); WiFi.println(F("User-Agent: ESP8266 (nothans)/1.0")); WiFi.println(F("Connection: close")); WiFi.println(); // Читаем результат (первую строку отправленного заголовка) Serial.printf("OpenMon :: send data \"%s\": %s\n", request.c_str(), WiFi.readStringUntil('\n')); result = true; } else { Serial.print(F("OpenMon :: Failed connection to ")); Serial.println(omApiServer); }; return result; }
Далее, где-то в другом месте Вашей программы, например после опроса сенсоров (но не чаще 1 раза в минуту), Вы должны вставить примерно следующий код:
// Формируем GET запрос String httpGet = String(omApiId) + String(9999) + omApiKey + "xXxXxX"; // Добавляем значения с сенсоров httpGet = httpGet + omApiField + String(1) + "=" + String(tempOutdoor); httpGet = httpGet + omApiField + String(2) + "=" + String(tempIndoor); ...здесь Вы вставляете нужное количество данных для отправки... // Отправляем запрос на сервер omPublish(httpGet);
Как видите – ничего сложного. В данном случае отсутствие HTTP упрощает задачу, так как ESP8266 не умеет только проверять сертификаты. По крайней мере пару лет назад, когда я имел с ними дело, это было не решенной задачей.
Отправка данных из ESP32 c помощью фреймворка ESP-IDF
Если Вы программируете ESP32 с помощью Arduino Framework, то Вам необходимо использовать предыдущий пример. Я же отказался от Arduino вообще, перейдя на “родной” для ESP32 фреймворк ESP-IDF (framework-espidf). Для работы с HTTP(S) запросами в нем существует специальный модуль – “esp_http_client.h“, которым я и воспользовался. Кроме того, отказался от class String, предпочитая выделять память под строки из кучи “вручную” (не забывая потом удалять их). Пример отправки сообщения теперь выглядит следующим образом:
omSendStatus_t omSendEx(uint32_t id, const char* key, char* data) { bool _result = true; char* get_request = nullptr; // Create the text of the GET request if ((id>0) && (key) && (data)) { get_request = malloc_stringf("cid=%d&key=%s&%s", id, key, data); }; if (get_request) { // Configuring request parameters esp_http_client_config_t cfgHttp; memset(&cfgHttp, 0, sizeof(cfgHttp)); cfgHttp.method = HTTP_METHOD_GET; cfgHttp.host = "open-monitoring.online"; cfgHttp.port = 80; cfgHttp.path = "/get"; cfgHttp.query = get_request; cfgHttp.use_global_ca_store = false; cfgHttp.transport_type = HTTP_TRANSPORT_OVER_TCP; cfgHttp.is_async = false; // Make a request to the API esp_http_client_handle_t client = esp_http_client_init(&cfgHttp); if (client != NULL) { esp_err_t err = esp_http_client_perform(client); if (err == ESP_OK) { int retCode = esp_http_client_get_status_code(client); if ((retCode == 200) || (retCode == 301)) { _result = true; rlog_i(logTAG, "Data sent # %d: %s", ctrl->id, get_request); } else { _result = false; rlog_e(logTAG, "Failed to send message, API error code: #%d!", retCode); }; } else { _result = false; rlog_e(logTAG, "Failed to complete request to open-monitoring.online, error code: 0x%x!", err); }; esp_http_client_cleanup(client); } else { _result = false; rlog_e(logTAG, "Failed to complete request to open-monitoring.online!"); }; }; // Remove the request from memory if (get_request) free(get_request); return _result; }
Для размещения в куче форматированной строки используется следующий код:
char * malloc_stringf(const char *format, ...) { uint32_t len; va_list args; char *ret; // get the list of arguments va_start(args, format); // calculate length of resulting string len = vsnprintf(NULL, 0, format, args); // allocate memory for string ret = (char*)malloc(len+1); if (ret) { memset(ret, 0, len+1); // get resulting string into buffer vsnprintf(ret, len+1, format, args); }; va_end(args); return ret; }
Теперь для отправки пакета данных необходимо вызвать примерно такой код:
char* data = malloc_stringf("p1=%f&p2=%f&p3=%f&p4=%f", tempOutdoor, humOutdoor, tempIndoor, humIndoor); if (data) { omSendEx(9999, "xXxXxX", data); free(data); };
где tempOutdoor, humOutdoor, tempIndoor, humIndoor – Ваши данные, которые необходимо отправить.
Библиотека для ESP32 и фреймворка ESP-IDF
Для своих устройств я написал чуть более сложную версию библиотеки, называется она reOpenMon. Отправка данных на Open Monitoring осуществляется в контексте специально созданной отдельной задачи FreeRTOS, это позволяет минимизировать простои на отправку данных со стороны “рабочих” задач. Кроме собственно приведенной выше отправки данных, эта библиотека контролирует минимальный интервал отправки данных на тот или иной контроллер (отдельно для каждого из зарегистрированных контроллеров). И если интервал времени с прошлой передачи слишком мал – отложит передачу данных на необходимое время, чтобы избежать потери данных. Но если в очереди на отправку уже имеются какие-либо данные, они будут затёрты последними. Данные можно отправлять в очередь даже в оффлайне, после восстановления доступа к серверу на сервер будут отправлены все данные из очереди. После отправки строка с данными будет автоматически удалена из кучи – Вам не нужно следить за этим.
Пример использования:
// Создаем и запускаем задачу отправки данных на OpenMon omTaskCreate(false); // Инициализируем контроллер, минимальное время отправки - 61 сек omControllerInit(9999, "xXxXxX", 61000); while (1) { // Читаем данные с сенсоров (условно) tempOutdoor = readTempOutdoor(); ... // Помещаем данные в очередь контроллера, остальное - не наша забота omSend(9999, malloc_stringf("p1=%f&p2=%f&p3=%f&p4=%f", tempOutdoor, humOutdoor, tempIndoor, humIndoor)); // Ждем 5 минут VTaskDelay(pdMS_TO_TICKS(300000)); }
Более подробные сведения Вы можете почерпнуть из README.md к самой библиотеке на GitHub
Скачать актуальную версию библиотеки reOpenMon можно с моего аккаунта GitHub:
Библиотека для отправки данных на Open Monitoring с устройств на базе ESP32 + ESP-IDF. Открыть на GitHub
Засим прощаюсь, благодарю за внимание
Если у Вас возникли вопросы – пишите в комментарии или в личные сообщения. Комментарии премодерируемые, так как спам уже достал. Контакты для личных сообщений есть на главной странице сайта внизу.
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью: