Найти тему
K12 :: О ESP32 и не только

Термостат на ESP32 с удаленным управлением. Часть 8. Класс rSensor и как заменить сенсоры на другие

Оглавление

Добрый день, уважаемые читатели! Данная статья продолжает цикл статей, посвященных самодельному устройству на базе ESP32 DevKitC WROOM-32x и фреймворка Espressif IoT Development Framework. В прошлых статьях я рассказывал, как и из чего собрать устройство, а так же как создать самый простой вариант прошивки - устройство телеметрии для дачи, гаража или квартиры.

Я совсем не случайно в прошлой статье повел рассказ про сенсоры (а вы что подумали, когда я вдруг вспомнил про них?)... Если кто-то из вас все-таки читал предыдущие статьи данной серии, то наверное обратили внимание - к данному устройству подключены только датчики:

  • DS18B20 - температура теплоносителя
  • BME280 - погода в доме
  • DHT22 (AM2302) - погода за окном

Конечно, одни из самых распространенных сенсоров. Но ведь кроме этого есть и масса других, не менее прекрасных сенсоров. Неужели нельзя заменить их на что-то другое? Конечно можно! Кроме, пожалуй DS18B20 - для измерения температуры на выходе из котла ничего лучше ещё не придумали, поэтому и замена его не требуется.

Вот в этой статье я и расскажу как...

rSensor и rSensorItem - что это за фрукты-овощи и с чем их едят

Но вначале потребуется поближе познакомится с классом-предком всех без исключения драйверов датчиков и сенсоров в моей прошивке. Называется он rSensor, а найти его можно найти в библиотеке https://github.com/kotyara12/reSensors/blob/master/reSensor.

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

Итак, перейдем к делу.

Класс rSensor

Класс rSensor предоставляет программисту базовый набор функций, свойственных для любого драйвера сенсора в прошивке:

  • Служит контейнером, объединяющим в единое целое один или несколько экземпляров класса rSensorItem, которые в свою очередь, непосредственно хранят и при необходимости фильтруют физические данные с сенсора (об нем мы поговорим ниже)
  • Предоставляет системе и программисту публичные функции опроса данных с сенсора с контролем минимального интервала чтения данных для избежания его саморазогрева - если мы хотим получить данные с сенсора слишком часто, класс просто вернет последние считанные данные.
  • Контролирует текущее состояние сенсора (норма, нет связи, ошибка CRC и т.д.) с возможностью уведомления пользователя о проблемах.
  • Генерирует динамически и хранит топик для публикации на MQTT-брокере.
  • Генерирует JSON-пакет с данными всех измеряемых физических величин для данного сенсора и публикует его на MQTT-брокере
  • Обеспечивает сохранение экстремумов измеряемых физических величин в NVS разделе flash-памяти
  • Обеспечивает регистрацию и работу с подсистемой хранения параметров (типы фильтров, размеры буферов, корректировки и т.д.)

Базовый класс rSensor нельзя непосредственно использовать в прошивке, так как он содержит чисто виртуальные методы, которые программист должен переопределить в классах-потомках. От него отпочковались следующие классы:

  • rSensorX1 - для создания драйверов, которые обрабатывают только одну физическую величину, например температуру. На базе этого класса созданы такие драйверы, как DS18B20, ADC, Capacitive Soil Moisture Sensor v1.2, QDY30A и т.д.
  • rSensorX2 - для создания драйверов, которые обрабатывают уже две физические величины, например температуру и влажность. Но непосредственно для создания дайверов датчиков температуры и влажности его я не использую (хм, странно не правда ли..)
  • rSensorHT - промежуточный класс, образованный от rSensorX2 и специально предназначенный как раз для создания дайверов датчиков температуры и влажности. Обратите внимание - в нем первым элементом хранилища всегда является влажность, а не температура. Кроме всего прочего, что умеет rSensorX2, он может вычислять точку росы, если это необходимо.
  • rSensorX3 - для создания драйверов, которые обрабатывают три физические величины, например температуру, влажность, давление.
  • rSensorX4 - для создания драйверов, которые как вы уже поняли обрабатывают уже четыре физические величины, например температуру, влажность, давление, IAQ
  • rSensorX5 - ну и наконец самый "емкий" на текущий момент вариант, который может обрабатывать ажно целых 5 физических величин.

От этих классов непосредственно и образованы все написанные мной драйверы сенсоров и датчиков.

Что нужно знать об этом классе в первую очередь?

Любого потомка rSensor можно инициализировать двумя способами: динамически и статически.

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

Я расскажу о статической инициализации, так как если поймете её, до с динамической проблем не будет.

Пример статической инициализации элементов-хранилищ для физических данных
Пример статической инициализации элементов-хранилищ для физических данных

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

-3
  • const char* sensorName - условное уникальное имя сенсора не длиннее 15 символов. Используется для уведомлений и хранения данных в NVS
  • const char* topicName - топик сенсора, сюда драйвер будет публиковать данные на MQTT сервере. Здесь должен быть указан не полный топик, а только его часть после %location%/%device%/..., полный топик будет сгенерирован автоматически.
  • const bool topicLocal - использовать ли только локальную схему топиков (только для случаев, когда в вашей прошивке используется локальный и публичный брокер одновременно)
  • DHTxx_TYPE sensorType, const uint8_t gpioNum, const bool gpioPullup, const int8_t gpioReset, const uint8_t levelReset - параметры подключения, которые зависят от конкретной модели сенсора: номер шины, адрес, физические параметры. Поэтому в каждом драйвере этот набор аргументов будет уникальным.
  • rSensorItem* item1, rSensorItem* item2 - указатели на элементы хранилища. Их может быть от 1 до 5.
  • const uint32_t minReadInterval - минимальный интервал физического обращения к сенсору в миллисекундах. Это позволяет защитить сенсоры от саморазгрева при частых опросах. Если это не нужно - поставьте 0.
  • const uint16_t errorLimit - минимальное количество ошибок, которые должны произойти при попытке чтения данных с сенсора, прежде чем система уведомит вас о его неисправности. Позволяет "проглатывать" единичные сбои сенсоров и не беспокоить по пустякам.
  • cb_status_changed_t cb_status - функция обратного вызова, которая будет вызвана при изменении состояния сенсора. Её можно использовать для необычно "хитрых" уведомлений, но обычно можно оставить NULL.
  • cb_publish_data_t cb_publish - функция обратного вызова для публикации данных на брокере, должна быть указана обязательно (если вы используете MQTT), но она очень простая и используется сразу для всех сенсоров в системе. Как вы думаете, почему я не прописал mqttPublish(...) непосредственно внутри rSensor?
-4

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

Статус (состояние) драйвера можно получить с помощью функции getStatus(). Если функция вернула 2 или SENSOR_STATUS_OK, то это означает, что с сенсором всё в порядке, и его данные достоверны. Иначе данные с сенсора прочитать то можно, но можно ли доверять? А всего статусов на текущий момент столько:

-5

Обратите внимание: драйвера сенсоров bosch при указании аппаратных фильтров больше 1 (OSM_X2 и выше) в при сбросе всегда будут возвращать статус NO_DATA - для них это нормально! Поэтому для bosch стоит задирать errorLimit повыше.

Класс rSensorItem

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

  • Конвертация физически измеряемых RAW (сырых) данных в удобный пользователю вид. Например градусы Цельсия в Фаренгейты, Паскали в миллиметры ртутного столба, миллиВольты в влажность почвы и т.д. и т.п.
  • Фильтрация по среднему значению или медианный фильтр. Размер буфера и тип фильтра можно изменить не только при программировании, а и во время работы программы. Но для этого буфер для фильтра приходится выделять из общей кучи.
  • Хранение последних полученных данных и фиксацию отметок времени их получения.
  • Фиксацию экстремумов (минимумов и максимумов) за последние сутки, неделю и все время работы устройства).
  • Выдачу данных по запросу пользователя, ну то есть пользовательской прикладной программы.
  • Упаковку всего этого добра, то есть последних значений, отметок времени, и экстремумов в свою часть общего JSON по запросу вышестоящих органов (то есть экземпляра rSensor, к которому он прикреплен).

Надеюсь, вы поняли общую идею. А для удобства пользования есть несколько предопределенных классов, назначение которых понятно уже из их названия:

  • rTemperatureItem - для обработки и хранения данных о температуре
  • rPressureItem - для обработки и хранения данных о давлении
  • rMapItem - хитровывернутый класс для преобразования входных данных RAW в выходные с помощью функции map() с возможностью автоматического смещения диапазонов. Используется в основном в драйвере Capacitive Soil Moisture Sensor v1.2 или датчике уровня воды для пересчета в %.

Инициализация каждого элемента производится следующим образом:

-6

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

  • rSensor *sensor - указатель на экземпляр класса сенсора. Нужен только при динамической инициализации внутри драйвера. Так как мы создаем экземпляр класса rSensorItem статически, мы можем просто передать NULL или nullptr (это равнозначные определения).
  • const char* itemName - условное имя параметра, например temperature
  • const unit_temperature_t unitValue - для некоторых предопределенных классов вы должны указать желаемую единицу измерения. Есть не у всех элементов
  • const sensor_filter_t filterMode, const uint16_t filterSize - режим фильтрации и размер буфера фильтра. На данный момент доступен фильтр по среднему и медианный фильтр
  • const char* formatNumeric - формат для вывода в JSON числовых значений. Здесь вы можете указать количество знаков после запятой, например так: "%.3f"
  • const char* formatString - формат для вывода в JSON в текстовом виде. Здесь по желанию можно кроме формата добавить префикс или постфикс (например единицу измерения)
  • const char* formatTimestamp - формат отметки времени, например "%d.%m.%y %H:%M"
  • const char* formatTimestampValue и const char* formatStringTimeValue позволяют "смешивать" измеренное значение с временем его получения для удобства его отображения в mqtt клиентах. У меня стоит так: "%d|%H:%M" и "%s\n%s", то есть в первой строке будет само значение, а внизу день месяца и время измерения без секунд
-7

Это пока всё, что вам нужно знать, чтобы выбрать другой удобный сенсор для вашего устройства. Остальное вы можете узнать из исходников библиотеки. Есть вопросы - задавайте в комментариях...

reSensors/reSensor at master · kotyara12/reSensors

Перейдем к практической части...

Замена BME280 в проекте на BMP280

Перейдем к практической реализации.

Для начала советую обновить локальные библиотеки из нового архива, который к моменту публикации статьи будет уже на GitHub. Как это сделать, я уже писал в одной из предыдущих частей серии: удалить старые библиотеки из C:\PlatformIO\libs и распакуйте туда новый архив.

Допустим, у меня нет BME280, а есть BMP280. О горе, мне горе! (иду за пеплом и посыпаю им голову) Не беда, это дело поправимое...

Для начала исправим вызов библиотеки драйвера в lib\sensors\sensors.h:

-8

Затем там же не забываем изменить тип используемого драйвера:

На скриншоте есть ошибка. Найдете?
На скриншоте есть ошибка. Найдете?

Кстати, цифра в конструкторе класса - это индекс группы сенсоров, может быть от 0 до 7. По нему прошивка определяет, все ли сенсоры в порядке. Если сенсоров меньше 8, настоятельно рекомендуется пронумеровать их от 1 до 7, если больше - назначьте второстепенным индекс 0.

И.. сразу же пробуем компилировать! Дабы не шарится вслепую по sensors.cpp, компилятор сам подскажет нам где мы напортачили и даже даст ссылки:

-10

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

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

-11
-12

Знакомимся и приводим в соответствие. Не забываем удалить элемент, в которым ранее хранилась влажность! Новая процедура инициализации выглядит так:

-13

Пробуем компилировать опять.... Опять ругается - и правильно ругается...

-14

Третьего элемента в драйвере уже нет, а мы его пытаемся использовать!

Исправляем и помним, что первым элементом у нас идет давление, а вторым - температура. Порядок следования элементов всегда строго определен в конкретном драйвере, сверяйтесь с определением класса.

Приводим в соответствие:

-15

И не забудьте привести в соответствие передачу данных на внешние серверы, если вы их используете (соответственно контроллер придется перенастроить или даже возможно создать новый):

-16

Разобрались? Хорошо идем дальше...

Замена DHT22 в проекте на SHT31

Теперь давайте попробуем заменить драйвер DHT22 на какой-нибудь другой, например мой любимый SHT31. Приступим...

Опять же, меняем включение библиотеки:

-17

Затем переделываем блок объявления констант и переменных:

-18

Действуя по прежней схеме, правим блок инициализации. Здесь, кстати, гораздо проще - ведь набор внутренних элементов ничуть не изменился, а значит исправления потребуется внести в одном единственном месте:

-19

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

По аналогичной схеме вы можете заменить и BMP/BME на любой другой сенсор, в том числе на rs485/modbus, но там свои тонкости. Если интересует - пишите. И так очень длинная статья для Дзена

Полный список поддерживаемых драйверов вы найдете в папке libs\sensors или на GitHub:

GitHub - kotyara12/reSensors: Библиотеки для получения данных с различных сенсоров / Libraries for receiving data from various sensors

Выбирайте любой удобный... Про сами сенсоры можно почитать в предыдущей статье:

_____

ПыСы. А у меня, увы, сдох китайский клон DS18B20 в баке с водой. Как не обмазывал его силиконом, как не изолировал - не помогло. Китайская нержавейка - отнюдь не нержавейка 🤷‍♂️. Теперь теплица плюется ядом, но работает.

-20

Ладно, "не ходовая часть - на скорость не влияет" как говорил мой наставник, когда я служил ремонтником станков с ЧПУ.

Заказал хороший, в хорошем исполнении:

-21

Получим - посмотрим.

_______________

На этом пока всё, до встречи на сайте и на dzen-канале! Всем добра!

👍 Понравилась статья? Поддержите канал лайком или комментарием! Каналы на Дзене "живут" только за счет ваших лайков и комментариев. Простого "спасибо" или "+" (ну или даже "не зачот") будет вполне достаточно.

📌Подпишитесь на канал и вы всегда будете в курсе новых статей.

🔶 Полный архив статей вы найдете здесь