Поскольку я родом из крупного сибирского промышленного города, тема качества воздуха меня беспокоит довольно сильно. Я видел статистику онкобольных и корреляцию с показателями экологического надзора, и решил, что лучше обкладываться датчиками 80 лет, чем прожить 30.
Поэтому, даже переехав в более чистый город, я по привычке (и, надо сказать, не зря) отслеживаю температуру, влажность, мелкодисперсные частицы pm2.5 и содержание CO2, чтобы понимать, когда я просто не хочу работать, а когда - в этом виноват воздух в комнате.
А поскольку, одно из моих хобби - это мелкоэлектронные самоделки, собрать что-то свое для отслеживания среды, в которой я живу, мне хотелось давно. Поэтому, сегодня я покажу, как собрать простой и дешевый датчик летучей органики, который даст понять, если ваша мебель вас медленно отравляет, а проветривать комнату в час пик - не лучшая идея.
Почему важно отслеживать VOC
VOC - это летучая органика. Строго говоря, это вообще любая органика, давление насыщенного пара которой в нормальных условиях достаточно чтобы содержаться в воздухе.
И, несмотря, на то, что технически, апельсиновое эфирное масло - это вполне себе летучая органика, обычно под VOC - понимают нечто вредное. Например, формальдегид, который еще недавно можно было прикупить себе домой в комплекте с дешевой мебелью. В малых дозах он никак не определяется организмом, зато отлично раздражает слизистые, приводит к головной боли, усталости и злокачественным опухолям. Или, например, бензол, который в избытке можно встретить около любой автомагистрали, и который приводит к примерно таким же неприятным последствиям.
И, если вы тоже живете в крупном городе с окнами видом на широкий проспект, то новости не самые приятные.
Каждый раз, когда вы проветриваете комнаты, вместе с кислородом к вам домой может попадать добрый десяток сложных соединений, которые не отслеживаются ни популярными нынче датчиками PM2.5, ни менее привычными CO2 сенсорами.
Изобретаем сенсор
Давайте возьмем кремниевый кристалл и изолируем его плоскую поверхность диоксидом кремния. Сверху разместим нагреватель и термодатчик, а на него - газочувствительный слой из олова или любого другого металлооксидного проводника.
Теперь нагреем датчик до некоторой температуры, чтобы разогретые соединения летучей органики из воздуха оседали на пленке и изменяли ее теплопроводность.
К газочувствительной пленке подключим АЦП и получим почти прямую зависимость напряжения от количества летучей органики в воздухе.
Все это я, конечно же, не изобретал. Все уже изобретено и запатентовано. Сенсоры, использующие в своей работе MOX принцип очень распространены, и, как раз такой вполне подойдет чтобы отслеживать VOC в доме.
Собираем прототип
Чтобы построить наш датчик, определимся с сенсором, который он будет использовать.
После поиска на алиэкспрессе и в магазинах электроники Питера, я остановился на CCS811:
Судя по описанию - это то, что нам нужно. Он умеет измерять VOC в PPM, причем реагирует на большое количество соединений и отдает уже посчитанные данные по I2C шине, а стоит от пятиста рублей у китайцев до двух тысяч в рознице.
Давайте соберем что-нибудь на ардуинке и, на самом ли деле сенсор так хорош.
Надо сказать, прототип получился в лучших традициях: с кучей соплей, навесного монтажа, быстрых правок прошивки уже после заливания всей схемы термоклеем и плохой 3D-печати. Но свою задачу он худо-бедно выполнил, и показал что датчик действительно хороший.
Он прекрасно реагирует на растворители вроде спиртов, чуть хуже - на толуол и краску и почти не реагирует на изобутан. Что главное - он реагирует на открытое окно в часы пик, то есть подходит чтобы следить за VOC значениями, которые прилетают с улицы.
Не без нюансов
Термостабилизация
Самая главная проблема датчика содержится в принципе его работы. Чтобы разогревать летучую органику, он греется сам. А, поскольку термодатчика в нем нет, его температура перегрева всегда примерно одинакова и без какой-то термостабилизации сенсор начинает показывать скорее свой температурный дрейф, чем что-то полезное.
К счастью для нас, датчик поддерживает внешнюю стабилизацию по температуре и влажности. Он может принимать данные о температуре и влажности, причем в готовом виде, по той же I2C шине. Так что ситуация решается любым дешевым термометром.
DHT11. А почему бы и нет? Нам здесь не нужна какая-то большая точность или стабильность показаний, нам нужно только получать температуру до градуса и влажность до нескольких процентов, чтобы передавать их в газоанализатор. При этом датчик можно купить где угодно и стоит он от 50 до 200 рублей.
Энергопотребление
Вторая проблема датчика - это его чувствительность к питанию, которая тоже вытекает из принципа его работы. В режиме активного измерения значений, он потребляет до 30 мА при напряжении 3.3 вольта. При этом, даже если загнать его в медленный режим, пиковые потребления не только никуда не деваются, а становятся намного более выраженными.
А если вспомнить, что никакого термометра внутри сенсора нет, придется смириться с отсутствием обратной связи по нагреву. То есть, если датчик не смог себя догреть - показания будут ниже, чем нужно. Если перегрел - выше.
В прототипе, от одного и того же стабилизатора 3.3 вольта ардуинки у меня был запитан еще и маленький OLED - экран, и этого уже хватало, чтобы показания были тем ниже, чем больше пикселей экрана светилось.
Поэтому в готовом датчике весь стабилизатор будет отдан датчику, а вместо экрана будем выводить данные в последовательный порт и почитывать их моим умным домом.
Плохо работает в корпусе
Пожалуй, стоило по-другому разместить сенсор в корпусе, потому что отверстий в задней стенке корпуса, которых было достаточно чтобы поддерживать примерно одинаковые температуру и влажность в корпусе и снаружи, для VOC сенсора уже не хватило.
Чтобы сенсор адекватно изменял свои показания и делал это достаточно быстро, корпус для него должен хорошо проветриваться, а сам сенсор не должен быть перекрыт монтажом или креплениями.
Рассчитанные значения CO2 никуда не годятся
Датчик, казалось бы, умеет репортить не только показатели VOC, но и мгновенные значения CO2 в воздухе. Особенность в том, что VOC он действительно измеряет, а уровень углекислоты - рассчитывает по своим формулам, поскольку корреляция между этим показателями действительно есть
В действительности же, это работает крайне плохо: значениям eCO2 можно доверять только при околонулевых значениях летучей органики и нормальных условиях в отсутствии открытых окон и ветра.
Собираем датчик
Итак, сам сенсор работает, если его правильно приготовить.
Пусть новое устройство будет максимально тонким бекендом и просто отдает сырые результаты измерений, которые будут обрабатываться отдельно.
Схема тоже простая и максимально дешевая. Ардуинку, если что, можно заменить на практически любой контроллер с одним-единственным IO портом и реализованной I2C шиной. Я ее выбрал здесь скорее потому, что на ней распаян неплохой понижающий стабилизатор на 3.3 вольта и ее можно прошивать через usb.
Прошивка железки получилась максимально простая и почти никак не использует периферии контроллера. В основном цикле с программной задержкой просто крутится последовательное чтение данных с датчиков и корректировка газоанализатора.
void loop() {
read_ccs_sensor();
read_dht_sensor();
set_ccs_sensor_environment_data();
report_state();
delay(MEASUREMENT_TIMEOUT);
}
Ссылки на репозиторий с кодом я оставлю ниже, а пока можно собрать весь девайс и посмотреть, как он работает.
Корпус я тоже перемоделировал: вытравить печатную плату мне сейчас нечем, а ждать, пока это сделает JLPCB я не хочу. Поэтому я разместил ардуинку в воздухе на ножках корпуса, а датчики уложил в кроватки и прихватил термоклеем.
Простенький баш-скрипт позволит нам почитать данные и убедиться, что все работает.
Из температуры и влажности, кстати, становится понятно, насколько сильно греется газоанализатор: температура в комнате как минимум на пару градусов ниже, а влажность весной в Петербурге редко опускается ниже 40% даже в квартирах.
После нескольких падений стало понятно, что ничего никуда не отвалится, так что можно накрывать железки крышкой, не забыв сделать в ней хорошую вентиляцию.
Такие отверстия у девайса по всем вертикальным граням, кроме фронтальной, а сзади - их вдвое больше. По идее, это, вместе с компоновкой деталей, должно дать возможность воздуху проходить через корпус максимально свободно, чтобы датчик мог показывать что-то вменяемое.
Запитываем на ардуинке RESET через резистор подтяжки и склеиваем половинки корпуса.
Пишем клиент
Практически все датчики в доме у меня репортят свои данные в 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)
Для начала подойдет. Теперь подождем какое-то время и посмотрим, что происходит с воздухом в квартире.
И есть сразу две плохие новости. Во-первых, не стоило проветривать квартиру под вечер, а во-вторых, клиент и график никуда не годятся.
Датчик, пусть и термостабилизированный, выдает мгновенные значения, превращая график в шум, из которого можно оценить, в лучшем, случае, порядок значений.
Кроме того, скрипт на 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 и развернуть на домашнем сервере, правильно настроив ему конфиг.
Время взглянуть на графики
Подождем еще пару дней чтобы набрать новых значений и посмотрим, во что превратился наш график после усреднения значений.
Здесь отлично видно, что в половину третьего ночи датчик отреагировал на клей для 3D принтера, который стоит в нескольких метрах, а к утру - на просыпающийся город.
Реакция на клей для принтера, а если точнее, на растворитель в нем, более детально. При этом, производитель на голубом глазу гарантирует безопасность. Пожалуй, перейду на карандашный клей.
И, наверное, начну открывать окна перед тем как открывать банку с изопропиловым спиртом. Впрочем, датчик говорит, что выветривается он так же быстро, как и появляется.
А это коротко о безопасности домашней 3D печати. Если на PLA, PETS и SBS датчик не отреагировал практически никак, то от попытки попечатать ABS без принудительной вентиляции количество органики выросло до совсем уж нездоровых значений.
Проветривание здорового человека. Отлично видно, как упало значение VOC. Примерно так же отреагировал и отдельный датчик углекислоты, который стоит у меня уже давно.
А на этом графике можно наблюдать, как в городе изменился ветер и дунул с залива, да так, что чуть не унес меня, вместе со всей остальной органикой.
Проверяем правильность показаний
Для начала, убедимся, что наш датчик действительно термостабилизированный. Поскольку, я отслеживаю температуру во всех комнатах, несложно завести еще один виджет в Home Assistant, на котором поискать зависимости графиков температуры и VOC.
При примерно одной и той же температуре, значение VOC выросло почти вдвое. Впрочем, это как раз ничего не доказывает: температурного дрейфа тут и быть не могло. Давайте поищем что-нибудь более явное.
К слову сказать, такой график можно видеть практически каждый вечер: выхлопных газов больше, солнечного тепла меньше. Грустно, но температурного дрейфа здесь тоже нет. Поищем что-нибудь еще.
А здесь наоборот, типичная картина для середины дня: транспорта вокруг меньше, а солнце как раз входит в свой зенит.
Калибровка датчика
Поскольку датчик ничего не знает о референсных значениях, его автоматическая ежедневная калибровка состоит в обновлении бейзлайна по нижнему значению.
Это подходит для большинства сценариев домашнего использования, но, поскольку, калибровочное значение сбрасывается после перезагрузки, иногда можно видеть такую картину.
Здесь произошло сразу две вещи: сброс бейзлайна и остывание датчика, которому потребовалось какое-то время на то, чтобы снова разогреться до рабочей температуры.
Начинать верить показаниям сенсора, если ориентироваться на сравнении расчитанного им CO2 и точно известным значением от другого датчика, можно через несколько часов непрерывной работы, а на то, чтобы откалиброваться полностью, датчику требуется чуть больше суток.
Для меня, у которого этот датчик работает всегда, это совершенно нормально, но в прошивке несложно добавить ручную калибровку, подобно тому, как это сделано для термостабилизации.
После того, как датчик самостоятельно откалибровался, его показания не будут меняться без изменения окружающей среды.
Стоимость
Я обещал хороший датчик 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 к питанию
Здесь можно почитать про скользящее среднее
Описание патента на MOX датчик
Автор: Макс Граков