Добрый день, уважаемый читатель!
Данная статья продолжает цикл статей, посвященных самодельному устройству на базе ESP32 DevKitC WROOM-32x и фреймворка Espressif IoT Development Framework. В прошлых статьях я рассказывал, как и из чего собрать устройство, а так же как создать самый простой вариант прошивки – устройство телеметрии для дачи, гаража или квартиры.
Я совсем не случайно в прошлой статье повел рассказ про сенсоры (а вы что подумали, когда я вдруг вспомнил про них?)… Если кто-то из вас все-таки читал предыдущие статьи данной серии, то наверное обратили внимание – к данному устройству подключены только датчики:
- DS18B20 – температура теплоносителя
- BME280 – погода в доме
- DHT22 (AM2302) – погода за окном
Конечно, это одни из самых распространенных сенсоров. Но ведь кроме этого есть и масса других, не менее прекрасных сенсоров. Неужели нельзя заменить их на что-то другое? Конечно можно! Кроме, пожалуй DS18B20 – для измерения температуры теплоносителя “на выходе” из котла ничего лучше ещё не придумали, поэтому и замена его не требуется.
Вот в этой статье я и расскажу как…
Небольшое предупреждение: в данной статье описана несколько устаревшая версия библиотеки reSensor, однако она вполне рабочая и проверенная, да и в пакете с проектом используется именно описываемая версия. Поэтому статью пока что переписывать нет смысла. Но не удивляйтесь, если классы в основной ветке библиотеки reSensor будут немного отличаться от описанных в статье.
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:
GitHub – kotyara12/reSensors: Библиотеки для получения данных с различных сенсоров
Выбирайте любой удобный…
Про сами сенсоры можно почитать в предыдущей статье: Датчики температуры и влажности для Arduino и ESP
На этом пока всё, до встречи на сайте и на dzen-канале! Всем добра!
Все статьи цикла “Термостат и ОПС”:
- Часть 1. Вводная: общее описание и возможности
- Часть 2. Перечень необходимых компонентов, схемы отдельных узлов, печатная плата
- Часть 3. Минимальный вариант: только телеметрия через MQTT брокер
- Часть 4. Описание генерируемых устройством MQTT-топиков
- Часть 5. Добавляем выгрузку данных на внешние сервисы
- Часть 6. Изменения в прошивке под требования на ESP-IDF 5.0.1
- Часть 7. Автоматический контроль диапазонов температуры
- Часть 8. Класс rSensor и как заменить сенсоры на другие из списка поддерживаемых
- Часть 9. Термостат и управление нагрузкой
- Часть 10. Охранно-пожарная и аварийная сигнализация
Прошивка K12 для ESP32 и ESP-IDF:
Дополнительные статьи, которые применимы к любым устройствам, запрограммированным с помощью тех же самых библиотек.
- Прошивка для ESP32 на основе ESP-IDF: описание модулей и библиотек
- Настройка Android-приложения MQTT Dash для работы с устройством
- rLoadControl: индикация состояния нагрузки на MQTT DASH
- Команды управления
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью:
Добрый день.
Не уверен, что мой вопрос относится к теме этой статьи, но всё же.
Я хочу отправлять данные с некоторых (или всех) сенсоров на LCD дисплей.
Экран подключил и он работает. Теперь стоит задача, получить данные с сенсора и отобразить на дисплее.
Я представил себе следующую последовательнось действий:
– добавляю новый event_id (RE_SENSOR_SEND_DATA_TO_DISPLAY), например сюда sensor_event_id_t
– в задаче sensorsTaskExec получаю данные с сенсора и помещаю их в очередь с соответствующим event_id
– подписываюь на событие eventHandlerRegister(RE_SENSOR_EVENTS, RE_SENSOR_SEND_DATA_TO_DISPLAY, &sensorUpdateData, nullptr), функция sensorUpdateData будет вытягиваь данные из очереди и сохранять в некую переменную
– в задаче которая отображает данные на дисплей, значение переменной, с предыдущего шага, отправляем на экран;
Вроде всё просто, но не обошлось без НО…
В функции void sensorsTaskExec(void *pvParameters) я получаю данные с сенсора и пытаюсь их запихнуть в очередь. Примерно так:
sensorBoilerIn.readData();
if (sensorBoilerIn.getStatus() == SENSOR_STATUS_OK) {
value_t raw = sensorBoilerIn.getValue(false).rawValue;
value_t filter = sensorBoilerIn.getValue(false).filteredValue;
value_t minExt = sensorBoilerIn.getExtremumsDaily(false).minValue.filteredValue;
value_t maxExt = sensorBoilerIn.getExtremumsDaily(false).maxValue.filteredValue;
rlog_i(“BOILER_IN”, “Values raw: %.2f °С | out: %.2f °С | min: %.2f °С | max: %.2f °С”, raw, filter, minExt, maxExt);
eventLoopPost(RE_SENSOR_EVENTS, RE_SENSOR_SEND_DATA_TO_DISPLAY, &filter, sizeof(filter), portMAX_DELAY);
};
Но когда выполняется eventLoopPost устройство перезагружается. Я что-то делаю не так?
Может надо в rSensor определить метод в котором выполнять отправку данных сенсора в очередь?
Спасибо.
Мой вам совет. Пореже используйте события. Особенно для медленных устройств типа LCD. Иначе они стопорят весь цикл событий.
Если вы уж создали отдельную задачу для LCD, то создайте для нее же и входящую очередь и кидайте данные в нее, напрямую.
И еще. Как только вы стали обращаться к шине I2C из разных задач. вы обязаны включить блокировку доступа к шине через #define CONFIG_I2C_LOCK 1
И ещё…
В момент перезагрузки вам выдается обратная трассировка стека вызовов функций. Расшифруйте ей и вам станет ясно, “что вы делаете не так”. Как это сделать было уже несколько статей.
Спасибо за Ваш совет.
Буду пробовать с отдельной очередью.
Это #define CONFIG_I2C_LOCK 1 обязательно надо? Дисплей у меня на второй, ранее не используемой, шине I2C.
Если две разных задачи обращаются к одной и той же шине, то нужен. В том числе, например из обработчиков событий / прерываний. Если вы уверены, что совместного доступа не произойдет, то не нужен
Добрый вечер.
Если установить в конфигурации:
// EN: Allow the publication of sensor data in a simple form (each value in a separate subtopic)
// RU: Разрешить публикацию данных сенсора в простом виде (каждое значение в отдельном субтопике)
#define CONFIG_SENSOR_AS_PLAIN 1
то последняя верся reSensor.cpp не компилируется.
Да, скорее всего это так. Это рудимент, от которого я в следущей версии избавлюсь. Так как все остальные компоненты уже генерируют данные только в JSON. И, кроме того, публикация в PLAIN часто вызывает переполнение очереди отправки MQTT клиента.
Спасибо за оперативный ответ. Подскажите а где можно посмотреть пример, как прикруть какой нибудь дисплей для отображения времени/информации с датчиков ?
Скоро будет. Заходите ещё.
Спасибо. Колоссальную работу проделали. Жаль я с C++ на вы.