Больше 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 должен выглядеть так:
Инициализация. Хост отправляет устройству HID-запрос SET_REPORT типа Feature с 8-байтным ключом (в hidraw нулевой байт - номер репорта, дальше ключ)
-
Поток данных. После этого датчик сам начинает слать 8-байтные пакеты через interrupt-эндпоинт, которые читаются обычным
read()При этом формат пакета следующий:
Байт
Назначение
0
Код величины
1-2
Значение (big-endian)
3
Контрольная сумма: (байт 0 + байт 1 + байт 2) mod 256
4
Всегда
0x0D5-7
Не используются
Величины бывают такие:
Код
Величина
Перевод значения
0x50CO2, ppm
как есть
0x42Температура, 1/16 K
°C = значение / 16 - 273,15
Некоторые ревизии датчика измеряют еще относительную влажность и шлют значение с кодом
0x41, но мой конкретный экземпляр так не умеет Расшифровка. Существует две ревизии контроллера датчика, с разной степенью дружелюбия. У ранних прошивок пакеты зашифрованы с использованием ключа: байты переставляются по фиксированной таблице, XOR-ятся с ключом, сдвигаются циклически на 3 бита между соседними байтами, затем из каждого вычитается байт константы
"Htemp99e"с переставленными полубайтами. В новых ревизиях шифрование убрали, и данные приходят открытым текстом
То есть датчик - это обычное HID-устройство с опциональным несложным скремблингом данных и единственным interrupt-эндпоинтом: достаточно один раз отправить ему feature-репорт с ключом, и дальше он сам, без запросов, шлет 8-байтные пакеты с показаниями
Пишем драйвер
Чтобы интегрировать датчик углекислоты в мой умный дом, мне нужен драйвер
В этой статье я не буду делать различий между драйвером как софтом, который интегрируется в операционную систему, и драйвером как адаптером для чтения данных: для поста это неважно и задачу не меняет
Задача драйвера сводится к тому, чтобы устанавливать соединение с железкой, читать пакеты данных, при необходимости расшифровывать, проверять контрольную сумму и по коду величины раскладывать значения по нужным полям
Чтобы сосредоточиться на реализации самого драйвера, я вытащил из шкафа свободную Raspberry Pi и решил попробовать реализовать протокол обмена поверх полноценной ОС. Linux уже поддерживает USB-шину и умеет работать с HID-устройствами, так что останется реализовать только прикладную часть конвертера: читать данные и передавать их дальше
Я планировал использовать версию для малинки только для того, чтобы убедиться в возможности стянуть данные с датчика, так что какой-то экспорт данных дальше мне не был нужен
Работа с данными реализована в 3 этапа:
Поиск устройства.
Драйвер обходит/sys/class/hidraw/*, вdevice/ueventищет строкуHID_ID=0003:000004D9:0000A052и подтягивает USB-путь порта, product и serial-
Открытие сессии.
Найденный/dev/hidrawNоткрывается на чтение-запись и получает feature-репорт через ioctl HIDIOCSFEATURE со стартовым ключомc4 c6 c0 92 40 23 dc 96Новые ревизии датчика шлют данные и без этого рукопожатия, так что ошибки драйвер игнорирует
Чтение кадров.
Последовательно выполняется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, которая позволяет делать что-то похожее, но без операционки и самостоятельно
Процесс работы с датчиком тогда выглядит так:
Поиск устройства. Вместо обхода sysfs регистрируемся клиентом USB Host и ждем события подключения. Когда устройство появляется на шине, открываем его, читаем дескриптор и сверяем VID:PID с
04d9:a052, после чего захватываем HID-интерфейсСтартовый ключ. Вместо
ioctl(HIDIOCSFEATURE)формируем управляющий запрос руками:SET_REPORTкласса HID (bmRequestType = 0x21,bRequest = 0x09,wValue = 0x0300- тип Feature, репорт 0) с теми же 8 байтами ключа в поле данныхЧтение кадров. Вместо
poll()иread()ставим в очередь interrupt-передачу на эндпоинт0x81длиной 8 байт. Когда датчик присылает кадр, срабатывает колбэк: отдаем данные и сразу ставим следующую передачуОтключение. Вместо
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)

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

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

GrakovNe Автор
30.10.2026 08:12там даже даташита толком нет, по крайней мере публично. Все что нарылось - это реверс-инжиниринг на который я ссылаюсь в посте и несколько производных от него
Как-то попробовать повзаимодействовать с девайсом я попробовал без нейронок и с ними, но подход ничем и я забил
Скорее всего контроллер можно прошить, если втереться к нему по SPI или отрыть какой-то хитрый boot-mode по UART, но радиоинтерфейс от этого не появится, а цифры по проводу я снимать и без прошивки умею

NutsUnderline
30.10.2026 08:12Неожиданно снижение CO2 по ночам помогло мне поправить качество сна, после того как я не мог выспаться все лето и собирался идти с этим к доктору
я тут обратил внимание что лучше сплю на квартире где датчик показывает 1500 нежели где датчик показывает 700. К врачу я ходил, узнал много интересного, но, пожалуй, ни слова про CO2
датчик кстати с bluetooth

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

Vindicar
Решал похожую задачу с тем же девайсом этим летом. К сожалению, не смог найти, что означают другие отдаваемые по USB значения - в инете везде упоминаются только температура (код B) и уровень CO2 (код P).
Брокера событий у меня нет, так что я подцепил девайс к минисерверу (он всё равно спрятан в тумбочке), написал скрипт, который скидывает данные в SQLite и натравил на базу графану. Пока что работает.
max9
а подскажите какое железо брали? а то сколько не смотрю на озонах там непонятное
Vindicar
Брал всё в формфакторе десктопный 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 мм, шуршит тихонько (сплю без проблем), и не особо греется на моих задачах. Так что я доволен.
NutsUnderline
это нонче уже прям олдскульно
max9
нене, я про датчик.
Vindicar
А! Так в точности тот же, что и у автора: Даджет KIT MT8057. Если память не подводит, я брал тут.
max9
ага, спасибо!