Добрый день, уважаемые читатели! Данная статья продолжает цикл статей, посвященных самодельному устройству на базе 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 подробнее я расскажу ниже, а пока продолжу. Кроме указателей на элементы для измеряемых данных, при инициализации любого драйвера сенсора мы должны указать множество аргументов:
- 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?
Для динамической инициализации все то же самое, но вместо указателей на элементы мы должны указать параметры фильтрации данных.
Статус (состояние) драйвера можно получить с помощью функции getStatus(). Если функция вернула 2 или SENSOR_STATUS_OK, то это означает, что с сенсором всё в порядке, и его данные достоверны. Иначе данные с сенсора прочитать то можно, но можно ли доверять? А всего статусов на текущий момент столько:
Обратите внимание: драйвера сенсоров bosch при указании аппаратных фильтров больше 1 (OSM_X2 и выше) в при сбросе всегда будут возвращать статус NO_DATA - для них это нормально! Поэтому для bosch стоит задирать errorLimit повыше.
Класс rSensorItem
Как я уже написал, этот класс служит для обработки и хранения одной измеряемой физической величины. Его назначение таково:
- Конвертация физически измеряемых RAW (сырых) данных в удобный пользователю вид. Например градусы Цельсия в Фаренгейты, Паскали в миллиметры ртутного столба, миллиВольты в влажность почвы и т.д. и т.п.
- Фильтрация по среднему значению или медианный фильтр. Размер буфера и тип фильтра можно изменить не только при программировании, а и во время работы программы. Но для этого буфер для фильтра приходится выделять из общей кучи.
- Хранение последних полученных данных и фиксацию отметок времени их получения.
- Фиксацию экстремумов (минимумов и максимумов) за последние сутки, неделю и все время работы устройства).
- Выдачу данных по запросу пользователя, ну то есть пользовательской прикладной программы.
- Упаковку всего этого добра, то есть последних значений, отметок времени, и экстремумов в свою часть общего JSON по запросу вышестоящих органов (то есть экземпляра rSensor, к которому он прикреплен).
Надеюсь, вы поняли общую идею. А для удобства пользования есть несколько предопределенных классов, назначение которых понятно уже из их названия:
- rTemperatureItem - для обработки и хранения данных о температуре
- rPressureItem - для обработки и хранения данных о давлении
- rMapItem - хитровывернутый класс для преобразования входных данных RAW в выходные с помощью функции map() с возможностью автоматического смещения диапазонов. Используется в основном в драйвере Capacitive Soil Moisture Sensor v1.2 или датчике уровня воды для пересчета в %.
Инициализация каждого элемента производится следующим образом:
Сложно? Да просто громоздко и макросы препроцессора еще ясности не добавляют. Но они пока необходимы, возможно я от них избавлюсь в ближайшем будущем. Итак...
- 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", то есть в первой строке будет само значение, а внизу день месяца и время измерения без секунд
Это пока всё, что вам нужно знать, чтобы выбрать другой удобный сенсор для вашего устройства. Остальное вы можете узнать из исходников библиотеки. Есть вопросы - задавайте в комментариях...
Перейдем к практической части...
Замена BME280 в проекте на BMP280
Перейдем к практической реализации.
Для начала советую обновить локальные библиотеки из нового архива, который к моменту публикации статьи будет уже на GitHub. Как это сделать, я уже писал в одной из предыдущих частей серии: удалить старые библиотеки из C:\PlatformIO\libs и распакуйте туда новый архив.
Допустим, у меня нет BME280, а есть BMP280. О горе, мне горе! (иду за пеплом и посыпаю им голову) Не беда, это дело поправимое...
Для начала исправим вызов библиотеки драйвера в lib\sensors\sensors.h:
Затем там же не забываем изменить тип используемого драйвера:
Кстати, цифра в конструкторе класса - это индекс группы сенсоров, может быть от 0 до 7. По нему прошивка определяет, все ли сенсоры в порядке. Если сенсоров меньше 8, настоятельно рекомендуется пронумеровать их от 1 до 7, если больше - назначьте второстепенным индекс 0.
И.. сразу же пробуем компилировать! Дабы не шарится вслепую по sensors.cpp, компилятор сам подскажет нам где мы напортачили и даже даст ссылки:
Находим красную строку с указанием модуля, номера строки и номера символа, жмем батон CTRL и одновременно левый батон мышки. Вуаля - мы перешли на то место в коде, которое очень не нравится компилятору. Кстати, а на Arduino IDE такое не прокатит - листайте вручную, дружочки, ха...
Очевидно, что компилятору не нравятся параметры, которые были указаны для BME, изменяем их на другие. Чтобы узнать правильный список - нажмите на строку объявления функции и выберите правильную - среда сама перебросит вас на нужный блок кода:
Знакомимся и приводим в соответствие. Не забываем удалить элемент, в которым ранее хранилась влажность! Новая процедура инициализации выглядит так:
Пробуем компилировать опять.... Опять ругается - и правильно ругается...
Третьего элемента в драйвере уже нет, а мы его пытаемся использовать!
Исправляем и помним, что первым элементом у нас идет давление, а вторым - температура. Порядок следования элементов всегда строго определен в конкретном драйвере, сверяйтесь с определением класса.
Приводим в соответствие:
И не забудьте привести в соответствие передачу данных на внешние серверы, если вы их используете (соответственно контроллер придется перенастроить или даже возможно создать новый):
Разобрались? Хорошо идем дальше...
Замена DHT22 в проекте на SHT31
Теперь давайте попробуем заменить драйвер DHT22 на какой-нибудь другой, например мой любимый SHT31. Приступим...
Опять же, меняем включение библиотеки:
Затем переделываем блок объявления констант и переменных:
Действуя по прежней схеме, правим блок инициализации. Здесь, кстати, гораздо проще - ведь набор внутренних элементов ничуть не изменился, а значит исправления потребуется внести в одном единственном месте:
Ну вот и все, компилируем, подключаем новые датчики и заливаем прошивку в контроллер.
По аналогичной схеме вы можете заменить и BMP/BME на любой другой сенсор, в том числе на rs485/modbus, но там свои тонкости. Если интересует - пишите. И так очень длинная статья для Дзена
Полный список поддерживаемых драйверов вы найдете в папке libs\sensors или на GitHub:
Выбирайте любой удобный... Про сами сенсоры можно почитать в предыдущей статье:
_____
ПыСы. А у меня, увы, сдох китайский клон DS18B20 в баке с водой. Как не обмазывал его силиконом, как не изолировал - не помогло. Китайская нержавейка - отнюдь не нержавейка 🤷♂️. Теперь теплица плюется ядом, но работает.
Ладно, "не ходовая часть - на скорость не влияет" как говорил мой наставник, когда я служил ремонтником станков с ЧПУ.
Заказал хороший, в хорошем исполнении:
Получим - посмотрим.
_______________
На этом пока всё, до встречи на сайте и на dzen-канале! Всем добра!
👍 Понравилась статья? Поддержите канал лайком или комментарием! Каналы на Дзене "живут" только за счет ваших лайков и комментариев. Простого "спасибо" или "+" (ну или даже "не зачот") будет вполне достаточно.
📌Подпишитесь на канал и вы всегда будете в курсе новых статей.
🔶 Полный архив статей вы найдете здесь