Удобное переключение wifi в режим точки доступа

По мотивам статей (раз,два).
Знаю, что на хабре и в гугле эту тему уже не раз обсуждали, тем не менее, когда я, по мануалам, решил сделать свою точку доступа «для друзей», я столкнулся с определенными сложностями, а готовые решения оказались сыроватыми. Потому предлагаю неопытным пользователям GNU/Linux сделать это так, как сделал я.

Раздать wi-fi с 3g модема (и с проводного интернета), когда ничего другого нет под рукой — дело благородное, потому мы пройдем по быстрому пути получения профита. Инструкция предназначена для debian-based дистрибутивов. Нам понадобится hostapd — собственно для раздачи wi-fi, dnsmasq — для раздачи ip-адресов и notify-send (не обязательно) — для оповещений. iptables на данный момент доступен из коробки. Ставим hostapd и останавливаем его:

aptitude install hostapd service hostapd stop

В файле /etc/default/hostapd раскомментируем и исправляем строку:

DAEMON_CONF="/etc/hostapd/hostapd.conf"

Создаем и редактируем файл /etc/hostapd/hostapd.conf

interface=wlan0 driver=nl80211 ssid=wifi_4_friends hw_mode=g channel=6 wpa=2 wpa_passphrase=12345678 wpa_key_mgmt=WPA-PSK wpa_pairwise=TKIP rsn_pairwise=CCMP auth_algs=1 macaddr_acl=0

Тут все просто — имя точки доступа, пароль, канал, на котором будет работать и драйвер.
Ставим dnsmasq и останавливаем его:

aptitude install dnsmasq service dnsmasq stop

dnsmasq хорош тем, что в нем все есть и он прост для настройки. Открываем файл конфигурации /etc/dnsmasq.conf:

interface=wlan0 dhcp-range=192.168.2.2,192.168.2.100,12h 

Тут все крайне просто, но если нужно, можно добавить альтернативный dns сервер, а также можно хосты принудительно направлять на 127.0.0.1, тем самым блокируя их. Подробности в справке man dnsmasq. Еще один момент, обязательно адреса dhcp-range должны быть в одной сети с wlan0. если Вы по каким-либо соображениям в скрипте запуска не будете принудительно менять ip адрес для wlan0, то укажите тут пул такой же, как в wlan0. Например дома есть роутер с адресом 192.168.1.1 и сеть 192.168.1.0/24, то dhcp-range нужно указать в пределах этого пространства, а также, чтобы он не пересекался с пулом адресов, выдаваемых dhcp-сервером роутера. Мы пойдем путем по-проще и сами укажем другую подсеть.

Теперь отключим автозагрузку демонов:

update-rc.d hostapd disable update-rc.d dnsmasq disable

Ко всему этому осталось только включать/отключать роутинг и добавлять/удалять правило из iptables

sysctl net.ipv4.ip_forward=1 iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

Это будем производить автоматически, с помощью скрипта. Все готово, а вот и сам скрипт wifi-ap:

#!/bin/bash #script to start/stop hostapd, dnsmasq, add/remove iptables rule  set -e exec 3>&1 exec 2>&1 >> /tmp/wifi-ap  function print_help(){ 	echo "Start/Stop Software Access Point" 	echo 	echo "Usage `basename $0` options..." 	echo "wifi-ap on to start Software AP" 	echo "wifi-ap off to stop Software AP" 	echo 	echo "log-file - /tmp/wifi-ap" 	echo } if [ $# = 0 ]; then 	print_help >&3 		exit 0 fi  if [ $1 = on ]; then 		ifconfig wlan0 192.168.2.1 		service dnsmasq start 		sysctl net.ipv4.ip_forward=1 		iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE 		service hostapd start 		notify-send --expire-time=4000 "Software Access Point" "<b>start</b>" 	exit 0 fi  if [ $1 = off ]; then 		service dnsmasq stop 		service hostapd stop 		ifconfig wlan0 192.168.1.4 		sysctl net.ipv4.ip_forward=0 		iptables -D POSTROUTING -t nat -o ppp0 -j MASQUERADE 		notify-send --expire-time=4000 "Software Access Point" "<b>stop</b>" 	exit 0 fi

Он принимает 2 параметра, on и off. Вы легко можете подкорректировать его под себя и, если нужно, заменить интерфейс ppp0 на eth0 (или другой, на Ваше усмотрение).

Я пользователь debian и не использую sudo, а Вам может понадобиться.

Приятного использования.

ссылка на оригинал статьи http://habrahabr.ru/post/210426/

О безопасности в Meteor и не только (часть 2)

Если вас не испугала первая часть, предлагаю продолжить разговор о механизмах безопасности Meteor. Начав с loginToken, выдаваемого клиенту, правил allow/deny при модификации базы данных клиентом, коснемся доверенного и недоверенного кода, серверных методов, использования HTTPS и пакета force-ssl, пакета browser-policy (Content Security Policy и X-Frame-Options), и закончим встроенным механизмом валидации данных (функция check() и пакет audit-arguments-check).

loginToken

После авторизации клиент получает временный токен, авторизующий текущего пользователя, который сохраняется в localStorage:

> localStorage.getItem("Meteor.loginToken")   "eEg4T3fNPGLns7MfY" 

Строго говоря, сохраняется он в объекте Meteor._locaStorage, который является оберткой window.localStorage для поддерживающих его браузеров.
Также можно узнать этот токен и через объект Accounts:

Accounts._storedLoginToken() 

Этот же токен сохраняется на сервере в коллекции Meteor.users:

> Meteor.user().services.resume   {     "loginTokens": [  {           "token":"DXC3BqekpPy97fmYs",           "when":"2014-01-31T10:53:54.347Z"         } ]   }

Разумеется, в консоли браузера это поле доступно только в том случае, если оно явно опубликовано.

Любой браузер, у которого есть пара токен + идентификатор пользователя, считается авторизованным. Чтобы в этом убедиться, можно залогиниться в браузере и получить текущие loginToken и userId:

localStorage.getItem("Meteor.loginToken"); localStorage.getItem("Meteor.userId"); 

Затем установить их в другом браузере:

localStorage.setItem("Meteor.loginToken", "'+loginToken+'"); localStorage.setItem("Meteor.userId", "'+userId+'"); 

И через несколько мгновений сессия браузера будет авторизована.

Время жизни токена

Токен существует до момента выхода пользователя из системы, либо истечения таймаута, задаваемого параметром (по умолчанию — 60 дней):

Accounts.config({loginExpirationInDays: 60}) 

Ограничение прав клиента на изменение коллекции — правила allow/deny

Если мы попробуем изменить поддокумент services, сделать это из браузера не получится:

> Meteor.users.update({ _id: Meteor.userId() }, {$set: { "services.test": "test" } })   undefined   update failed: Access denied  

Происходит это из-за того, что на сервере доступ к данному документу ограничен правилами allow/deny. Посмотрим, как этот механизм реализован в пакете accounts-base:

Meteor.users.allow({   // clients can modify the profile field of their own document, and   // nothing else.   update: function (userId, user, fields, modifier) {     // make sure it is our record     if (user._id !== userId)       return false;     // user can only modify the 'profile' field. sets to multiple     // sub-keys (eg profile.foo and profile.bar) are merged into entry     // in the fields list.     if (fields.length !== 1 || fields[0] !== 'profile')       return false;     return true;   },   fetch: ['_id'] // we only look at _id. }); 

Из кода видно, что разрешены изменения только документа, userId которого совпадают с текущим пользователем, и можно вносить изменения только в поддокументе profile. Параметр fetch сообщает Meteor, что для проверки полномочий не требуется получать модифицируемый документ целиком (он может быть большим), для проверки достаточно только одного поля _id. Так как правило allow объялвено только для операции update, операции insert и remove для клиента запрещены:

> Meteor.users.insert({})   "qs8HbcSDjgbgb3vgS"   insert failed: Access denied. No allow validators set on restricted collection for method 'insert'. 

Правилом deny можно запретить операцию, разрешенную allow. Т.е.если одно из правил allow (может задаваться более одного правила) вернуло true, то это разрешение может быть перекрыто, если одно из правил deny вернет true, и запись в этом случае будет запрещена, несмотря на правила allow.

Проверка прав доступа к поддокументам

С операцией update возможности проверки несколько ограничены. Например, если необходимо запретить запись в какое-либо поле поддокумента, например, doc.field1, но разрешить в другое его поле, например, doc.field2 нашей коллекции test, сделать это просто так не получится. Посмотрим, какие параметры передаются в этом случае в правило, добавив на сервере вывод входных параметров правил allow и deny:

Test.allow({   update: function (userId, document, fields, modifier) {     console.log('Test.allow(): userId:', userId, '; document:', document, '; fields:', fields, '; modifier:' , modifier);     return true;   } });  Test.deny({   update: function (userId, document, fields, modifier) {     console.log('Test.deny(): userId:', userId, '; document:', document, '; fields:', fields, '; modifier:' , modifier);     return false;   } });            

И выполним операцию update для поля doc.field1, предварительно узнав _id одного из документов (убедитесь, что у коллекции Test нужные поля опубликованы, задав в коде нашего примера переменную projection = {}, иначе результат не будет виден):

> Test.findOne({_id: "FG7FaQqYgB7Rs9RDy"})   Object {_id: "FG7FaQqYgB7Rs9RDy", name: "First", value: 1} > Test.update({_id:"FG7FaQqYgB7Rs9RDy"}, { $set: { "doc.field1": "value1" } } )   undefined > Test.findOne({_id: "FG7FaQqYgB7Rs9RDy"})     Object {_id: "FG7FaQqYgB7Rs9RDy", name: "First", value: 1, doc: Object} > Test.findOne({_id: "FG7FaQqYgB7Rs9RDy"}).doc.field1   "value1" 

В логе сервера будет выведено:

I20140131-13:31:27.582(4)? Test.deny(): userId: kL7Fkuk29ci4vz8q4 ; document: { _id: 'FG7FaQqYgB7Rs9RDy', name: 'First', value: 1 } ; fields: [ 'doc' ] ; modifier: { '$set': { 'doc.field1': 'value1' } } I20140131-13:31:27.582(4)? Test.allow(): userId: kL7Fkuk29ci4vz8q4 ; document: { _id: 'FG7FaQqYgB7Rs9RDy', name: 'First', value: 1 } ; fields: [ 'doc' ] ; modifier: { '$set': { 'doc.field1': 'value1' } } 

В параметре fields передается пассив полей только самого вернего уровня, т.е.на основании него определить права доступа к полю doc (и всем его поддокументам), но применить разные права к полям doc.field1 и doc.field на основании этого массива невозможно. Для этого можно использовать параметр modifier, в котором передается объект, содержащий операцию MongoDb, и, чтобы не проводить полный анализ операции, разрешить только какой-то жесткий его формат и запрещая все его остальные варианты как-то так:

Test.allow({   update: function (userId, user, fields, modifier) {     console.log('Test.allow(): userId:', userId, '; document:', document, '; fields:', fields, '; modifier:' , modifier);     var setData = modifier["$set"];     return setData && Object.keys(setData).length===1 && setData["doc.field1"];   } }); 

Разумеется, работают правила allow/deny только если пакет insecure убран из проекта. К слову, эти обработчики также можно использовать для на стороне сервера изменений, производимых клиентом

Доверенный и недоверенный код

До сих пор мы изменяли запись по её идентификатору. Дело в том, что клиент не может выполнить операцию update, указывая в селекторе запроса что-либо другое, например:

Test.update({ value: 1 }, { $set: { "doc.field1": "value1" } } ) Error: Not permitted. Untrusted code may only update documents by ID. [403] 

Происходит это из-за того, что Meteor разделяет доверенный и недоверенный код. Доверенным считается код, выполняемый на сервере, включая серверные методы, вызываемые клиентом. Недоверенный — код, выполняемый на стороне клиента в браузере.
Недоверенному коду разрешена модификация документов только по одному, с указанием _id документа и проверкой правил allow/deny. Также ему запрещена операция upsert (вставка документа при его отстутсвии). Операция remove аналогичным образом может быть применена только к отдельному документу, с указанием его _id. Подробнее см документацию docs.meteor.com/#update и docs.meteor.com/#remove.

Серверные методы

В качестве альтернативы прямому доступу клиента к базе данных можно использовать серверные методы. Так как код, выполняемый на сервере, считается доверенным, можно логику критичных операций разместить на сервере, запретив изменения соответствующих коллекций на клиенте. Например, добавим на сервере:

Meteor.startup(function() {   Meteor.methods({     testMethod: function(data) {       console.log('testMethod(): data:', data);       return 'testMethod finished (data:',data,')';     }   }); }); 

И вызовем со стороны клиента, передав последним параметром callback, вызываемые при завершении выполнения метода:

> Meteor.call('testMethod', 'test data', function(err, result) {console.log(err, result);})   undefined   undefined "testMethod finished (data:test data)" 

HTTPS и пакет force-ssl

Сам по себе Meteor не включает поддержки HTTPS, и для него необходим промежуточный сервер, терминирующий SSL, на котором размещается сертификат. Встроенный пакет force-ssl позволяет перенаправить подключение по протоколу HTTP на HTTPS URL, за исключение подключений с localhost.
При использовании Nginx в этом пакете нет необходимости, так как перенаправление можно реализовать следующим образом

(пример настройки Nginx на localhost в качестве прокси для Meteor, включая генерацию автоподписанного сертификата)

Сгенерировать ключ и сертификат

$ openssl genrsa -des3 -out localhost.key 1024 $ openssl req -new -key localhost.key -out localhost.csr Common Name (eg, YOUR name) :localhost $ openssl x509 -req -days 1024 -in localhost.csr -signkey localhost.key -out localhost.crt 

Скопируем сертификат и ключ в папку /etc/nginx/ssl

$ mkdir /etc/nginx/ssl $ cp ./localhost.key /etc/nginx/ssl $ cp ./localhost.crt /etc/nginx/ssl 

Конфигурация Nginx

Создать файл /etc/nginx/sites-available/meteor.conf (если Nginx ставится «с нуля», необходимо удалить или перенастроить расположенный в том же каталоге файл default, в котором прописаны те же порты):

server {   listen 80;   server_name localhost;   # $scheme will get the http protocol   # and 301 is best practice for tablet, phone, desktop and seo   # return 301 $scheme://example.com$request_uri;   # We want to redirect people to the https site when they come to the http site.   return 301 https://localhost$request_uri; }  server {   listen   443;   server_name localhost;   client_max_body_size 500M;    access_log /var/log/nginx/meteorapp.access.log;   error_log /var/log/nginx/meteorapp.error.log;    location / {     proxy_pass http://localhost:3000;     proxy_set_header X-Real-IP $remote_addr;     proxy_http_version 1.1;     proxy_set_header Upgrade $http_upgrade;     proxy_set_header Connection "upgrade";   }    ssl on;   ssl_certificate /etc/nginx/ssl/localhost.crt;   ssl_certificate_key /etc/nginx/ssl/localhost.key;   ssl_verify_depth 3; } 

Создать ссылку:

ln -s /etc/nginx/sites-available/meteor.conf /etc/nginx/sites-enabled/meteor.conf 

Рестартовать Nginx:

$ sudo service nginx restart 

Пакет browser-policy, Content Security Policy и X-Frame-Options

Фактически за browser-policy скрывается два других пакета, каждый из которых может быть использован по отдельности, browser-policy-content и browser-policy-framing. Первый из них предоставляет интерфейс для определения правил Content Security Policy, с помощью которых задается белый список источники для загрузки различных типов ресурсов. Второй — параметра X-Frame-Origin, разрешающего отображать страницу внутри тегов frame или iframe, в зависимости от URI сайта, который пытаетcя это делать (на данный момент указание URI источника в X-Frame-Origin поддерживается только Firefox и IE 8+).
Добавление пакета включает политику по умолчанию, при этом загрузка контента разрешается только с того же сайта, что и сама страница, запросы XMLHTTPRequest и соединения WebSocket могут направляться на любый сайты. Кроме этого, блокируются функции типа eval() и приложение может быть включено в frame и iframe только тем же сайтом, с которого оно загружено.

При этом к заголовку ответа сервера при загрузке страницы добавляются следующие параметры:

content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline'; x-frame-options: SAMEORIGIN 

И, в нашем примере, изображения пользователя с внешних сайтов (Google и Facebook) перестанут отображаться со следующим сообщением в консоли:

Refused to load the image 'https://lh6.googleusercontent.com/-aCxpjiDMNcM/AAAAAAAAAAI/AAAAAAAAJMY/9hZytqLLZ6Q/photo.jpg' because it violates the following Content Security Policy directive: "img-src data: 'self'". 

Чтобы изображения с внешних сайтов снова начали отображаться, необходимо на сервере добавить следующие строки:

Meteor.startup(function() {   BrowserPolicy.content.allowImageOrigin("https://*.googleusercontent.com");   BrowserPolicy.content.allowImageOrigin("http://profile.ak.fbcdn.net");   BrowserPolicy.content.allowImageOrigin("http://graph.facebook.com"); }); 

При этом заголовок станет выглядеть так:

content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src * 'self'; img-src data: 'self' https://*.googleusercontent.com http://profile.ak.fbcdn.net http://graph.facebook.com; style-src 'self' 'unsafe-inline'; x-frame-options: SAMEORIGIN 

В дополнение к ограничениям, установленным по умолчанию, в документации Meteor рекомендуется запрещать выполнение inline Javascript на странице, вызвав BrowserPolicy.content.disallowInlineScripts() на стороне сервера (конечно, если не используется inline Javascript).

Валидация данных: функция check() и пакет audit-arguments-check

В Meteor предусмотрен механизм валидации данных, передаваемых серверным методам и функциям publish. Для этого предназначна функция check(), которой передается проверяемое значние и шаблон для проверки. Шаблон может быть явным указанием типа, либо объектом Match, определяющим более сложные правила проверки (см.http://docs.meteor.com/#match)
Установка пакета audit-argument-checks блокирует выполнение методов и функций публикации, которым были переданы данные, не прошедшие валидации.
Если валидация не требуется, можно вызвать функцию check со следующим параметром

check(arguments, [Match.Any])

Добавим пакет

$ mrt add audit-argument-checks 

Теперь попытка вызвать серверный метод вернет ошибку:

> Meteor.call('testMethod', 'test data', function(err, result) {console.log(err, result);})   undefined   errorClass {error: 500, reason: "Internal server error", details: undefined, message: "Internal server error [500]", errorType:"Meteor.Error"…}   undefined  

И на сервере:

 Exception while invoking method 'testMethod' Error: Did not check() all arguments during call to 'testMethod' 

После добавления валидации в серверный метод он вновь начнет корректно отрабатывать:

  check(data, String); 

Вместо заключения

Пытаясь поделиться своими наработками я совершенно не заметил, насколько большой по объему получился материал, при том, что по сути своей смог затронуть только очень и очень небольшую часть Meteor.
Надеюсь, этот текст поможет поближе познакомиться с Meteor и узнать о нем что-то новое.

ссылка на оригинал статьи http://habrahabr.ru/post/211020/

Контроллер центральный домашний, всемогущий КЦД-В-2-12

История появления на свет центрального домашнего контроллера довольно запутанна. Мне кажется, если на секунду отвлечься и представить его в виде зимнего леса (вид сверху), то можно будет увидеть беспорядочные тропинки, плохо замаскированные ямы и, возможно, бродящего где-то в глуши И. Сусанина.

Функционал наращивался постепенно: сначала подключил беспроводные розетки, потом замахнулся на выключатели света. Аппетиты росли — датчики протечки, задымления, дверей, метеодатчики, радиореле и управление AV-техникой. Мастерство росло не так быстро. Поэтому получилось то, что получилось: вещь, бесконечно далекая от гайдлайнов по программированию и устройству электронных схем, но вполне работоспособная.

И знаете что? Меня это устраивает.

Если вы вдруг пропустили содержание первой серии, на всякий случай напоминаю. Этот текст будет не столь зажигательным, поскольку больше инструкция. К тому же, в самом начале я планирую масштабный «отказ от гарантий». Дело в том, что я исключительно далек и от схемотехники, и от программирования. Поэтому то, что у меня получилось, получилось именно так, как получилось (я хотел использовать еще больше получилось, но не получилось).

Я, разумеется, попробую ответить на вопросы «почему так, а не так?», но во многих случаях это будет похоже, как если бы в спросили у блондинки, почему она включила правый поворотник, и ушла с крайнего правого ряда своей полосы на крайний правый ряд встречной. Практически все, что сейчас есть действует по основополагающему принципу «Работает? Не трогай!».

По этой же причине я покажу код и схему в оригинальной конфигурации с минимально возможными изменениями (в основном, анонимизация персональных данных). Это должно с высокой степенью вероятности позволить повторить ее в рабочем состоянии. А потом сможете все причесать и украсить, как захотите. Если, конечно, захотите повторять.

Пожалуйста, поймите, что я так и не дошел до модификации Arduino для загрузки скетчей через локальную сеть. И любая модификация кода оборачивается беготней с табуретками и головной болью с возвращением в работоспособное состояние датчика движения. Поэтому я априори буду в штыки воспринимать даже стоящие предложения. Скорее всего, потом остыну и не только приму к сведению, но и реализую ваши советы.

Поэтому заранее прошу прощения за возможно бурную реакцию.

Функционал

Итак, центральный контроллер. Для начала повторю его функции:

1) Управление четырьмя радиорозетками;

2) Управление 8 кнопками четырех радиовыключателей света;

3) Перезагрузка маршрутизатора при пропадании интернета;

4) Включение и выключение веб-камеры;

5) Контроль 8 беспроводных датчиков ОПС (три датчика задымления, три датчика протечки, пара датчиков открытия двери);

6) Получение данных с двух метеодатчиков и передача их в интернет;

7) Управление ТВ, медиа-плеером и кондиционером;

8) Управление кормушкой для котов;

9) Контроль проводного датчика движения;

10) Уведомление о событиях по электронной почте.

Сервисные функции (сценарии):

1) Автоматическое управление светом в гардеробе;

2) Автоматическое управление светом в прихожей;

3) Автоматическое управление ночным освещением на кухне;

4) Автоматическое управление светом и музыкой в ванной (на момент написания — два варианта: самим контроллером и командами контроллеру в ванной);

5) Выключение всего, что выключается дистанционно, включение камеры и переключение датчика движения в режим охраны при выходе из дома; и выключение камеры с включением фонового света, света в прихожей и переключением датчика движения в предыдущий, перед охраной, режим по возвращении.

Зачем мне два блока питания

Как ни парадоксально, но центральный контроллер подключен сразу к двум сетевым адаптерам, что связано с разным напряжением питания управляемых им веб-камеры и маршрутизатора.

Иными словами, блок 9В питает контроллер, а также коммутируется его реле на питание маршрутизатора, а блок 5В применяется для питания веб-камеры и вспомогательного питания периферии, потому что на всех выводов и возможностей Arduino не хватает.

Уже сейчас я понимаю, что хватило было бы одного достаточно мощного 9В блока и одного 5В стабилизатора. И, возможно, позже я к этому вернусь. А вы можете сразу исключить лишнюю железку из списка.

Кстати, любопытно, я только сейчас осознал весь трагический идиотизм ситуации. Блондинка, короче.

Железки

Для приготовления контроллера понадобится (ссылки исключительно для примера):

1) Arduino Uno — 1 шт.
2) Ethernet-шилд с чипом Wiznet W5100 — 1 шт.
3) ASK/OOK-приемник 433 МГц — 1 шт.
4) ASK/OOK-передатчик 433 МГц — 1 шт.
5) Датчик движения HC-SR501 — 1 шт.
6) Блок (не менее 2 шт. в блоке) реле 5В с ключами — 1 шт.
7) Пьезокерамическая пищалка — 1 шт.
8) Источники питания. Как минимум — 5В для Arduino и ее периферии. Лучше — 9В и стабилизатор 5В для периферии.
9) Антенны для приемника и передатчика, если их нет.
10) Макетные или иные провода, разъемы и корпус по вкусу.

В основном, проблем с подбором компонентов нет. Наиболее критичны приемник, передатчик (обычно продаются комплектами) и их антенны. На мой взгляд, кроме цены, особой разницы между суперрегенеративными и супергетеродинными приемниками нет (возможно я не умею их готовить).

. обычный передатчик. Надпись в расфокусе гласит FS1000A
image

Правда, мне понравился передатчик, купленный по случаю на DX. Он, кроме того, что дороже других, уже оснащен спиральной антенной и субъективно работает лучше, чем совсем дешевые передатчики, которые я пытался оснастить самодельными и спиральными «заводскими» антеннами.

. обычный суперрегенеративный приемник, на который я уже успел запаять «антенну»
image
А вот купленный там же супергетеродинный приемник, как я говорил выше, не впечатлил.

К вопросу антенн я, кстати, еще вернусь — это у меня было очень больное место. А пока напомню, что дешевые комплекты приемника и передатчика продаются без антенн. И это не значит, что так и должно быть. Это значит, что антенны вам придется или сделать самостоятельно, или купить готовые.

Пьезокерамическая пищалка получила это место потому, что ей не нужны дополнительные компоненты. Внутреннее сопротивление позволяет подключаться напрямую к Arduino. А еще она довольно громко орет и очень компактна.

И, чтобы два раза не вставать: Arduino Uno потому, что я выбирал первый контроллер. По этой же причине — сетевой шилд с W5100. Ведь судя по тогдашним мнениям, это были оптимальные вещи для начинающих. Я же не знал, что все зайдет так далеко.

. шилд вообще никак не мешает собирать прототип
image

Предназначение компонентов довольно прозрачно. Ethernet-шилд обеспечивает работу веб-сервера на Arduino и, соответственно, удаленное управление, конфигурирование и мониторинг состояния. Приемник слушает датчики, передатчик отправляет команды контроллера на исполнительные устройства (розетки, выключатели) и сервисные контроллеры.

Прикрепленный к контроллеру датчик движения используется для режимов охраны и автоматического управления светом. А пищалка информирует об успешном включении, приеме некоторых радиокоманд (например — подтверждает, что услышала кнопку включения кормушки, чтобы не давить ее до полного и окончательного удовлетворения) и применяется для настройки того же датчика движения (писк при срабатывании датчика здорово помогает правильно выставить направление, чувствительность и выявить ложные срабатывания).

Реле включают и выключают маршрутизатор и веб-камеру, что весьма актуально, если учесть, что у меня маршрутизатор скрыт мало того, что с глаз долой, так еще и на высоте выше моего роста. Результат — перезагрузка сети без проблем и физическое выключение камеры, когда она не используется, что дает некоторый психологический комфорт.

. удобный блок реле с прямым подключением к Arduino. Просто реле подключить нельзя — слишком велик потребляемый катушкой ток. А здесь на плате оптронная развязка и транзисторный ключ плюс светодиод-индикатор состояния
image

Сборка контроллера ничего экстраординарного не представляет. Только, разумеется, не забудьте сразу нацепить Ethernet-шилд на Arduino Uno и помните, что сетевой шилд использует пины 4, 10, 11, 12 и 13 Arduino Uno, поэтому они не используются в функциях контроллера. По счастью, у Uno остается вполне достаточно цифровых пинов и полный комплект аналоговых.

Текущая конфигурация отражена в скетче и, на всякий случай, здесь.

A0 — DATA датчика движения
2 — DATA приемника
5 — к управляющему входу блока реле маршрутизатора
6 — к управляющему входу блока реле камеры
8 — DATA передатчика
9 — к плюсу пищалки

Все GND (земля) всех компонентов и все VCC (плюсы) соединяем соответствующим образом. Т.е. земля общая у всех, а если напряжение питания различается, каждый компонент должен получать свое.

Для простоты и расширяемости я распаял на разъеме 5В сразу несколько (четыре, что ли) отрезка макетного провода с «мамами», чтобы можно было, если что быстро подключить к питанию новые компоненты. Причем вышло, как обычно у меня — по дурацки. Не сразу распаял, а когда понял, что питающих пинов Arduino не хватает.

Другая важная особенность — подключение периферийных устройств к реле. Камеру я подключил к нормально разомкнутым контактам, а маршрутизатор — к нормально замкнутым. Так что в исходном состоянии системы маршрутизатор всегда включен, а камера — выключена. Но есть у меня подозрение, что и здесь что-то недодумал, поскольку забыл проверить, что случается при потере питания контроллером. Просто имейте в виду эту особенность.

. в итоге должно получиться что-то вроде этого. А это, судя по всему, КЦД-В-1-1 (первая аппаратная и софтовая версия контроллера). Приемник для простоты замотан в черную изоленту — а то мало ли что.

image

Для ориентира: стоимость первой версии контроллера, включая корпус и разъемы, составила около $60. Это уже потом я пошел в разнос и стал экспериментировать с УЗ-датчиком и в отчаянии накупил кучу приемников-передатчиков и антенн, чтобы, наконец, решить проблемы неуверенного приема и передачи сигналов.

Уже все собрали? Поздравляю! Теперь самое время еще два раза проверить подключение сигнальных проводов и три — питания. Словами не передать, сколько раз я ошибался в рутинных операциях, особенно, если при перекомпоновке приходилось по нескольку раз менять подключение компонентов. Не хочу, чтобы то же самое случилось с вами.

Периферия

Действующий состав периферийных устройств и взаимодействие с ними — цепь случайных совпадений и, в общем, достояние истории. Так как я сначала купил радиорозетки и выключатели, то сначала танцевал от них, а потом — к новым исполнителям с учетом того, что уже было.

Радиорозетки — самые простые (мои были куплены в Leroy Merlin, под их же маркой). Существуют под массой разных имен, а суть одна: обычно в комплекте три розетки и пульт (почему-то на четыре розетки). На спине розеток — переключатель, который выбирает канал. Если все поставить на один, то будут включаться одной кнопкой на пульте, что подходит для организации групп. Причем команды включения и выключения разные, поэтому за один канал-группу отвечает пара кнопок. Это удобно — ведь обратной связи нет, поэтому при любых сомнениях можно просто повторить команду на включение (выключение) и уж с десятого раза она точно дойдет. Рабочая частота — 433 МГц.

. это, надеюсь, в последний раз, когда пощу розетки, больше не буду
image

Максимальная коммутируемая мощность — что-то около киловатта. Я их использую для управления фоновым светом и включения-выключения планшета и акустики на кухне.

.
image

Вторым номером идут двухкнопочные радиовыключатели Livolo (есть и другие варианты, но я обещал про свою конфигурацию). Их я купил как упомянутая выше блондинка. Увидел и все — ми-ми-ми и хочу-не-могу. Влюбился с первого взгляда. Тоже, как выяснилось, 433 МГц и тоже обратной связи нет.

.
image

Плюсы выключателей в том, что они идеально (на мой вкус) выглядят и исключительно просто (даже с моим радиусом кривизны рук) монтируются. Еще лучше то, что они ориентированы на двухпроводную систему подключения и не требуют дополнительного питания, поэтому органично заменяют обычные клавишные выключатели. У меня Livolo работают и с энергосберегающими лампами, и со светодиодными (12В с трансформатором и 220В) — ни те, ни другие не мерцают в выключенном состоянии.

. стандартная круглая коробка. Для эстетов бывают в квадратном UK-исполнении
image

Минус, однако, в том, что для включения и выключения используется одна и та же команда. Поэтому в отсутствие обратной связи и в случае помех или каких-то сбоев невозможно однозначно выключить или включить какой-либо выключатель. Положение, впрочем, спасает штатная команда одновременного выключения вообще всех привязанных к конкретному пульту выключателей (чем я пользуюсь при выключении электроприборов и света, когда выхожу из дома).

В результате на руках имею одну несущую частоту и два идентичных по сути, но разных по исполнению и, как следствие, несовместимых радиопротокола. Один — для выключателей света, другой — для радиорозеток и датчиков. Вот последний и вынесем на обсуждение коллективом в первую очередь.

Розетки хотя, повторюсь, и просты до безобразия, но довольно эффективны. Внутри — простой преобразователь напряжения, обычный (точно такой же, как вы купите для Arduino) приемник, микросхема-декодер и реле. В пульте — кодирующая микросхема и передатчик. Особенность протокола в том, что он подразумевает многократное повторение одной и той же кодовой посылки (уникальный номер устройства), пока нажата кнопка пульта или пока устройство (например, датчик двери) находится в активном состоянии.

. не очень богатый, но функциональный внутренний мир розетки
image

Эта схема получила исключительно широкое распространение во всевозможных сигнализациях и исполнительных устройствах, и реализуется на нескольких наиболее распространенных чипах. Обычно они ищутся по названиям SC2260/SC2262 или PT2260/2262, а для Arduino существует библиотека RC-Switch, которая без проблем управляет всеми подобными совместимыми чипами.

. SC2262 в пульте ее. Что и требовалось
image

Полагаю, что когда эта информация устаканилась в моей голове, и случилось озарение. Я тогда сделал три вещи: подключил приемник к Arduino, загрузил пример кода-приемника и нажал кнопку домашнего беспроводного звонка. Затем посмотрел на экран лаптопа и забил в поиск Aliexpress что-то на тему беспроводных датчиков. И почувствовал себя, как в магазине игрушек, честное слово.

Представьте: я понял, что смогу получать уведомления о том, что кто-то нажал на кнопку звонка, и могу подключить массу различных датчиков, причем вообще без хлопот. Потому что эти датчики совершенно автономны, очень долго работают от одной батарейки и не требуют вообще никаких проводов.

Пришлось собрать глаза в кучку, обуздать желания и заказать то, что действительно необходимо (ссылки архивные, как отправная точка):

1) Датчики задымления — 3 шт.
2) Датчики протечки — 3 шт.
3) Датчики двери — 2 шт.
4) Датчик движения — 1 шт.

Можете не совершать мою ошибку, и не заказывать датчик движения. Поясню. Эта штука эффективна исключительно в охранных системах (где активное движение — признак тревоги, а не обычной жизни), а в умном доме беспроводной датчик движения будет только мешаться. Все потому, что он «кричит» каждый раз при срабатывании, и тем самым, во-первых, отвлекает контроллер, а, во-вторых, создает помехи остальным датчикам.

Очевидно, что таких «отвлеченных моментов» будет крайне много для нормальной работы контроллера, ведь не секрет, что люди имеют обыкновение ходить по дому. А штатной возможности дистанционно выключать этот датчик на время нет.

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

По факту можете получить провод длиной сантиметров 20, а вместо платы с дорожками — коробочку, внутри которой термоклеем закреплен зачищенный огрызок того же провода. С одной стороны, принципиальной разницы нет, поскольку все работает. А с другой — крепить такое неудобно, да и пока эта коробка водой наполнится (а то еще и всплывет) — поздно будет пить Боржоми.

. внутри моих датчиков протечки и двери чип CS5211AGP, который не упоминается в списке совместимости RC-Switch, но работает с ней. Это чтобы вы понимали, насколько популярный протокол.
image

Другой вариант китайского креатива — пропихнуть под «датчиком воды» датчик переполнения резервуара. Там вместо дорожек — корпус с поплавком. В корпусе расположен геркон, в поплавке — магнит. Дальше, полагаю, объяснять не надо. Для регистрации протечек эта штука совершенно не подходит.

Отсюда вывод: признаки расово правильного датчика протечки — длинный провод к сенсору влажности и сенсор, выглядящий как плата с двумя вытравленными дорожками, расположенными на небольшом расстоянии друг от друга.

. расово верный датчик протечки, рядом с ним — датчик двери

Датчики задымления я разместил на кухне, в прихожей и в комнате. Датчики протечки — под раковиной на кухне, в сантехническом коробе в ванной и под стиральной машиной.

. датчик задымления на месте
image

Датчики двери использовал на дверях гардероба и ванной комнаты для управления светом. Сейчас, впрочем, датчик в ванной комнате не нужен — там светом рулит выделенный контроллер.

. датчик двери на месте. Родную его антенну оторвал во время антенной истерики. Теперь там кусок провода совершенно произвольной длины. Тем не менее, работает. Потому и не трогаю. Этот датчик закреплен на торце полки в гардеробе. А мимо катается раздвижная дверь, на которую наклеен магнит.
image

Завершают список периферии три кнопки и веб-камера.

1) Кнопка беспроводного звонка (433 МГц) для уведомлений о звонящих в дверь.

2) Беспроводная кнопка «паники» от сигнализации (она используется для управления кормушкой котов).

. вот такая «паника»
image

3) Кнопка присутствия для переключения режимов охраны и света при уходе и возвращении домой. В целом аналогична кнопке «паники», но свою я переделал из кнопки звонка (купил 315 МГц, не посмотрев) и китайского пульта-брелка. Собственно, кнопку звонка покупал потому, что она была привлекательна внешне и удобна — большая мягкая клавиша. Сама переделка упоминания не стоит — лучше купить готовую (звонок или панику).

. Я дома. Симпатичнее паники по-любому

Принцип работы и подготовка периферии для контроллера

Принцип работы беспроводных датчиков следующий:

1) Датчик задымления издает звуковой сигнал и постоянно передает сигнал по радио, пока превышен порог задымления. Если честно, натурных испытаний не проводил — пользовался кнопкой теста. Орет сильно.

2) Датчик двери передает свой сигнал в течение двух секунд после срабатывания.

3) Датчик протечки передает свой сигнал все время, пока превышен порог влажности. Как высохнет — замолкает.

Автономность датчиков приличная. С момента покупки в июне прошлого года батарейки пришлось менять только в датчиках дверей — но они же и открываются не один раз в день.

4) Кнопки передают сигнал, пока нажаты.

Собственно сам сигнал представляет собой уникальную кодовую посылку длиной 24 бита. Обычно ее можно менять по желанию: на плате есть или обычные перемычки, или же площадки, которые можно перемкнуть припоем. Посылка монолитна в эфире, но структурно состоит из двух частей: адреса устройства и уникального номера. Адрес обычно используется в системе сигнализации, что позволяет логически объединять датчики в охранные зоны.

Мне эти зоны не нужны, поэтому я всю посылку считаю уникальным номером и слежу разве что за тем, чтобы эти номера были действительно уникальными. То есть — чтобы не повторялись среди моей периферии, и чтобы у соседей не было похожих (это можно вычислить по ложным срабатываниям, чего, впрочем, у меня не случалось).

Именно эта кодовая посылка используется для идентификации центральным контроллером сработавшего датчика. Иногда адрес и номер пишут на корпусе датчика. Но лично я узнал числовые имена датчиков с помощью Arduino, приемника и библиотеки RC-Switch. Нужно просто подключить приемник к контроллеру (по умолчанию DATA приемника к 2 цифровому пину) и загрузить пример ReceiveDemo из папки RC-Switch.

Затем по очереди активируем датчики и записываем кого как зовут. Листочек не выкидываем — пригодится при адаптации кода центрального контроллера.

Реализацией этого протокола в RC-Switch я воспользовался и для других вещей: придуманные уникальные номера использую как команды и транспорт данных для сервисных контроллеров. Например, центральный контроллер говорит 012345678, а сервисный контроллер получает этот код и знает, что ему нужно отправить команду на включение ТВ. Или вот, к примеру метеодатчик выдает трель вида 1281265739, а центральный контроллер знает, что 1281 — это имя комнатного датчика, 265 — это температура (только поделить на 10), а 739 — атмосферное давление.

То есть вполне себе двусторонний протокол, хотя и с небольшой скоростью и без коррекции ошибок.

С выключателями Livolo почти так же, но не совсем. Кодовая посылка все так же делится на две части, но первая — уникальный идентификатор пульта ДУ, а вторая — код нажатой на пульте кнопки. Так как выключатели обучаемые, то им совершенно неважно какой у пульта идентификатор (они его запомнят при знакомстве), а коды кнопок формируются по очень простому правилу.

Что мы в итоге имеем? Если есть пульт и хочется использовать его вместе с центральным контроллером, то необходимо выяснить уникальный идентификатор пульта. И здесь нам поможет вот этот код. Все аналогично RC-Switch: подключаем приемник, загружаем код, жмем на кнопку пульта и записываем идентификатор и код кнопки. Повторяем со всеми кнопками, которые собираемся использовать.

Если же пульта нет, и необходимости в нем тоже нет, тогда достаточно просто библиотеки для управления Livolo. В комплекте с ней — описание использования и пример, а также коды ряда кнопок. Таким образом можно сделать совершенно произвольное количество пультов и, подозреваю, произвольное количество кнопок, если соблюдать правила (16 бит — идентификатор пульта, 7 бит — код кнопки).

О том, как появилась эта библиотека я постараюсь рассказать в отдельном тексте. А для наиболее пытливых умов сообщаю, что хотя «виртуальные» пульты действительно работают, по какой-то причине подходят не все 16-битные идентификаторы пульта.

Наконец, веб-камера. У меня простая и купленная давным-давно D-Link DCS-930. Особенностей у нее нет, разве что угол обзора объектива увеличен с помощью широкоугольной насадки (рыбий глаз) для мобильного телефона.

Антеннам посвящается

Сначала я использовал в качестве антенны обычные отрезки макетных проводов длиной 173 мм — под четверть длины волны. И до определенного момента это работало, но с увеличением количества беспроводных периферийных устройств стали случаться чудеса. Периферия то работала, то не работала.

Было очевидно, что проблема или в коде, или в приемо-передающей части. Код я проверил, и ошибок найти не смог. Значит, дело в радиоканале. Чтение текстов, посвященных диапазону и типам антенн 433 МГц меня не обрадовало: оказалось, что связь на этой частоте довольно капризна, а волны имеют свойство здорово отражаться от ближайших поверхностей и складываться вплоть до самоликвидации.

Одновременно я выяснил, что, возможно, не слишком удачно выбрал длину. Один товарищ на DIY-форуме рекомендовал использовать «электрическую» длину проводника, что по его расчетам в среднем составляло 0,95 от расчетной. То есть, если 1/4 волны 433 МГц = 173 мм, то электрическая длина — 165 мм. Антенны я немедленно переделал, но особой эффективности не заметил.

На следующем этапе я решил попробовать спиральные антенны. Про них говорили, что они гораздо менее капризны, чем штыревые (кусок провода — суть аналог штыревой в отвратном исполнении), хотя и чуть менее эффективны. В спираль я скручивал те же 165 мм отрезки, но уже первого попавшегося под руку провода, который можно было скрутить, то есть классический электрический кабель сечением 1.5 мм2.

. во как мою антенну (в центре) скрутило
image

Удивительно, но эти штуки были уже гораздо стабильнее и лучше. Но все равно не очень, да и эстетика, опять же. Поэтому в итоге я заказал на Ebay пачку готовых спиральных антенн.

. а это заводская. Хотя все мы знаем, какие в Китае заводы
image

В целом они решили мои проблемы, хотя чудес не случилось. Интересно и то, что вне зависимости от того, какие антенны я использовал в контроллере, дополнительных сервисных контроллерах и самодельных периферийных устройствах, заводские беспроводные датчики и выключатели работали стабильнее.

Поэтому резюме: крайне важны хорошие антенны и приемники-передатчики, а также, по возможности, продуманное расположение центрального контроллера относительно периферии.

Резюме второе: если что-то идет не так, возможно, что дело не в коде, а в радиоканале.

Планировка событий

На момент сборки у меня не было часов реального времени, а переключать режимы автосвета в зависимости от времени суток хотелось. А когда часы появились, то уже не было места на библиотеку для них. Поэтому я воспользовался очевидным (для себя) решением — аппом Tasker, который давным давно поселился в смартфоне.

Идеология простая.

1) Завожу профили в с контекстом, где указан необходимый временной интервал.
2) Для каждого профиля добавляю необходимые задачи (Task) с действиями из категории Сеть (Net) — HTTP Post.
3) В строке адреса для каждого POST-запроса указываю адрес контроллера, включая команду, которую нужно выполнить. Например: 192.168.10.48/?Switch1-on

Сценарий получается, если в одной задаче собрать несколько действий. Иными словами, часть логики выносится за рамки контроллера, что дает весьма ощутимые плюсы:

1) Реализация сценариев без расхода ресурсов контроллера
2) Элементарное добавление новых сценариев
3) Простое и быстрое изменение настроек
4) Всегда точное время

Минусы тоже есть. Если Android с логикой на борту разрядится или зависнет (а то и сеть пропадет) — сценарий не будет выполнен.

С помощью этого решения у меня переключаются режимы дневного и ночного автоматического света. То есть, «Автосвет день» = включение автоматического света в прихожей, разрешение музыки в ванной и выключение автоматического света на кухне. А «Автосвет ночь» — противоположное действие.

Кроме того я попросту вытащил некоторые полезные (для меня) функции контроллера в виде ярлыков задач на один из экранов смартфона. Так достаточно просто одного нажатия, как на обычную сенсорную кнопку. И никаких длинных адресов в браузере.

В конечном итоге эта «логика» переехала со смартфона в Android-брелок, подключенный к ТВ. Так и за сеть можно меньше переживать, и за питание.

Основные алгоритмы

Сейчас я пройдусь по коду и постараюсь описать хотя бы основные алгоритмы работы контроллера, чтобы стало немного яснее, что же я все-таки имел в виду.

Включение

Успешное включение контроллера подтверждает короткий двойной звуковой сигнал. Это означает, что контроллер перешел в дежурный режим. Одновременно на сервисный адрес почты отправляется уведомление «System started».

При включении устанавливаются следующие основные параметры:

— «Я дома» — истина
— Автосвет в прихожей — выключен
— Автосвет на кухне — выключен
— Автосвет в ванной — включен
— Время поворота сектора кормушки — 15 секунд
— Веб-камера — выключена
— Состояние датчиков задымления и протечки — нет тревоги
— Музыка в ванной — выключена, разрешена
— Датчик движения в прихожей — включен
— Режим отладки для датчика движения — выключен

Периодические действия

С интервалом в две минуты контроллер проверяет наличие интернета: выполняет подключение к Яндексу. Если подключились — не будет ничего. А если нет — выполняется перезагрузка маршрутизатора выключением и включением.

Всего выполняется две перезагрузки подряд. Если и после этого интернет не появился — таймаут в час. Потом снова проверка.

Второе периодическое действие — отправка погоды в интернет с интервалом в один час на сервис "Народный монитор". Используется протокол HTTP, метод POST. Для использования этой функции вам понадобится эккаунт на сайте сервиса.

Отправляются показания любого из датчиков при условии, что датчики передали все параметры, которые могут передать. Если датчик не передал один из параметров, считается, что показаний нет (отказ датчика).

Если показания передали оба датчика, в интернет передаются оба набора одновременно.

Данные от метеодатчиков передаются в контроллер по радиоканалу с помощью примитивного протокола на основе RC-Switch:

1) Комнатный датчик передает числа вида:

а) Давление: 1210XXXX, где XXXX — атмосферное давление в hPa с точностью до целых.
б) Температура: 1310SXXX, где S — 1 (ниже нуля) или 0 (выше нуля) в зависимости от знака температуры, а XXX — температура с точностью до десятых, умноженная на 10.

2) Уличный датчик передает числа вида 161HSXXX, где H — признак влажности, S — знак температуры, XXX — значение климатического параметра с точностью до десятых, умноженное на 10.

Если H = 1, контроллер считает, что переданы показания влажности. Если H = 0, контроллер думает, что получил температуру.

Оба параметра передаются каждым метеодатчиком с небольшим интервалом.

Готовый POST-запрос для двух датчиков выглядит примерно таким образом:

POST http://narodmon.ru/post.php HTTP/1.0 Host: narodmon.ru Content-Type: application/x-www-form-urlencoded Content-Length:51  ID=MAC00&MAC01=24.6&MAC02=1025 

Здесь MAC00 — логический адрес головного устройства, MAC01 — логический адрес датчика температуры, MAC02 — логический адрес датчика давления. При этом физически датчики температуры могут быть одним или разными устройствами. Например, мой домашний датчик использует сенсор BMP085, который одновременно измеряет температуру и атмосферное давление. При этом для него назначается два адреса (MAC01 и MAC02), а адрес головного устройства это вещь, в общем, не привязанная ни к чему. Это просто адрес некоего виртуального владельца отдельных датчиков.

Действия по датчику движения

Если датчик выключен — действия не выполняются. Иначе выполняются следующие действия.

1) Если выключено автоматическое управление светом и если дома кто-то есть, датчик не выполняет никаких функций.

2) Если включен автосвет в прихожей, однократный проход вызывает включение света в прихожей на 1 минуту. Повторный проход в течение этой минуты продлевает время работы света до 3 минут. Каждый последующий проход аналогично продлевает время работы света до трех минут. Если в течение трех минут движение не зафиксировано, свет выключается.

3) Если включен автосвет на кухне, проход вызывает включение света на кухне на 10 минут. Каждый повторный проход не изменяет время работы света, но приводит к выдаче команды на его включение. Причина в том, что иногда свет на кухне не включается с первой попытки (радиоканал или занятость контроллера). Тогда, если еще из коридора видно, что свет не включился, достаточно сделать шаг назад — под датчик.

4) В отсутствие дома людей срабатывание датчика приводит к уведомлению об этом событии по электронной почте.

5) При включенном режиме отладки датчика движения каждое зафиксированное движение подтверждается звуковым сигналом. Если одновременно включен автосвет на кухне, дополнительно при каждом срабатывании датчика на 30 секунд включается свет на кухне. Это для более наглядной отладки и заодно было для отладки автосвета на кухне.

Действия по датчику двери в гардеробе

Каждое открытие двери приводит к выдаче команды на выключатель света. Каждая такая команда включает или выключает свет, в зависимости от того, в каком состоянии он был до выдачи команды.

Особенность алгоритма в том, что включение света производится с задержкой в 1.7 секунды. Это необходимо, так как датчик «кричит» порядка 2 секунд, и если выдать команду на включение света сразу же, то контроллер может успеть «поймать» конец трели датчика, и еще раз переключит свет. Задержка не создает особого дискомфорта, зато предотвращает выключение света сразу после включения.

Аналогичный алгоритм работает и в случае использования автосвета по датчику двери в ванной.

Действия по кнопке «Я дома»

Нажатие кнопки при выходе из дома приводит к выполнению следующего сценария:

1) Пауза в 1 минуту
2) Выключение всех радиорозеток
3) Выключение всех радиовыключателей света
4) Выключение автосвета в прихожей, если он был включен
5) Выключение музыки в ванной
6) Переключение датчика движения в режим охраны с уведомлением о срабатываниях по почте
7) Включение веб-камеры с уведомлением по почте

Нажатие кнопки по возвращении домой выполняет другой сценарий:

1) Выключение веб-камеры с уведомлением по почте
2) Включение фонового света в комнате
3) Включение фонового света на кухне
4) Включение автосвета в прихожей, если он был включен перед уходом; или включение света в прихожей, если автосвет в прихожей был выключен на момент ухода из дома

Кнопка работает по принципу триггера: каждое нажатие переключает режим на противоположный (дома-не дома).

Действия по кнопке «Кормушка»

Кормушка (модифицированная из обычной Feed-Ex) представляет собой миску с четырьмя отделениями-секторами, закрытыми крышкой. Вырез в крышке открывает доступ только к одному сектору. Сектора заполняются кормом через один. Таким образом, каждый поворот на сектор или открывает корм, или прекращает к нему доступ.

. кормушка в исходном состоянии
image

Это не живодерство, а насущная необходимость, поскольку одного кота нужно держать на диете, и нельзя оставлять корм в доступе все время.

. принцип заполнения
image

При нажатии на кнопку «Кормушка» выполняется поворот кормушки на один сектор (по таймеру) и задержка в этом состоянии на 2.5 минуты, чтобы коты успели дойти и поесть.

Через 2.5 минуты выполняется поворот кормушки еще на сектор (пустой). Таким образом кормление прекращается.

Если кнопка нажата повторно до истечения 2.5-минутного интервала, выполняется немедленный поворот на сектор (закрытие), а поворот по таймеру отменяется. Это процедура экстренного закрытия, если кто-то рядом с кормушкой и видит, что к ней приложился не авторизованный для этой операции кот.

Действия по срабатыванию датчика двери в ванной

При срабатывании датчика выдается команда на переключение света и включение музыки, если включение музыки разрешено. При повторном срабатывании датчика снова выдается команда на переключение света и на выключение музыки.

В случае, если во время работы света и музыки контроллер получает команду на запрет музыки в ванной, музыка выключается.

Действия по срабатыванию датчиков задымления и протечки

1) При срабатывании датчика задымления контроллер уведомляет по почте письмом с указанием места расположения датчика

2) При срабатывании датчика протечки контроллер уведомляет по почте письмом с указанием места расположения датчика, одновременно выдается 10 звуковых сигналов с интервалом в 10 секунд, так как датчики протечки не имеют функции звукового уведомления

3) Срабатывание любого датчика считается критическим событием, после которого прием сигналов от датчиков блокируется до выполнения сброса состояния датчиков через веб-интерфейс.

Причина блокировки в том, что при срабатывании датчики передают сигнал постоянно. Это значит, что контроллер (без блокировки) будет реагировать на него сразу, как освободится от предыдущего уведомления по почте. Особого смысла я в этом не вижу, поэтому — блокировка до ручного сброса.

Действия при получении команд для аудио- видеотехники

Контроллер передает с помощью библиотеки RC-Switch числовую команду для медиаконтроллера. О медиаконтроллере — в будущих выпусках 🙂

Отправка почты

В отсутствие парка почтовых голубей в специально оборудованных клетках с электромеханическими замками, у всех возникает понятное желание воспользоваться для отправки писем простым и довольно удобным протоколом SMTP. Однако очень многие почтовые сервисы перешли на авторизацию, защищенную SSL, а у Arduino попросту нет на это сил.

Решение? Идем на компромисс с безопасностью, ищем и эксплуатируем почтовый сервер, способный на открытую авторизацию. Именно поэтому я использую Mail.Ru — он отвечает требованиям.

Я прекрасно знаю недостатки текущей реализации — она просто останавливает всю активность на время отправки письма за счет задержек delay, необходимых, чтобы гарантированно дождаться ответа сервера. Интерактивная схема с чтением полученных от сервера строк была бы немного быстрее, но у меня сразу не получилось ее «завести». А вот этот алгоритм, подхваченный на форумах Arduino.cc, заработал сразу.

По этой, кстати, причине, уведомление о срабатывании датчика движения выдается с 30-секундной задержкой. Так сделано для того, чтобы было время нажать на кнопку «Я дома» при возвращении. Если этой задержки нет — при входе сразу же отправляется письмо, и контроллер перестает реагировать на внешние раздражители в течение примерно десятка секунд.

В итоге для почтовых уведомлений вам понадобится:

1) Ящик на Mail.Ru
2) Логин в кодировке BASE64
3) Пароль в кодировке BASE64

Для того, чтобы закодировать свои логин и пароль можно воспользоваться одним из многочисленных (тысячи их) онлайновых сервисов.

Веб-интерфейс

Страничка по адресу контроллера отображает последнее значимое состояние (согласно списку строк состояния в скетче), а также последние полученные данные метеодатчиков.

Скетч всемогущий

Ну что тут скажешь? Смотрите и трепещите, поскольку зрелище это столь же увлекательное, сколь, полагаю, ужасное — с точки зрения любого более-менее адекватного программиста.

Функции, переменные и проч, и проч, добавлялись по мере необходимости, что объясняет абсолютную нелинейность кода. Тем не менее я постарался максимально подробно комментировать практически все, что делал. В первую очередь — для себя. Потому что через неделю уже забываю что, зачем и почему именно так, а не иначе.

Для корректной работы скетча требуется несколько нестандартных библиотек. Не забудьте скопировать их в папку libraries вашего каталога среды Arduino:

1) RS-Switch для управления розетками, радиореле и обмена с метео- и сервисными датчиками.

2) Livolo для управления одноименными выключателями света.

3) SimpleTimer для отсчета интервалов времени.

Адаптация кода, если я ничего не путаю, включает в себя следующие изменения:

1) Определения пинов. Если вы захотите подключить периферию Arduino по-другому — придется поменять, а если подключите, как в коде — то и трогать не надо. Для удобства пины определены в секции переменных, остальной код шерстить не нужно.

2) Параметры сети.

3) Параметры почты (процедура sendMail).

4) Секция обработчика сигналов беспроводных датчиков. Здесь необходимо заменить номера датчиков на свои.

5) Команды периферийных устройств в коде. Их можно найти по названию процедуры «передатчика» txSwitch (это для розеток, радиореле и сервисных контроллеров) и по названию функции библиотеки livolo (для выключателей света).

6) Секция параметров погодных и климатических данных на сервис "Народный монитор".

7) Секция обработки команд веб-интерфейса. Здесь можно поменять имена команд на те, что вам больше нравятся. А можно не менять.

Критически важные пункты: 2 — 6. Если не настроите сеть и почту (для почты еще понадобится эккаунт на почтовом сервисе с SMPT и открытой авторизацией) — не будет дистанционного управления и уведомлений по почте. Если не измените идентификаторы датчиков на свои — контроллер их не «услышит». А если не поменяете команды розеток — то не сможете ими управлять. Для Livolo, впрочем, коррекция кодов не является жизненно важной, потому что если поставить выключатель в режим обучения, то он подхватит команду «на лету».

Аналогично, если захотите выдавать погоду в интернет, необходимо зарегистрироваться на сайте «Народного монитора» вбить MAC своего головного устройства (контроллера) и датчиков. Адреса можно взять произвольные. Берете, к примеру, старую сетевую карту или любой другой сетевой девайс, ищете наклейку с MAC и берете его как адрес головного устройства. А потом меняете последние (или какие вообще нравятся) разряды, чтобы получить необходимое количество адресов. Для текущего кода всего нужно пять адресов: головное устройство и четыре датчика (каждый параметр считается датчиком, а их четыре: температура внутри и снаружи, давление и влажность).

Ядро скетча составляют веб-сервер с обработчиком команд веб-интерфейса и обработчик сигналов от беспроводных датчиков. Все остальное — что-то вроде периферийной обвязки.

Последнее: я постарался ничего не испортить при обезличивании. Но если испортил — прошу прощения, и будем разбираться вместе.

А теперь слайды.

/*  15.08.2013 - новая процедура выдачи команд на розетки 22.09.2013 - новая процедура управления светом в коридоре (всегда включение на минуту, при повторном проходе таймаут 3 мин) 26.09.2013 - апдейт управления светом в коридоре, вместо счетчика простые признаки света 27.09.2013 - флаги вместо выключения приема, потому что при выключенном приемнике нет реакции на другие события и вообще это неправильно 29.09.2013 - увеличено время работы кормушки, увеличено время таймаута в ванной до 1,6 сек., увеличено время передачи Livolo (180 вместо 150) 30.09.2013 - минимум переменных 06.10.2013 - снова выключение приемника вместо флагов 27.09.2013, а то свет в шафу работает отвратно 26.10.2013 - новые процедуры отправки климатических данных и управления Livolo 28.10.2013 - удален вывод в OWM, автосвет в коридоре и ванной переведен на медиаконтроллер в комнате 18.11.2013 - добавлена куча выключений приема RC-Switch, чтобы прерывания не мешали коду; автосвет: добавлено включение света на кухне при каждом проходе 22.11.2013 - вернул задержки при выключении освещения, добавил дублирование "выкл. все" Livolo в swOff  29.11.2013 - вернул управление светом в гардеробе и ванной в центральный контроллер 27.01.2014 - управление светом в ванной отдельным контроллером, новые команды для web (musicOn, musicOff)  A0 - PIR-датчик зеленый (минус - коричневый/оранжевый, плюс - красный) A1 - серый (свободный) 0 - свободный 1 - свободный 2 - приемник 3 - резерв под датчик движения в коридоре 4 - Ethernet 5 - модем 6 - камера 7 - свободный 8 - передатчик 9 - пищалка 11, 12, 13 - Ethernet */  #include <SPI.h> #include <Ethernet.h> #include <avr/pgmspace.h> // для PROGMEM #include <RCSwitch.h> //   http://code.google.com/p/rc-switch/ #include <SimpleTimer.h> // http://playground.arduino.cc//Code/SimpleTimer // #include <Twitter.h> //http://playground.arduino.cc/Code/TwitterLibrary #include <livolo.h>    // инициализация сети byte ip[] = { 192,168,10, 48 };                        // IP Address byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // MAC Address byte servergoogle[] = { 213, 180, 204, 3 }; // Yandex byte servermail[] = { 94, 100, 177, 1 }; // Mail.Ru byte gateway[] = { 192, 168, 10, 1 }; byte subnet[] = { 255, 255, 255, 0 }; byte mydns[] = { 192, 168, 10, 1 };  // строки состояния в PROGMEM prog_char statusString_0[] PROGMEM = "Room softlight ON"; prog_char statusString_1[] PROGMEM = "Kitchen softlight ON"; prog_char statusString_2[] PROGMEM = "Humidifier ON"; prog_char statusString_3[] PROGMEM = "Socket #4 ON"; prog_char statusString_4[] PROGMEM = "Room softlight OFF"; prog_char statusString_5[] PROGMEM = "Kitchen softlight OFF"; prog_char statusString_6[] PROGMEM = "Humidifier OFF"; prog_char statusString_7[] PROGMEM = "Socket #4 OFF"; prog_char statusString_8[] PROGMEM = "Room main light #1"; prog_char statusString_9[] PROGMEM = "Room main light #2"; prog_char statusString_10[] PROGMEM = "Hall light"; prog_char statusString_11[] PROGMEM = "Wardrobe light"; prog_char statusString_12[] PROGMEM = "Bathroom light"; prog_char statusString_13[] PROGMEM = "Kitchen main light"; prog_char statusString_14[] PROGMEM = "All main light OFF"; prog_char statusString_15[] PROGMEM = "System started"; prog_char statusString_16[] PROGMEM = "Camera ON"; prog_char statusString_17[] PROGMEM = "Camera OFF"; prog_char statusString_18[] PROGMEM = "Router reset"; prog_char statusString_19[] PROGMEM = "Doorbell"; prog_char statusString_20[] PROGMEM = "Water leak: KITCHEN"; prog_char statusString_21[] PROGMEM = "OK"; prog_char statusString_22[] PROGMEM = "Motion"; prog_char statusString_23[] PROGMEM = ""; prog_char statusString_24[] PROGMEM = "Water leak: WASHER"; prog_char statusString_25[] PROGMEM = "Water leak: MAIN"; prog_char statusString_26[] PROGMEM = "Smoke: KITCHEN"; prog_char statusString_27[] PROGMEM = "Smoke: HALL"; prog_char statusString_28[] PROGMEM = "Smoke: ROOM"; prog_char statusString_29[] PROGMEM = "Sensors RESET"; prog_char statusString_30[] PROGMEM = "Kitchen auto light ON"; prog_char statusString_31[] PROGMEM = "Kitchen auto light OFF"; prog_char statusString_32[] PROGMEM = "Hall auto light ON"; prog_char statusString_33[] PROGMEM = "Hall auto light OFF"; prog_char statusString_34[] PROGMEM = "Bathroom auto light ON"; prog_char statusString_35[] PROGMEM = "Bathroom auto light OFF";  // табличка указателей на строки состояния PROGMEM const char *statusString[] = 	    {      statusString_0,   statusString_1,   statusString_2,   statusString_3,   statusString_4,   statusString_5,   statusString_6,   statusString_7,   statusString_8,   statusString_9,   statusString_10,   statusString_11,   statusString_12,   statusString_13,   statusString_14,   statusString_15,   statusString_16,   statusString_17,   statusString_18,   statusString_19,   statusString_20,   statusString_21,   statusString_22,   statusString_23,   statusString_24,   statusString_25,   statusString_26,   statusString_27,   statusString_28,   statusString_29,   statusString_30,   statusString_31,   statusString_32,   statusString_33,   statusString_34,   statusString_35};  char statusStringBuf[40];    // буфер для чтения строк состояния   byte currentStatus=21; // статус на старте boolean camStatus = false; // состояние камеры boolean iamhome = true; // инициализация кнопки Я дома boolean water1= false; // датчики протечки. Нужно, чтобы отправить письмо только один раз, а не слать, пока датчик в воде boolean water2= false; boolean water3= false; boolean smoke1= false; // датчики дыма аналогично воде boolean smoke2= false; boolean smoke3= false;  // автомузыка в ванной boolean me = true; // признак разрешенной музыки boolean mOn = false; // признак включенной музыки  // блок автосвета  boolean kitchenLight = false; // автосвет на кухне boolean kitchenLightOn = false; // состояние света на кухне boolean hallLight = false; // автосвет в коридоре boolean hallLightOn = false; // состояние света в коридоре boolean bgLight = true; // включение фонового света одновременно с верхним в коридоре  int hallTimerID; // идентификатор основного таймера света в коридоре int firstLightID; // идентификатор первого (минутного) таймера света в коридоре boolean firstLight = false; // признак включения первого (минутного) интервала света в коридоре  #define modemPin  5 // пин реле модема и маршрутизатора #define camPin  6 // пин реле камеры #define PIRPin  A0 // пин датчика движения byte prevPIR = 0; // состояние датчика движения на старте  boolean hallLightReEnable = false; // для включения автосвета в коридоре после кнопки Я дома  boolean usB = false; // флаг таймаута датчика после срабатывания, чтобы не запускалось много таймеров для отправки уведомлений boolean feedB = false; // флаг работающей кормушки, чтобы не реагировала на повторные нажатия пока работает boolean mR = false; // признак рестарта модема boolean aL = false; // признак для звукового оповещения при протечках для однократного срабатывания boolean sR = false; // признак рестарта системы boolean aCl= true; // признак автозакрывания кормушки  int feedID; // идентификатор таймаута на закрытие кормушки int feedTimer = 15000; // предустановка времени поворота кормушки на один сектор boolean turnIt = false; // признак завершенного поворота при автоповороте кормушки boolean debug = false; // пищалка на датчик движения boolean PIRenabled = true; // чтобы включать и выключать датчик движения дистанционно  EthernetServer server(80);                           // Server Port 80 EthernetClient mail; // на выход для Твиттера и почты   SimpleTimer myTime; // создаем объект таймера для проверки соединения, игнорирования RC Switch на время работы датчика, кнопки Я дома  // создаем объект RCSwitch RCSwitch mySwitch = RCSwitch();  #define  txPin  8 // пин передатчика  byte lineDead = 0; // признаки для перезагрузки маршррутизатора byte lineRest = 0;  // УПРАВЛЕНИЕ Livolo, создаем объект livolo Livolo livolo(8);  // БЛОК ПОГОДЫ  boolean outData = false; // признак актуальности данных внешнего метеодатчика boolean inData = false; // признак актуальности данных внутреннего метеодатчика  int humidityRH=0; int tempC=0; int tempIn=0; int pressurehPa=0; int weatherData = 0; byte narodLength; byte owmLength;  byte servernarodmon[] = { 91, 122, 49, 168 }; // narodmon.ru   // В PROGMEM строки обмена с серверами для конструирования POST-запроса  prog_char openWeatherString_0[] PROGMEM = "POST http://narodmon.ru/post.php HTTP/1.0"; prog_char openWeatherString_1[] PROGMEM = "Host: narodmon.ru"; prog_char openWeatherString_2[] PROGMEM = "Content-Type: application/x-www-form-urlencoded"; prog_char openWeatherString_3[] PROGMEM = "Content-Length: "; prog_char openWeatherString_4[] PROGMEM = "ID=MAC00"; // 15 narodmon начало (MAC головного устройства) prog_char openWeatherString_5[] PROGMEM = "&MAC01="; // 14 температура снаружи prog_char openWeatherString_6[] PROGMEM = "&MAC02="; // 14 влажность prog_char openWeatherString_7[] PROGMEM = "&MAC03="; // 14 температура внутри prog_char openWeatherString_8[] PROGMEM = "&MAC04="; // 14 давление  // табличка указателей на строки обмена с серверами  PROGMEM const char *openWeatherString[] = 	    { openWeatherString_0,   openWeatherString_1,   openWeatherString_2,   openWeatherString_3,   openWeatherString_4,     openWeatherString_5,	   openWeatherString_6,   openWeatherString_7,   openWeatherString_8};      char sendDataStringBuf[47];     /**  * Setup  */ void setup() {  pinMode(PIRPin, INPUT);  myTime.setInterval(120000, checkLine); // запуск проверки соединения с Интернетом каждые две минуты  myTime.setInterval(3600000, resetGood); // отправка погоды каждые 60 минут  Ethernet.begin(mac, ip, mydns, gateway, subnet); // инициализация сети  server.begin(); // старт веб-сервера   mySwitch.enableTransmit(txPin); // инициализация передатчика в RC-Swich   mySwitch.enableReceive(0);  // инициализация приемника в RC-Switch   pinMode(camPin, OUTPUT); // инициализация реле камеры   pinMode(modemPin, OUTPUT); // инициализация реле модема   digitalWrite(camPin, HIGH);   digitalWrite(modemPin, HIGH);   pinMode(txPin, OUTPUT); //инициализация передатчика   sR = true; // признак рестарта системы   delay(10000); // устаканиваемся   sendMail(15); // пишем себе письмо при каждом старте  sendMail();    makeSound(); // пищалка   delay(1000);   makeSound();  }  // отправка погоды  void resetGood() { // раз в 60 (или по-другому) минут отправляем погоду и сбрасываем признаки актуальности    sendWeather();             outData = false;   inData = false;   narodLength=0;   owmLength=0;   humidityRH=0;   tempC=0;   tempIn=0;   pressurehPa=0;    }  void sendWeather() { // подготовка параметров для конструирования POST-запроса с погодой  if (inData == true && outData == true) {   selectData(5, 8, 71+owmLength+narodLength);  	 } else { if (outData == true) {             selectData(5, 6, 43+owmLength);           }           if (inData == true) {             selectData(7, 8, 43+narodLength);            } 	} }   void selectData (byte startPos, byte endPos, byte lenPos) { // заголовки owm или narodmon, конечная позиция счетчика, длина фиксированных строк, отправка POST-запроса с погодой    if (mail.connect(servernarodmon, 80)) {   for (byte sendDataCount = 0; sendDataCount < 3; sendDataCount++) {     strcpy_P(sendDataStringBuf, (char*)pgm_read_word(&(openWeatherString[sendDataCount]))); // читаем строку POST-запроса из PROGMEM     mail.println(sendDataStringBuf);     }  strcpy_P(sendDataStringBuf, (char*)pgm_read_word(&(openWeatherString[3]))); // читаем строку PROGMEM   mail.print(sendDataStringBuf);    mail.println(lenPos);   mail.println();  strcpy_P(sendDataStringBuf, (char*)pgm_read_word(&(openWeatherString[4]))); // читаем строку PROGMEM   mail.print(sendDataStringBuf);     for (byte i=startPos; i<endPos+1; i++) {     strcpy_P(sendDataStringBuf, (char*)pgm_read_word(&(openWeatherString[i]))); // читаем строку PROGMEM 		mail.print(sendDataStringBuf); 		switch (i) { 			case 5: 				mail.print(tempC/10.0, 1); 			break; 			case 6: 				mail.print(humidityRH/10.0, 1); 			break; 			case 7: 				mail.print(tempIn/10.0, 1); 			break; 			case 8: 				mail.print(pressurehPa); 			break;			 			} 	} 	mail.println();   }   mail.stop(); }  // ПИЩАЛКА  void makeSound() {   tone(9, 900, 450);   // delay(100);   // tone(9, 900, 450); }      // автосвет  void autoLights() { 		if (kitchenLight == true) { // если автосвет на кухне разрешен 			txSwitch(136101); // включаем свет                     if (kitchenLightOn == false) { 			if (debug == true) {myTime.setTimeout(30000, kitchenLightOff);} // для отладки 30-секундное включение света и пищалка 			else { 			myTime.setTimeout(600000, kitchenLightOff); // таймер на рабочее выключение света 			}                     }                     kitchenLightOn = true; 		}          		if (hallLight == true) { // если автосвет в коридоре разрешен  			if (hallLightOn == true)	{ // если включен "длинный свет" и случился проход	 					myTime.restartTimer(hallTimerID); // сбрасываем таймер "длинного" света для продления времени освещения еще на 3 мин 				}  			if (firstLight == true && hallLightOn == false) { // если после первого прохода случился второй 					hallLightOn = true; // выставляем признак включенного "длинного" света 					myTime.deleteTimer(firstLightID); // выключаем таймер "короткого" света 					hallTimerID=myTime.setTimeout(180000, hallLightsOff); // заводим таймер "длинного" света: если ничего не случится, свет будет выключен через 3 мин. 				}  			if (firstLight == false) { // если первый проход под датчиком                                         mySwitch.disableReceive(); 					livolo.sendButton(8500, 24);  // включаем свет 					firstLight = true; // выставляем признак прохода 					firstLightID=myTime.setTimeout(60000, hallLightsOff); // заводим таймер света на минуту: если ничего не случится, свет будет выключен через 1 мин.                                         mySwitch.enableReceive(0); 				} 				 					 		}			 }   // выключение света в коридоре void hallLightsOff() {  mySwitch.disableReceive();  livolo.sendButton(8500, 24);  hallLightReset();  mySwitch.enableReceive(0); }   // выключение света на кухне по таймеру void kitchenLightOff() {  txSwitch(136100);  kitchenLightOn = false; }  // Отправка уведомления о срабатывании датчика движения в режиме охраны void sendMotion(){   sendMail(22);   usB = false; }  // Уведомление о переключении режима "Я дома" void sendHome() {      sendMail(17);    }  // сброс автосвета в коридоре в исходное состояние void hallLightReset() {      hallLightOn = false;    firstLight = false;   myTime.deleteTimer(hallTimerID);    myTime.deleteTimer(firstLightID);    }   // звуковая сигнализация о протечке void soundAlarm() {    if (aL == false) {    myTime.setTimer(10000, makeSound, 10);    aL = true;  } }    // процедура повтора команд на розетки - для более стабильного включения/выключения void txSwitch(long cmd) {   mySwitch.disableReceive();     for (byte txC=0; txC<1; txC++) {     mySwitch.send(cmd, 24);   mySwitch.enableReceive(0);     } }  void feedAuto() { // поворот кормушки на сектор     feedOn();     myTime.setTimeout(feedTimer, feedOff); }  void feedOn() { // включение кормушки    if (feedB == false) {     txSwitch(46151);     feedB = true;} }  void feedOff() { // выключение кормушки    if (feedB == true) {     txSwitch(46150);     feedB = false;} }  // закрытие кормушки void feedClose() {   feedAuto();   turnIt = false; }  // выключение света при выходе из дома и включение камеры void swOff() {     if (hallLight==true) {hallLight = false; hallLightReEnable=true; hallLightReset();} // если был включен автосвет в коридоре - снова включим его по возвращении домой     livolo.sendButton(8500, 106); // Выключить все Livolo         delay(550);     txSwitch(133028); // выключение розетки #1     delay(550);     txSwitch(136100);  // выключение розетки #2     delay(550);     txSwitch(130740);  // выключение розетки #3     delay(550);     txSwitch(131588);  // выключение розетки #4     delay(550);     txSwitch(1386100); // выключение музыки     mySwitch.disableReceive();     digitalWrite(camPin, LOW); // включение камеры     camStatus = true;     delay(550);     livolo.sendButton(8500, 106); // Выключить все Livolo     sendMail(16); // письмо Не дома     usB = false;     mySwitch.enableReceive(0); }  // свет в гардеробе void wardrobeOn() {   livolo.sendButton(8500, 80);   mySwitch.enableReceive(0); }    // свет в ванной void bathroomOn() {   livolo.sendButton(8500, 108); // переключаем свет   if (me == true) { // если музыка разрешене     if (mOn == false) { // если музыка выключена       mOn=true; // установка признака включенной музыки       txSwitch(1386101);} // включение музыки     else {txSwitch(1386100); // иначе выключаем музыку           mOn=false;} // снимаем признак включенной музыки   }   mySwitch.enableReceive(0); // включение RC Switch теперь в главном цикле }  /**  * Loop  */ void loop() { if (lineRest > 30) {lineDead = 0; lineRest = 0;} // обнуляем счетчики количества безуспешных попыток соединения и таймаута ожидания соединения  myTime.run(); // старт и проверка таймеров SimpleTimer        char* command = httpServer(); // запуск вебсервера  if (PIRenabled == true) { // если включен датчик движения 	if (prevPIR == 0 && analogRead(PIRPin) > 500) { // если сработал датчик - переход из 0 в 1 		prevPIR=1; // сохраняем признак для переключения 		if (debug == true) {makeSound();} // пищалка для отладки по срабатыванию датчика 		if (iamhome == false) { //  если никого нет дома 			if (usB == false) { // если не установлена отсрочка срабатывания, чтобы исключить блокировку при отправке почты по возвращении домой 				usB = true; 				myTime.setTimeout(35000, sendMotion); 			} 		} else { // если мы дома 			if (hallLight == true || kitchenLight == true) { // если разрешен автосвет 				autoLights(); // запускаем процедуру автосвета 			}       		}     } else if (prevPIR == 1 && analogRead(PIRPin) == 0) { 		prevPIR = 0; // переключение признака срабатывания датчика 		} }  // и поехали по кругу // СЕКЦИЯ обработки сигналов беспроводных датчиков    if (mySwitch.available()) { // проверяем датчики     int value = mySwitch.getReceivedValue();     if (value != 0) {       switch (mySwitch.getReceivedValue()) {  	         case 1418288: // Water leak KITCHEN протечка на кухне       if (water1 == false) { // флаг для уведомлений только в том случае, если событие происходит в первый раз         sendMail(20); // уведомление по почте         soundAlarm(); // звуковой сигнал         water1 = true;} // флаг произошедшего события       break;      	case 3470133: // Water leak WASHER протечка под стиральной машиной       if (water2 == false) {         sendMail(24);         soundAlarm();          water2 = true;}       break;        case 3592421: // Water leak MAIN протечка в сантехкоробе       if (water3 == false) {         sendMail(25);         soundAlarm();         water3 = true;}       break;        case 1084887: // Smoke KITCHEN задымление на кухне       if (smoke1 == false) {         sendMail(26);         smoke1 = true;}       break;        case 2184536: // Smoke HALL задымление в прихожей       if (smoke2 == false) {         sendMail(27);         smoke2 = true;}       break;        case 12602757: // Smoke ROOM задымление в комнате       if (smoke3 == false) {         sendMail(28);         smoke3 = true;}       break;        case 3395440: // Doorbell ringing звонок в дверь       sendMail(19);       break;        case  15421849: // беспроводной датчик движения на кухне - автосвет (альтернативный вариант)       if (PIRenabled == false) {autoLights();} // использовать беспроводной датчик на кухне только если выключен проводной у контроллера       break;              case 15741424: // кнопка Кормушка       mySwitch.disableReceive(); //      tone(9, 400, 450); 	  if (feedB == false) { // если кормушка не вращается               tone(9, 400, 450); 		if (aCl == true) { // если включено автозакрытие 			if (turnIt == true) { // кнопка уже была нажата 				myTime.deleteTimer(feedID); // отключение таймера 				feedAuto(); // поворот сектора 				turnIt = false; // сброс признака повторного нажатия кнопки 			} else { // если кнопка нажата в первый раз 				feedAuto(); // поворот сектора 				feedID=myTime.setTimeout(150000+feedTimer, feedClose); // таймер на второй поворот 				turnIt = true; // установка признака повторного нажатия 				} 		} else { 			feedAuto(); 			} 	  }       mySwitch.enableReceive(0);       break;              // свет в гардеробе     case 6041743: // автосвет в гардеробе - датчик         mySwitch.disableReceive(); 	myTime.setTimeout(1700, wardrobeOn); // сбрасываем флаг занятости после срабатывания датчика       break;         case 1548296: // автосвет в ванной - датчик (альтернативный вариант)           mySwitch.disableReceive(); // выключаем RC Switch, чтобы исключить повторные срабатывания и вообще, чтобы ничего не мешало           // if (bathLight == true) {           myTime.setTimeout(1700, bathroomOn); // включаем свет после окончания работы датчика           // }       break;          	  case 5391052: // Нажата кнопка Я дома                          mySwitch.disableReceive(); 			if (iamhome == false) { 				iamhome = true; 				makeSound(); 				livolo.sendButton(8500, 106); // Выключаем все Livolo, чтобы не выключить свет в коридоре вместо включения 				digitalWrite(camPin, HIGH); // выключаем камеру 				camStatus = false; 				if (bgLight == true) { // включаем фоновый свет, если нужно 					txSwitch(133029); // фоновый свет в комнате 					// delay(350); 					txSwitch(136101); // фоновый свет на кухне 				} 				// delay(350);     				txSwitch(131589); // планшет и звук 				if (hallLightReEnable == true) { 					hallLight = true; 					hallLightReEnable = false; // сбрасываем признак включения автосвета после кнопки я дома 				} else 					{livolo.sendButton(8500, 24);} // если автосвет был включен - включаем его снова; если нет - просто включаем свет в коридоре 				myTime.setTimeout(15000, sendHome); // письмо Я дома 				delay(1000); // на случай удержания кнопки                                 mySwitch.enableReceive(0); 		} else { 		iamhome = false;   		makeSound();                 delay(1500); // на случай удержания кнопки 		myTime.setTimeout(60000, swOff); 		}            	   break;     }        	// получение метеоданных 	 if (mySwitch.getReceivedValue()/100000 == 161) {          weatherData = mySwitch.getReceivedValue() - 16100000; 	if (weatherData > 10000) { // пришла влажность 		humidityRH = (weatherData - 10000); 		} 	else { // пришла температура 			if (weatherData > 1000) { // минусовая температура 				tempC = -(weatherData - 1000); 				owmLength=9; 				} 			else { // плюсовая температура 					tempC = weatherData; 					owmLength=8; 				  } 		} }    if (mySwitch.getReceivedValue()/100000 == 121) {     pressurehPa = (mySwitch.getReceivedValue() - 12100000)/1.33; // пришло давление 	}  	if (mySwitch.getReceivedValue()/100000 == 131) {     weatherData = mySwitch.getReceivedValue() - 13100000; 		if (weatherData > 1000) { // минусовая температура 			tempIn = -(weatherData - 1000); 			narodLength=8; 			} 		else { // плюсовая температура 				tempIn = weatherData; 				narodLength=7;				 			  } 	}  if (tempIn!=0 && pressurehPa!=0) {inData = true;} if (tempC!=0 && humidityRH!=0) {outData = true;}      }        mySwitch.resetAvailable();   //    mySwitch.enableReceive(0); // включение RC Switch   }    }  /**  * СЕКЦИЯ обработки команд веб-интерфейса  */ void processCommand(char* command) {   if        (strcmp(command, "Switch1-on") == 0) { // первая розетка     txSwitch(133029);     currentStatus=0;   } else if (strcmp(command, "Switch1-off") == 0) {     txSwitch(133028);     currentStatus=4;   } else if (strcmp(command, "Switch2-on") == 0) { // вторая розетка     txSwitch(136101);     currentStatus=1;   } else if (strcmp(command, "Switch2-off") == 0) {     txSwitch(136100);     currentStatus=5;   } else if (strcmp(command, "Switch3-on") == 0) { // третья розетка     txSwitch(130741);     currentStatus=2;   } else if (strcmp(command, "Switch3-off") == 0) {     txSwitch(130740);     currentStatus=6;   } else if (strcmp(command, "Switch4-on") == 0) { // четвертая розетка     txSwitch(131589);     currentStatus=3;   } else if (strcmp(command, "Switch4-off") == 0) {     txSwitch(131588);     currentStatus=7;   } else if (strcmp(command, "router") == 0) {     modRestart();   } else if (strcmp(command, "webCamOn") == 0) {     if (!camStatus) { 	digitalWrite(camPin, LOW); // включение камеры, только если выключена. По умолчанию реле в режиме NO (камера всегда выключена) 	camStatus = true;     currentStatus=16; 	sendMail(currentStatus); 	}   } else if (strcmp(command, "webCamOff") == 0) {     if (camStatus) { 	digitalWrite(camPin, HIGH); // выключение камеры, только если включена 	camStatus = false;     currentStatus=17;     sendMail(currentStatus); 	}   } else if (strcmp(command, "Livolo1") == 0) {     livolo.sendButton(8500, 0); // Livolo кнопка 1 комната     currentStatus=8;   } else if (strcmp(command, "Livolo2") == 0) {     livolo.sendButton(8500, 96); // Livolo 2 комната     currentStatus=9;   } else if (strcmp(command, "Livolo4") == 0) {     livolo.sendButton(8500, 24); // Livolo 4 прихожая     currentStatus=10;   } else if (strcmp(command, "Livolo5") == 0) {     livolo.sendButton(8500, 80); // Livolo 5 гардероб     currentStatus=11;   } else if (strcmp(command, "Livolo7") == 0) {     livolo.sendButton(8500, 108); // Livolo 7 ванная     currentStatus=12;   } else if (strcmp(command, "Livolo3") == 0) {     livolo.sendButton(8500, 120); // Livolo 3 кухня     currentStatus=13;   } else if (strcmp(command, "Livolo11") == 0) {     livolo.sendButton(8500, 106); // Выключить все Livolo     currentStatus=14;   } else if (strcmp(command, "resetSensors") == 0) {     water1=false; // сброс признаков датчиков     water2=false; // сброс признаков датчиков     water3=false; // сброс признаков датчиков     smoke1=false; // сброс признаков датчиков     smoke2=false; // сброс признаков датчиков     smoke3=false; // сброс признаков датчиков         aL=false;     currentStatus=29;   } else if (strcmp(command, "autoLightKitchenOn") == 0) {     kitchenLight = true; kitchenLightOn = false; txSwitch(136100);// включено автоматическое управление фоновым светом на кухне     currentStatus=30;   } else if (strcmp(command, "autoLightKitchenOff") == 0) {     kitchenLight = false; txSwitch(136100);// выключено автоматическое управление фоновым светом на кухне     currentStatus=31;   } else if (strcmp(command, "autoLightHallOn") == 0) {     hallLight = true; hallLightReset(); // включено автоматическое управление фоновым светом в коридоре     currentStatus=32;   } else if (strcmp(command, "autoLightHallOff") == 0) {     hallLight = false; // выключено автоматическое управление фоновым светом в коридоре     currentStatus=33;   } else if (strcmp(command, "autoBathLightOn") == 0) { // автосвет в ванной     txSwitch(1486221);   } else if (strcmp(command, "autoBathLightOff") == 0) {     txSwitch(1486220);   } else if (strcmp(command, "autoMusicOn") == 0) { // автомузыка в ванной     txSwitch(1486231);   } else if (strcmp(command, "autoMusicOff") == 0) {     txSwitch(1486230);   } /* else if (strcmp(command, "blight") == 0) {     bathLight =!bathLight; // включено автоматическое управление светом в ванной     if (bathLight == true) {currentStatus=34;} else {currentStatus=35;}   } */ else if (strcmp(command, "LGTV-on") == 0) { // блок команд для медиаконтроллера (управление ТВ, плеером и кондиционером)     txSwitch(88100);   } else if (strcmp(command, "LGTV-back") == 0) {     txSwitch(88116);   }  else if (strcmp(command, "LGTV-volup") == 0) {     txSwitch(88132);   }  else if (strcmp(command, "LGTV-voldown") == 0) {     txSwitch(88164);   }  else if (strcmp(command, "LGTV-chup") == 0) {     txSwitch(88128);   }  else if (strcmp(command, "LGTV-chdown") == 0) {     txSwitch(88256);   }  else if (strcmp(command, "LGTV-mute") == 0) {     txSwitch(88512);   }  else if (strcmp(command, "LGTV-menu") == 0) {     txSwitch(89100);   }  else if (strcmp(command, "LGTV-ok") == 0) {     txSwitch(89108);   }  else if (strcmp(command, "LGTV-up") == 0) {     txSwitch(89116);   }   else if (strcmp(command, "LGTV-down") == 0) {     txSwitch(89132);   }  else if (strcmp(command, "LGTV-left") == 0) {     txSwitch(89164);   }  else if (strcmp(command, "LGTV-right") == 0) {     txSwitch(89128);   }  else if (strcmp(command, "LGTV-av") == 0) {     txSwitch(89256);   }  else if (strcmp(command, "GMiniMedia-on") == 0) {     txSwitch(89512);   }  else if (strcmp(command, "GMiniMedia-off") == 0) {     txSwitch(87108);   }  else if (strcmp(command, "GMiniMedia-play") == 0) {     txSwitch(87116);   }  else if (strcmp(command, "GMiniMedia-back") == 0) {     txSwitch(87132);   }  else if (strcmp(command, "GMiniMedia-volup") == 0) {     txSwitch(87164);   }  else if (strcmp(command, "GMiniMedia-voldown") == 0) {     txSwitch(87128);   }  else if (strcmp(command, "GMiniMedia-ff") == 0) {     txSwitch(87256);   }  else if (strcmp(command, "GMiniMedia-rew") == 0) {     txSwitch(87512);   }  else if (strcmp(command, "GMiniMedia-menu") == 0) {     txSwitch(92108);   }  else if (strcmp(command, "GMiniMedia-set") == 0) {     txSwitch(92116);   }  else if (strcmp(command, "GMiniMedia-up") == 0) {     txSwitch(92132);   }  else if (strcmp(command, "GMiniMedia-down") == 0) {     txSwitch(92164);   }  else if (strcmp(command, "GMiniMedia-left") == 0) {     txSwitch(92128);   }  else if (strcmp(command, "GMiniMedia-right") == 0) {     txSwitch(92256);   }  else if (strcmp(command, "GMiniMedia-ok") == 0) {     txSwitch(92512);   }  else if (strcmp(command, "LGAC-cool") == 0) {     txSwitch(95108);   }  else if (strcmp(command, "LGAC-heat") == 0) {     txSwitch(95116);   }  else if (strcmp(command, "LGAC-on") == 0) {     txSwitch(95132);   }  else if (strcmp(command, "LGAC-off") == 0) {     txSwitch(95164);   }  else if (strcmp(command, "catFeedl") == 0) { // коррекция таймера поворота кормушки +0.5 сек        feedTimer=feedTimer+500;   }  else if (strcmp(command, "catFeeds") == 0) { // коррекция таймера поворота кормушки  -0.5 сек       feedTimer=feedTimer-500;   }  else if (strcmp(command, "catFeedrst") == 0) {       feedTimer=15000; feedB=false; turnIt = false; // сброс таймера поворота кормушки в исходное значение   }   else if (strcmp(command, "musicEnabled") == 0) { // переключение разрешения и запрета музыки в ванной        me=!me;     if (me == false) { // при отключении флага разрешенной музыки одновременно выключаем музыку       txSwitch(1386100);       mOn=false;}   }  else if (strcmp(command, "catFeed") == 0) {       feedOn(); // включаем кормушку   }  else if (strcmp(command, "catFeedstop") == 0) {     feedOff(); // выключаем кормушку   }  else if (strcmp(command, "catFeedAuto") == 0) { // поворот кормушки на сектор     feedAuto();   }  else if (strcmp(command, "catFeedCloseOn") == 0) { // включение функции автоповорота по таймеру       aCl=true; 	  turnIt = false;   }   else if (strcmp(command, "catFeedCloseOff") == 0) { // выключение функции автоповорота по таймеру       aCl=false;       turnIt = false;   }   else if (strcmp(command, "debugPIR") == 0) { // режим отладки для датчика движения и автосвета на кухне     debug=!debug;   }   else if (strcmp(command, "enablePIR") == 0) { // включение датчика движения     PIRenabled=true;   }   else if (strcmp(command, "disablePIR") == 0) { // отключение датчика движения     PIRenabled=false;   }   else if (strcmp(command, "backLightOff") == 0) { // запрещение фонового света     bgLight=false;   }   else if (strcmp(command, "backLightOn") == 0) { // разрешение фонового света     bgLight=true;   } /*   else if (strcmp(command, "rst") == 0) {     resetFunc(); // сброс   }*/ }  // ВЕБ-сервер  void httpResponseHome(EthernetClient c) {   c.println("HTTP/1.1 200 OK");   c.println("Content-Type: text/html");   c.println();   c.println("<html>");   c.println("<head>");   strcpy_P(statusStringBuf, (char*)pgm_read_word(&(statusString[currentStatus])));     c.println(statusStringBuf);     c.print(";");         c.print(tempC/10.0, 1);     c.print(";");         c.print(humidityRH/10.0, 1);     c.print(";");             c.print(pressurehPa);     c.print(";");             c.println(tempIn/10.0, 1);       c.println("</body>");   c.println("</html>"); }  /**  * HTTP Redirect to homepage  */ void httpResponseRedirect(EthernetClient c) {   c.println("HTTP/1.1 301 Found");   c.println("Location: /");   c.println(); }  /**  * Process HTTP requests, parse first request header line and   * call processCommand with GET query string (everything after  * the ? question mark in the URL).  */ char*  httpServer() {   EthernetClient client = server.available();   if (client) {     char sReturnCommand[32];     int nCommandPos=-1;     sReturnCommand[0] = '\0';     while (client.connected()) {       if (client.available()) {         char c = client.read();         if ((c == '\n') || (c == ' ' && nCommandPos>-1)) {           sReturnCommand[nCommandPos] = '\0';           if (strcmp(sReturnCommand, "\0") == 0) {               httpResponseHome(client);           } else {             processCommand(sReturnCommand);             httpResponseRedirect(client);           }           break;         }         if (nCommandPos>-1) {           sReturnCommand[nCommandPos++] = c;         }         if (c == '?' && nCommandPos == -1) {           nCommandPos = 0;         }       }       if (nCommandPos > 30) { //        httpResponse414(client);         sReturnCommand[0] = '\0';         break;       }     }     if (nCommandPos!=-1) {       sReturnCommand[nCommandPos] = '\0';     }     // give the web browser time to receive the data     delay(1);     client.stop();          return sReturnCommand;   }   return '\0'; }  // перезагрузка маршрутизатора  void modRestart() {    digitalWrite(modemPin, LOW); // рестарт модема. По умолчанию реле в режиме NC (модем всегда включен), на время рестарта - переключение     delay(10000);     digitalWrite(modemPin, HIGH);       mR = true; // признак рестарта для уведомления по почте }  // проверка интернета  void checkLine(){   if (lineDead < 2) {     if (mail.connect(servergoogle, 80)) { 	// связь есть, закрываем сеанс 	mail.stop();          if (mR == true) { // чтобы не задерживать работу для отправки почты после рестарта. При рестарте ставим признак, здесь - почта, признак сбрасываем            sendMail(18);            mR = false;            }          if (sR == true) { // чтобы не задерживать работу для отправки почты после рестарта. При рестарте ставим признак, здесь - почта, признак сбрасываем            sendMail(15);            sR = false;            }  	} else { 		// связи нет, рестарт                 modRestart();                 lineDead = lineDead + 1; // увеличиваем счетчки неуспешных попыток соединения 	}   } // linedead if    else { lineRest = lineRest + 1; } // каждый цикл - минута таймаута без проверки соединения  }  // отправка почты  void sendMail(byte statusStringN){        if (mail.connect(servergoogle, 80)) {     mail.stop();         strcpy_P(statusStringBuf, (char*)pgm_read_word(&(statusString[statusStringN]))); // читаем строку состояния из PROGMEM //                twitter.post(statusStringBuf); // в твиттер  		mail.connect(servermail, 25);// идем на smtp.mail.ru		 		mail.println("EHLO ArduinoMail"); // привет                  delay(1500); // wait for a response	 		mail.println("AUTH LOGIN"); // старт авторизации                 delay(1500); // wait for a response		 		mail.println("ВАШ логин в BASE64"); // логин                 delay(1500); // wait for a response                delay(wait); // wait for a response		 		mail.println("ВАШ пароль в BASE64"); // пароль                 delay(1500); // wait for a response         		mail.println("MAIL From: адрес присвоенный контроллеру @mail.ru"); // identify sender, this should be the same as the smtp servergoogle you are using                 delay(1500); // wait for a response                 mail.println("RCPT To: адрес@получателя"); // identify recipient                 delay(1500); // wait for a response                 mail.println("DATA"); // identify recipient                  delay(1500); // wait for a response                 mail.println("To: адрес@получателя"); // identify recipient                 mail.print("Subject: "); // insert subject                 mail.println(statusStringBuf);                 mail.println(statusStringBuf); // insert body //              client.println(portmap.externalIp());                 mail.println("."); // end email                 mail.println("QUIT"); // terminate connection                  delay(1500); // wait for a response                 mail.println();                 mail.stop();                 } else {mail.stop();} }  

ссылка на оригинал статьи http://habrahabr.ru/post/210830/

Карманный PaaS c Dokku

В своей прошлой статье я упомянул Dokku, как важную составляющую нашей инфраструктуры и сегодня хочу раскрыть эту тему подробнее.

Dokku это средство простого трансформирования Ubuntu сервера, в мини-Heroku. После установки dokku, вы получаете возможность делать:

$ git push production master 

для могих популярных платформ (Node.js, Java, PHP, Python etc). Результатом процесса развертования, есть запущенное приложение, к которому сразу можно получить доступ по http/https.

Как это работает?

Если открыть репозиторий проекта, то в описании можно увидеть строчку — «Docker powered mini-Heroku in around 100 lines of Bash» — около 100 строчек баш кода, который иммитирует работу Heroku. Это довольно «легкая» реализация, как для такой большой проблемы, которую он решает.

Все объясняется тем, что Dokku стоит на плечах таких технологий как: Docker, Heroku Buildpacks, Nginx, Git.

Docker

Первое и пожалуй самая важная составляющая часть это Docker.

Docker решает проблему контейнеров.

Контейнеры это свежий взгляд на разветование приложений, классикой в которой считатся виртуализация. Докер контейнеры имеют ряд приемуществ по сравнение с виртуальными машинами, такие как: время бутстрапа машины, размер образа для запуска, версионность образов (с инкрементальными изменениями), а также общий индекс, с доступными к использованию образами.

Внутри контейнера можно запусить процесс, который будет полностью изолированным от внешней среды, со своей операционной системой, файловой системой и сетевым интерфейсом. При этом, из одного и того же образа, можно запускать сколь угодно (пока хватит ресурсов машины) процессов. Образы можно «наследовать» друг от друга, например если мы имеем образ Ubuntu сервера скажем размером 1GB, но хотим сделать свой с MongoDB, размер нового образа будет не 1.3GB а 300MB.

Docker файлы, позволяют в декларативном виде «конструировать» нужный образ, который можно «собрать», поместить в индекс, после этого распространить на нужные машины и запустить их там. При этом Docker гарантирует, что запускаемые внутри контейнера процессы будут работать абсолютно одинаково, вне зависимости от конкретной машины.

Docker образы, иммутабельны. Т.е. любое, даже самое деструктивное действие типа rm -rf /, не пренесет ему вреда, если измнения не будут закомиченны.

Docker собрал вокруг себя очень продуктивное сообщество, также многие крупные компании как Yandex, Red Hat, Facebook, Spotify берут эту технолонию на вооружение.

Как же его использует Dokku?

Прежде всего, в Dokku использвуется свой базовый образ, т.н. Buildstep. По его докер файлу видно, что это ubuntu:quantal, на который устанавливается ряд необходимых пакетов, а также билд паки в основном от Heroku, но также и некоторые от сообщества (например для Perl, Dart, Static Apache etc.)

Внутри базового образа есть скрипт builder, в задачу которого входит «поиск» подходящего для вашего приложения билд пака и запуск его.

Heroku Buildpacks

Билд паки, это набор шел или руби скриптов, типично состоящих из нескольких файлов, detect, compile, release. Задача которых, детектировать соответвует ли приложение заданному типу (как правило по файлам в корне проекта, Gemfile — Ruby, package.json — Node.js и т.д.), скомпилировать и выпустить приложение, соответвенно.

Билд пак, запущенный внутни контейнера создает всю необходимую среду выполнения (качает нужную версию Node.js, или OpenJDK..), установит все необходимые приложению зависимости (npm, maven, pip..) К моменту завершения его работы, приложение будет готово к старту.

Если вы использовали Heroku, то весь этот вывод, который видно на экране при пуше приложения в Heroku, это и есть результат работы buildpack.

Nginx

Docker обладает возможностью маппить сетевые порты, которые использует приложение внутри контейнера, на порт «внешненей» машины. Dokku передает внутрь приложения номер порта 5000. Внешний, берется из диапазона портов выше 49200.

СONTAINER ID        IMAGE                       COMMAND                CREATED             STATUS              PORTS                     NAMES a60c2af71770        app/app:latest              /bin/bash -c /start    30 hours ago        Up 30 hours         0.0.0.0:49264->5000/tcp   prickly_curie 9c3c58b649df        app/likeastore.com:latest   /bin/bash -c /start    47 hours ago        Up 47 hours         0.0.0.0:49253->5000/tcp   sad_lumiere 1b55d9087d23        app/tour:latest             /bin/bash -c /start    8 days ago          Up 8 days           0.0.0.0:49228->5000/tcp   suspicious_wozniak f700b5db1100        app/demo:latest             /bin/bash -c /start    2 weeks ago         Up 2 weeks          0.0.0.0:49159->5000/tcp   sleepy_heisenberg 4df87e09611d        app/analytics:latest        /bin/bash -c /start    2 weeks ago         Up 2 weeks          0.0.0.0:49153->5000/tcp   tender_curie 

Трафик из/в контейнер проксирует Nginx. Его никак не надо конфигурировать, Dokku сделает это сам. Для всех приложений он создает конфигурационный файл, примерно такого вида:

upstream app { server 127.0.0.1:49264; } server {   listen      [::]:80;   listen      80;   server_name app.likeastore.com;   return 301 https://$host$request_uri; } 

Т.е. использует upstream модуль для трафика из контейнера, на порт 80 сервера.

Git

Многие знают Git, как отличную систему конроля версий. Диапазон использования Git шире, чем просто контроль версий, многие называют Git — новым FTP. Именно git используется как транспорт исходников на сервер.

Dokku использует git-hooks, и после того как исходники «пушнуты» на сервер, запускает dokku скрипт. Это собственно и есть, тот самый «around 100 lines of bash» скрипт, в задачу которого входит создание нового docker образа из базового (buildstep), запуск «builder» скрипта для инициализации среды, запуск самого приложения и рестарта nginx.

Обратитие внимание на то, что с каждым пушем приложение, разворачивается в «чистой» среде, в новом контерейнере, созданном специально для него.

Установка

Все довольно подробно рассказано в документации проекта. Bootstrap скрипт установит вне нужные зависимости — git, nginx, docker, etc… А также установит базовый образ buildback.

Конфигурация

После установки Dokku на сервере, необходимо сделать 3 вещи.

1. Загрузить свой ssh публичный ключ на сервер, чтобы можно было сделать git push.

$ cat ~/.ssh/id_rsa.pub | ssh root@yourserver.com "sudo sshcommand acl-add dokku progrium" 

2. В .git/config приложения надо настроить remote бранч

[remote "staging"] 	url = dokku@stage.likeastore.com:app-stage.likeastore.com 	fetch = +refs/heads/*:refs/remotes/deploy/* [remote "production"] 	url = dokku@likeastore.com:app 	fetch = +refs/heads/*:refs/remotes/deploy/* 

Обратите внимание, на имена — с полно-квалифицированным именем, типа app-stage.likeastore.com, приложение запустится на http://app-stage.likeastore.com, и базовым именем, типа app, на http://app.likeastore.com.

3. В корне проекта сделать файл Procfile, который содержит инструкцию на запуск приложения, для Node.js

web: node app.js

После этого приложение готово к деплойменту.

Деплоймент

Тут все просто, как сказано выше:

$ git push production master 

В результате, вы увидете что похожее на следующий вывод:

› git push staging development:master -----> Cleaning up ... -----> Building app-stage.likeastore.com ...        Node.js app detected -----> Requested node range:  0.10.x -----> Resolved node version: 0.10.25 -----> Downloading and installing node -----> Restoring node_modules directory from cache -----> Pruning cached dependencies not specified in package.json -----> Installing dependencies -----> Caching node_modules directory for future builds -----> Cleaning up node-gyp and npm artifacts -----> Building runtime environment -----> Discovering process types        Procfile declares types -> web -----> Releasing app-stage.likeastore.com ... -----> Deploying app-stage.likeastore.com ... =====> Application deployed:        https://app-stage.likeastore.com  To dokku@stage.likeastore.com:app-stage.likeastore.com    77008a6..99dfe55  development -> master 

После первого деплоймента, по требованию, можно настроить переменные окружения для приложения и поддержку SSL.

Плагины

Базовый Dokku можно считать довольно ограниченным, но он отлично расширяется за счет плагинов.

Плагины делятся на несколько типов, datastores — с готовыми решениями развертывания хранилищ (MariaDB, PostgreSQL, MongoDB etc.), process managers — для поддержки менеджеров процессов (Circus, Shoreman etc.) и other — всякие полезные штуки (Bower, Grunt, Elasticsearch, SSH Deployment keys).

Наш опыт использования Dokku в Likeastore оказался очень положительным, чего и Вам искренне желаю!

ссылка на оригинал статьи http://habrahabr.ru/company/likeastore/blog/211016/

Последствия поглощения Motorola Mobility альянсом IBM-Lenovo

Когда в 2005 году IBM продала своё подразделение Personal Systems Group компании Lenovo, то эти компании заключили стратегический альянс — см.: «IBM and Lenovo». И по широкому соглашению обе эти компании взаимно дополняют друг друга и могут предлагать клиентам через свои дистрибьюторские каналы продукты обеих компаний как IBM так и Lenovo.

Мало того с 2005 года IBM владела долей ~ в 20% акций Lenovo, потом правда она их продала различным американским инвестфондам (но есть упорные слухи, что IBM через свои же инвестфонды владеет чуть-ли не блокирующим пакетом акций Lenovo).

Стратегический альянс IBM-Lenovo действует до сих пор. А 23 января 2014 года было объявлено о заключения очередной сделки, по ней IBM опять получает акции Lenovo и ещё больше расширяет свои взаимосвязи с этой китайской компанией — см.: «IBM прощается с x86 серверами, вся линейка продана Lenovo».
После этой сделки появился ряд статей и интервью в которых активно обсуждается как эти две компании будут продавать технику и продукты друг друга — взаимно дополняя и обогащая друг друга:
1. «Как сделка IBM-Lenovo отразится на рынке серверов и СХД?»;
2. «IBM-Lenovo: что дальше? Отвечает шеф канала Lenovo»;
3. «Последствия сделки IBM-Lenovo».

В общем кто кого купил, даже и не совсем понятно, потому как бизнесы IBM и Lenovo всё больше и теснее переплетаются:)
У меня лично, создаётся ощущение, что «Lenovo» — это такой IBM`ский бренд для массового рынка — для дешёвой ширпотребной техники.
А чтобы не запачкать и не повредить элитарность своего родного бренда «IBM» — они стараются несколько дистанцироваться и всячески скрывают свои тесные взаимопереплетения с Lenovo.

После этого можно прямо говорить, о том что Motorola Mobility была куплена именно альянсом IBM-Lenovo!
При этом в IBM последние годы активно готовятся к широкому внедрению планшетов и смартфонов на корпоративный рынок — см.: «IBM выходит на рынок мобильных корпоративных решений».
И теперь, после покупки Motorola Mobility альянсом IBM-Lenovo планшеты и смартфоны под брендом Motorola/Lenovo с помощью дистрибьюторов IBM и Lenovo будут активно продвигаться именно на корпоративный рынок, где могут окончательно вытеснить BlackBerry из этого бизнеса — см.: «Партнеры Lenovo приветствуют покупку Motorola Mobility»!

Ну простым потребителям может показаться все эти рассуждения далёкими от жизни (ведь многие не связаны с крупным корпоративным бизнесом, да и с дистрибьюторами не сталкиваются).
Мне думается, что здесь очень важно понимать, что такой крупный игрок IT-рынка как IBM по-сути стала союзником корпорации Google и выбрал для поддержки смартфоны и планшеты на платформе Google Android — а это значит, что именно в эту платформу в ближайшие годы будут вложены миллиарды корпоративных денег.

Да ещё надо заметить, что корпорация IBM в последние годы всячески поддерживает развитие ОС Linux, и в частности активно внедряя Linux на своих корпоративных серверах построенных на процессорной платформе POWER, да и вот теперь ещё становится и активным сторонником Linux-подобной ОС Google Android.

Похоже что Linux вместе с Google Android скоро будут править как минимум корпоративным миром (что означает неизбежный MS Windows — R.I.P.).
И конечно же тут очевидно, что конкуренты Google Android на мобильных платформах в первую очередь в лице Microsoft и Apple могут потерпеть ещё одно поражение — теперь уже на корпоративном рынке (про BlackBerry вообще наверное скоро забудут — они уже всё что могли потеряли или потеряют в ближайшее время), и возможно должны как-то изменить свои программные платформы в сторону большей дружелюбности и взаимодействия с Google Android да и с другими Linux-платформами:)

P.S.: Что вы думаете по этому поводу?

ссылка на оригинал статьи http://habrahabr.ru/post/211008/