Приветствую всех читателей Habr. Время от времени я выкладываю в открытый доступ некоторые свои проекты по электронике. Сегодня я хочу представить устройство, которое делал с особым удовольствием - EFEKTA Open PM Monitor.
Это Zigbee-датчик мониторинга твердых частиц (PM1.0, PM2.5, PM10). В версии 2.0 в проект добавился сенсор ENS160 для измерения TVOC, eCO2 и AQI, а также небольшой дублирующий OLED-дисплей 128×64.
Но главная его «фишка» - аналоговый стрелочный индикатор.
Я прекрасно понимаю: смотреть на цифры проще и точнее. Но суть стрелочного индикатора не в удобстве считывания данных. Суть - в ламповости, в ощущениях, в отсылке к прошлому, когда приборы были живыми. Дрожащая стрелка, плавно реагирующая на изменение воздуха - это заходит. Это создает атмосферу.
Цифры вы и так увидите в Zigbee2MQTT, в Home Assistant, на OLED-дисплее. А стрелка - это душа устройства. Я делал этот проект в первую очередь для себя, потому что мне самому захотелось иметь на столе не просто бездушный датчик (начало).

Что умеет эта коробочка?
Измерения:
PM1.0, PM2.5, PM10 (сенсор ASAIR APM10, в версии 2.0 добавлена поддержка APM2000)
PM2.5 Index - индекс качества воздуха на основе PM2.5 по стандарту EPA
В версии 2.0 - TVOC, eCO2, общий AQI (сенсор ENS160)
Индикация:
Аналоговый стрелочный дисплей
RGB-подсветка
OLED-дисплей 128×64 (опционально, версия 2.0)

Управление:
Удаленное включение/выключение подсветки
Удаленная регулировка яркости
Калибровка стрелочного индикатора
Инверсия OLED-дисплея


Сетевые возможности:
Zigbee 3.0
Роутер сети
OTA-обновления

Сервисные функции:
Идентификация устройства (Identify) - по команде из сети датчик мигает подсветкой, чтобы вы могли понять, какой именно датчик перед вами
Автоопределение PM-сенсора (APM10 или APM2000)
Автоопределение OLED-дисплея и ENS160

Для разработчиков:
Полностью открытый исходный код
Теперь к деталям
Раз проект с открытым исходным кодом, я приглашаю вас заглянуть «под капот».
В основе всего лежит SoC CC2530 от Texas Instruments - классика для DIY Zigbee-устройств. Архитектура 8051, что для многих может показаться архаикой, но для моих задач это идеально: низкое энергопотребление, стабильная работа, огромное количество документации.
Плата в проекте одна. Все компоненты распаяны на одной печатной плате.
Все сенсоры в проекте подключены по шине I2C, сенсор твердых частиц тоже.
Я специально выбрал ASAIR APM10 (и APM2000 в версии 2.0) потому что это один из немногих сенсоров на рынке, который имеет несколько интерфейсов (UART и I2C), при этом стоит недорого. I2C удобнее UART: можно подключать несколько устройств на одну шину, проще работа с протоколом. ENS160 и OLED-дисплей (SSD1306) тоже подключены по I2C.
Автоопределение сенсоров.
Не люблю, когда пользователь должен переключать тумблеры или заливать разные прошивки под разные конфигурации. Поэтому я реализовал в проекте автоопределение подключаемого оборудования.
// Автоопределение дисплея displayPresent = oled_Detect(); // Автоопределение ENS160 ens160Present = ens160_Detect();
Как это работает:
OLED - попытка записать команду по I2C-адресу 0x3C. Если есть ACK - дисплей на месте.
ENS160 - читается регистр PART_ID. Если он равен ожидаемому значению - сенсор на месте.
Если дисплей не обнаружен - все вызовы oled_* функций игнорируются. Если ENS160 не обнаружен - устройство работает только с PM-сенсором.
Модель PM-сенсора - в драйвере pm_sensor_driver анализируется формат ответа.
Логика работы стрелки и подсветки (версия 2.0)
В версии 2.0 стрелка и RGB-подсветка работают по конкурентному принципу. Они показывают то, что «загрязненнее» - если TVOC высокий, а PM2.5 в норме, индикация будет по TVOC, и стрелка покажет его уровень.
Если ENS160 присутствует, дисплей циклически выводит PM2.5, TVOC и eCO2. Если ENS160 отсутствует - только PM2.5.
Архитектура: отдельный таск для подсветки на таймерах и событиях
Подсветка и управление стрелкой вынесены в отдельный LED-таск. Это видно из OSAL_App.c:
const pTaskEventHandlerFn tasksArr[] = { macEventLoop, nwk_event_loop, Hal_ProcessEvent, // ... pwm_rgb_event_loop, led_task_event_loop, // <-- LED-таск zcl_event_loop, bdb_event_loop, zclApp_event_loop, // ... };
Но просто вынести в отдельный таск - мало. Главное - как это работает внутри.
#define LED_TASK_ARROW_EVT 0x0002 // Анимация стрелки #define LED_TASK_FADE_EVT 0x0004 // Плавное изменение цвета #define LED_TASK_IDENTIFY_EVT 0x0008 // Режим идентификации
Почему это важно?
В первую очередь, это Zigbee-устройство. Оно передает телеметрию, принимает команды, и, самое главное - оно является роутером сети. Это значит, что через него идет чужой трафик: соседние устройства отправляют свои пакеты, координатор отвечает, и всё это должно обрабатываться в реальном времени.
Если бы анимации стрелки и RGB-подсветки реализовывалась с помощью блокирующих циклов (задержки через DelayMs(), пустые циклы while или for с NOP), то микроконтроллер на время этих анимаций ничего бы не делал. Он бы просто крутил цикл, меняя ШИМ.
Чем это грозит?
Потеря пакетов
Увеличение времени отклика на команды
Падение производительности сети
Поэтому вся анимация построена на таймерах и событиях.
Как это работает на примере движения стрелки:
Основной таск (zclApp) вычисляет новое положение стрелки на основе показаний сенсоров.
Вызывается led_task_send_arrow(new_value) - это просто отправка события в очередь LED-таска.
LED-таск получает событие, запоминает целевое значение и запускает таймер.
По каждому тику таймера стрелка сдвигается на один шаг.
Когда стрелка достигает цели - таймер останавливается.
Никаких блокирующих циклов. Пока стрелка плавно движется от 20 до 80, контроллер продолжает:
Опрашивать сенсоры по таймеру
Принимать Zigbee-пакеты от соседних устройств и ретранслировать их
Отвечать на команды из сети
OTA-обновления и автоматизация склейки прошивки
Поддержка OTA-обновлений (обновление прошивки «по воздуху») - это фича, которую я реализовал не с нуля, а на основе открытого проекта моего коллеги-разработчика Сергея Коптякова. Я взял за основу его репозиторий OTAClient, где автор подробно описал, как добавить OTA-клиент к любому проекту на CC2530.
В его инструкции предлагается ручная склейка двух HEX-файлов:
Сначала скомпилировать приложение, получить EndDeviceEB-OTAClient.hex.
Потом скомпилировать бутлоадер (Boot.hex) из примеров Z-Stack.
Открыть оба файла в текстовом редакторе, удалить определенные строки (первую строку в HEX-файле приложения, последние две строки в HEX-файле бутлоадера).
Скопировать содержимое бутлоадера в начало файла приложения.
Сохранить объединенный файл.
И только потом загружать прошивку через программатор.
Я посмотрел на этот процесс и подумал: «Серьезно? Каждый раз делать это руками?»
Во-первых, это неудобно - открывать редактор, удалять строки, копировать, сохранять. Во-вторых, это опасно - случайно можно удалить не ту строку или не туда скопировать, и устройство после прошивки не запустится.
Я решил, что этот процесс нужно автоматизировать. Самое простое решение, которое пришло мне в голову - написать bat-скрипт, который делает всё это автоматически. Всего несколько строк кода, и разработчику больше не нужно думать о склейке.
Вот как выглядит мой скрипт пост-билд:
batch @echo off echo Starting post-build... REM Конвертация SIM-файла в OTA-образ (для обновлений по воздуху) "%~dp0..\..\..\..\tools\OTA\OtaConverter\Release\OtaConverter.exe" ^ "%~dp0Router\Exe\OpenPM_Router.sim" ^ -o"%~dp0Router\Exe" -t0x1000 -m0x5678 -v10063202 -pCC2530DB REM Склейка бутлоадера и приложения в один HEX-файл REM (автоматизирует ручную операцию из инструкции OTAClient) powershell -Command "& { $boot = Get-Content '%~dp0Boot.hex'; $app = Get-Content '%~dp0Router\Exe\OpenPM_Router.hex'; $boot[0..($boot.Length-3)] + $app[1..($app.Length-1)] | Out-File '%~dp0..\firmware\Merged_Firmware.hex' -Encoding ASCII }" echo Completed
Склейка через PowerShell - это моя автоматизация ручного процесса. Команда читает файл бутлоадера (Boot.hex), читает файл приложения (OpenPM_Router.hex), отрезает лишние строки (аналогично тому, что предлагает делать руками автор OTAClient), и склеивает их в один файл Merged_Firmware.hex. Готово к прошивке через CC-Debugger.
Насколько это оптимально?
Честно говоря, я не знаю. Может быть, есть более красивые или правильные способы автоматизации. Может быть, можно было написать Python-скрипт или встроить всё в make-файлы. Но для меня bat-файл с PowerShell оказался самым простым и быстрым решением. Он работает, не требует установки дополнительных инструментов, и любой разработчик, который откроет мой проект, сразу поймет, что здесь происходит. Иногда простота это и есть оптимальность.
Схема
Основные элементы
U2 - CC2530, сердце устройства.
U1 - M25PE20-VMN6TP (SPI Flash). Это внешняя память для OTA-обновлений.
Сенсоры (подключаются внешними проводами к разъему J3 на плате):
APM10 или APM2000 (c версии 2.0) - сенсор твердых частиц (I2C)
ENS160 (c версии 2.0) - опциональный сенсор TVOC/eCO2/AQI (I2C)
OLED-дисплей SSD1306 (c версии 2.0) (I2C)
В схеме реализован конвертер уровней (level shifter) для шины I2C.
RGB-подсветка - подключается к выводам PWM2Ch, PWM3Ch, PWM4Ch (это каналы ШИМ таймера 1 на ножках P0.4, P0.5, P0.6) через транзисторы. Каждый цвет управляется отдельным каналом, логика управления инвертирована.
Стрелочный индикатор подключается к PWM1Ch (P0.3).
Кнопка KEY1, используется для ввода в сеть, выхода из сети и принудительной отправки данных.
Питание +5V USB Type-C. В схему добавлены резисторы на линиях CC (Configuration Channel), 5.1 кОм на каждую (CC1 и CC2) к земле.
Современные зарядки с поддержкой быстрых протоколов (Quick Charge, Power Delivery, VOOC и т.д.) перед подачей напряжения «общаются» с устройством через линии CC.
Эти два маленьких резистора «говорят» зарядке: «Я обычное 5-вольтовое устройство, давай стандартное напряжение». Без них датчик не включится от большинства современных блоков питания, только от старых USB-портов (компьютер, старая зарядка без протоколов). Я добавил их, чтобы пользователь мог подключить датчик в любую зарядку и он гарантированно заработал.

Корпус датчика
Корпус для EFEKTA Open PM Monitor я проектировал в SolidWorks.
Дизайном корпуса я наверное немного вдохновлялся датчиками IKEA VINDRIKTNING - знаете, такие белые прямоугольные коробочки для измерения качества воздуха. Мне всегда нравилась их скандинавская эстетика: лаконичные формы, строгие линии, отсутствие лишних деталей. Взяв эту философию за основу, я переработал её под свои задачи.
Спереди устанавливается стрелочный индикатор (вольтметр), он крепится к передней панели, немного выступая над поверхностью корпуса. Там же, на передней панели снизу, сделана специальная прорезь для забора воздуха сенсором PM. Сенсор твердых частиц работает на основе лазерной спектрометрии: он засасывает воздух, прогоняет его через измерительную камеру и выдувает обратно. Если нет нормальной циркуляции, показания будут занижены. Прорезь обеспечивает эту циркуляцию.
Внутри корпуса есть направляющие, на которые устанавливаются плата устройства и сенсор PM. Плата задвигается и фиксируется, сенсор PM задвигается в отсек, сориентированный так, чтобы его воздухозаборник совпал с прорезью на передней панели.
Корпус печатается на FDM принтере без поддержек, это было одним из ключевых требований при проектировании. Я хотел, чтобы любой человек мог распечатать корпус на обычном принтере, не возясь с удалением поддержек. Ориентация на столе передней панелью вниз. Время печати около часа.
OLED дисплей и крепление для ENS160 (Версия 2.0)
С появлением версии 2.0 с опциональным OLED-дисплеем и сенсором летучих органических соединений корпус пришлось дорабатывать.
На верхней поверхности появилось прямоугольное отверстие ровно под размер дисплея 128×64. На задней крышке добавлено крепление под сенсор tVOC (ENS160). Это важно, потому что сенсор должен иметь доступ к воздуху, но при этом не болтаться внутри. Крепление представляет собой две пластиковые направляющие, в которые сенсор вставляется с небольшим усилием и фиксируется. Воздухозаборные отверстия напротив сенсора обеспечивают циркуляцию.


Стикер
Стрелочный индикатор (вольтметр) это сердце устройства с точки зрения эстетики. Обычно вольтметры, которые можно купить на AliExpress идут с универсальной шкалой 0-1V, 0-3V или 0-30V и т.д. Для датчика качества воздуха это не очень подходит, нужна своя градуировка.
Дизайн наклейки мне сделала дочка, сама разместила заказ в Копирке (вроде бы), я со своей стороны просто сделал замеры алюминиевой основы и через пару дней забрал у курьера пакет с наклейками.

Процесс наклейки несложный, но требует аккуратности.
Поддержка в системах умного дома
Первая версия датчика (только PM-сенсор, без ENS160 и OLED) поддерживается в Z2M нативно. Вторая версия (с ENS160 и OLED) пока требует установки внешнего конвертера. Так же датчик нативно поддерживается в Спрут Хабе - российском контроллере умного дома. Для работы датчика ZHA, необходимо добавить квирк (quirk). Есть поддержка в HOMEd - ещё одна отечественная платформа для умного дома. Все конвертеры, квирки, шаблоны, расширения доступны на гитхаб проекта.
Информация по настройке среды разработки, прошивке через CC-Debugger и SmartRF Flash Programmer описана в README.md.
Видео обзор
Видео инструкция по сборке
Приглашаю читателей обсудить это и любые другие устройства, прошивки и прочий софт, работающий с Zigbee, в самое большое русскоязычное сообщество в Телеграм (более 11000 участников) — Вокруг да около Zigbee.
Моя группа в телеграм DIY DEV. Тут можно пообщаться на тему разработки DIY устройств, рассказать о своих проектах, или поделится интересными открытыми проектами, узнать больше информации о других датчиках Efekta. В ближайшее время в группе сделаю розыгрыш этих собранных датчиков.
GitHub: github.com/smartboxchannel/EFEKTA-Open_PM_Monitor
Берите, изучайте, повторяйте, модифицируйте.
Всем чистого воздуха и стабильного Zigbee!
