Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет! Меня зовут Антон, я ведущий разработчик в команде рекламного фронтенда ВКонтакте. Мои рабочие задачи связаны с развитием рекламного кабинета и возможностей для продвижения сообществ в приложении VK. Здесь результаты можно видеть только в браузере и телефоне, но мне давно хотелось научиться управлять объектами и в реальном мире — например, в своей квартире. Таким опытом я и хочу поделиться в этой статье: опишу, как создал и развивал свой умный дом, с какими проблемами столкнулся по ходу проекта и как их решал.
Первая часть будет максимально простой: датчики, свет, диммирование, контроллеры. Если вы уже достаточно на это насмотрелись, то переходите сразу к части II. Там я рассказываю, как интегрировался с обычным многоквартирным домофоном, чтобы открывать дверь с помощью распознавания лица и не только.
Часть I
Разбирая старые вещи, наткнулся на коробку с датчиками Arduino: пять лет назад при переезде в новую квартиру хотел сделать себе умный дом. Тогда не сложилось, получился только небольшой прототип: Arduino + датчик движения + геркон. Всё это успешно интегрировалось для управления светом в туалете у деда — к слову, до сих пор работает. И вот я снова подумал, не попробовать ли. И понеслась...
Цель
Изначально я хотел реализовать вот что:
полностью автоматизировать управление светом в коридоре, ванной и туалете (то есть совсем исключить человека и выключатели из этой системы);
написать весь код самому на NodeJS (давно хотел через JavaScript управлять объектами в реальном мире), чтобы максимально настроить под себя всю логику работы;
сделать управление через бота ВКонтакте;
внедрить всё без штробления стен, с минимальными манипуляциями на уровне отделки и как можно незаметнее.
Из стартовых знаний у меня пятилетний микропроект на Arduino, школьные и университетские познания в электротехнике — ну и большой опыт в JS, так как это моя основная работа.
За основу решил взять Raspberry PI 2. Из датчиков использовать, как и в прототипе, инфракрасный датчик движения HC-SR501 и геркон для входной двери. Это магнитный размыкатель: когда дверь закрыта, геркон замкнут, ток идёт; а при открытии контакты размыкаются. Причём идея была такая: установить по одному датчику движения внутрь каждого помещения (в туалет, ванную и коридор) и ещё по одному во все дверные проёмы, чтобы фиксировать, когда и где человек проходит. Для этого я планировал в проёмах ставить PIR-элемент без линзы (ИК-датчик движения) — была гипотеза, что так он будет работать в очень узком диапазоне.
Тут меня поджидал первый сюрприз: оказывается, PIR-элемент не может работать без линзы Френеля, да и сам HC-SR501 — это громоздкая бандура, которая некрасиво смотрится в интерьере. Погуглил и нашёл ещё ряд более компактных датчиков (HC-SR505, AM312, SR602). У всех линза примерно 10 мм в диаметре. Я остановился на AM312 и подобрал высоту таким образом, чтобы он не реагировал на передвижения кошек по полу. Самая очевидная часть решена, но как определить пересечение линии дверного проёма?
Датчик определения пересечения линии
Пытаясь подобрать что-то, отвечающее моим требованиям, я перебирал все возможные датчики на AliExpress — надеялся найти что-то готовое, но не получалось. Максимально подходящим по свойствам оказался датчик обнаружения препятствия: он состоит из инфракрасных светодиода и фотодиода. Когда появляется препятствие, ИК-свет отражается от него и фиксируется через фотодиод.
Этот датчик был простым, недорогим и показывал отличное быстродействие. Единственным недостатком была дальность срабатывания: порядка 15 см. Дальше я начал экспериментировать: нужно было заставить его корректно работать на расстоянии 1 м. Первое, что я сделал, — просто отпаял светодиод и начал светить напрямую в фотодиод. Это увеличило расстояние до 30–40 см. Затем поиграл с разным номиналом сопротивлений для ИК-светодиода. Опытным путём установил, что при сопротивлении примерно в 50 омов дальность срабатывания становится около 110 см — то что нужно. Но при таком токе очень сильно греется резистор.
Мне подсказали, что ИК-светодиоды можно использовать с таким током в импульсном режиме. Главное ограничение — средний ток должен быть в диапазоне допустимых для этого светодиода (15–20 мА). Пришло время небольших расчётов: из закона Ома (I = U / R) получаем, что ток у нас порядка 60 mA = (5 V − 2 V) / (50 Om), где 2 V — это потребление ИК-светодиода. Получилась следующая схема импульсной работы: включаю светодиод на 20 мс (за это время фотодиод успевает зафиксировать свет), считываю значение датчика и выключаю светодиод на 50 мс. Имеем цикл в 14 Гц, со средним током 17 мА и холодным резистором, отлично!
Но оказалось, что нельзя просто так подключить к Raspberry этот датчик, потому что у GPIO-пинов ограничен ток на 30 мА (а мы имеем в пике 60 × 4 = 240 мА). Что делать? Оказывается, есть транзисторы, которые могут запросто решить мою задачу. Несколько дней ушло на то, чтобы понять, что такое транзисторы и с чем их едят. На выходе получилось следующее: основная нагрузка легла на пин питания Raspberry (который выдерживает до 500 мА), а через GPIO-пин мы только открываем и закрываем транзистор.
Первый прототип
Итак, у нас есть датчик пересечения дверного проёма и датчик движения. На этом уже можно сделать прототип, чтобы в реальности поэкспериментировать с данными и алгоритмом. Для коммутирования нагрузки и света я использую твёрдотельное реле OMRON G3MB-202P. В саму проводку решил врезаться параллельно основному выключателю: это обеспечит полноценную работу механического выключателя в случае отказа автоматики. Вот что получилось:
Отдельным вызовом была пайка контактов SMD-транзистора: я не знал, что такое SMD-корпус, когда заказывал компоненты, да и паять не умел :) И ещё важное замечание: не любой транзистор подойдёт. Необходим такой, чтобы пороговое напряжение было не больше 3 В, так как пины у Raspberry выдают всего 3,3 В.
На этом этапе я оттачивал алгоритмы включения и выключения света. Причём в туалете вместе со светом включается и вытяжка (достаточно громкая), поэтому основных задач было несколько:
Включить свет максимально быстро, как только обнаружен человек.
Не выключать свет, пока человек внутри помещения.
Выключить свет сразу после выхода человека.
Свет не должен реагировать на кошек.
Исключить ложные срабатывания.
В итоге алгоритм сводился к настройке тайм-аутов выключения света при разных событиях с учётом текущего состояния. Например, если сработал датчик пересечения линии и свет выключен — значит, входим и нужно включить свет. Задача кажется простой, но на практике это не так: нужно учитывать много разных ситуаций. Вот некоторые:
входя, человек шёл широкими шагами или размахивал руками — получим подряд два срабатывания датчика пересечения;
человек зашёл, а потом потянулся закрыть за собой дверь — снова два срабатывания;
человек может уже выйти, а датчик движения до сих пор активен;
и самый сложный кейс: человек зашёл и находится какое-то время без движения (датчик не фиксирует). По алгоритму надо выключать свет, так как прошёл тайм-аут с момента последнего движения. Как быть?
Таких вопросов накопилось достаточно много. И для всех надо было найти решение, чтобы автоматика работала ровно так, как интуитивно хочется.
Arduino
Это был мой первый опыт работы с Raspberry, так что о некоторых особенностях я узнавал в процессе. О чём речь:
на Raspberry пинов общего назначения всего 11 (из 40 доступных), то есть их можно использовать, не отключая никакие заложенные возможности и протоколы;
все пины цифровые — чтобы подключить аналоговые датчики, как в Arduino, надо использовать не самую удобную схему с 1-wire;
через JS неудобно оперировать задержками в микросекунды — а они уже маячили на горизонте для управления диммированием и для считывания показаний датчика тока (но об этом позже).
Учитывая все эти моменты, я решил разделить логику: всю работу с датчиками перенести на Arduino, а через Raspberry управлять только бизнес-логикой (нагрузкой). Rasberry 15 раз в секунду по протоколу I2C запрашивает у Arduino текущее состояние датчиков. Оно состоит всего из 5 байт: первые 2 — это стейт всех 14 цифровых пинов, а ещё 3 байта распределяются по одному на каждый из трёх аналоговых датчиков, которые будут использованы (здесь я нормирую диапазон 0-1023 аналоговых значений к диапазону 0-255).
Изначально использовался SPI-протокол — и это более правильно. Но что-то у меня не завелось, поэтому я задаунгрейдился до I2C. Вот он отлично завёлся, и в целом всё стабильно работает.
Код Arduino получился очень простым: в основном цикле опрашиваем все датчики и складываем результат в 5 байтов ответа. По прерыванию запроса I2C отправляем их на Rasberry. Я соединил Raspberry и Arduino через USB, чтобы можно было удалённо прошивать Arduino прямо из Raspberry, а не бегать с ноутбуком.
Диммирование
Очень хотелось сделать включение света плавным, чтобы ночью он не слепил. Изначально казалось, что это вообще ерунда: в каждом магазине продают механические регуляторы яркости. В моей обывательской картине мира существовал «чёрный ящик», куда передаёшь уровень яркости, а он выполняет роль такого регулятора. Но на практике всё оказалось иначе.
Потратив немало времени, я таки нашёл на AliExpress девайс за 500 рублей, который выполняет нужную мне функцию: позволяет программно управлять яркостью светодиодной лампы (если она поддерживает диммирование).
Он принимает на вход ШИМ от контроллера и преобразовывает его в ШИМ уже для нагрузки. Единственный нюанс — маркировки L и N у него перепутаны. На столе всё заработало, но при установке в стенд-прототип я умудрился его спалить — так что в боевых условиях проверить не смог. Заказал ещё новых, но когда они пришли, устанавливать в блок управления не стал — там уже не было места. Оставил для следующего проекта.
Датчики, сборка и результат первого этапа
Когда прототип повисел пару недель, я обработал и устранил большинство ошибок — и решил, что пора расширяться на всю площадь. Всего я использовал:
4 ИК-датчика пересечения линии дверного проёма;
3 ИК-датчика движения;
2 датчика тока;
1 геркон (для индикации открытия входной двери);
1 лазерный дальномер.
С ИК-датчиками и герконом всё понятно, расскажу про остальное. Чтобы алгоритм управления светом в коридоре был максимально адаптивным, необходимо знать о состояниях в соседних комнатах. Например, если свет включён и на кухне, и в зале, то в коридоре (промежуточном помещении) он тоже горит постоянно. Если освещается только зал или кухня, то на коридор устанавливается определённый таймаут (я задал 30 минут). Если свет вообще выключен, то в зависимости от времени суток включается или ненадолго основной свет, или ночное освещение. Поэтому внутрь выключателей света в зале и на кухне, в разрыв цепи, я поставил вот такие датчики тока.
Ночное освещения я реализовал просто как пять параллельно соединённых белых светодиодов — их вполне хватает. А так как это всего 100 мА тока, то я через GPIO-пин и транзистор сделал диммирование — и ночью свет включается плавненько и красиво.
Лазерный дальномер я установил в туалет, чтобы решить проблему с выключением света, если человек не двигается. То есть дальномер помогает определить, есть ли кто на унитазе :) Сам прибор работает по протоколу UART. Расстояние от контроллера до датчика порядка 5 метров, и для надёжности я хотел поставить на линию передачи данных преобразователи в RS232. Но модули, которые я купил на AliExpress, оказались нерабочими, поэтому в итоге я оставил провода как есть. Повезло, что особо ошибок не наблюдается и датчик работает.
Ещё я хотел поэкспериментировать с 3D-печатью. Поэтому корпус блока управления спроектировал сам и отдал на печать.
После расстановки всех начальных компонентов получилось так:
Изначально я собирался напрямую втыкать провода в пины контроллера, но мне подсказали, что так лучше не делать. Из доступного я выбрал клеммники. Но, как покажет дальше практика, это решение тоже не супер и лучше использовать быстросъёмные коннекторы. Например, мне показалось, что удобным будет коннектор RJ45, так как все провода — это витые пары UTP 4 Cat 5e. Но это была ошибка, и я потратил немало сил и нервов, пытаясь что-либо замерить или изменить в блоке. Дело в том, что он установлен в шкафу 18 см шириной — и невозможно его оттуда вынуть, не отвинчивая все клеммники.
И вот как в итоге выглядят мои датчики (дальномер оставил пока так, его надо аккуратно врезать в наличник двери, пока руки не дошли):
Часть II. Домофон
Когда у нас появился ребёнок, домофон перешёл в режим «тихо» — то есть стал фактически беззвучным. Поэтому, работая над умным домом, я решил интегрировать в него и домофон. Сначала хотел просто сделать уведомление в боте о том, что идёт звонок, но в процессе список задач расширился до такого:
сохранить полную работоспособность основного домофона;
детектировать звонок и отправлять уведомление;
делать фотографию с подъездной камеры и отправлять в бота VK;
распознавать лицо звонящего и, если это я или жена, сразу открывать дверь;
добавить команду «курьер», по которой голосом Алисы будут воспроизводиться быстрые инструкции («этаж такой-то, затем направо») и откроется дверь подъезда;
задать команду «открыть», по которой воспроизводим «Заходите!» и открываем дверь;
реализовать автоответчик. Если звонок длится больше заданного интервала, воспроизводим текст («Никто не может подойти, оставьте сообщение после сигнала»), записываем 10–15 секунд видео и отправляем его сообщением в бота.
Аудио
Для детектирования звонка сделал такую схему:
Параллельно входной линии домофона подключаем эту схему. При звонке, то есть наличии сигнала в линии, имеем положительный потенциал на затворе n-канального полевого транзистора. Я использовал делитель напряжения, так как у меня транзистор с максимальным напряжением (затвор-исток) 10 В, а в линии потенциально может быть до 12 В. Открываясь, он меняет потенциал в точке Sout с высокого на низкий, что мы и детектируем в Raspberry. Входной сигнал — это звуковой сигнал звонка, так что мы детектируем множественные срабатывания и фильтруем их программно. В процессе ещё возникла идея установить конденсатор, чтобы сгладить пульсации, но я уже не стал лишний раз лезть в корпус.
Чтобы открывать дверь, записывать аудиосигнал и передавать его обратно в линию, я решил использовать схему самой дешёвой домофонной трубки. То есть просто купил ещё одну трубку (в моём случае — УПК-7 VIZIT), выпаял кнопку открывания, динамик и микрофон, а вместо них сделал пины, через которые подключаюсь уже к контроллеру. К сожалению, отдельного фото итоговой схемы нет, поэтому покажу промежуточный вариант — тут ещё динамик и микрофон на месте:
При поступлении сигнала я отправляю сообщение в бота. Далее, чтобы выполнить любую логику, необходимо через реле отключить основной домофон и подключить мою модернизированную трубку.
Вместо кнопки открытия я поставил тоже n-канальный полевик и по команде подаю на затвор высокий потенциал (+3,3 на пине). С этим проблем не возникло — всё заработало с первого раза (большая редкость).
Затем я научился передавать звук. Аудиовыход Raspberry соединил со входом M− бывшего микрофона. Это важно, так как изначально я интуитивно соединил M+ c аудиовыходом, а M− c audio GND. И долго искал проблему: звук воспроизводился, но был очень тихим. А дело в том, что через audio GND получился делитель напряжения, то есть в схему трубки сигнал почти не попадал. Воспроизвожу звук я просто через вызов утилиты aplay из кода NodeJS.
C записью звука пришлось немного попотеть. В Raspberry нет микрофонного входа, поэтому пришлось купить за 300 рублей аудиокарточку с USB. Затем передавать выход S+ через конденсатор (это важно) в микрофонный вход этой карты. Опять же: только один провод, потому что гальванической развязки нет и GND общая. Записываю через утилиту arecord. Качество не очень высокое (есть наводка 50 Гц) — но раз это нужно только для кейса с автоответчиком, пока решил оставить как есть.
arecord -D hw:2,0 --format=S16_LE --rate=16000 --file-type=wav -d 10 out.wav
Видео
Отвинтив домофонную трубку от стены, я увидел, что в квартиру подведены четыре провода: два задействованы (LN+ и LN−), а два нет (те, что отвечают за видеосигнал). Сразу захотелось их использовать. Почитал в интернете и выяснил, что в нашем домофоне аналоговый видеосигнал стандарта PAL. Поэтому пришлось купить ещё один внешний USB-девайс для захвата аналогового видео. В целом он работает, но не без сюрпризов: пришлось несколько раз переподключать его. Плюс в том, что никаких драйверов для него не потребовалось — ни на маке, ни на Raspberry. Захват кадра делаю через утилиту ffmpeg:
ffmpeg -loglevel error -i /dev/video0 -vframes 2 -r 5 output/grab-%d.jpg
Вот я протестировал всё на столе, собираю демостенд и хочу увидеть уже реальную картинку с домофона. Но не тут-то было: оказывается, ко мне не подведён видеосигнал. Пришлось звонить в управляющую компанию, чтобы мастер скоммутировал его в мою квартиру.
Чтобы проверить стабильность, я решил написать программу, которая будет раз в секунду делать захват кадра, распознавать там лица и отправлять мне картинку, если лицо найдено. Я думал, что сделаю это за 5 секунд, ведь есть face-api.js. Запустить это на Raspberry было не очень просто, но получилось. Однако спустя рандомное время работы программы Raspberry просто крашилась без внятных логов. Я решил: ок, попробую что-то на питоне (OpenCV). Нашёл, запустил — эффект тот же. Но одна фотка всё же успела распознаться:
Потерпев неудачу, я решаю немного изменить формулировку задачи. Теперь я хочу не распознавать лицо, а определять движение в кадре с помощью библиотеки sharp-phash и после этого отправлять изображение. Получилось очень чётко:
Этот алгоритм можно применять и интереснее: например, выявлять движения в больших помещениях, не монтируя десятки датчиков. Можно поставить одну камеру, разбить картинку на зоны и в каждой определять движение. В следующей квартире я обязательно использую этот подход.
Идея с распознаванием лица меня не отпускала, поэтому решил поискать веб-сервисы. И сразу наткнулся на Amazon Rekognitoin, который впоследствии и использовал. Но для интеграции с их API потребовалось чуть больше времени, чем я ожидал: даже по всем туториалам не сразу всё получилось. Самый жёсткий прикол, который там меня ожидал: если отправляешь две фотографии на сравнение и в одной из них нет лица, то AWS отвечает: «Invalid request params!» WTF?!
В итоге получилась следующая схема: поступает звонок, я делаю две фотографии с интервалом 200 мс. Отправляю обе на сравнение с предзагруженными снимками в AWS (на которых я и жена). Если получил совпадение больше 95% в обоих случаях, открываю дверь. На все манипуляции в среднем уходит 2,4 секунды, из них 1,3 — это захват кадра. Если знаете, как можно ускорить, — подскажите :)
В первой итерации для автоответчика я записывал только звук — но раз у меня уже есть видео, грех не записать его. Так как видео и аудио идут из разных устройств, то для совместной записи я хотел использовать такую команду:
ffmpeg -y -i /dev/video0 -t 10 -f alsa -ac 1 -i hw:2,0 -t 10 -pix_fmt yuv420p output/video.mp4
То есть указываю для ffmpeg два input: видео и аудио. Ожидаю, что на выходе будет видео со звуком — но это так и не получилось завести, ffmpeg упорно отказывался (а вот если указать в качестве аудиоинпута просто файл, то всё работает). Поэтому пришлось сделать небольшой workaround:
await Promise.all([
execute(`ffmpeg -y -i /dev/video0 -t 10 -pix_fmt yuv420p output/video.mp4`),
execute(`ffmpeg -y -f alsa -ac 1 -i hw:2,0 -t 10 output/audio.mp3`)
]);
await execute('ffmpeg -y -i output/video.mp4 -i output/audio.mp3 -c:v copy -c:a copy output/merge.mp4');
Записываю отдельно в два потока видео и аудио, конвертирую их по ходу дела в mp4 и mp3 соответственно и следующим проходом объединяю эти два файла. Ещё нюанс: флаг -pix_fmt yuv420p важен, чтобы видео в приложении отображалось и воспроизводилось корректно.
Отдельно хочу описать проблему, на понимание которой ушло много времени. Протестировав всё по отдельности, я добавил новые элементы в блок управления. Всё работало, но вдруг мне приходит сообщение о звонке в домофон... И я ничего не понимаю. Смотрю на домашний видеофон (установил его вместо трубки): стоит незнакомая девушка, звонит. И тут я понял, что это не случайность: абсолютно все звонки коммутировались в нашу квартиру. Я быстро отключил нововведения, и шквал прекратился. Я долго искал проблему и в итоге установил, что дело в отсутствии гальванической развязки на видеосигнале. Поэтому GND видеосигнала через USB объединяется с LN−, и это даёт такой неожиданный результат. Устранить это получилось, установив в разрыв видеосигнала видеотрансформатор. Финальный аккорд в эпопее — все поставленные цели выполнены!
Вот упрощённая схема моего умного дома:
И небольшое видео с демонстрацией работы.
Рад, что вы дочитали до конца, и надеюсь, статья была для вас полезной. Спасибо за внимание!