Больше 10 лет назад я купил датчик углекислого газа, чтобы отслеживать состояние воздуха в спальне

Чтобы начать действительно им пользоваться, мне пришлось собрать еще одну железку, которая подключает его к умному дому и собирает показания в красивый график

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

Разбираем датчик

В центре внимания вот эта железка

В России она была популярна в первой половине десятых, и у меня лежит с тех времен

Но на маркетплейсах устройство все еще можно без проблем купить за 40-50 евро, и это один из лучших датчиков для измерения углекислоты в сегменте

Внутри этой коробки полноценный NDIR-сенсор, который считает именно CO2, а не коррелирующую с ним летучую органику

Сам принцип измерения нехитрый и строится на оптической непрозрачности углекислого газа для волны длиной 4,26 мкм

Внутри датчика находится трубка, у которой с одной стороны - ИК-излучатель, а с другой - интерференционный фильтр и фотоприемник

Воздух попадает в трубку, часть света поглощается молекулами углекислоты, а оставшиеся фотоны попадают на приемник, где контроллер пересчитывает излучение в конкретное значение CO2 с поправкой на температуру

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

Так, например, стандартный китайский «датчик чистоты воздуха для дома и офиса» обычно считает концентрацию летучей органики TVOC и, делая допущение, что где органика - там и выдыхаемая углекислота, по хитрой формуле выводит примерные цифры

Я пробовал сравнивать показания, и в бытовых условиях обычной квартиры они не сильно отличаются от результатов прямого измерения: то тут, то там есть расхождения, которые в общем и целом на картину не влияют

Но если условия станут менее обычными, проку от такого датчика будет немного, а я держу свой датчик именно для отслеживания проблемных моментов

Поэтому вместо того, чтобы пойти и купить в продуктовой лавке дешевый TVOC, который притворяется датчиком углекислоты, эти же деньги я потрачу на переходник с уже работающего устройства на современный мир

Читаем данные

Сам по себе датчик может работать автономно. У него есть дисплей, который отображает текущие значения температуры и углекислоты, три светодиода для оценки состояния «хорошо/плохо», звуковая индикация «совсем плохо», а micro-USB-разъем используется для питания железки

Тем не менее, если подключить девайс к USB-шине и опросить его, то выяснится, что он - обычное HID-устройство, которое идентифицируется операционкой

$ lsusb
...
Bus 001 Device 005: ID 04d9:a052 Holtek Semiconductor, Inc. USB-zyTemp

Я сделал поверхностный гуглеж железки по ее VID и PID и быстро нашел оригинальное устройство, которое до брендирования российским продавцом называлось ZyTemp CO2 Monitor и выпускалось компанией ZyAura

Если поискать чуть более глубоко, то выяснится, что его протокол обмена данными нетривиален, но хорошо изучен и описан больше 10 лет назад

Документ выше обещает, что процесс работы с датчиком по USB должен выглядеть так:

  1. Инициализация. Хост отправляет устройству HID-запрос SET_REPORT типа Feature с 8-байтным ключом (в hidraw нулевой байт - номер репорта, дальше ключ)

  2. Поток данных. После этого датчик сам начинает слать 8-байтные пакеты через interrupt-эндпоинт, которые читаются обычным read()

    При этом формат пакета следующий:

    Байт

    Назначение

    0

    Код величины

    1-2

    Значение (big-endian)

    3

    Контрольная сумма: (байт 0 + байт 1 + байт 2) mod 256

    4

    Всегда 0x0D

    5-7

    Не используются

    Величины бывают такие:

    Код

    Величина

    Перевод значения

    0x50

    CO2, ppm

    как есть

    0x42

    Температура, 1/16 K

    °C = значение / 16 - 273,15

    Некоторые ревизии датчика измеряют еще относительную влажность и шлют значение с кодом 0x41, но мой конкретный экземпляр так не умеет

  3. Расшифровка. Существует две ревизии контроллера датчика, с разной степенью дружелюбия. У ранних прошивок пакеты зашифрованы с использованием ключа: байты переставляются по фиксированной таблице, XOR-ятся с ключом, сдвигаются циклически на 3 бита между соседними байтами, затем из каждого вычитается байт константы "Htemp99e" с переставленными полубайтами. В новых ревизиях шифрование убрали, и данные приходят открытым текстом

То есть датчик - это обычное HID-устройство с опциональным несложным скремблингом данных и единственным interrupt-эндпоинтом: достаточно один раз отправить ему feature-репорт с ключом, и дальше он сам, без запросов, шлет 8-байтные пакеты с показаниями

Пишем драйвер

Чтобы интегрировать датчик углекислоты в мой умный дом, мне нужен драйвер

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

Задача драйвера сводится к тому, чтобы устанавливать соединение с железкой, читать пакеты данных, при необходимости расшифровывать, проверять контрольную сумму и по коду величины раскладывать значения по нужным полям

Чтобы сосредоточиться на реализации самого драйвера, я вытащил из шкафа свободную Raspberry Pi и решил попробовать реализовать протокол обмена поверх полноценной ОС. Linux уже поддерживает USB-шину и умеет работать с HID-устройствами, так что останется реализовать только прикладную часть конвертера: читать данные и передавать их дальше

Я планировал использовать версию для малинки только для того, чтобы убедиться в возможности стянуть данные с датчика, так что какой-то экспорт данных дальше мне не был нужен

Работа с данными реализована в 3 этапа:

  1. Поиск устройства.
    Драйвер обходит /sys/class/hidraw/*, в device/uevent ищет строку HID_ID=0003:000004D9:0000A052 и подтягивает USB-путь порта, product и serial

  2. Открытие сессии.
    Найденный /dev/hidrawN открывается на чтение-запись и получает feature-репорт через ioctl HIDIOCSFEATURE со стартовым ключом c4 c6 c0 92 40 23 dc 96

    Новые ревизии датчика шлют данные и без этого рукопожатия, так что ошибки драйвер игнорирует

  3. Чтение кадров.
    Последовательно выполняется poll() с таймаутом, а затем read(), который принимает 8 байт. Кадр сначала расшифровывается алгоритмом выше, и если у результата сходится контрольная сумма, берется он. Иначе проверяется исходный кадр как открытый текст
    Далее из результата чтения вытягиваются данные и код величины, которые нам и нужны

В рабочем режиме драйвер публикует данные пользователю раз в 5 секунд, а при потере связи с датчиком заново проходит этапы 1-2 и переподключается к тому же USB-порту, постепенно увеличивая паузу между попытками

Я отправил нейронку его писать и тестить на уже подключенной железке, после чего немного прибрался в кодовой базе и запустил на Raspberry

Переносим драйвер на ESP32

В виде прототипа устройство работает. Малинка интегрирована с датчиком, умеет находить устройство на шине и стабильно шлет данные в консоль

Но я не хочу гонять целую Raspberry Pi как транспорт двух чисел между USB-шиной и умным домом

Моя оригинальная идея была в том, чтобы получить отдельное устройство на каком-нибудь микроконтроллере, достаточно мощное, чтобы подключаться к домашней Wi-Fi-сети, но при этом в несколько раз меньше и без пингвина на борту

Требований к контроллеру, на котором будет работать финальная версия драйвера, немного

Он должен иметь достаточно памяти, поддерживать работу с Wi-Fi и реализовывать полноценный режим хоста на USB-шине, чтобы иметь возможность работать с датчиком

Под все эти задачи с большим запасом подходит отладочная плата ESP32-S3-DevKitC-1 N16R8

На ней распаяно целых два порта Type-C: один - настоящий USB, разведенный от чипа по GPIO19/GPIO20, второй - USB-UART-преобразователь на CH9102

Для меня это невероятно удобная конфигурация: я могу подключить датчик углекислоты по USB-шине и прошивать чип или читать с него данные по последовательному порту без необходимости переключать периферию или зависеть от OTA-обновлений

Часть драйвера, написанного для Raspberry, можно перенести почти без изменений: сам конвертер работает с уже полученными кадрами и поэтому не зависит от платформы

Все остальное придется написать заново, под новую платформу, причем на уровень ближе к железке

Linux предоставлял готовую инфраструктуру: ядро само опознавало устройство при подключении и позволяло писать и читать его как обычный файл

Для ESP32 есть библиотека USB Host, которая позволяет делать что-то похожее, но без операционки и самостоятельно

Процесс работы с датчиком тогда выглядит так:

  1. Поиск устройства. Вместо обхода sysfs регистрируемся клиентом USB Host и ждем события подключения. Когда устройство появляется на шине, открываем его, читаем дескриптор и сверяем VID:PID с 04d9:a052, после чего захватываем HID-интерфейс

  2. Стартовый ключ. Вместо ioctl(HIDIOCSFEATURE) формируем управляющий запрос руками: SET_REPORT класса HID (bmRequestType = 0x21, bRequest = 0x09, wValue = 0x0300 - тип Feature, репорт 0) с теми же 8 байтами ключа в поле данных

  3. Чтение кадров. Вместо poll() и read() ставим в очередь interrupt-передачу на эндпоинт 0x81 длиной 8 байт. Когда датчик присылает кадр, срабатывает колбэк: отдаем данные и сразу ставим следующую передачу

  4. Отключение. Вместо POLLHUP приходит событие отключения устройства: освобождаем интерфейс, закрываем устройство и снова ждем подключения

Кроме этого, ESP должна поднимать собственный веб-сервер, чтобы отдавать данные не только по MQTT, через него же принимать конфигурацию поведения и настроек сети и делать всякие полезные мелочи вроде auto discovery, чтобы адаптер был максимально легким в настройке

Если версию драйвера для Linux я писал фронтирной Fable 5 еще летом, то адаптировать код под ESP32 я предложил локальному Qwen 3.8 Flash Next в Q5-кванте, который крутится на домашнем Ryzen AI Max+ 395

В обоих случаях я отдал написание кода только что вышедшим LLM, чтобы потестить их на реальных задачах, а после сделал ревью и вычитал код, чтобы иметь возможность гарантировать хоть какой-то результат

Локальная нейронка смогла адаптировать весь код и дописать часть с UI, но не справилась с получением сырых кадров от датчика

ZyTemp инициализировался, на шине USB появлялись флаги готовности, но кадры на хост передавать никто не собирался

В процессе отладки я выяснил, что датчик углекислоты не отдает данные автоматически при подключении. Вместо этого он ждет, пока хост прочитает у него report descriptor. Linux-ядро делает это автоматически, а у нейросети не было даташита на датчик, так что ей неоткуда было взять ожидаемое поведение

Зачем датчику дожидаться запроса дескриптора, я не понимаю, тем более что сам дескриптор ничего полезного не сообщает. Он описывает один безымянный vendor-defined репорт на 8 байт, а формат кадров и шифрование в нем никак не отражены

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

После этого я предложил фронтирному Opus 5 построить мне релиз и дописать простенький web-flasher, чутка вручную поправил дизайн веб-странички и отправил релизную сборку прошивки на чип

Собираем железку

Поскольку ZyTemp, судя по разъему micro-USB, рассчитан на работу от сети, пытаться сделать ESP32-прокладку достаточно энергоэффективной, чтобы питать ее от батарейки, я не стал, а без требований к энергоэффективности мне ничего не мешало продолжать использовать отладочную плату в готовом устройстве

Тем более что текущая конфигурация позволяла мне вообще не решать задачу о сборке девайса: на шину настоящего USB я подвесил датчик углекислоты, а ESP32 запитал через второй разъем Type-C, который разведен как USB-UART

В итоге датчик оказался запитан транзитно, через адаптер. Я пару дней погонял эту сборку в спальне, убедился в ее стабильности и наскоро спроектировал какой-то корпус под 3D-печать

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

Остается только воткнуть два USB-кабеля: к питанию и датчику, зарегать железку в домашней сети и разрешить Home Assistant импортировать MQTT-автоконфигурацию

У меня получилось продолжить использовать свой датчик CO2 и интегрировать его в умный дом по цене отладочной платы на ESP32 - вместо того чтобы искать новый сенсор и выяснять, точно ли он измеряет именно углекислоту в воздухе, а не коррелирующие значения

Я снова, после большого перерыва, начал смотреть на качество воздуха дома и проветривать от показателей датчиков, а не от интуиции

Неожиданно снижение CO2 по ночам помогло мне поправить качество сна, после того как я не мог выспаться все лето и собирался идти с этим к доктору

Адаптер практически не занимает места на прикроватной тумбе, и я могу таскать его с собой при переездах так же, как таскаю сам датчик углекислого газа

Послесловие

Большая часть того, что я делаю, находится под свободными лицензиями в открытом доступе

Этот проект - не исключение. Поэтому ниже я оставлю ссылки на репозитории с кодом, STL-файлами и описанием, чтобы вы смогли собрать у себя такой же или похожий конвертер данных и вернуть к жизни полезные, но неудобные в 2026 году железки

https://github.com/GrakovNe/holtek-usb-zy-temp-driver - вариант драйвера для Linux. Там есть скрипт, который позволит автоматически установить все зависимости и сам драйвер. Сверх этого останется только обеспечить транспорт значений в удобное место

https://github.com/GrakovNe/esp_32_holtek-usb-zy-temp-driver - вариант для ESP32. Прошивка адаптирована практически под любой чип ESP32-S3, который имеет достаточно памяти, так что искать конкретно мой вариант не нужно. Кроме кода, там же лежит моделька корпуса

Нейропослесловие

Это мой первый проект, где я попробовал использовать агентов, в том числе локальных, для разработки физических устройств

В целом и фронтирные, и локальные модели лажают там, где не хватает ясности, или на редких задачах вроде инициализации штучного HID-устройства на USB-шине конкретного чипа

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

Я искренне считаю, что все, что может быть упрощено автоматизацией, должно быть упрощено автоматизацией, причем максимально удобным и простым, но не примитивным способом

Макс Граков, Санкт-Петербург, 2026

Комментарии (14)


  1. Vindicar
    30.10.2026 08:12

    Решал похожую задачу с тем же девайсом этим летом. К сожалению, не смог найти, что означают другие отдаваемые по USB значения - в инете везде упоминаются только температура (код B) и уровень CO2 (код P).

    Брокера событий у меня нет, так что я подцепил девайс к минисерверу (он всё равно спрятан в тумбочке), написал скрипт, который скидывает данные в SQLite и натравил на базу графану. Пока что работает.


    1. max9
      30.10.2026 08:12

      а подскажите какое железо брали? а то сколько не смотрю на озонах там непонятное


      1. Vindicar
        30.10.2026 08:12

        Брал всё в формфакторе десктопный miniITX.
        Материнка ASRock H610M-ITX/eDP - намучался, пока не понял, что ей обязательно нужен монитор для загрузки (пришлось брать заглушку-фейк).
        Оперативка DDR4 UDIMM ADATA Premier (16 гигов).
        Проц Intel Core i3-12100 OEM, дискретной видяхи нет, но мне не под нейронки он нужен.
        Запитано от Accord ACC-TFX450
        Засунул всё в корпус ExeGate FL-102, но возможно, корпус был ошибкой. Пришлось снять морду, чтобы он влез в ящик (плюс место для проводов и пары 120 мм кулеров сзади). Также пришлось просверлить пару отверстий для крепления полноценного HDD, так как корпус рассчитан на 2.5".

        Получился тот ещё колхоз, но в итоге всё влезло в габарит примерно 320х300х90 мм, шуршит тихонько (сплю без проблем), и не особо греется на моих задачах. Так что я доволен.


        1. NutsUnderline
          30.10.2026 08:12

          это нонче уже прям олдскульно


        1. max9
          30.10.2026 08:12

          нене, я про датчик.


          1. Vindicar
            30.10.2026 08:12

            А! Так в точности тот же, что и у автора: Даджет KIT MT8057. Если память не подводит, я брал тут.


            1. max9
              30.10.2026 08:12

              ага, спасибо!


  1. optim2004
    30.10.2026 08:12

    спасибо за проект - раньше (лет 7 назад) приходилось делать через подключение к шине i2c датчика и последующее. подключение к esp32 через проект esp32home, но проводов больше было и питание отдельное для каждого из устройств

    Ваше решение выглядит лучше и удобнее


  1. NutsUnderline
    30.10.2026 08:12

    а holtek в датчике прошивается? наверное хитрый программатор нужен? есть sdk? умеют ли llm программировать данный контроллер? :)


    1. GrakovNe Автор
      30.10.2026 08:12

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

      Как-то попробовать повзаимодействовать с девайсом я попробовал без нейронок и с ними, но подход ничем и я забил

      Скорее всего контроллер можно прошить, если втереться к нему по SPI или отрыть какой-то хитрый boot-mode по UART, но радиоинтерфейс от этого не появится, а цифры по проводу я снимать и без прошивки умею


  1. NutsUnderline
    30.10.2026 08:12

    Неожиданно снижение CO2 по ночам помогло мне поправить качество сна, после того как я не мог выспаться все лето и собирался идти с этим к доктору

    я тут обратил внимание что лучше сплю на квартире где датчик показывает 1500 нежели где датчик показывает 700. К врачу я ходил, узнал много интересного, но, пожалуй, ни слова про CO2

    датчик кстати с bluetooth


  1. makondo
    30.10.2026 08:12

    Можете показать суточный график изменения CO2 в одном помещении?


    1. GrakovNe Автор
      30.10.2026 08:12

      пожалуйста!


  1. bomitau
    30.10.2026 08:12

    Раньше подключал этот датчик к компьютеру с zabbix. С автоматически регулируемой приточкой было бы круто, а вот когда сам проветриваешь, смысла нет. На светодиод глянуть проще, чем открывать страницу с графиком, а по желанию он ещё и пищать может при превышении.