Перейти к публикации
iT4iT.CLUB

Kitsum

Пользователи
  • Публикации

    424
  • Зарегистрирован

  • Посещение

  • Дней в лидерах

    234

Сообщения, опубликованные пользователем Kitsum


  1. @Alex_DIY Все же это не дает мне покоя

    В данный момент у меня нет доступа к домашнему серверу, но доступна другая машина с Linux Ubuntu 16.04 на борту и оснащенная:

    • Процессор Intel Core i3-3220 3300MHz/3Mb
    • Оперативная память DDR3 4x2Gb 1600MHz
    13 часа назад, Alex_DIY сказал:

    Алгоритм подсчета md5, мне кажется один и стандартизован, поэтому где он реализован быстрее в phyton или Mysql ... думаю скорости сопоставимы

    Сам алгоритм безусловно один, иначе и быть не может, но то, как он реализован, это совсем другое. Допустим, мы имеем две функции перед которыми поставлена одинаковая задача, но реализация будет разная, при этом они будут возвращать идентичный результат. Пусть они будут написаны на Python, но это уже не имеет особого значения, если только они не будут оптимизированы третьей стороной перед исполнением.

    def test1(a):
        b = a
        c = b + 1
        return c
    
    def test2(a):
        return a + 1

    Логично, что для их исполнения нужно разное количество операций. Конечно, чтобы увидеть результат и разницу по времени, придется вызвать их N-ное количество раз. Я вызвал их в порядке их описания по 10 000 000 раз каждую и получил следующее время.

    0:00:01.461331
    0:00:01.145653

    Тест повторил несколько раз и получил аналогичные результаты.

    Я понимаю, что возможно все это глупости, и я ловлю не тех "блох", но я отталкиваюсь от той мысли, что мой код далек от совершенства и дабы оптимизировать его, и тем самым ускорить исполнение, я пытаюсь переложить нагрузку на код написанный Программистами совсем другого уровня, чья цель максимально быстро выполнить необходимые вычисления и перейти к следующей операции. Ведь скорость вычисления в СУБД, это один из критических факторов.

    Поэтому прошу проникнуться моей идеей и рассмотреть следующий тест.

    Проведем по десять тестов с вычислением 1 000 000 MD5 хешей из одной фиксированной строки - "$SYS/broker/version".

    mysql> select benchmark(1000000, md5('$SYS/broker/version'));

    MySQL выполнил 10 тестов по 1 000 000 вычислений за следующее время

    1 row in set (0,20 sec)
    1 row in set (0,22 sec)
    1 row in set (0,23 sec)
    1 row in set (0,23 sec)
    1 row in set (0,23 sec)
    1 row in set (0,22 sec)
    1 row in set (0,23 sec)
    1 row in set (0,21 sec)
    1 row in set (0,21 sec)
    1 row in set (0,22 sec)

    Лишний мусор я вырезал оставив только результат по затраченному времени.

    Для Python я набросал следующий скрипт. Постарался упростить тест и до его начала произвел преобразование строки в UTF-8.

    #!/usr/bin/env python
    # coding=utf-8
    
    import hashlib
    import datetime
    
    def speedTest(message):
        start = datetime.datetime.now()
        for i in range(0, 1000000):
            hashlib.md5(message).hexdigest()
        finish = datetime.datetime.now()
        print(finish - start)
    
    message = "$SYS/broker/version".encode('utf-8')
    for i in range(0, 10):
        speedTest(message)

    Те же 10 тестов по 1 000 000 вычислений дают следующий результат.

    root@asupmonitor:/media# ./speed.py
    0:00:00.832224
    0:00:00.833014
    0:00:00.782348
    0:00:00.788166
    0:00:00.782401
    0:00:00.785473
    0:00:00.784118
    0:00:00.782414
    0:00:00.789315
    0:00:00.788393

    Получается, что MySQL производит вычисления MD5 хеша, примерно, в 3 раза быстрее.

    Теперь вернемся к самому демону. Я упоминал о вызове функции on_mseeage при поступлении сообщения от MQTT брокера. Насколько я понял, все сообщения обрабатываются библиотекой paho по очереди и никакой многопоточности нет. Чем дольше работаем с сообщением, тем позже перейдем к следующему, а последнее сообщение в очереди будет уныло скучать. Назвать все сообщение равноценными я не могу т.к, температура на улице не имеет такую важность как тревога о протечке воды в квартире или превышение уровня газа в помещении с нагревательным котлом. И с ростом переправляемого трафика увеличится время на его обработку. Я старался оставить в on_message только самое важное.

    Возможно я не прав, но позже я планирую уделить больше времени MQTT. Если у Вас есть идеи по оптимизации демона, обязательно пишите и мы вместе улучшим его работу. Еще раз спасибо, что уделили время.


  2. Обновление от 28.11.2017

    Доброе время суток друзья. Последнее время начал образовываться зоопарк из датчиков используемых в проекте. Это привело к появлению нескольких копий метеостанции поддерживать которые попросту утомительно и, как показала практика, описания по переходу на альтернативные сенсоры не пользуются популярностью и могут устаревать. Конечно хочется угодить всем, но для перехода на тот или иной датчик людям приходится вносить изменения, как минимум, в трех разных частях кода проекта. Соответственно появляются вопросы, которых можно было бы избежать.

    Теперь в проект добавлена поддержка как основных, так и альтернативных датчиков, но т.к вероятность замены сенсоров не высока, то нет смысла нагружать микроконтроллер одновременной поддержкой всего парка. И делать смену модели датчика в web интерфейсе. Для решения этой задачи были добавлены директивы условной компиляции. Это позволяет нам с Вами указывать используемые датчики только в одном месте кода, а именно, при подключении необходимых библиотек. Далее, при компиляции проекта, компилятор, опираясь на Ваш выбор, произведен необходимые изменения в коде. Но, чтобы это работало мы должны условиться использовать определенные библиотеки. Список поддерживаемых датчиков и соответствующих библиотек был обновлен и находится в первом посте темы.

    IMG_0614.JPGIMG_0615.JPG

    Рассмотрим как это работает на примере выбора датчика влажности. На момент публикации этого сообщения имеется поддержка датчиков HDC1080, SI7021 и HTU21D. Все необходимые библиотеки уже указаны в коде проекта. Если Вы хотите использовать SI7021, то просто раскомментируйте строки отвечающие за подключение соответствующей библиотеки и объявления переменной для датчика в начале кода программы.

    Скрытый текст
    
    /*
       датчик влажности и температуры
       HDC1080 https://github.com/closedcube/ClosedCube_HDC1080_Arduino
       SI7021  https://github.com/LowPowerLab/SI7021
       HTU21D  https://github.com/sparkfun/SparkFun_HTU21D_Breakout_Arduino_Library
    */
    //#include "ClosedCube_HDC1080.h"
    //ClosedCube_HDC1080 HDC1080;
    
    #include <SI7021.h>
    SI7021 SI7021;
    
    //#include "SparkFunHTU21D.h"
    //HTU21D HTU21D;

     

    Учтите, что если Вы раскомментируйте несколько библиотек, использующих датчики на одном адресе I2C шины, то ни к чему хорошему это не приведет. Один адрес = один датчик = одна библиотека. Допустимо использование однотипных датчиков, но только с разными адресами и Вам придется самостоятельно добавить необходимую логику для второго датчика т.к это выходит за приделы общей логики проекта. Такие варианты по-прежнему будут рассматриваться отдельно.

    Теперь, когда компилятор дойдет до функции Setup и инициализации датчиков, отталкиваясь от нашей библиотеке, будет произведен выбор между нужными вариантами кода, а все остальное будет выкинуто из программы.

    Скрытый текст
    
      // HDC1080, SI7021 и HTU21D
      Wire.beginTransmission(0x40);
      if (Wire.endTransmission() == 0) {
        #ifdef _CLOSEDCUBE_HDC1080_h
          HDC1080.begin(0x40);
          temperature.status = humidity.status = true;
        #endif
        #ifdef si7021_h
          temperature.status = humidity.status = SI7021.begin(pin_sda, pin_scl);
        #endif
        #ifdef HTU21D_ADDRESS
          HTU21D.begin();
          temperature.status = humidity.status = true;
        #endif
      }

     

    Аналогичный выбор будет сделан при компиляции функции отвечающую за чтение данных с датчиков

    Скрытый текст
    
    void readSensors() {
      timerReadSensors = millis();
      /* BH1750 */
      #ifdef BH1750_h
        if (light.status) light.data = BH1750.readLightLevel();
      #endif
      /* HDC1080 */
      #ifdef _CLOSEDCUBE_HDC1080_h
        if (temperature.status) temperature.data = HDC1080.readTemperature();
        if (humidity.status) humidity.data = HDC1080.readHumidity();
      #endif
      /* SI7021 */
      #ifdef si7021_h
        if (temperature.status) temperature.data = SI7021.getCelsiusHundredths() * 0.01;
        if (humidity.status) humidity.data = SI7021.getHumidityPercent();
      #endif
      /* HTU21D */
      #ifdef HTU21D_ADDRESS
        if (temperature.status) temperature.data = HTU21D.readTemperature();
        if (humidity.status) humidity.data = HTU21D.readHumidity();
      #endif
      /* BMP085 */
      #ifdef ADAFRUIT_BMP085_H
        if (pressure.status) pressure.data = BMP085.readPressure() / 133.3;
      #endif
      /* BME280 */
      #ifdef TG_BME_280_I2C_H
        if (temperature.status) BME.read(pressure.data, temperature.data, humidity.data, BME280::TempUnit_Celsius, BME280::PresUnit_torr);
      #endif
    }

     

    Для выбора между частями кода используются те или иные константы, объявленные авторами библиотек.

    Добавлена поддержка нового датчика BME280. Сам датчик предоставляет показания температуры, влажности и атмосферного давления. По умолчанию используется адрес на I2C шине 0x76.

    Также в код добавлена функция расчета абсолютной влажности, но она не задействована по умолчанию. Используйте её если это потребуется в Вашем проекте. 

    Скрытый текст
    
    /*
      Функция расчета абсолютной влажности воздуха (грамм воды на один кубический метр воздуха)
      https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
    */
    float sensorsAbsoluteHumidity(float t, float h) {
      double tmp = pow(2.718281828, (17.67 * t) / (t + 243.5));
      return (6.112 * tmp * h * 2.1674) / (273.15 + t);
    }

     

    Также были внесены мелкие доработки, не влияющие на общий функционал проекта.

    • Thanks 1

  3. @Alex_DIY Я внес исправления в пост и SQL файл.

    3 часа назад, Alex_DIY сказал:

    Просто на картинке в первом посте столбца md5_UNIQUE не наблюдается

    Так и должно быть т.к это имя индекса для выбранного поля. Вы можете дать ему другое имя или вообще не задавать его.

    3 часа назад, Alex_DIY сказал:

    Я так понимаю, что это md5 от md5?

    Нет, это не так. Вас скорее всего запутало имя индекса, если бы оно выглядело иначе, например, "abc_UNIQUE", то все было бы яснее. В любом случае стоит пояснить, что происходит на самом деле. Нам необходимо производить быстрый поиск по таблице и сделать это без индекса невозможно. Самым логичным решением было бы использовать в качестве первичного ключа имя топика, но оно имеет тип text, а индексация по этому типу невозможна. Поле должно иметь тип с явно определенной длинной, но тогда мы рискуем обрезать имена топиков и потерять уникальность поля, что приведет к проблемам. Если я ошибаюсь, прошу меня поправить.

    Для решения этой проблемы было введено поле с MD5 хешем вычисленным из имени топика. Теперь у нас есть уникальное поле с типом VARCHAR и фиксированной длинной в 32 байта. Оно позволяет четко идентифицировать запись и соответствует конкретному топику. Это поле можно использовать для индексации записей.

    mysql> use `mqtt`
    Database changed
    mysql> EXPLAIN SELECT * FROM topics WHERE md5 = md5('$SYS/broker/version');
    +----+-------------+--------+------------+-------+--------------------+---------+---------+-------+------+----------+-------+
    | id | select_type | table  | partitions | type  | possible_keys      | key     | key_len | ref   | rows | filtered | Extra |
    +----+-------------+--------+------------+-------+--------------------+---------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | topics | NULL       | const | PRIMARY,md5_UNIQUE | PRIMARY | 98      | const |    1 |   100.00 | NULL  |
    +----+-------------+--------+------------+-------+--------------------+---------+---------+-------+------+----------+-------+
    1 row in set, 1 warning (0,00 sec)

    А вот так бы происходил поиск записи без использования индекса

    mysql> EXPLAIN SELECT * FROM topics WHERE topic = '$SYS/broker/version';
    +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
    | id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
    +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
    |  1 | SIMPLE      | topics | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   48 |    10.00 | Using where |
    +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
    1 row in set, 1 warning (0,00 sec)

    Никаких вычислений MD5 из MD5 не происходит.

    3 часа назад, Alex_DIY сказал:

    Нагружать за каждое сообщение от брокера повторным расчетом хэша мне кажется это избыточно

    Я не буду утверждать, но мне кажется, что расчет MD5 хеша на уровне MySQL сервера будет выполнен с большей эффективностью чем на стороне Python. Особенно учитывая, что вычисленный хеш индексируется только один раз при добавлении записи. А значит можно провести простой тест.

    mysql> select benchmark(1000000, md5('$SYS/broker/version'));
    +------------------------------------------------+
    | benchmark(1000000, md5('$SYS/broker/version')) |
    +------------------------------------------------+
    |                                              0 |
    +------------------------------------------------+
    1 row in set (0,65 sec)

    Данные расчеты произведены на Foxconn NanoPC nT-i1250 c процессором Intel Dual Core Atom D2550 1.86GHz и памятью DDR3 объемом 2Gb. Расчеты для Python не производил т.к для этого необходимо написать программу, но если Вам будет интересно, то я сделаю это позже. Но думаю, что полученные результаты для MySQL должны быть вполне убедительны.

    Также вынос MD5 расчета был обусловлен разделением обязанностей. Мне показалось более красивым и правильным передавать на MySQL сервер имя топика, а сервер уже самостоятельно принимает решение что и как с ним делать. Также это разделение мне нравится из-за возможности вносить изменения в работу программы не останавливая демона. Это также позволяет расширять функционал, например, добавить необходимое Вам логирование конкретных топиков при этом сам демон не увидит никакой разницы при работе с базой. Ну и до кучи, это логика INSERT/UPDATE без штрафа на передачу данных между демоном и базой, но это уже не имеет никакого отношения к заданному Вами вопросу.

    3 часа назад, Alex_DIY сказал:

    Если и python и база на одном и том железе, то по идее какая разница какой процесс процессорное время скушает.

    Разница будет в реализации алгоритмов, и для одной и той же задачи процессору понадобится разное количество тактов на выполнение. Совсем глубоко я не копал, но это интересный вопрос и, как мне кажется, работа базы более оптимизирована. Также меня волновал момент с реализацией вызова функции on_message в библиотеки paho и я бы хотел перейти максимально быстро к разбору следующего в очереди сообщения. Также я прекрасно отдаю себе отчет, что мой код очень далек от идеала и я трачу много времени на лишние операции и имею большие паузы между ними и это довольно весомый фактор, по крайней мере для меня.

    Я буду рад любым предложениям по оптимизации. И еще раз спасибо за интересные и правильные вопросы.


  4. 10 часов назад, Alex_DIY сказал:

    Получается демон просто хранит в базе всего лишь последние значения каждого топика насколько я понял? То есть я не смогу из таблицы topics сделать выборку записей определенного топика за какой-то промежуток времени?

    Именно так. Чтобы реализовать выборку за определенный промежуток времени, необходимо переработать саму таблицу.

    10 часов назад, Alex_DIY сказал:

    Почему выбран вариант подсчет md5 в виде процедуры базы данных, а не силами python?

    Это попытка переложить часть работы на базу, в нагрузку к реализации логики выбора типа запроса. В Вашем случае придется ввести иной ключ и подумать об оптимизации выборки.

    11 час назад, Alex_DIY сказал:

    не понял что делает эта конструкция  UNIQUE KEY `md5_UNIQUE` (`md5`)?

    Создает уникальный ключ с именем md5_UNIQUE для поля md5. Как Вы и заметили, присутствие данной записи избыточно из-за наличия PRIMARY KEY для этого поля. Относится он к первому варианту таблицы которая была переработана, а признак уникального поля был оставлен по ошибке. Это будет исправлено.


  5. @Alex_DIY на данный момент - это тестовый вариант демона т.к это мой второй в жизни опыт написания программ на Python. Но я очень рад, что он может быть Вам полезен. Изначально логика работы подразумевает передачу данных только в одном направлении - от MQTT брокера в базу данный MySQL. Мы получаем полный дубликат данных всех топиков на которые подписаны. Я старался, чтобы данные передавались в реальном времени, но не тестировал демона при большой нагрузке, например, несколько сотен сообщений в секунду.

    Если Вам понадобится обратная связь, то это вполне можно реализовать, но Вы должны понимать, что передача в обратную сторону будет с задержкой т.к нет явного события, происходящего в момент обновления данных. То есть, при обновлении данных на MQTT брокере, мы получим оповещение, но при обновлении данных в базе MySQL такого события не будет (его можно создать искусственно, но процесс будет сложен для обывателя, не безопасен и не верен с точки зрения задач, возложенных на базу данных), придется периодически опрашивать базу, выявлять изменения и обновлять данные на брокере.

    PS: В любом случае, буду рад Вам помочь в изменениях программы, если они понадобятся. Но не представляю, как на маршрутизаторе будет работать MySQL. Я у себя дома завел маленький nettop на Intell Atom. Маленькое энергопотребление и небольшие ресурсы, но их более чем достаточно. Сам компьютер находится за приделами спальных комнат, а питание подается по Passive POE.

    IMG_0585.JPGIMG_0584.JPG


  6. @Alex_DIY Я опирался на описание eCO2 

    Цитата

    CCS811 supports intelligent algorithms to process raw sensor measurements to output a TVOC value or equivalent CO2 (eCO2) levels, where the main cause of VOCs is from humans.

    То, что датчик более подходит для помещений, я полностью согласен, тем более, что об этом есть упоминание в технической спецификации. Но это только упоминание, а не предписание. Рабочие температурные ограничения меня радуют, но огорчают приделы влажности, но скорее всего, сенсор просто нельзя использовать в открытом виде. Возможно нужна какая-то система вокруг него, пока не знаю, это только мысли... В любом случае, попробовать хочется. Что касаемо измерения метеостанцией содержания CO2, то для меня это аналитический интерес т.к мой дом располагается вдоль дороги с интенсивным движением. Скорее всего будут явные пики на графике. Возможно, это также будет интересно держателям теплиц, или иных объектов, в которых могут использовать метеостанцию.

    В общем, на счет дополнительных датчиков есть очень много мыслей, порой и противоречив. И раз уж затронули, то одна из зудящих идей, это счетчик Гейгера на Attiny85, или другом мелком микроконтроллере, в паре с одной иди двумя трубками СБМ20. Самостоятельно работающее устройство с I2C интерфейсом и возможностью подключения к метеостанции на ESP8266 в качестве одного из внешних датчиков. Готов к куче летящий камней в мой адрес, но это личная "хатенка" и, со временем, она получит жизнь, возможно и без описания на форуме.

    Скрытый текст

    IMG_0557.JPG

    @mag21 На сколько я понимаю, MH-Z19 выдает готовые данные по UART или аналоговый сигнал. Максимальная сложность может быть только при синтаксическом анализе (парсинге) данных и некоторые накладные расходы по времени при их передаче. Возможно, в данном случае, выгоднее использовать порт Vo и АЦП микроконтроллера, думаю, что это будет быстрее. Это беглый взгляд, нужно читать документацию по данному сенсору.

    На данный момент, я не готов приобрести такой датчик, но буду рад помочь советом, если он вообще понадобится, при интеграции MH-Z19 в проект.


  7. @mag21 доброе время суток. В планах попробовать датчик CCS811.

    ccs811.jpg

    • Питание от 1.8 до 3.6V
    • Средний потребляемый ток 30 mA
    • Пиковый потребляемый ток 54 mA
    • Рабочая температура от -40 до 85 °С
    • Относительная влажность (без конденсации) от 10 до 95%
    • Приделы измерения CO2 от 400 до 8194 ppm
    • Приделы измерения TVOC от 0 до 1187 ppb

    Он не из дешевых, но как по мне, лучше чем датчики из серии MQ.


  8. @EndWar @Alex_DIY друзья, прошу Вас не спорить. Мы все люди, где-то ошибаемся, а где-то, думая, что совершили ошибку, на самом деле, сделали верный выбор. В любом случае, отталкиваясь от тематики этого раздела, мы пытаемся получить тот результат, который в итоге устроил бы лично нас. И очень хорошо, что мы готовы делиться нашими наработками и самое главное - опытом. И даже не важно, положительный он или нет, ведь то, что было провалом для одного, станет отправной точной для другого. Я очень благодарен Вам, за вклад в развитие проекта и за столь продолжительный интерес к нему. И я уверен, что с увеличением популярности домашней автоматизации, стандартом будут становится именно открытые платформы, разработанные сообществом людей.

    • Like 1

  9. Доброе время суток.

    Сегодня пойдет речь о том, как переправлять данные с MQTT брокера в базу данных MySQL. Транспортировать будем как сами адреса, так и значения всех топиков, на которые оформлена подписка. Данную задачу нельзя назвать распространенной, но все же, она имеет место быть и может пригодиться в том случае, если данные востребованы в системах не способных работать с MQTT протоколом самостоятельно или брокер находится в изолированной системе, а данные востребованы, например, в GUI за её приделами.

    mqtt_to_mysql_by_it4it.club.png

    Для осуществления задуманного нам потребуется самостоятельный процесс, который сыграет роль транспортного узла между MQTT брокером и базой данный MySQL. А значит, его придется где-то держать. В моем случае, это сервер под управлением операционной системой Linux Ubuntu 16.04.3 и дальнейшее описание будет под неё, но для других ОС действия аналогичные.

    Сам демон будет написан на Python и для его работы нам потребуется:

    1. python3
    2. python-pip
    3. python-dev
    4. libmysqlclient-dev
    5. библиотека paho-mqtt для python https://pypi.python.org/pypi/paho-mqtt
    6. библиотека mysqlclient для python https://github.com/PyMySQL/mysqlclient-python

    Но начнем мы, в первую очередь, с подготовки базы данных.

    На плечи MySQL ляжет не только хранение, но и частичная обработка данных. Для этих целей нам потребуется отдельная база, хранимая процедура и функция с реализацией небольшой логики, и пользователь с ограниченными правами под чьим именем мы будем обращаться к ним. В конце поста, кроме самого демона, будет опубликован .sql файл, который достаточно просто импортировать, например с помощью стандартный средств базы данных

    mysql -uroot -p

    Вводим пароль администратора MySQL и импортируем наш .sql файл, но прежде, дочитайте статью до конца, возможно, Вы захотите внести свои изменения.

    mysql> source /media/mqttMySqlClient.sql

    После этого будет получен следующий результат:

    1. Создана база (схема) с именем mqtt
    2. Пользователь с именем mqtt-agent и паролем p@$$w0rd имеющий возможность подключаться с внешних адресов
    3. Пользователю будут назначены ограниченные права (только EXECUTE) в этой схеме
    4. Будет добавлена процедура update_topic, на которую ляжет задача добавления и обновления данных
    5. Будет добавлена функция get_topic для упрощения поиска данных

    На тот случай, если Вы захотите внести изменения или создать все ручками, рассмотрим содержимое sql файла.

    Если схема mqtt не существует, она будет создана

    CREATE DATABASE IF NOT EXISTS `mqtt`;

    Аналогичным образом будет создан пользователь mqtt-agent. Если необходимо конкретизировать, с какого адреса будет производиться подключение под этим пользователем, то замените % на доменное имя или ip адрес хоста. Если планируется использовать демона на том же сервере где установлен MySQL, замените % на localhost. Также разрешено не более 2 активных подключений, измените это значение на необходимое Вам.

    CREATE USER IF NOT EXISTS 'mqtt-agent'@'%' IDENTIFIED BY 'p@$$w0rd' WITH MAX_USER_CONNECTIONS 2;

    Пользователю будут выставлены ограниченные права. Ему будет разрешено пользоваться только хранимыми процедурами и функциями. Никакие самостоятельные запросы выполнять нельзя.

    GRANT EXECUTE ON `mqtt`.* TO 'mqtt-agent'@'%';

    Переходим к работе с самой схемой

    USE `mqtt`;

    Будет создана таблица topics со следующей структурой.

    • md5 - содержит уникальный одноименный хеш полученный из полного адреса топика. Именно по этому ключу и будет производиться поиск данных. Почему именно по нему, а не по самому имени? Дело в том, что md5 хеш имеет фиксированную, заранее известную, длину, что нельзя сказать о имени топика. Именно это ограничение не позволит сделать имя топика первичным ключом и явно идентифицировать данные в таблице.
    • time - содержит UNIX время добавления/обновления данных по конкретному топику (по умолчанию GMT+0)
    • topic - адрес топика. В контексте, упомянутого ранее, поля md5, не несет для нас никакой смысловой нагрузки.
    • value - данные опубликованные в топике.
    DROP TABLE IF EXISTS `topics`;
    CREATE TABLE `topics` (
      `md5` varchar(32) NOT NULL,
      `time` bigint(20) DEFAULT NULL,
      `topic` text,
      `value` text,
      PRIMARY KEY (`md5`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    Теперь необходимо создать хранимые процедуры и функции, но сделать это будет невозможно из-за присутствие в их синтаксисе разделителя совпадающего с концом данных в sql запросе - ";" Чтобы избежать этот неловкий момент, изменяем разделить на произвольный.

    DELIMITER $$

    Создает процедуру update_topic. Она принимает в качестве входных параметров два значения, адрес топика и опубликованные данные. Оба параметра являются текстовыми. Процедура, вычисляет md5 хеш из адреса топика и уже по нему производит поиск записи в таблице. Если запись не будет найдена, она будет создана, в противном случае данные в поле value будут обновлены. Данная процедура должна ускорить работу демона и избавить его от задержек, которые были бы неминуемы при выполнении этих же запросов на стороне клиента.

    DROP PROCEDURE IF EXISTS `update_topic`$$
    CREATE DEFINER=CURRENT_USER() PROCEDURE `update_topic`(topic text, value text)
    BEGIN
    	declare nMD5 varchar(32) default md5(topic);
    	declare NUM bit;
    	declare uTime bigint(20) default UNIX_TIMESTAMP();
    
    	SELECT COUNT(t.md5) INTO NUM FROM topics t WHERE t.md5 = nMD5;
    	if NUM <> 1 then
    		INSERT INTO topics VALUES(nMD5, uTime, topic, value);
    	else
    		UPDATE topics t SET t.time = uTime, t.value = value WHERE t.md5 = nMD5;
    	end if;
    END$$

    Также будет добавлена функция get_topic. Она необходима для запроса данных от имени созданного ранее пользователя и ограничений, наложенных на него. Функция принимает адрес топика в текстовом виде, производит вычисление md5 хеша и основываясь на его совпадении с имеющимися записями выводит значение поля value искомого топика.

    DROP FUNCTION IF EXISTS `get_topic`$$
    CREATE DEFINER=CURRENT_USER() FUNCTION `get_topic` (topic text)
    RETURNS text
    BEGIN
    	declare hMD5 varchar(32) default md5(topic);
    	declare topicValue text;
    	SELECT t.value INTO topicValue FROM topics t WHERE t.md5 = hMD5;
    RETURN topicValue;
    END$$

    И в завершении всего, будет восстановлено стандартное значение разделителя.

    DELIMITER ;

    На этом разбор sql файла можно считать законченным. Он не содержит каких-либо сложным манипуляций и должен быть понятен. Все эти операции можно выполнить руками, но я совету воспользоваться импортом, как и было описано ранее.

    Переходим к демону

    В первую очередь устанавливаем необходимые пакеты.

    sudo apt-get install python3 python-pip python-dev libmysqlclient-dev

    Устанавливаем недостающие библиотеки для Python

    pip install paho-mqtt mysqlclient

    Добавим пользователя из-под которого будет запускаться демон

    sudo useradd --shell /usr/sbin/nologin --system mqtt-agent

    Выставляем все необходимые права (каталог /media как пример)

    sudo chown mqtt-agent:mqtt-agent /media/mqttMySqlClient.py
    sudo chmod 0700 /media/mqttMySqlClient.py

    Добавляем демона в автозагрузку через планировщик задач и от имени созданного пользователя

    sudo crontab -u mqtt-agent -e

    Добавляем в конец следующую запись

    @reboot /media/mqttMySqlClient.py start

    Запускаем демона от имени все того же пользователя

    sudo -u mqtt-agent /media/mqttMySqlClient.py start

    Это основное, что требуется сделать на сервере для организации работы демона.

    Переходим к разбору настроек программы

    Т.к изначально за основу была взята концепция другого демона из ветки Zabbix, то конфигурация перекочевала оттуда и аналогично разбита на несколько секций.

    """ Настройки MQTT """
    mqtt_server = "mqtt.it4it.club"
    mqtt_port = 1883
    mqtt_login = ""
    mqtt_password = ""
    mqtt_client_id = "mqttMySqlClient"

    Настройки подключения к брокеру. Все должно быть интуитивно понятным. Помните, что при совпадении идентификаторов клиентов, они начнут конкурировать за подключение и по очереди терять связь. Не допускайте их совпадений.

    """ Список топиков для подписки """
    subscribe = {
        '$SYS/#',
        '#',
    }

    Список топиков для подписки указывается через запятую и в кавычках.

    """ Настройки MySQL """
    mysql_host = "127.0.0.1"
    mysql_port = 3306
    mysql_user = "mqtt-agent"
    mysql_passwd = "p@$$w0rd"
    mysql_schema = "mqtt"
    mysql_log_file = "/var/log/mqttMySqlClient.log"

    Настройки подключения с MySQL серверу также не должны вызывать вопросов.

    """ Настройки общие """
    pid_file = "/tmp/mqttMySqlClient.pid"

    Последний параметр указывает на размещение .pid файла демона.

    Команды управления классические

    • start - запуск в режиме демона
    • stop - остановка в режиме демона
    • restart - перезапуск в режиме демона
    • window - запуск в оконном режиме, также позволяет запускать процесс в операционных системах Windows

    После запуска, демон пытается установить связь с MQTT брокером и пока это не произойдет, связь с MySQL сервером устанавливаться не будет. Если во время работы, связь с брокером будет потеряна то в принудительном порядке, будет разорвано соединение с базой данных. Таким образом, по активным сессиям MySQL сервера можно судить о наличии связи у демона с брокером. Во время простоя, а в нашем случае, это отсутствие потока данных от брокера, для проверки связи с сервером базы данных будет использована процедура эмуляции ping для MySQL сервера. Она представляет из себя простую арифметическую задачу на сложение не приводящей к работе с данными в базе. Операция выполняется крайне быстро и её удачное выполнение сигнализирует клиенту о наличии связи с базой, а базе об активности клиента. В связи с этим, периодическая активность клиента при отсутствие данных от брокера, является показателем нормальной работы.

    И на последок

    Если вы хотите произвести выборку из таблицы под именем пользователя используемым по умолчанию и с его ограничениями. Необходимо воспользоваться функцией get_topic с указанием полного адреса, интересующего топика.

    select mqtt.get_topic('$SYS/broker/version');

    В ответ мы получим

    +---------------------------------------+
    | mqtt.get_topic('$SYS/broker/version') |
    +---------------------------------------+
    | mosquitto version 1.4.8               |
    +---------------------------------------+
    1 row in set (0,00 sec)

    На этом пока все.

    Файлы проекта: 

    PS: Это тестовая версия демона и возможно она будет претерпевать некоторые изменения.

     

    • Like 1

  10. @zloydimo4ka доброе время суток. Осмелюсь предложить Вам отказаться от связки BMP + DHT и заказать BME280. Данный датчик позволит получить информацию как о давление с температурой, так и о влажности. Я уже упоминал о нем ранее, а на днях получил модуль с таким сенсором и обязательно поделюсь информацией о нем.


  11. @Alex_DIY горизонтально, датчиком вниз. Такая установка не обусловлена ничем кроме моих собственных предпочтений и мыслей. Не знаю на сколько это правильно. Сама будка, если память не подводит, выполнена тоже из АБС пластика белого цвета, но, как и ожидалось, со временем он пожелтел. Никаких конструктивных особенностей я не вносил, хоть и были мысли окрасить её серебрянкой, как предлагал исполнитель заказа на печать этой самой будки. Возможно сделаю это весной, ведь по сути, это тестовый вариант метеостанции и можно смело экспериментировать.


  12. @Alex_DIY это очень интересное наблюдение.

    В библиотеке от LowPowerLab имеется функция setHeater для включения обогрева датчика.

    void SI7021::setHeater(bool on) {
        byte userbyte;
        if (on) {
            userbyte = 0x3E;
        } else {
            userbyte = 0x3A;
        }
        byte userwrite[] = {USER1_WRITE, userbyte};
        _writeReg(userwrite, sizeof userwrite);
    }

    Если имеется возможность провести эксперимент, было бы великолепно узнать о результатах, но это не при автономном питании, как Вы и указали.

    Возможно есть еще что-то, что осталось не замеченным.


  13. @Slava хорошо, давайте отталкиваться от этой информации. Но хотелось бы уточнить, скорость Serial монитор совпадала со скоростью выставленной в контроллере? По умолчанию 115200.

    Что бросилось в глаза:

    4 часа назад, Slava сказал:

    вариант2 - подключаю езернет, подключаю юсб (питание) - все отлично, вытаскиваю езернет - кнопка работает на карточки не реагирует.

    вариант3 - НЕ подключаю езернет, подключаю юсб (питание) - карточка не работает, КНОПКА также не работает.

    а также то, что

    4 часа назад, Slava сказал:

    5) ничего. молчит. просто пустой

    Т.к Вы не увидели никакой информации, в том числе и приветствие, я делаю вывод, что функция Setup не была завершена, а значит, до функции loop дело не дошло. Это объясняет, почему не отрабатывает кнопка, как и все остальные функции замка. Также, учитывая, что во втором варианте, при старте с сетью и последующем отключении от неё, кнопка продолжала работать, можно сделать вывод, что программа вышла из функции Setup и выполняет loop.

    Могу предположить, что при старте контроллер пытается инициализировать сетевое соединение и т.к ожидаются настройки от DHCP сервера, которых мы не получим из-за отсутствия связи, то на ожидание уходит какое-то время. И вероятнее всего, оно превышает установленный придел времени для сторожевого таймера, что приводит к перезапуску контроллера. И данный цикл повторяется до появления сети. Думаю, это можно проверить, запустив контроллер без сети, и открыв Serial монитор, подключить сеть обратно.

    Предлагаю попробовать уйти от DHCP сервера и прописать всю необходимую информацию самостоятельно. Но перед этим добавим тестовый вывод после инициализации Serial.

    Найдите

    Serial.begin(115200);
    while (!Serial);

    Добавьте тестовый вывод

    Serial.begin(115200);
    while (!Serial);
    Serial.println(F("Start"));

    Далее пропишем все данные для сети самостоятельно. Найдите в функции Setup строку

    Ethernet.begin(mac);

    И замените её на следующий блок

    IPAddress ip(192, 168, 0, 100);
    IPAddress subnet(255, 255, 255, 0);  
    IPAddress gateway(192, 168, 0, 1);
    IPAddress dns(192, 168, 0, 1);
    
    Ethernet.begin(mac, ip, dns, gateway, subnet);

    Укажите настройки, соответствующие Вашей сети.


  14. @Alex_DIY доброе время суток.

    Датчиком SI7021 я пользовался с момента тестовой обкатки, примерно с середины декабря 2016, и вплоть до июля 2017. Вот лог с Народного мониторинга. На имя датчика не обращайте внимание, данные до первых чисел июля, фактически принадлежат SI7021, а все, что дальше по времени уже HDC1080.

    SI7021_ESP8266_1.png

    Максимальное значение 99.64% относительной влажности были зафиксированы 22 февраля 2017. Сравнил с архивом погоды на это число в моем регионе, это 90%-95% влажности, что примерно совпадает с реальностью. Естественно не учитываем погрешности в связи с расположением самой будки с датчиками.

    Также хочу отметить, что сам датчик очень хорош, но имеет полимерную пленку над входным отверстием. Я эту пленку повредил и данным с датчика нельзя было доверять т.к калибровка на заводе изготовителя учитывает её наличие. После этого инцидента, датчик был заменен на аналогичный по характеристикам (возможность использования датчика на улице) - HDC1080. Об этом было упоминание в этой ветке

    Возможно Вы имеете схожую проблему.

    Что касаемо HDC1080. Датчик также очень хорошо себя показал, начиная с июля 2017 и по сей день. На момент написания поста - 14 ноября 2017, уже около недели стоит очень сырая погода и идут дожди. Относительная влажность в максимуме 99%, вот график с самой метеостанции за последние 24 часа.

    HDC_1080_ESP8266_1.png

    Вывод, который я могу сделать - оба датчика очень хороши, но SI7021 требует бережного обращения при транспортировке и установке. Крайне зависим от состояния полимерной пленке на лицевой стороне сенсора. Оба датчика были установлены на одной высоте и в одном положении в будке (на середине и горизонтально). Но опять обращаю внимание, что SI7021 я повредил сам, что и привело к искажению показаний.

     


  15. @Slava Изначально не предусматривалось использование локальных ключей, только внешняя база. Но и под внешней базой подразумевается MySQL сервер расположенный в пределах локальной сети. Использование внешнего сервера в интернете не безопасно. Но об этом будет другой проект с замком.

    Что по поводу кнопки. Я буду рассуждать в контексте оригинальной программы.

    Реализация механизма открытия с кнопки очень проста и работает независимо от сети.

    // Открытие двери с кнопки
    key_open.update();
    if(!key_open.read() and openTimer == 0 and !mode) {
      if(modeLock or (!modeLock and digitalRead(PIN_RELAY))) {
        openTimer = millis()/1000;
        digitalWrite(PIN_RELAY, LOW);
        Serial.println(F("The door opened from the inside\n"));
        squeaker(5, 3200, 100, 300);
      }
      delay(2000);
    }

    На работоспособность кнопки влияет смена режима работы замка - режим программирования. В этом состоянии замок открыт и ожидает новые ключи для записи в eeprom.

    Но я не могу утверждать, что не происходит никакой нештатной ситуации т.к все мы люди и все ошибаемся. В данный момент у меня нет возможности собрать схему замка и проверить все самостоятельно. Но попробовать разобраться в ситуации можно.

    1. В каком режиме работает закрытие замка (авто/ручной)?
    2. Используете ли Вы сторожевой таймер?
    3. Внесите следующие изменение в работу кнопки
      Скрытый текст
      
      // Открытие двери с кнопки
      key_open.update();
      if(!key_open.read() and openTimer == 0 and !mode) {
        Serial.println(F("Key down")); // Ожидаем увидеть этот вывод в Serial
        if(modeLock or (!modeLock and digitalRead(PIN_RELAY))) {
          openTimer = millis()/1000;
          digitalWrite(PIN_RELAY, LOW);
          Serial.println(F("The door opened from the inside\n")); // И этот тоже
          squeaker(5, 3200, 100, 300);
        }
        delay(2000);
      }
    4. Воссоздайте ситуацию и опишите полный порядок Ваших действий.
    5. Что прилетает в Serial монитор?

     

     


  16. Маленькое дополнение к последнему посту с мониторингом MQTT брокера Mosquitto в реальном времени.

    В шаблоне не было отображено как определить отсутствие данных. В принципе, это не такое серьезное упущение т.к целью было получение самих данных, но все же. Т.к мы использовали тип элемента данных "Zabbix траппер", то самым явным индикатором будет отсутствие входящих данных по одному из элементов в течении некоторого промежутка времени. При этом, мы должны быть уверены, что данные отправляются нам систематически с фиксированным интервалом времени.

    Для этих целей можно взять, как опорный, топик из раздела $SYS, например, время работы сервера

    $SYS/broker/uptime

    По умолчанию раздел $SYS обновляется каждые 10 секунд, но это значение может быть иным или обновления могут быть отключены полностью. Проверьте Ваше значение в конфигурационном файле /etc/mosquitto/mosquitto.conf

    sys_interval 10

    Далее переходим в Zabbix и создаем новый триггер. Сам Zabbix предоставляет функция nodata(), работающую совместно с траппами и возвращающую:

    • 1 - если не было получено данных за указанный промежуток времени в секундах. Период не может быть меньше 30 секунд.
    • 0 - наоборот

    Таким образом, наша проверка будет выглядеть следующим образом

    {broker:topic[uptime].nodata(60)}=1

    или, если используются полные имена, так

    {broker:topic[$SYS/broker/uptime].nodata(60)}=1

    где broker - имя узла сети в Zabbix.

    В итоге, мы получаем взведенный триггер, если данные о времени работе брокера не будут обновлены в течении одной минуты.

    • Thanks 1

  17. @LogOFF хорошо, с гаражом мы разберемся. Но в любом случае, вся силовая часть ляжет на Ваши плечи и именно Вам решать, как управлять вентиляторами и нагревателями.

    Что касаемо метеостанции на улице, то в принципе все уже готово, плюс ко всему будет описание, как перевести все на единый датчик BME280. А значит уменьшится бюджет всего проекта. Пока ждем датчики. Теплицы тоже подпадают под этот пункт.

    А вот общение с центральным сервером это уже интересный вопрос связанный непосредственно с коммуникацией. Могу предположить, что имеются три разных объекта:

    1. Квартира с центральным сервером
    2. Удаленный участок с домиком и теплицей
    3. Гараж

    Таким образом на всех трех объектах должна быть инфраструктура, позволяющая выходить в интернет и если он есть, то не вижу никаких трудностей с организацией передачи данных. В противном случае, придется запланировать дополнительные расходы.


  18. Друзья, в очередной раз приветствую Вас.

    Сегодня мы доведем до ума нашу идею с дружбой Zabbix и MQTT (Message Queue Telemetry Transport) протокола. Все описанные ранее варианты также жизнеспособны, но хотелось чего-то большего и в первую очередь, избавиться от задержки, из-за которой клиентам приходилось рассылать сообщения с параметром "-r, --retain". То есть была такая ситуация, что Zabbix сервер получал сообщения не тогда, когда они по факту приходили брокеру, а через какое-то время, равное интервалу между запросами к брокеру через вызов /usr/bin/mosquitto_sub. И именно из-за такой логики работы, клиентам приходилось выставлять флаг "сохранить" при отправке сообщения.

    Ну и пусть, скажете Вы, подождем, но как всегда есть нюансы:

    • Сервер получает последнее сообщение в топике, а значит, в промежуток между их сбором, данные могут обновиться несколько раз, и мы потеряем часть информации. Это может быть актуально в некоторых случаях, например, если требуется контролировать состояние двери в серверной комнате (открыта/закрыта) ну или в других более серьезных задачах.
    • Если требуется оперативно поднять соответствующий триггер, особенно если на него опирается логика других триггеров.
    • Просто хочется поддержки MQTT протокола в реальном времени. Это действительно важный пункт и если предыдущие можно проигнорировать, то этот ни в коем случае.

    Долой слоупока, обучаем Zabbix работать с MQTT протоколом в реальном времени!

    Стоит понимать, что сам по себе Zabbix сервер не способен работать с данным протоколом и именно из-за этого нам требуются различные посредники. Раньше мы обращались к ним самостоятельно через Zabbix агента или внешнюю проверку (скрипт) и сообщали, что и как мы хотим получить. Теперь мы пополним арсенал сервера внешним демоном/сервисом/службой, нужное подчеркнуть. Именно на этот самостоятельный процесс ляжет задача по поддержанию постоянного соединения с MQTT брокером, и при появлении нового сообщения, он будет передавать его серверу через Zabbix траппер.

    Ну, что же, на словах все просто, как это будет на деле.

    При написании демона я использовал язык Python в силу его популярности, простоты и доступности некоторого набора готовых библиотек. Но это моя первая в жизни программа на Python и, возможно, Вы захотите её доработать. Также на моем сервере установлена операционная система Linux Ubuntu 18.04 и дальнейшее описание будет именно под неё, но я уверен, что Вам не составит труда установить следующие пакеты под любой ОС:

    1. python3
    2. python3-pip
    3. библиотека paho-mqtt для python https://pypi.python.org/pypi/paho-mqtt
    4. zabbix-sender

    Получить список установленных пакетов, связанных с python и Zabbix, можно так

    dpkg -l | grep -E "python|zabbix"

    Устанавливаем пакеты (уже установленные можно выкинуть из списка)

    sudo apt install python3 python3-pip zabbix-sender

    Подгружаем библиотеку

    pip3 install paho-mqtt

    Далее нам понадобится сама программа, скачать её можно в конце этого поста

    Копируем её в любое удобное Вам место, но, чтобы пользователь, от которого будет запускаться программа, имел туда доступ, например, /media/zabbixMqttClient.py и выставляем права ограниченного пользователя, пусть это будет пользователь Zabbix

    sudo chown zabbix:zabbix /media/zabbixMqttClient.py
    sudo chmod 0700 /media/zabbixMqttClient.py

    Далее необходимо добавить программу в автозагрузку. Сделаем это через планировщика задач crontab и опять же от имени пользователя zabbix

    sudo crontab -u zabbix -e

    Добавляем следующую запись

    @reboot /media/zabbixMqttClient.py start

    Запускаем демона от имени пользователя zabbix

    sudo -u zabbix /media/zabbixMqttClient.py start

    Далее переходим к разбору настроек программы

    Они разбиты на несколько секций

    """ Настройки MQTT """
    mqtt_server = "mqtt.it4it.club"
    mqtt_port = 1883
    mqtt_login = ""
    mqtt_password = ""
    mqtt_client_id = "zabbixServer"
    mqtt_short_names = True

    Все параметры должны быть интуитивно понятны, кроме mqtt_short_name. Данный параметр заставит программу производить разбор топика с целью поиска в нем имени хоста, на который будут отсылаться сообщения Zabbix серверу. Если параметр будет выставлен в False, то в качестве параметра ключа будет использовано полное имя топика. Мы рассмотрим этот механизм подробнее ниже, при разборе механизма подписки на интересующие нас топики.

    Также по умолчанию мы используем TCP соединение т.к поддержка webSocket не предусматривалась, но программу можно легко доработать.

    """ Настройки Zabbix """
    zabbix_server = "127.0.0.1"
    zabbix_port = 10051
    zabbix_sender = "/usr/bin/zabbix_sender"
    #zabbix_sender = "C:\\ZabbixAgent\\bin\\win64\\zabbix_sender.exe"

    С настройками подключения к Zabbix серверу также все просто. Подключаться мы будем с помощью утилиты zabbix-sender, поэтому необходимо указать полный путь до неё. Скрипт также можно запускать из-под Windows, но только в оконном режиме и с параметром window. При использовании параметра start, получите шлак ошибок т.к она отвечает за запуск программы в качестве демона в Unix подобных системах.

    """ Настройки общие """
    pid_file = "/tmp/zabbixMqttClient.pid"

    В общих настройках описано только расположении pid файла. Можно оставить без изменений.

    """ Список топиков для подписки и идентификаторы Zabbix хостов на которые требуется их переслать """
    subscribe = {
        '$SYS/#':  'broker',
        'kitsum/espWeatherStation/#': 2,
        'log/+/#': 2,
    }

    Ну вот мы и добрались до самого главного - оформление подписки на топики. Я долго ломал голову над тем, как угодить всем и организовать переправку любых сообщений на те Zabbix хосты, для которых они предназначены. И сделать это без внесения транспортной информации в само тело сообщение, лично я считаю это плохой практикой, но судя по сообщениям, за которыми я наблюдал на некоторых популярных брокерах, люди считают это хорошей идеей. Просто какое-то безумие... Думаю, что Арлен Ниппер и Станфорд-Кларком, разработчики протокола, это не одобрили, хотя сам протокол не несет никаких ограничений на передаваемую информацию. Но давайте отталкиваться от идеологии протокола.

    Сам по себе топик представляет: стандартизированный, упорядоченный и интуитивно понятный набор информации, а также является адресом получателя. То есть, если адрес топика будет соответствовать адресу проживания реального человека, то вид его будет похож на что-то подобное

    <страна>/<штат или область>/<город>/<район или улица>/<дом>/<квартира>/<имя получателя>

    Думаю, что Вы согласитесь со мной, ведь нет смысла писать только половину адреса, а оставшуюся часть вкладывать в сообщение. Проще, после имени получателя, дописать адрес, но уже в контексте той нагрузки, которую несет сообщение.

    <имя получателя>/<помещение>/<источник информации>
    <имя получателя>/<группа>/<подгруппа>/<источник информации>
    ...
    
    building/serverRoom/door/1/state
    ...
    building/serverRoom/zone1/smokeDetectors/1
    building/serverRoom/zone2/smokeDetectors/1
    building/serverRoom/zone3/smokeDetectors/5
    ...
    building/serverRoom/+/smokeDetectors/#

    Все понятно и даже задумываться не нужно, о чем идет речь.

    Также стоит обратиться к принципу работы zabbix-sender, той самой утилиты, которая и будет финальной в цепочке передачи. Вот еще немного подробностей про неё. Как видим, кроме параметров подключения мы обязаны передать:

    1. Имя узла сети, которому адресовано сообщение
    2. Ключ элемента данных
    3. Сами данные

    И если с последним параметром все понятно, то два предыдущих я предлагаю брать из самого топика, а также не брезговать принудительным указанием имени узла сети для конкретного топика.

    Синтаксис правил следующий

    'адрес топика в формате mqtt': 'имя узла сети в zabbix',

    В таком случае, все сообщения, подпадающие под установленное правило будут адресованы заранее указанному узлу сети. Но что делать, если имя узла неизвестно, и мы ожидаем данные от любых источников с адресом топика подпадающим под установленную маску? В таком случае мы можем указать порядковый номер субтопика, который содержит это самое имя. Указывать его нужно в виде целого числа, как положительного, так и отрицательного. В зависимости от знака, отсчет будет начинаться с начала конца адреса топика.

    Saint Petersburg/office1/temperature
    Saint Petersburg/office213/Wi-Fi/signalStrength

    Эти два топика могут подпадать под правило

    'Saint Petersburg/+/#': 2,

    Таким образом, второй элемент с начала топика выпадает на субтопик "+" и, следовательно, имя узла сети будет соответствовать номеру офиса: office1 и officce213.

    Тоже самое можно делать и в обратном порядке, то есть с отрицательными значениями номера субтопика, но при этом, Вы должны быть уверены в длине адреса топика, а точнее, в количестве его субтопиков.

    Moscow/Odintsovo/+/temperature

    Этот шаблон может подпадать под два разных правила

    'Moscow/Odintsovo/+/temperature': 3,
    'Moscow/Odintsovo/+/temperature': -2,

    Что в итоге будут опять ссылаться на субтопик "+" и соответствовать его любому значению.

    И последнее правило

    'NewYorkCity/+/gasSensors/+/#': 4,

    Которое соответствует второму значению "+" (четвертый субтопик), что совпадет с разными топиками и может не соответствовать нашим ожидания т.к мы получим два разных значения ссылающиеся на один узел сети "sensor 3".

    NewYorkCity/office16/gasSensors/sensor3/value/current
    NewYorkCity/office85/gasSensors/sensor3/value/current

    В таком случае мы должны быть уверены, что нумерация датчиков уникальна для всего набора зданий или на стороне Zabbix сервера мы ожидаем увидеть полный адрес топика, а значит, для второго варианта развития событий переменная mqtt_short_names должна быть выставлена в Flase. Таким образом мы можем кардинально поменять логику работы при построении ключей и выглядеть они будут следующим образом. Но отправлены на один узел - "sensor3"

    mqtt_short_names = True

    # отправлено хосту sensor3 (значения перезаписывают друг друга)
    topic[value/current]
    topic[value/current]

    mqtt_short_names = False

    # отправлено хосту sensor3 (ключи уникальны)
    topic[NewYorkCity/office16/gasSensors/sensor3/value/current]
    topic[NewYorkCity/office85/gasSensors/sensor3/value/current]

    Или исправить правило на следующее

    'New York City/+/gas sensors/+/#': 2,

    И при mqtt_short_names = True, данные будут переданы на разные узлы сети с именами office16 и office85 соответственно, но при этом ключи будут выглядеть одинаково и не будут пересекаться.

    topic[gasSensors/sensor3/value/current] # отправлено хосту office16
    topic[gasSensors/sensor3/value/current] # отправлено хосту office85

    Возможно в теории все не так явно, но на практике это очень удобно.

    И важный момент - все описанные Вами правила будут рассматриваться аналогично правилам любого межсетевого экрана (firewall). Вышестоящее правило в списке имеет приоритет над нижестоящим. И если топик попал под действие одного из шаблонов, то все нижестоящие правила будут проигнорированы. Поэтому будьте внимательны.

    Вот пример того явной ошибки построения правил.

    # ошибочная конфигурация правил
    subscribe = {
        '#': 'rubbish',
        '$SYS/#': 'broker',
        'log/+/#': 2,
    }

    Как мы видим, под первое (жадное) правило будут подпадать абсолютно все топики и данные будет получать только узел с именем rubbidh. Два оставшихся правила никогда не будут выполнены т.к уже имеют более низкий логический приоритет.

    Чтобы исправить данную ситуацию, опустим самое "жадное" правило в конец списка.

    # правильная конфигурация правил
    subscribe = {
        '$SYS/#': 'broker',
        'log/+/#': 2,
        '#': 'rubbish'
    }

    Что мы получим теперь.

    • Правила не пересекаются
    • Все данные с системного топика $SYS (данные по работе брокера) будут переданы Zabbix и адресованы узлу broker
    • Все данные с топика log (условный раздел для логов) будут переданы узлам c именами соответствующими второму субтопику
    • Все остальное (вообще все) будет передано узлу rubbish

    Также хочу отметить, что если, при использовании коротких имен топиков (mqtt_short_names = True), имя имя узла сети не будет найдено в адресе топика, то в ключе будет использован полный адрес топика, как будто бы короткие имена топика не используются вовсе (mqtt_short_names = False).

    mqtt_short_name = True
    subscribe = {
        'serverRoom/rack2/zone4/temperature': 'zabbixServer',
        'serverRoom/rack3/zone4/temperature': 'rack3',
    }

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

    topic[serverRoom/rack2/zone4/temperature] # отправлено узлу zabbixServer
    topic[zone4/temperature] # отправлено узлу rack3

    Короткие имена очень удобны в том случае, если требуется избавиться от лишних элементов в ключе. Например, если данные явно адресованы и сам узел сети несет информацию, связанную непосредственно только с ним. Это может быть, как сам брокер, так и набор различных сенсоров, определенных на Zabbix сервере как самостоятельные единицы.

    Рассмотрим это на примере шаблона для мониторинга MQTT брокера Mosquitto. Саму процедуру установки и настройки брокера я рассматривать не буду т.к предполагается, что брокер у Вас уже имеется.

    Мониторинг MQTT брокера Mosquitto

    В первую очередь нам понадобится сам шаблон, найти его можно в конце статьи.

    Импортируем его на Zabbix сервере, создаем узел сети, описывающий наш брокер и подключаем шаблон.

    zabbix_mqtt_monitor1.pngzabbix_mqtt_monitor2.png

    Переходим в раздел "Мониторинг -> Последние" данные, выбираем наш узел и смотрим, что прилетает нам.

    zabbix_mqtt_monitor3.png

    Если Вы все сделали правильно, то увидите все данные по работе брокера. И как мы можем наблюдать, все ключи имеют короткие имена.

    Чтобы создать собственные элементы данных необходимо:

    1. Хость на, который отправляются данные, существовал на Zabbix сервере. Обращение идет по полю "Имя узла сети"
    2. Элемент данных был с типом "Zabbix траппер"
    3. Адрес обязан быть заключен в ключ topic[], например, topic[bytes/received]

    zabbix_mqtt_monitor4.png

    На этом все.

    Файлы проекта

     

    PS: Приятного использования и обязательно делитесь своими наработками и идеями.

    • Thanks 2

  19. Доброе время суток @LogOFF  Думаю, что никаких трудностей быть не должно. 

    Я бы взял за основу два датчика BME280 т.к судя по общедоступной информации он имеет вывод SDO, что позволяет изменять его адрес на I2C шине (0x76/0x77) и работать в паре с аналогичным сенсором. Данный датчик я себе уже заказал, к сожалению, только один, но раз есть потребность, значит закажу еще несколько.

    bme_sch.png

    https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BME280_DS001-11.pdf

    Далее остается дело за малым - по очереди опрашивать датчики, производить необходимые вычисления и при достижении установленных Вами границ подать на порт 1 или 0, в зависимости от того, как и чем, Вы будите управлять нагрузкой.

    Возможно будут сложности в компоновке данных на графике, если он вообще востребован, но все это решаемо.

     


  20. Привет друзья.

    В данной теме пойдет речь о конфигурации микроконтроллера через UART (Universal Asynchronous Receiver-Transmitter) интерфейс. А рассмотрим мы это на примере MQTT логгера. В данном случае, это будет логгер температуры. Мне это устройство потребовалось на работе, даже не мне, а моим коллегам, и оно действительно работает и приносит огромную пользу т.к контроль температуры производится совместно с отличной, на мой взгляд, системой мониторинга Zabbix с оперативными оповещениями, построением графиков, блэк-джеком и... Подробнее о дружбе Arduino и Zabbix можно почитать тут

    Но как всегда, есть нюансы. А заключаются они в том, что в будущем, обслуживать армию мелких контроллеров придется людям, которые заняты своими задачами и им попросту некогда изучать Arduino, не говоря уже о серьезных альтернативах, разбираться в том, как прописать нужные значения переменных в программу и загрузить её в микроконтроллер. Все настройки необходимо производить быстро, с явным указанием изменяемого параметра и его значения. Ровно также, как это делается с любым промышленным оборудованием.

    И тут на помощь приходит UART

    Микросхема UART to USB имеется в большинстве плат семейства Arduino, а там, где её нет, обычно выведены соответствующие "пины". И все это очень облегчает жизнь т.к позволяет общаться с контроллером, просто подключив его к компьютеру напрямую или через переходник, благо их везде навалом, да и стоят они как пачка семечек. Остается только запустить любой терминал, который умеет доставлять в конец строки символ "перевод строки", что известен в народе как "\n", а в ASCII таблице имеет номер 0A.

    Кстати, в Serial мониторе Arduino IDE выставить символ конца строки можно так

    uart-to-usb-nl.png

    Ну а дальше только, что и остается, как общаться с устройством на той стороне. И тут мы переходим к основному алгоритму программы. Но перед этим хочу отметить, и это ВАЖНО, что за любое упрощение жизни, всякие красивости и прочее, приходиться платить, и цена довольно высока! В данном случае, это ОЗУ микроконтроллера. Поэтому не раскатываем губы, а если очень хочется, то берем следующий по характеристикам микроконтроллер. А начинать мы будем с ATmega328P, что известен в народе как Arduino UNO, Arduino Nano, IBoard v1.1 и т.д по списку. Заканчивать Вы можете чем угодно, хоть ATmega2560, ESP8266 или ESP32. В противном случае, производим оптимизацию кода, отказываемся от громоздких библиотек, или вообще, от Arduino IDE.

    Что мы хотим получить

    1. Вся конфигурация микроконтроллера должна храниться в энергонезависимой памяти (ПЗУ) известной нам как EEPROMM.
    2. Если в ПЗУ конфигурация отсутствует, необходимо иметь резервный план. И им станет сброс конфигурации на настройки по умолчанию. Это поведение знакомо всем, особенно по домашним дешевым маршрутизаторам, а значит, интуитивно понятно.
    3. Выводить справку при начале общения пользователя и устройства, на мой взгляд, как манеры высшего общества. Контроллер должен представляться и сообщать всю необходимую информацию о себе и о том, как с ним вести диалог.
    4. Все команды должны быть просты и иметь не двусмысленное значение.
    5. И конечно, мы должны иметь возможность просмотра текущего состояния датчиков или процессов, которыми занимается устройство в свободное от общения с нами время.

    Как сохранять конфигурацию в EEPROM

    Пожалуй, стоит начать с того, как сохранить конфигурацию микроконтроллера в энергонезависимую память. Для этих целей, в стандартный набор инструментов Arduino IDE входит библиотека для работы с EEPROM.

    #include <EEPROM.h>

    На данный момент нас интересуют две функции, это чтение и запись

    EEPROM.get(address, variable);
    EEPROM.put(address, variable);

    Обе принимают два параметра:

    1. Адрес, начиная с которого будет произведено чтение или запись данных в память
    2. Переменная чье содержимое надо сохранить или в которую нужно из памяти прочитать

    Особенность работы этих функция заключается в том, что в зависимости от типа переданной им переменной во втором параметре, будет произведено чтение или запись ровно того количества данных которое соответствует размеру типа этой самой переменной. На простом языке это означает, что если переменная variable будет иметь типа byte, то и работать мы будем с объемом памяти в 1 байт. И тоже самое произойдет с абсолютно любым типом данных пока мы не упремся в размеры самого EEPROM или ОЗУ микроконтролера. Из этого всего следует, что мы можем создать свой собственный тип данных, разместить в нем необходимую нам информацию и всего лишь двумя функциями помещать его в память и извлекать обратно.

    И в этом нам поможет пользовательский составной тип - структура (struct). Данный тип позволяет объединить в себе различные типы данных, упорядочить их и присвоить им понятные имена.

    Скрытый текст
    
    // Описываем структуру
    struct user {
      char name[40];
      byte age;
    };
    // Объявляем новую переменную с нашим типом и помещаем в неё нужные нам значения
    user anna;
    anna.name = "Smirnova Anna";
    anna.age  = 15;
    // Записываем данные в EEPROM
    EEPROM.put(0, anna);
    // Объявляем еще одну переменную с нашим типом
    user human;
    // Читаем данные из EEPROM
    EEPROM.get(0, human);
    // Выводим содержимое
    Serial.println(human.name); // Smirnova Anna 
    Serial.println(human.age);  // 15

     

    Это общий пример для большего понимания, как объединить несколько типов данных в одной структуре, получить к ним доступ, записать и прочитать их из EEPROM.

    Наша структура будет немного сложнее, но суть остается той же самой.

    // Дополнительная структура описывающая IPv4 адреса
    struct addres {
      byte a;
      byte b;
      byte c;
      byte d;
    };
    
    // Структура объекта конфига для хранения в EEPROM
    struct configObj {
      addres ip;
      addres subnet;
      addres gateway;
      addres dns;
      byte mac[6];
      byte hex;
      char server[40];
      char topic[40];
    } config;

    Данная структура хранит сетевые настройки для работы с Ethernet модулем (w5100 и выше) Arduino, базовые настройки для связи с MQTT брокером. Сразу при описании структуры мы объявили новую переменную с именем config с типом нашей структуры.

    ВАЖНО: кроме наших данных в структуре имеется дополнительная переменная с именем hex. Её задача, это контроль наличия наших данных в EEPROM. Она всегда должна содержать одно и тоже значение. Представьте ситуацию, что вы взяли контроллер в EEPROM которого находится какая-либо информация (может там чисто, но мы этого не знаем наверняка) и мы прочитаем данные и поместим их в нашу переменную. В итоге мы получим данные которым нельзя доверять, а что еще хуже, это если эти самые данные нарушат работу внешнего оборудования.

    Более правильным, на мой взгляд, будет проверка значений по конкретно определенным адресам. Например, мы знаем, что в 16 байте должно быть значение 0xAA и если оно действительно там, то мы убеждаемся, что это наша информация. Естественно, что контрольных точек может быть несколько и разумеется с разными значениями, это увеличит гарантию того, что данные являются нашими, но 100% гарантии не даст. Для более серьезных проектов есть более серьезные методы, например, подсчет контрольной суммы всего набора данных.

    Также структура может иметь вложенные структуры, у нас ими являются: ip, subnet, gateway, dns. Вы можете отказаться от такого варианта и записывать данные просто в массив байт, как это было сделано с MAC адресом. Естественно, что обращаться к этим полям нужно по-разному.

    Запись данных в поле subnet

    config.subnet = {255, 255, 255, 0};

    Запись данных в поле mac

    byte mac[] = {0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02};
    memcpy(config.mac, mac, 6);

    С записью данных в поле server все еще проще

    config.server = "mqtt.it4it.club";

    Функция, которая возвращает нашу структуру данных с полностью заполненными полями.

    // Начальный конфиг
    configObj defaultConfig() {
      configObj config = {
        {192, 168, 0, 200},
        {255, 255, 255, 0},
        {192, 168, 0, 1},
        {192, 168, 0, 1},
        {0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02},
        0xAA, // Не трогать! Используется для проверки конфигурации в EEPROM
        F("mqtt.it4it.club"),
        F("arduino/serial/config")
      };
      return config;
    }

    К примеру, два последних значения записывать не обязательно, тогда эти поля останутся пусты если использовать данную функцию для возврата к "заводским" настройкам.

    Вот пример того, как используя описанную нами структуру, мы проверяем целостность настроек в EEPROM и в случае не совпадения hex значений, загружаем настройки по умолчанию.

    const byte startingAddress = 9;
    bool configured = false;
    
    void loadConfig() {
      EEPROM.get(startingAddress, config);
      if (config.hex == 170) configured = true;
      else config = defaultConfig();
      configEthernet(); // Функция производящая настройку сети
    }

    Как контроллеру начать понимать, что от него хотят

    В Arduino имеется функция, вызываемая каждый раз, когда в передаваемый буфер данных попадает знакомый нам символ перевода строки.

    void serialEvent() {
        // Вызывается каждый раз, когда что-то прилетает по UART
        // Данные передаются посимвольно. Если в строке 100 символов, то функция будет вызвана 100 раз
    }

    И в контексте обсуждаемой нами программы, мы можем представить ее в следующем виде

    void serialEvent() {
      serialEventTime = millis();
      if (console.available()) {
        char c = (char)console.read();
        if (inputCommands.length() < inputCommandsLength) {
          if (c != '\n') inputCommands += c;
          else if (inputCommands.length()) inputCommandsComplete = true;
        }
      }
    }
    

    Её задача, символ за символом, собрать в кучу все переданные нами данные и при получении заветного символа перевода строки (именно он даст нам понять, что передача сообщения завершена) сообщить, что команда получена и передать накопленный буфер данных своей напарнице по цеху.

    Но перед тем как это сделать стоит рассмотреть альтернативную ветку развития событий, а именно тот факт, что нам попросту могут прислать огромную, кучу шлака без волшебного символа, а раз могут, значит рано или поздно пришлют. И мы бесполезно потратим ценные ресурсы микроконтроллера, что может привести к непредсказуемым результатам в дальнейшем. Поэтому, в логику функции, мы добавим дополнительное ограничение на количество переданных символов, если оно достигнуто, то попросту перестаем воспринимать последующие данные.

    Останется только избавиться от них, и самым удобным моментом будет, когда этот поток шлака прекратиться. Чтобы об этом узнать мы будем запоминать время, когда пришел каждый из символов переданной строки перезаписывая соответствующую временную переменную данными о следующем символе и т.д пока поток не иссякнет. И как только расхождение текущего времени CPU и времени, когда поступил последний символ превысит некоторое значение, пусть это будет 1 секунда, мы очистим нашу память. Этот простой механизм напоминающий амнезию позволит избавить нас от лишних проблем.

    Переменная отвечающая за размер принимаемого буфера

    const byte inputCommandsLength = 60;

    Теперь можно переходить к напарнице предыдущей героини и, по совместительству, основной рабочей лошадки - функции обрабатывающей адекватно полученные данные.

    void serialEventHandler() {
        // вызывается в loop и проверяет взведена ли переменная inputCommandsComplete
        // в полученных данных пытается распознать команды
    }

    По началу я хотел описать данную функцию в упрощенном варианте, но в процессе понял, что ничего хорошего из этого не выйдет, и я решил описать только ключевые моменты, но их будет достаточно, на мой взгляд.

    Разбор serialEventHandler

    Полученные данные будут переданы нам в переменной inputCommands с типом String

    В первую очередь стоит почистить ее от лишних пробельных символов. Они часто встречаются в начале и в конце строки если пользователь копирует текст, а не набирает его самостоятельно. Это распространенная ситуация, приводящая к отказу принятия команды и бороться с ней очень просто.

    inputCommands.trim();

    Далее стоит отсеять команды, не несущие никакой динамической информации, например, help, restart, reset и т.п это предписывающие команды которые заставляют контроллер выполнять строго описанные функции без вмешательства в их работу.

    if (inputCommands == F("help")) {
        consoleHelp();
    } else if (inputCommands == F("restart")) {
        resetFunc();
    } else {
        // Все сложные команды обрабатываются в этом блоке
    }

    Как Вы видите, все очень просто и скучно. Но не в том случае если команда динамическая, то есть содержит не только саму команду (заголовок) но и полезную нагрузку (параметр) которая может меняться раз от раза. Простой пример это команда изменения ip адреса и её варианты:

    • ip 37.140.198.90
    • ip 192.168.0.244
    • ip 10.10.10.88

    В данном случае, нам стоит понять, относится ли данная команда именно к ip адресу. Для этого в наборе String имеется отличный метод, позволяющий производить сравнение переданного ему параметра с началом строки.

    if (inputCommands.startsWith(F("ip"))) {
        // Строка inputCommands начинается с пары символов "ip"
    }

    Если все идет так, как мы задумали, то нам стоит отделить динамическую часть - наш параметр, от заголовка и получить полезную нагрузку. В этом нам поможет, опять же из набора String, метод substring позволяющий получать часть строки с указанием начального и конечного символа подстроки. Последний параметр указывать не обязательно и в таком случае мы получим всю строку начиная с указанного символа.

    inputCommands.substring(4)

    В данном случае начиная с 4-его и заканчивая последним. И как Вы успели заметить, отсчет мы начинаем не с третьего символа, что соответствует нашей строке без вступительного "ip", а на один больше т.к между заголовком и параметром имеется разделяющий символ в виде пробела.

    Далее, полученную строку мы передадим в функцию, занимающуюся разбором на компоненты и принимающую следующие параметры:

    1. Указатель на переменную с типом char, для этого нам потребуется преобразовать наш тип String
    2. Символ разделителя, что для IPv4 является точка "."
    3. Указатель на массив типа byte, которому будет присвоен результат разбора
    4. Количество искомых элементов в строке
    5. И система счисления, подразумеваемая в качестве исходной для записи элементов подстроки
    /*
        Парсинг
        https://stackoverflow.com/questions/35227449/convert-ip-or-mac-address-from-string-to-byte-array-arduino-or-c
    */
    void parseBytes(const char* str, char sep, byte* bytes, int maxBytes, int base) {
        for (int i = 0; i < maxBytes; i++) {
            bytes[i] = strtoul(str, NULL, base);
            str = strchr(str, sep);
            if (str == NULL || *str == '\0') break;
            str++;
        }
    }

    В нашем случае выглядеть это будет следующим образом

    byte ip[4];
    parseBytes(inputCommands.substring(4).c_str(), '.', ip, 4, 10);

    А дале все становится еще проще, попросту проверить попадает ли наш ip адрес, в список правильных адресов. И самой простой проверкой послужит проверка первого байта адреса на несоответствие не угодным нам сетям (0, 127, 255)

    if (ip[0] != 127 and ip[0] != 255 and ip[0] != 0) {
        // Производим необходимые нам действия с ip адресом, например, запись в конфиг
        config.ip = {ip[0], ip[1], ip[2], ip[3]};
    }

    Вы в праве реализовать собственные проверки, какие только душе угодны.

    Также хотелось бы отметить, что обрабатывать некоторые параметры проще и быстрее через их короткие записи. К таким можно отнести маску подсети устройства. Например, привычный дня нас адрес 192.168.0.1 с маской подсети 255.255.255.0 можно записать в виде 192.168.0.1/24, где цифра 24 указывает нашу подсеть в краткой форме. А, следовательно, мы можем записать несколько кратких форм масок подсети в следующем виде:

    1. subnet 255.255.255.0 или subnet 24
    2. subnet 255.255.0.0 или subnet 16
    3. subnet 255.0.0.0 или subnet 8

    Это основные маски, и я не описывал все существующие т.к в этом нет нужды, но если Вам интересно, то почитать про них можно в wikipedia.

    if (inputCommands.startsWith(F("subnet"))) {
        String input = inputCommands.substring(8);
        if (input == F("24"))      config.subnet = {255, 255, 255,   0};
        else if (input == F("16")) config.subnet = {255, 255,   0,   0};
        else if (input == F("8"))  config.subnet = {255,   0,   0,   0};
        else {
            // Все остальные маски попадают в этот блок
            byte subnet[4];
            parseBytes(input.c_str(), '.', subnet, 4, 10);
            config.subnet = {subnet[0], subnet[1], subnet[2], subnet[3]};
        }
    }

    MAC адрес хранится у нас в виде массива байт. Его перезапись другим массивом производится с помощью функции memcpy

    if (inputCommands.startsWith(F("mac"))) {
        byte mac[6];
        parseBytes(inputCommands.substring(4).c_str(), ':', mac, 6, 16);
        memcpy(config.mac, mac, 6);
    }

    Изменение адреса MQTT сервера

    if (inputCommands.startsWith(F("server"))) {
        String server = inputCommands.substring(8);
        server.trim();
        if (server.length() < 40) server.toCharArray(config.server, 40);
    }

    В принципе теперь понятно, как производить получение, разбор и сохранение конфигурации в EEPROM микроконтроллера.

    Как это выглядит на практике

    Заливаем программу в микроконтроллер и подключаемся к Arduino по usb или через переходник. Открываем терминал и нас приветствуют краткой справкой с описанием доступных команд.

    - ---------------------------------------------------------------------------------------
    # Sensor with data sending to mqtt server (c) it4it.club
    # Use the "config" command to view the current configuration
    # To change the configuration, specify the parameter name and its new value with a space,
    # for example "ip 192.168.0.200", "subnet 255.255.255.0" or "mac AA:BB:CC:DD:EE:FF"
    # You can also specify a subnet using the mask 24, 16 or 8
    # Additional commands:
    # sensors - view current data from sensors
    # config - view current configuration
    # save - saves the current configuration
    # reset - resets all settings
    # restart - restarts the device
    # eeprom clear - removes all contents of eeprom
    # help - view this help
    - ---------------------------------------------------------------------------------------

    Т.к. в EEPROM микроконтроллера не была обнаружена конфигурация (волшебный hex байт нам подсказал), то были задействованы стандартные настройки. Просмотреть текущую конфигурацию можно командой config

    config
    # ip: 192.168.0.200
    # subnet: 255.255.255.0
    # gateway: 192.168.0.1
    # dns: 192.168.0.1
    # mac: 00:AA:BB:CC:DE:02
    # server: mqtt.it4it.club
    # topic: arduino/serial/config

    чтобы изменить значение любого из этих параметров необходимо передать имя параметра и его новое значение, разделенные между собой пробелом.

    ip 10.10.10.99
    # ok
    gateway 10.10.10.1
    # ok
    dns 10.10.10.1
    # ok

    После окончания конфигурации необходимо сохранить настройки и если были затронуты критические параметры, например, сеть, то перезапустить Arduino соответствующей командой.

    save
    # ok
    restart
    # ok
    # restarting device...

    Если параметр был успешно принят, то контроллер ответит нам "ok", а в противном случае ругнется.

    ip 127.0.0.1
    # bad ip

    Также мы получим негативный ответ если команда не была распознана.

    qwerqwer1243
    # bad command

    С остальными командами Вы разберетесь самостоятельно.

    arduino-zabbix-mqtt2.JPGarduino-zabbix-mqtt1.JPG

    arduino-zabbix-mqtt3.png

    Исходник: MQTT_CLIENT_328_SERIAL_CONFIG.zip

    PS: в общем то это статья родилась только для того, чтобы в соседнем форуме с системой мониторинга Zabbix появилась ссылка на устройство, но я надеюсь, что она также станет полезна любителям домашней автоматизации и не только.

    • Like 2
    • Thanks 1

  21. Привет друзья.

    Обкатка прошла успешно и пришло время расширить функционал и обзавестись дополнительными плюшками. А именно:

    1. Добавлять устройства по реальными ip адресами, а не эмитировать их через петлю сервера 127.0.0.1
    2. Реализовать возможность подключения как к приватному брокеру с системой авторизации, так и оставить поддержку открытых брокеров
    3. Перенести весь функционал в шаблон
    4. Предусмотреть возможность использования одного шаблона с различными брокерами. А также разделить адрес топика на две части - корневой путь, который можно выстраивать динамически у каждого узла и статическую часть, несущую как смысловую нагрузку, так и сами данные.

    Вам понадобится mosquitto-clients. У меня сервер под Linux Ubuntu поэтому от этого и будем отталкиваться. Если у Вас что-то иное, то действуйте по аналогии.

    sudo apt-get install mosquitto-clients

    Далее создаем дополнительный внешний скрипт для Zabbix. По умолчанию, все используемые сервером скрипты располагаются по адресу /usr/lib/zabbix/externalscripts

    Быстро узнать какой каталог использует Ваш сервер можно так

    cat /etc/zabbix/zabbix_server.conf | grep ExternalScripts

    Добавляем новый скрипт и заодно выставляем на него права. В моем случае, сервер работает от имени пользователя zabbix

    cd /usr/lib/zabbix/externalscripts
    touch ./mqtt
    chown zabbix:zabbix ./mqtt
    chmod 555 ./mqtt
    

    Скрипт станет посредником между Zabbix сервером и брокером. Его основная задача, это прием входных параметров, их подсчет и принятие решения о том чем эти самые параметры являются. А вот и сам скрипт

    Скрытый текст
    
    #!/bin/bash
    if [[ $# = 0 || $# > 4 ]]
    then
      echo "Bad arguments"
      exit 1
    fi
    BROKER="127.0.0.1"
    TOPIC=""
    LOGIN=""
    PASSW=""
    case $# in
      1 )
        TOPIC="$1"
        ;;
      2 )
        BROKER="$1"
        TOPIC="$2"
        ;;
      3 )
        TOPIC="$1"
        LOGIN="$2"
        PASSW="$3"
        ;;
      4 )
        BROKER="$1"
        TOPIC="$2"
        LOGIN="$3"
        PASSW="$4"
        ;;
    esac
    SELECT="/usr/bin/mosquitto_sub -h $BROKER -i zabbix -t $TOPIC -C 1 -N"
    if [[ -n "$LOGIN" && -n "$PASSW" ]]
    then
      SELECT="$SELECT -u $LOGIN -P $PASSW"
    fi
    $SELECT 2>/dev/null

     

    Скрипт способен принимать до 4 входных параметров и от их количества будет зависеть то, какое место займет каждый из перечисленных параметров.

    Теперь в самом zabbix сервере, при создании нового элемента данных, нам доступна внешняя проверка mqtt принимающая до 4 параметров

    1. mqtt[<топик>]
    2. mqtt[<адрес сервера>, <топик>]
    3. mqtt[<топик>, <имя пользователя>, <пароль>]
    4. mqtt[<адрес сервера>, <топик>, <имя пользователя>, <пароль>]

    А вот и сам тестовый шаблон: mqtt-temperature.zip

    Очень удобным решением будет использование пользовательских макросов в создаваемых шаблонах и в самих узлах. Создаем новый узел сети, теперь можно указывать его реальный ip адрес, и переходим в раздел "макросы". Макросы узла имеют приоритет над макросами прикрепленного к узлу шаблона.

    zabbix-mqtt-macros1.png

    Таким макросом мы можем указать корневой топик для всей веки собираемой информации. Если переключиться на "макросы узла сети и унаследованные", то мы получим весь список доступных макросов.

    zabbix-mqtt-macros2.png

    Таким образом мы можем указать основные настройки на уровне шаблона и вносить мелкие корректировки в эти самые настройки уже на уровне самого узла сети. Также Вы можете добавить собственные макросы и предусмотреть для них место в логике скрипта. Например, добавить возможность выбора порта брокера или иного параметра для mosquitto_sub

    Использовать макросы очень просто, достаточно указать их в качестве параметра или его части.

    zabbix-mqtt-macros3.png

    Далее работаем с ними также, как и с обычными параметрами

    zabbix-mqtt-macros4.pngzabbix-mqtt-macros5.png

    zabbix-mqtt-macros6.png

    Для проверки работоспособности удобно использовать программу mqtt-spy

    zabbix-mqtt-macros7.png

    ЕЩЕ РАЗ НАПОМИНАЮ: Клиенты рассылающие сообщения должны использовать параметр "-r, --retain" для сохранения сообщения у брокера. Без этого параметра Zabbix не сможет получить данные т.к не поддерживает постоянную связь с брокером, а лишь забирает последние данные по установленному интервалу времени.

    • Like 1
    • Thanks 1
×
×
  • Создать...