Pull to refresh

Собираем wi-fi устройство управления электроприборами с веб-сервером и JS-фронтэндом

Reading time 12 min
Views 254K
Добрый день, уважаемые хабровчане. В этой статье я несколько отойду от своего традиционного подхода к DIY – нашей основной целью станет быстрое и эффективное получение результата, а не изобретение велосипедов с целью самообучения, поэтому даже люди, впервые держащие паяльник, смогут все это повторить и получить готовое устройство за ~1000 рублей и один день.

Введение


Говорят, лень – двигатель прогресса. Это, в принципе, верное высказывание – лично меня всегда напрягала необходимость вставать и идти гасить свет, когда уже почти заснул. Поэтому еще на втором курсе университета я собрал простое устройство на AVRке, позволяющее включать и выключать люстру с помощью любого пульта дистанционного управления. Оно до сих пор висит в одной комнате и работает в режиме 24/5 уже 5 лет.
После была попытка избавиться от пульта – система распознавания голосовых команд, на такой же AVRке, которую я заодно сдал как курсовую в универе – она даже заработала, но не настолько удовлетворительно, как хотелось бы, поэтому менять шило на мыло я не стал.
Через некоторое время сформировалось понимание, что в подавляющем большинстве случаев, когда мне требуется включить или выключить свет, у меня в пределах досягаемости и непосредственной близости имеются либо ПК, либо планшет, либо коммуникатор – следовательно, наиболее удобным способом будет управление через них.
Я набросал вариант реализации на специальных радиомодулях, NRF24L01P – действительно потрясающих, так, например, размер SMD-варианта этого модуля составляет всего 10х15 мм! При этом ими удобно управлять, у них есть собственный протокол, который хендлит адресацию, мультиприем, авто-подтверждение приема и т.п.
Соответственно, по моим прикидкам, должен был быть какой-то USB-донгл с этим модулем, в перспективе – подключенный к компьютеру с веб-сервером, чтобы дать возможность управлять с мобильных устройств, и исполнительные девайсы с ответными трансиверами.
Очень долго у меня не доходили руки до всех этих систем – уже год у меня из стены торчали провода, которые я каждый раз замыкал руками, включая люстру, и говоря себе «ну вот разгребу дела на работе, и сяду разведу платы».
К счастью, последние проекты на работе были связаны с мобильными устройствами, которые должны были уметь несколько больше, чем может дать STM32 и любой другой Cortex M3, и я достаточно много времени провел работая с OpenWRT и устройствами типа всеми любимого TL-MR3020 и TL-WR703, о настройке которых я недавно написал статью. Вместе с этим опытом пришло осознание того, что подобные задачи решаются невероятно быстро и очень эффективно при помощи устройств с linux и wi-fi на борту. Хотя бы потому, что Wi-Fi дает куда более удобные (а в случае с необходимостью передачи видео/аудио – практически единственные доступные) средства беспроводной связи, поддерживаемые всеми современными устройствами.
Конечно, если объектов управления десятки, имеет смысл сделать более тупые исполнительные устройства на кастомных модулях, и оставить серверам с линуксом роль узлов. Но в случае, когда мы делаем продукт для реального использования у себя дома, а не в расчете на гипотетические объекты, такой подход приведет только к жуткой потере времени и денег.
Итак, давайте соберем за день устройство, которое позволит включать-выключать бытовой прибор (до 1.6КВт потреблением) по Wi-Fi, с красивым фронтэндом и без долгой возни с платами, прошивками и драйверами.

Программная часть


Начнем, вопреки традиции, не с железа, а с программной части, ибо она достаточно простая и проверить ее вы сможете сразу же после покупки роутера, даже без его разборки.
Надеюсь, к этому моменту вы уже успели купить TL-MR3020 и, может быть, даже прочитали мою статью по пересборке ядра. На самом деле, можно обойтись даже без пересборки, просто перешейте его прошивкой с официальной вики проекта OpenWRT – если потом возникнет желание, вы легко сможете сделать свою прошивку, встроив в нее необходимые скрипты.
Итак, как будет устроен наш прибор?

  1. Так как нам важно сохранить минимальное количество деталей, мы не будем использовать никаких внешних флешек и хабов, поэтому размер доступной нам памяти ограничен 4 МБ, из которых свободно, обычно, около 1-1.5 МБ.
  2. Учитывая пункт 1 выберем самый легковесный веб-сервер, поддерживающий CGI – идеальным вариантом стал 20-килобайтный mini-httpd безо всяких надстроек.
  3. В качестве cgi-скрипта воспользуемся обычными башевскими скриптами. В принципе, место позволяет поставить минимальную сборку питона. Правда, боюсь, без пересборки ядра вы это не осуществите – т.к. все, что вы ставите после прошивки устанавливается в JFFS, то и занимает оно намного больше, чем если бы было вбилдено изначально в SquashFS. В порядке эксперимента, я вбилдил и python-mini и mini-httpd прямо в образ, выкинув оттуда все лишнее – при билде JFFS образа система кинула эрроры о превышении размера, оставив на память недособранные куски, но SquashFS-образ собрался нормально, оставив мне еще ~250 КБ свободного места – для наших нужд вполне хватит. Еще одно замечание – старт питона с внутренней флешки выполняется намного медленнее, чем старт башевского интерпретатора (и явно медленнее, чем с внешней USB-флешки, сказывается SPI-интерфейс), поэтому при использовании питоновских скриптов для CGI отклик будет составлять около 1 секунды – не очень критично, но слегка раздражает. Впрочем, если бы я писал что-то более сложное, чем дрыганье одним GPIO, меня бы куда больше раздражал огромный и мерзкий баш-скрипт, и я бы предпочел смириться с секундным стартом интерпретатора питона
  4. Баш-скрипт будет дергать один из доступных нам GPIO, на котором висит светодиод. Светодиод мы с вами потом отпаяем и привесим на него твердотельное реле, управляющая цепь которого является по сути таким же светодиодом.
  5. Index.html будет содержать в себе небольшой js-код для отображения красивой картинки с лампочкой, которая будет зажигаться/гаснуть при нажатии, выполняя асинхронный запрос к CGI-скрипту, а также будет дергать его каждые N секунд, на случай если кто уже зажег лампочку с другого компьютера – чтобы обновить картинку в соответствии со статусом.
  6. Запитаем мы это все от идущего в комплекте блока питания, предварительно его разобрав, вынув из корпуса сам модуль, чтобы уменьшить размеры итогового девайса. Этот шаг опционален, если хотите – можете питать как вам удобнее.


Давайте начнем. На данный момент вы должны уже перешить роутер на OpenWRT и подключиться к нему по Ethernet-кабелю.
Заходим по SSH/WinSCP на роутер и идем в /etc

Настраиваем wi-fi

для этого идем в /etc/config/network и пишем там

config interface 'wlan'
option proto 'dhcp'

Это обеспечит получение wi-fi контроллером роутера IP от DHCP нашего домашнего раздатчика вай-фай в лице другого роутера.
В /etc/config/wireless заботливые first-start скрипты уже сгенерили настройки радио, осталось закомментить строку #option disabled 1 включая модуль wi-fi и в секции ниже, описателе интерфейса, написать что-то типа

config wifi-iface
	option device   radio0
	option network  wlan
	option mode     sta
	option ssid     тут-имя-вашей-точки
	option encryption psk2 
	option key тут-ваш-пароль

Данные настройки приведены для WPA2-PSK. Для WPA-PSK encryption будет psk, для открытой точки none, для вепа, соответственно, wep – более подробно лучше прочитать на официальной вики тут и тут.
Проверяем что все хорошо при помощи

wifi down
wifi
iwconfig wlan0

Если все ОК, iwconfig сообщит вам что вы присоединены к вашей точке доступа, после чего от эзернета его следует отрубить и продолжить настройку по wi-fi.

Настраиваем GPIO

Теперь нам надо дать доступ к GPIO пинам из юзер-спейса. Ядро это умеет само по себе, ничего делать для этого не нужно, но GPIO уже захвачены драйвером мигания светодиодами. При желании можно не трогать светодиоды и их драйвер, так как, согласно вики, есть несколько незадействованных GPIO, подтянутых к земле, подтяжку которых можно отпаять, после чего использовать их в собственных целях. В этом случае выгружать драйвер не нужно, достаточно только экспортировать GPIO (показано ниже). Но мы воспользуемся пинами со светодиодами, чтобы сразу увидеть результат трудов:
Пишем в /etc/rc.local

rmmod leds_gpio
echo 0 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio0/direction
echo 0 > /sys/class/gpio/gpio0/value

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

echo 1 > /sys/class/gpio/gpio0/value

А погасить – вот этой:

echo 0 > /sys/class/gpio/gpio0/value

Только не забываем, что наш скрип еще не выполнился, поэтому сначала перезагрузим роутер командой reboot, заодно проверив как после перезагрузки поднимается wi-fi.

Настраиваем mini-httpd

Если вы пересобрали ядро и встроили в него mini-httpd, то переходите сразу к настройке, если нет — ставим его из репозитория

opkg update
opkg install mini-httpd

Теперь внесем небольшие правки в /etc/mini-httpd.conf, так чтобы его первая строка выглядела как

cgipat=cgi-bin/**.sh

Теперь сервер будет пинать все что лежит в /www/cgi-bin/ и чье имя оканчивается на .sh как команду, транслируя ее ответ браузеру.

Настраиваем аппаратную кнопку

На случай если нам не захочется тянуться к wi-fi, повесим дублирующее действие на единственную кнопку: в /etc/hotplug2.rules убираем галочку перед словом buttons, чтобы нажатие на кнопки вызывало скрипты-обработчкики из соответствующей папки, строка теперь должна выглядеть так:

SUBSYSTEM ~~ (^net$|^input$|button$|^usb$|^ieee1394$|^block$|^atm$|^zaptel$|^tty$) {

После этого идем в /etc/hotplug.d/, создаем там папку button, а в ней – скрипт buttons (не забывая выставить ему права на выполнение командой chmod +x buttons!) со следующим содержимым:

#!/bin/sh
	if [ "$BUTTON" = "wps" ]; then
	if [ "$ACTION" = "released" ]; then
		state=`cat /sys/class/gpio/gpio0/value`
		if [ "$state" = "0" ]; then
			echo 1 > /sys/class/gpio/gpio0/value
		else
			echo 0 > /sys/class/gpio/gpio0/value
		fi
	fi
fi

Теперь нажатие на кнопку будет переключать состояние светодиода. При желании можно сделать чтобы светодиод под этой кнопкой светился в противофазе с управляющим, то есть чтобы при погашенной люстре кнопка аппаратного включения была подсвечена для упрощения ее поиска в потемках – для этого просто экспортируйте пин 26 в rc.local также, как экспортировали пин 0, и добавьте аналогичные строки вывода 1/0 в его value рядом с выводом в value нулевого пина.

Это можно проверить прямо сразу – если вы все сделали правильно, то нажатие на кнопку вызовет изменение состояния светодиода.

Пишем CGI-скрипт

Теперь покидаем /etc и идем в /www, где создаем директорию cgi-bin, а в ней – скрипт light-control.sh, которому делаем chmod +x, со следующим содержимым:

#!/bin/sh
	echo "content-type: text/html"
	echo ""
	input=$(echo $QUERY_STRING | tr "&" "\n") 
	for param in $input
	do		
		name=`echo $param | cut -f 1 -d "="`
		value=`echo $param | cut -f 2 -d "="`
		if [ "$name" = "action" ]; then
			if [ "$value" = "on" ]; then
				echo 1 > /sys/class/gpio/gpio0/value
			elif [ "$value" = "off" ]; then
				echo 0 > /sys/class/gpio/gpio0/value
			fi
		fi
	done
	cat /sys/class/gpio/gpio0/value

Первые две строки после #!/bin/sh обязательны для CGI-скрипта – говорим браузеру что все ок и мы сейчас выдадим хтмл, после чего шлем два \r\n в соответствии с требованиями стандарта.
Потом берем из переменной $QUERRY_STRING параметры, куда их поместил заботливый mini-httpd, и разбираем каждый на name и value. Вообще, тут будет использоваться только один параметр, но пусть код будет в более общем виде, на случай если решим расширить.
Дальше проверяем – если нам передали action=on, то зажигаем диод, если action=off – гасим.
А после всегда возвращаем то, что прочитали из value – 1 если диод горит, 0 – если нет. Таким образом, если мы дернем наш скрипт извне с каким-либо параметром кроме action=on/off или вообще без параметра, мы получим от него текущий статус светодиода.

Пишем фронтэнд

Кидаем в директорию /www скаченный с офф. сайта jquery — с ним действительно удобно делать аяксовые запросы – я видел все эти java-script’ы впервые в жизни (т.к. это самая дальняя от моих занятий и интересов область), но за 5 минут при помощи гугла осилил нужные мне действия. Сохраняем jquery под именем jquery.js
Туда же сохраняем представленные ниже картинки под именами lamp_on.jpg и lamp_off.jpg, символизирующие нашу люстру – их сделал из стащенной с гугла картинки путем разрезания ее на две части и легкой подгонки по размеру друг к другу. При желании можно использовать любые картинки.





Там же создаем index.html, в который пишем

<html>
	<head>
		<script type="text/javascript" src="jquery.js"></script>
		<script type="text/javascript">

			function LampImgOn()
			{
				$('#lamp_off').hide();
				$('#lamp_on').show();
			}

			function LampImgOff()
			{
				$('#lamp_on').hide();
				$('#lamp_off').show();
			}

			function ProcessResult(data)
			{
				if(data=="1\n")
					LampImgOn();
				else
					LampImgOff();				
			}
			
			$(document).ready(function()
			{
					$.ajaxSetup({
  					  cache: false
					});
				$('#lamp_on').hide();
				jQuery.get('/cgi-bin/light-control.sh', {'action': 'none'}, ProcessResult); 

				$('#lamp_on').click(function()
					{
						jQuery.get('/cgi-bin/light-control.sh', {'action': 'off'}); 
						LampImgOff();
					})
				
				$('#lamp_off').click(function()
					{
						jQuery.get('/cgi-bin/light-control.sh', {'action': 'on'}); 
						LampImgOn();
					})
				window.setInterval("jQuery.get('/cgi-bin/light-control.sh', {'action': 'none'}, ProcessResult)",2000);
			});
		</script>
	</head>

	<body bgcolor="#000000">
	<img id = 'lamp_off' src = 'lamp_off.jpg' height = '40%' style='display:none'>
	<img id = 'lamp_on' src = 'lamp_on.jpg' height = '40%' style='display:none'>
	</body>	
</html>


Тут мы на размещаем на страничке две изначально невидимые картинки с зажженной и погашенной лампой, а ява-скрипт после старта запрашивает аяксом от CGI (при помощи параметра action=none) текущий статус, а результат передаем на ProcessResult, который активирует ту или иную картинку. Эта же функция зовется после асинхронных запросов на переключение статуса (которые выполняются в обработчиках события клика на картинки), а также заводится таймер, которые каждые 2000 мс дергает скрипт на предмет статуса, чтобы отображать актуальные данные в случае, когда кто-то изменил их с другой страницы или аппаратной кнопкой.

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

Железо


Что касается аппаратной части, то тут ситуация двоякая. Собственно схема у нас простейшая – управляющая цепь твердотельного реле по сути – тот же светодиод, и все что нужно – обеспечить протекание через него достаточного для включения тока. У S202S02 это не меньше 8 мА.
К сожалению, напрямую завести его от GPIO не получится – напряжение на GPIO 2.6В, после чего стоит резистор в очень маленьком корпусе номиналом 260 Ом и светодиод с падением напряжения около 1.5В. Таким образом, через него течет около 4-5 мА. Падение на S202S02 от 1.2 до 1.4В, так что то там будет примерно такой же. Можно, конечно, поискать резистор в таком же корпусе, но меньшего номинала, перепаять его, и нагрузить несчастный GPIO на 10 мА, но лучше пойти более простым путем – роль светодиода у нас исполнит переход база-эммитер NPN-транзистора, через который при падении в 0.75В потечет ток в 0.7 мА а в коллектор мы ему включим управляющую цепь реле.
Проблемы же состоят в другом. Их, собственно, две:

  1. Нужно отпаять светодиод (у нас это светодиод LED4) и припаять вместо него наш транзистор
  2. Нужно как-то упихать в корпус реле (ну это нетрудно, оно мелкое) и внутренности блока питания.

Что касается пункта 1. Текстолит, на котором выполнена схема, попросту говоря дерьмовый.
Дорожки и контактные площадки срывает моментально, припой – подстать текстолиту – начинает плавиться когда этот самый текстолит уже начинает гореть.
Что касается пункта 2. Будь корпус на 5 мм выше – проблем бы не было. Но увы, конденсаторы блока питания и импульсный трансформатор слишком высоки, чтоб корпус закрылся.
К сожалению я не нашел какого-то идеального волшебного решения, позволяющего красиво и быстро запихать в корпус невпихуемое. Костыльных вариантов много – начиная от покупки пластикового корпуса, в который это все влезет (нормальный, в общем-то, вариант), и заканчивая синей изолентой и резиночками для удержания в целости исходного корпуса.
Я выбрал нечто среднее – т.к. у меня оставался полиморф-пластик, я проложил узкую его полоску по периметру исходного корпуса и вдавил в нее крышку.
Так что в этом вопросе я рекомендую каждому найти устраивающее его решение самостоятельно. Разумеется, если вы будете использовать большой корпус, либо приделаете БП вне этого мелкого, вам совсем не обязательно отпаивать от платы разъемы, я просто расскажу что сделал я.
Чтобы плата могла влезть в корпус вместе с блоком питания, я отпаял от нее все разъемы (Ethernet, USB-Host, micro-USB-Power), а также развязку от эзернета. Плата резко стала плоской. Если будете это делать – будьте очень, очень внимательны. Я поплатился за свою невнимательность, установив слишком сильный поток из фена, в результате чего отпаялся критичный конвертер U5, дающий 3.3 В системе. Более того, его сдуло на пол и пришлось его долго искать – но, к счастью, удалось припаять его обратно.



Обращаем внимание на перемычку R113 (обведена красным) – по умолчанию она разомкнута. Замкнув ее мы сможем подавать питание на роутер через разъем USB-Host – а туда подпаяться на порядок удобнее и надежнее, т.к. он не SMD. Поэтому замыкаем R113 и спокойно паяем два провода для питания в отверстия из-под разъема.
Заметили, как тяжело паять провод в «земляное» отверстие? Оно соединено с полигонами земли на всех слоях, где они есть – теплоотвод бешеный, мой паяльник еле справился.
А вот так выглядят внутренности блока питания (рядом с платой роутера для масштаба)



Отпаиваем два хилых провода, которые раньше шли к вилке, и припаиваем туда что-то более надежное, что потом соединим с сетью 220В. Заодно отпаиваем его USB-разъем, так как соединим его напрямую с нашими проводами питания роутера.
Готовим реле, паяя так, чтобы потом силовые провода ничего не коснулись. Я подпаял вот так:



После пайки запихиваем в термоусадку, либо заматываем изолентой, либо покрываем сверху каким-нибудь герметиком или чем-то аналогичным (кстати, на фото блока питания видно этот самый герметик — он был изначально, китайцы тоже заботятся о ТБ).
Далее паяем транзистор – Я взял первый попавшийся под руку 2N6517 и подпаял его базой к той площадке, где у LED4 было написано +, эммитером – ко второй.



Так вот. Так делать не надо. Потому что достаточно одного неверного движения и вы сорвете обе площадки, придется потом паяться к резистору. Лучше подпаяйте тоненькие проводки к обеим площадкам, закрепите их на плате и выведите в безопасное место. И – да – если вы будете паять неаккуратно, то рискуете, как я, коснуться паяльником выводов микросхемы памяти и закоротить их. После чего будете проклинать свои кривые руки и час пытаться очистить их оловоотсосами, оплетками для выпайки, иголками – проверено на себе. К счастью, очистить таки удалось, но напряжно было неимоверно.
Осталось совсем немного – теперь у нас есть выход типа «открытый коллектор» (оставшийся вывод транзистора), к которому паяем резистор из расчета падения 1.3 вольта на оптопаре – то есть, при питании от 3.3В (их проще всего взять, на нераспаяном разъеме P1 справа на рисунке выше это четвертый пин), мы должны обеспечить ток где-то в 10 мА, что означает ровно 200 Ом.
Теперь аккуратно кладем блок питания кондерами вниз на плату роутера, не забыв проложить бумажкой – металлический верх кондера не должен быть под напряжением, но перемкнуть что-нибудь может, так что пусть будет бумажка. Провода питания роутера запаиваем в отверстия от разъема USB на блоке, плюсовой пин реле припаиваем к 3.3В (отверстие 4 на P1), минусовой – через 200 Ом резистор к коллектору транзистора.
Спиливаем одну из бобышек крышки, чтобы она не упиралась в блок питания, выступ, предназначенный для нажатия на кнопку наращиваем при помощи обрезка стержня от ручки или того же полиморфа.
Ну и прижимаем это все крышкой, закрепляя ее как вам больше нравится – полиморфом, изолентой, резиночками – на что хватит фантазии и материалов.
Осталось подвести 220В к проводам питания роутера, а коммутируемые провода реле включить вместо выключателя люстры или любого другого бытового электроприбора.

Получилось? Поздравляю, теперь в вашей люстре есть веб-сервер с CGI под управлением Linux, отдающий пользователям веб-фронтэнд на JavaScript, и все это – примерно за 1000 рублей и один день возни.
Tags:
Hubs:
+88
Comments 37
Comments Comments 37

Articles