Поскольку я родом из крупного сибирского промышленного города, тема качества воздуха меня беспокоит довольно сильно. Я видел статистику онкобольных и корреляцию с показателями экологического надзора, и решил, что лучше обкладываться датчиками 80 лет, чем прожить 30.
Поэтому, даже переехав в более чистый город, я по привычке (и, надо сказать, не зря) отслеживаю температуру, влажность, мелкодисперсные частицы pm2.5 и содержание CO2, чтобы понимать, когда я просто не хочу работать, а когда - в этом виноват воздух в комнате.
А поскольку, одно из моих хобби - это мелкоэлектронные самоделки, собрать что-то свое для отслеживания среды, в которой я живу, мне хотелось давно. Поэтому, сегодня я покажу, как собрать простой и дешевый датчик летучей органики, который даст понять, если ваша мебель вас медленно отравляет, а проветривать комнату в час пик - не лучшая идея.
Почему важно отслеживать VOC
VOC - это летучая органика. Строго говоря, это вообще любая органика, давление насыщенного пара которой в нормальных условиях достаточно чтобы содержаться в воздухе.
И, несмотря, на то, что технически, апельсиновое эфирное масло - это вполне себе летучая органика, обычно под VOC - понимают нечто вредное. Например, формальдегид, который еще недавно можно было прикупить себе домой в комплекте с дешевой мебелью. В малых дозах он никак не определяется организмом, зато отлично раздражает слизистые, приводит к головной боли, усталости и злокачественным опухолям. Или, например, бензол, который в избытке можно встретить около любой автомагистрали, и который приводит к примерно таким же неприятным последствиям.
![](https://habrastorage.org/getpro/habr/upload_files/4a0/c4d/e22/4a0c4de229f790f34a7e0f10f917ed84.jpg)
И, если вы тоже живете в крупном городе с окнами видом на широкий проспект, то новости не самые приятные.
Каждый раз, когда вы проветриваете комнаты, вместе с кислородом к вам домой может попадать добрый десяток сложных соединений, которые не отслеживаются ни популярными нынче датчиками PM2.5, ни менее привычными CO2 сенсорами.
Изобретаем сенсор
Давайте возьмем кремниевый кристалл и изолируем его плоскую поверхность диоксидом кремния. Сверху разместим нагреватель и термодатчик, а на него - газочувствительный слой из олова или любого другого металлооксидного проводника.
Теперь нагреем датчик до некоторой температуры, чтобы разогретые соединения летучей органики из воздуха оседали на пленке и изменяли ее теплопроводность.
К газочувствительной пленке подключим АЦП и получим почти прямую зависимость напряжения от количества летучей органики в воздухе.
![](https://habrastorage.org/getpro/habr/upload_files/130/536/a60/130536a600ed26ce1a22a2639d4a11fd.jpg)
Все это я, конечно же, не изобретал. Все уже изобретено и запатентовано. Сенсоры, использующие в своей работе MOX принцип очень распространены, и, как раз такой вполне подойдет чтобы отслеживать VOC в доме.
Собираем прототип
Чтобы построить наш датчик, определимся с сенсором, который он будет использовать.
После поиска на алиэкспрессе и в магазинах электроники Питера, я остановился на CCS811:
![](https://habrastorage.org/getpro/habr/upload_files/739/b26/35e/739b2635ebd8cd8a6683b9c0e44a2e52.jpeg)
Судя по описанию - это то, что нам нужно. Он умеет измерять VOC в PPM, причем реагирует на большое количество соединений и отдает уже посчитанные данные по I2C шине, а стоит от пятиста рублей у китайцев до двух тысяч в рознице.
Давайте соберем что-нибудь на ардуинке и, на самом ли деле сенсор так хорош.
![](https://habrastorage.org/getpro/habr/upload_files/be3/8d2/493/be38d249316c397ddca03c1d234f57f9.jpeg)
Надо сказать, прототип получился в лучших традициях: с кучей соплей, навесного монтажа, быстрых правок прошивки уже после заливания всей схемы термоклеем и плохой 3D-печати. Но свою задачу он худо-бедно выполнил, и показал что датчик действительно хороший.
Он прекрасно реагирует на растворители вроде спиртов, чуть хуже - на толуол и краску и почти не реагирует на изобутан. Что главное - он реагирует на открытое окно в часы пик, то есть подходит чтобы следить за VOC значениями, которые прилетают с улицы.
Не без нюансов
Термостабилизация
Самая главная проблема датчика содержится в принципе его работы. Чтобы разогревать летучую органику, он греется сам. А, поскольку термодатчика в нем нет, его температура перегрева всегда примерно одинакова и без какой-то термостабилизации сенсор начинает показывать скорее свой температурный дрейф, чем что-то полезное.
К счастью для нас, датчик поддерживает внешнюю стабилизацию по температуре и влажности. Он может принимать данные о температуре и влажности, причем в готовом виде, по той же I2C шине. Так что ситуация решается любым дешевым термометром.
![](https://habrastorage.org/getpro/habr/upload_files/0a5/957/a2e/0a5957a2e7a46d976eebc55e7a739b5b.jpg)
DHT11. А почему бы и нет? Нам здесь не нужна какая-то большая точность или стабильность показаний, нам нужно только получать температуру до градуса и влажность до нескольких процентов, чтобы передавать их в газоанализатор. При этом датчик можно купить где угодно и стоит он от 50 до 200 рублей.
Энергопотребление
Вторая проблема датчика - это его чувствительность к питанию, которая тоже вытекает из принципа его работы. В режиме активного измерения значений, он потребляет до 30 мА при напряжении 3.3 вольта. При этом, даже если загнать его в медленный режим, пиковые потребления не только никуда не деваются, а становятся намного более выраженными.
А если вспомнить, что никакого термометра внутри сенсора нет, придется смириться с отсутствием обратной связи по нагреву. То есть, если датчик не смог себя догреть - показания будут ниже, чем нужно. Если перегрел - выше.
В прототипе, от одного и того же стабилизатора 3.3 вольта ардуинки у меня был запитан еще и маленький OLED - экран, и этого уже хватало, чтобы показания были тем ниже, чем больше пикселей экрана светилось.
Поэтому в готовом датчике весь стабилизатор будет отдан датчику, а вместо экрана будем выводить данные в последовательный порт и почитывать их моим умным домом.
Плохо работает в корпусе
Пожалуй, стоило по-другому разместить сенсор в корпусе, потому что отверстий в задней стенке корпуса, которых было достаточно чтобы поддерживать примерно одинаковые температуру и влажность в корпусе и снаружи, для VOC сенсора уже не хватило.
Чтобы сенсор адекватно изменял свои показания и делал это достаточно быстро, корпус для него должен хорошо проветриваться, а сам сенсор не должен быть перекрыт монтажом или креплениями.
Рассчитанные значения CO2 никуда не годятся
Датчик, казалось бы, умеет репортить не только показатели VOC, но и мгновенные значения CO2 в воздухе. Особенность в том, что VOC он действительно измеряет, а уровень углекислоты - рассчитывает по своим формулам, поскольку корреляция между этим показателями действительно есть
В действительности же, это работает крайне плохо: значениям eCO2 можно доверять только при околонулевых значениях летучей органики и нормальных условиях в отсутствии открытых окон и ветра.
Собираем датчик
Итак, сам сенсор работает, если его правильно приготовить.
Пусть новое устройство будет максимально тонким бекендом и просто отдает сырые результаты измерений, которые будут обрабатываться отдельно.
![](https://habrastorage.org/getpro/habr/upload_files/682/70a/41e/68270a41e91bd0f27207b3be3e26126f.png)
Схема тоже простая и максимально дешевая. Ардуинку, если что, можно заменить на практически любой контроллер с одним-единственным IO портом и реализованной I2C шиной. Я ее выбрал здесь скорее потому, что на ней распаян неплохой понижающий стабилизатор на 3.3 вольта и ее можно прошивать через usb.
Прошивка железки получилась максимально простая и почти никак не использует периферии контроллера. В основном цикле с программной задержкой просто крутится последовательное чтение данных с датчиков и корректировка газоанализатора.
void loop() {
read_ccs_sensor();
read_dht_sensor();
set_ccs_sensor_environment_data();
report_state();
delay(MEASUREMENT_TIMEOUT);
}
Ссылки на репозиторий с кодом я оставлю ниже, а пока можно собрать весь девайс и посмотреть, как он работает.
![](https://habrastorage.org/getpro/habr/upload_files/ab7/8bf/960/ab78bf960fcca4489b78da2e8678988c.jpg)
Корпус я тоже перемоделировал: вытравить печатную плату мне сейчас нечем, а ждать, пока это сделает JLPCB я не хочу. Поэтому я разместил ардуинку в воздухе на ножках корпуса, а датчики уложил в кроватки и прихватил термоклеем.
Простенький баш-скрипт позволит нам почитать данные и убедиться, что все работает.
![](https://habrastorage.org/getpro/habr/upload_files/0bd/78a/d88/0bd78ad8819c1cea248d0a7176e10546.png)
Из температуры и влажности, кстати, становится понятно, насколько сильно греется газоанализатор: температура в комнате как минимум на пару градусов ниже, а влажность весной в Петербурге редко опускается ниже 40% даже в квартирах.
После нескольких падений стало понятно, что ничего никуда не отвалится, так что можно накрывать железки крышкой, не забыв сделать в ней хорошую вентиляцию.
![](https://habrastorage.org/getpro/habr/upload_files/339/6b5/b21/3396b5b2139bc3149c3b9847d7f3c8fa.jpeg)
Такие отверстия у девайса по всем вертикальным граням, кроме фронтальной, а сзади - их вдвое больше. По идее, это, вместе с компоновкой деталей, должно дать возможность воздуху проходить через корпус максимально свободно, чтобы датчик мог показывать что-то вменяемое.
Запитываем на ардуинке RESET через резистор подтяжки и склеиваем половинки корпуса.
![](https://habrastorage.org/getpro/habr/upload_files/530/3e3/586/5303e35866337f8abd5938321fd61dce.jpg)
Пишем клиент
Практически все датчики в доме у меня репортят свои данные в Home Assistant через MQTT шину. Этот не должен стать исключением.
Выходит, что здесь нужен простенький клиент, который будет читать данные из последовательного порта, парсить строчку от датчика и паблишить значения в топики MQTT.
#!/usr/bin/python
import serial
import time
import os
ser = serial.Serial(
port='/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0',
baudrate = 9600,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=10
)
print "Starting read from organic sensor..."
while 1:
try:
data = ser.readline().split(';')
for item in data:
name = item.split(': ')[0].strip()
data = item.split(': ')[1].strip()
print name + "=" + data
os.system('mosquitto_pub -d -t organic/' + name + ' -m "' + data + '"')
except:
pass
time.sleep(1)
Для начала подойдет. Теперь подождем какое-то время и посмотрим, что происходит с воздухом в квартире.
![](https://habrastorage.org/getpro/habr/upload_files/cb5/fdc/e9f/cb5fdce9f4aa759bfa51a8f67b11c438.png)
И есть сразу две плохие новости. Во-первых, не стоило проветривать квартиру под вечер, а во-вторых, клиент и график никуда не годятся.
Датчик, пусть и термостабилизированный, выдает мгновенные значения, превращая график в шум, из которого можно оценить, в лучшем, случае, порядок значений.
Кроме того, скрипт на Python не умеет переподключаться к датчику, если его отключить, а делать это поначалу хотелось часто.
Пишем нормальный клиент
Наш новый клиент должен делать две вещи: правильно усреднять значения датчика и быть максимально автономным. То есть, если я вдруг выдерну девайс из порта и отключу MQTT сервер, а потом верну как было, клиент должен продолжить читать данные
Раз уж так получилось, что в последнее время, я использую Scala-стек, то и клиент для датчика будет на нем и классических акторах, которые я давно хотел попробовать
В качестве алгоритма усреднения данных, после пары экспериментов, я выбрал расчет скользящего среднего по последним N измерениям. Это дает возможность быстро видеть изменения показаний, сглаживать график и практически исключать влияние выбросов, хотя и не является робастной.
class MeasurementQueue(limit: Int) extends mutable.Queue[Measurement] {
def addMeasurementAndGetMeanValue(element: Measurement): Measurement = {
super.enqueue(element)
if (super.size > limit) {
super.dequeue()
}
meanValue
}
private def meanValue(extractor: (Measurement => Double)): Double = {
val rawMeanValue = this.map(extractor).sum / (if (this.isEmpty) 1 else this.size)
BigDecimal(rawMeanValue).setScale(2, RoundingMode.HALF_UP).doubleValue
}
private def meanValue: Measurement = Measurement(
tvoc = meanValue(_.tvoc),
carbonDioxide = meanValue(_.carbonDioxide),
temperature = meanValue(_.temperature),
humidity = meanValue(_.humidity)
)
}
object MeasurementQueue {
def apply(limit: Int): MeasurementQueue = new MeasurementQueue(limit)
}
Для того, чтобы клиент был максимально жизнеспособным, научим его переподключаться к порту и очереди, а все что можно - завернем в особоустойчивый ретрай.
class InfinitiveRetry {
private val log: Logger = LoggerFactory.getLogger(this.getClass)
private val secondInMills = 1000
def retry[T](
action: () => Option[T],
failedMessage: Option[String] = Option.empty,
timeoutMs: Int = secondInMills): T = Try(action.apply()) match {
case Success(Some(value)) => value
case Success(None) => onFailed(action, failedMessage, timeoutMs)
case Failure(ex) => onFailed(action, failedMessage.map(str => s"$str caught exception: $ex"), timeoutMs)
}
private def onFailed[T](action: () => Option[T], failedMessage: Option[String], timeoutMs: Int): T = {
failedMessage.foreach(log.warn)
Thread.sleep(timeoutMs) // todo: fix me?
retry(action, failedMessage, timeoutMs)
}
}
object InfinitiveRetry {
def apply(): InfinitiveRetry = new InfinitiveRetry()
}
Клиент можно собрать в jar - пакет, завернуть в systemcml и развернуть на домашнем сервере, правильно настроив ему конфиг.
Время взглянуть на графики
Подождем еще пару дней чтобы набрать новых значений и посмотрим, во что превратился наш график после усреднения значений.
![](https://habrastorage.org/getpro/habr/upload_files/f3e/7b1/68b/f3e7b168b979909f90db3b838c3a84c6.png)
Здесь отлично видно, что в половину третьего ночи датчик отреагировал на клей для 3D принтера, который стоит в нескольких метрах, а к утру - на просыпающийся город.
![](https://habrastorage.org/getpro/habr/upload_files/56d/a3f/786/56da3f7865afe11cb99ff26f55a89d81.png)
Реакция на клей для принтера, а если точнее, на растворитель в нем, более детально. При этом, производитель на голубом глазу гарантирует безопасность. Пожалуй, перейду на карандашный клей.
![](https://habrastorage.org/getpro/habr/upload_files/d3b/4d9/80c/d3b4d980c18decead65efe92dc985a77.png)
И, наверное, начну открывать окна перед тем как открывать банку с изопропиловым спиртом. Впрочем, датчик говорит, что выветривается он так же быстро, как и появляется.
![](https://habrastorage.org/getpro/habr/upload_files/f4e/137/002/f4e13700284a292ed927db9ed67d0bed.png)
А это коротко о безопасности домашней 3D печати. Если на PLA, PETS и SBS датчик не отреагировал практически никак, то от попытки попечатать ABS без принудительной вентиляции количество органики выросло до совсем уж нездоровых значений.
![](https://habrastorage.org/getpro/habr/upload_files/c20/562/371/c205623715e2bdbbe01abe29880cc1d8.png)
Проветривание здорового человека. Отлично видно, как упало значение VOC. Примерно так же отреагировал и отдельный датчик углекислоты, который стоит у меня уже давно.
![](https://habrastorage.org/getpro/habr/upload_files/480/1be/7fb/4801be7fb0bd7ff0d9aa33b0df15057f.png)
А на этом графике можно наблюдать, как в городе изменился ветер и дунул с залива, да так, что чуть не унес меня, вместе со всей остальной органикой.
Проверяем правильность показаний
Для начала, убедимся, что наш датчик действительно термостабилизированный. Поскольку, я отслеживаю температуру во всех комнатах, несложно завести еще один виджет в Home Assistant, на котором поискать зависимости графиков температуры и VOC.
![](https://habrastorage.org/getpro/habr/upload_files/460/e71/ae8/460e71ae85fd0aeccdf5de47aad589f6.png)
При примерно одной и той же температуре, значение VOC выросло почти вдвое. Впрочем, это как раз ничего не доказывает: температурного дрейфа тут и быть не могло. Давайте поищем что-нибудь более явное.
![](https://habrastorage.org/getpro/habr/upload_files/b94/6c8/572/b946c85722374fda5ef9a63a1786ba0e.jpg)
К слову сказать, такой график можно видеть практически каждый вечер: выхлопных газов больше, солнечного тепла меньше. Грустно, но температурного дрейфа здесь тоже нет. Поищем что-нибудь еще.
![](https://habrastorage.org/getpro/habr/upload_files/df2/243/1c5/df22431c5dbd62dad05ddf30a71fb113.jpg)
А здесь наоборот, типичная картина для середины дня: транспорта вокруг меньше, а солнце как раз входит в свой зенит.
Калибровка датчика
Поскольку датчик ничего не знает о референсных значениях, его автоматическая ежедневная калибровка состоит в обновлении бейзлайна по нижнему значению.
Это подходит для большинства сценариев домашнего использования, но, поскольку, калибровочное значение сбрасывается после перезагрузки, иногда можно видеть такую картину.
![](https://habrastorage.org/getpro/habr/upload_files/95c/cd7/5e4/95ccd75e4ed97d6bd2239953fb872735.png)
Здесь произошло сразу две вещи: сброс бейзлайна и остывание датчика, которому потребовалось какое-то время на то, чтобы снова разогреться до рабочей температуры.
Начинать верить показаниям сенсора, если ориентироваться на сравнении расчитанного им CO2 и точно известным значением от другого датчика, можно через несколько часов непрерывной работы, а на то, чтобы откалиброваться полностью, датчику требуется чуть больше суток.
Для меня, у которого этот датчик работает всегда, это совершенно нормально, но в прошивке несложно добавить ручную калибровку, подобно тому, как это сделано для термостабилизации.
![](https://habrastorage.org/getpro/habr/upload_files/07b/188/276/07b188276ffc17e6707b981137141e53.png)
После того, как датчик самостоятельно откалибровался, его показания не будут меняться без изменения окружающей среды.
Стоимость
Я обещал хороший датчик VOC по цене двух чашек кофе. В его качестве можно не сомневаться после всех графиков выше, так что теперь посчитаем стоимость компонентов:
Сенсор органики CCS811 - 593 рубля: https://aliexpress.ru/item/1005001543724637.html
Сенсор температуры и влажности DHT11 - 54 рубля: https://aliexpress.ru/item/32769460765.html
Arduino Nano - 155 рублей: https://aliexpress.ru/item/32341832857.html
12 грамм пластика, которые потребуются для корпуса - чуть больше 10 рублей
Прошивка и клиент - бесценны под MIT лицензией.
Итого вышло 812 рублей, что чуть-чуть дешевле, чем два стакана Декаф Ванильный Латте Миндальное Венти из старбакса по цене 420 рублей за чашку. На оставшиеся деньги можно как раз купить МГТФ кабель и подтягивающий резистор для датчика влажности.
За эти деньги можно, чутка поработав паяльником, получить хороший рабочий датчик летучей органики, который будет привлекать внимание не только к новой мебели, но и к тому, из чего она сделана.
А еще, хобби - это увлекательно.
Исходный код
Прошивка для Arduino Nano, принципиальная схема и STL модели корпуса
Почему в прошивке нет .ino файлов
Я действительно написал прошивку не в Arduino IDE, а в Platform.IO, которая позволяет получить все то же самое, но использует другой компилятор и может интегрироваться в Clion или VsCode. Но код, который получился в итоге, отлично может быть скомпилирован и из Arduino IDE, куда его придется перенести.
Ссылки
Здесь подробно описано, как работает DTR сигнал в Ардуинках и зачем подтягивать RESET к питанию