Приветствую всех читателей Habr. Время от времени я выкладываю в открытый доступ некоторые свои проекты по электронике. Сегодня я хочу представить устройство, которое делал с особым удовольствием - EFEKTA Open PM Monitor.

Это Zigbee-датчик мониторинга твердых частиц (PM1.0, PM2.5, PM10). В версии 2.0 в проект добавился сенсор ENS160 для измерения TVOC, eCO2 и AQI, а также небольшой дублирующий OLED-дисплей 128×64.

Но главная его «фишка» - аналоговый стрелочный индикатор.

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

Цифры вы и так увидите в Zigbee2MQTT, в Home Assistant, на OLED-дисплее. А стрелка - это душа устройства. Я делал этот проект в первую очередь для себя, потому что мне самому захотелось иметь на столе не просто бездушный датчик (начало).

EFEKTA Open PM Monitor
EFEKTA Open PM Monitor

Что умеет эта коробочка?

Измерения:

  • 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

Идентификация устройства (Identify)
Идентификация устройства (Identify)

Для разработчиков:

  • Полностью открытый исходный код

Теперь к деталям

Раз проект с открытым исходным кодом, я приглашаю вас заглянуть «под капот».

В основе всего лежит 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). Это важно, потому что сенсор должен иметь доступ к воздуху, но при этом не болтаться внутри. Крепление представляет собой две пластиковые направляющие, в которые сенсор вставляется с небольшим усилием и фиксируется. Воздухозаборные отверстия напротив сенсора обеспечивают циркуляцию.

версия 2.0
версия 2.0

Стикер

Стрелочный индикатор (вольтметр) это сердце устройства с точки зрения эстетики. Обычно вольтметры, которые можно купить на 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!