Привет, Хабр ие. Хочу рассказать об одном эксперименте, который провел на днях. Очень мне понравились два девайса из статей от Madrobots: лампы Marlight и часы Pebble. Причем понравились именно тем, что конфигурируемы, т.е. подвластны так сказать воле и воображению хозяина. Пока ребенок на даче, решил я устроить ему сюрприз и показать настоящую современную «техномагию», а именно: управление светом жестами.
Задача: с помощью неких движений выбирать комнату, а затем взмахом руки или включаем, или выключаем свет в выбранной комнате.
В процессе работы над поставленной задачей, было использовано несколько любопытных решений, которые могут пригодится в разнообразных проектах домашней автоматизации, кому интересно — добро пожаловать под кат.
Многие из статей, что я здесь читал… Они классные! Читаются на одном дыхании и кажется, что господи – ведь так же просто все, чего ж я сам-то не сделаю так же. Собственно именно такие мысли и натолкнули меня на вот это все, что ниже описано. НО! Есть одно важное «но». Я хотел помимо конечного результата описать еще свои мытарства в областях, в которых я, строго говоря, ничего не смыслю. Я никогда не писал под php и js, а c++ был в институте и очень давно. Кому-то покажется, что я очень много внимания уделяю элементарным вещам, а может даже в некоторых моментах я откровенно ошибаюсь или что-то понимаю не правильно, однако я добился поставленной задачи и хотел продемонстрировать не только результат (возможно не такой уж и впечатляющий), но и путь, которым я к нему шел.
Итак, что у нас есть из железа:
- наручные часы pebble с акселерометром
- телефон с iOS на борту
- комп с виндой, выполняющий функции сервера (т.е. включенный 24х7)
- wi-fi коннектор marlight, подключенный к домашнему wi-fi и имеющий статический IP
- освещение в квартире на базе ламп marlight (8шт) в каждой комнате(3)
Лампы купил сразу все. Здесь. Не дешево конечно, но они все же экономичные по энергопотреблению, а менять – так уж все сразу. Так что у меня четыре лампы в большой комнате, три в детской и одна в спальне (мало! Надо минимум три – световой поток одной лампы в максимальной яркости: 500 люмен, что не дотягивает даже до 50 ваттной лампы накаливания). Каждая комната – отдельный канал на коннекторе, и даже еще один остается на расширение: в ванную, например (pebble же водонепрницаемые… Чем плохо: лежишь в ванной и управляешь светом! Правда, придется сделать больше функций, чем просто включение-выключение).
C wifi-коннектором вышла легкая заминка: при включении он предлагает выбрать тип шифрования и не смотря на наличие инструкции в которой в качестве примера показано WPA2, в списке выбора такого варианта не обнаруживается. Однако специалисты тех. поддержки магазина мне разъяснили, что выбирать тип шифрования не надо – мол указываешь SSID, пароль и вуаля – коннектор в сети. Собственно все это было и есть в инструкции у них на сайте.
Биндинг ламп на конкретный канал так же не вызвало затруднений – благо я уже знал, где лежит инструкция! (Что характерно – в бумажке, что шла с коннектором процедура была описана несколько иначе – так вот работает именно та, что на сайте продавца).
Протестировав штатное iOS-приложение, я убедился, что могу управлять освещением с телефона (что само по себе уже удивительно), причем мне для этого не надо переключать сеть, как было бы, будь коннектор в stand-alone режиме. Пора переходить к тому, что я раньше никогда не делал: настраивать web-сервер.
Зачем web-сервер, спросите вы? А потому, что в упомянутой выше статье про лампочки была ссылка на php-скрипт, который при получении GET (и POST) запросов – пересылает соответствующий udp-пакет коннектору, заставляя того выполнить то или иное действие с освещением. Я кстати вполне допускаю, учитывая архитектуру pebble, о которой речь пойдет чуть ниже, что без сервера можно было обойтись, но… Я был вполне уверен, что http-запрос с часов инициировать можно, а вот на счет всего остального уверен не был.
Запустить php под IIS7 мне помогла вот эта хабра-статья. В принципе ничего сложного, главный урок, который я для себя извлек: у MS весьма своеобразное представление о user-frendly экране об ошибке. Только отключив эту опцию, я понял, что php у меня работает, а ошибки следует искать в скрипте (вернее в отсутствии библиотеки sockets, которую скрипт использует). Пробуем через localhost – бинго! Работает.
Установив на pebble/телефон приложение smartwatch+ пробую http-запросы. Конечно же натыкаюсь на брандмауэр, после добавления нужного порта в исключения – я таки могу уже управлять светом в комнате используя часы. Правда, со сторонним приложением. Значит нам пора в cloudpebble.net
С этого момента хабр ответы на возникающие вопросы не давал, пришлось гуглить. Гуглил я много и далеко не всегда результативно, пока не набрел на вот этот замечательный ресурс. Беглого просмотра оказалось достаточно, что бы понять, что это то, что нам нужно. И вот тут меня ждал сюрприз! Оказывается sdk pebble предоставляет возможность разработать решение, включающее в себя связку из кода, исполняемого как на часах, так и на телефоне! Уже потом я нагуглил, что у них так же есть наработки по js-приложению, которое полностью исполняется на телефоне, используя pebble сугубо как устройство вывода, но сейчас речь не об этом.
Итак, зачем же нам использовать два устройства и как это работает. Ну, по задумке у нас на часах акселерометр и именно его мы и хотим использовать в качестве управляющего устройства. А вот http-запрос… Даже если предположить, что это возможно напрямую (использую телефон в качестве условного «роутера»), то это неоправданно сложно. Возникает вопрос: зачем? Если реализован механизм обмена сообщениями, и на каждой из сторон может быть выполнен свой код? Как показала практика http запрос на стороне телефона – не просто, а очень просто!
Начнем пожалуй. В качестве темплейта нового приложения воспользуемся демкой из sdk pebble – appMessage. У нас сразу создастся два исходника — .c для часов и .js для телефона. Да-да, на телефоне используется java-sсript (помните, я упомянул, что вполне вероятно можно udp-пакеты управляющие напрямую слать на коннектор? Я просто не силен в js, поэтому понятия не имею можно ли это сделать и если можно, то как, но… Что-то мне подсказывает, что если есть в php, то должно быть и в js. Разве что возможности виртуальной машины в приложении pebble могут быть сильно урезаны).
Вот как выглядит мой файл .js (от дефортного он отличается, прямо скажем, не сильно):
// Событие при запуске - можно протестировать доступность URL... Но я просто шлю сообщение
Pebble.addEventListener("ready",
function(e) {
Pebble.sendAppMessage({0: “Ready!”});
});
// Обработка входящих сообщений: отправляем GET запрос, посылаем "эхо" обратно на часы
Pebble.addEventListener("appmessage",
function(e) {
var msg = e.payload.message; // SETTINGS : PEBBLE KIT JS : MESSAGE KEYS
var req = new XMLHttpRequest();
req.open('GET', "http://srv:8080/marlight.php?command="+msg, true);
req.send(null);
Pebble.sendAppMessage({0: msg});
});
Знаете, что я долго не мог понять? Что такое e.payload.message. Нет, серьезно! Я понимаю, что мы как-то передаем сообщение, что эта вот конструкция должна то, что мы передаем возвращать, но… Как? Как видно из исходника модуля для часов мы по сути передаем словарь, где значения идут по индексу… Оказывается дело в этом:
Это настройки именно для этого проекта в облачной IDE. И именно это связывает e.payload.message и MESSAGE_KEY в строчке dict_write_cstring(iter, MESSAGE_KEY, command), модуля для часов, и там и там некое осмысленно представление индекса.
В общем, не суть. Все тут понятно – при получении сообщения, мы извлекаем из него строку команды, которая должна дополнить имеющийся у нас URL, для получения конкретного GET-запроса, который заставит наш скрипт попросить коннектор переключить лампочки! Круто. На всякий случай шлем «эхо» обратно на часы.
Так, переходив в модуль .c, который соответственно исполняется на часах. Процедура отправки сообщения с часов на телефон у нас есть по дефолту:
/* Запись сообщения (параметр command) в буфер и отправка на телефон */
void send_message(char* command){
DictionaryIterator *iter; // создание словаря
app_message_outbox_begin(&iter); // начало сообщения
dict_write_cstring(iter, MESSAGE_KEY, command); // отправка команды на телефон
dict_write_end(iter); // конец сообщения
app_message_outbox_send(); //отправка
}
Ну, почти по дефолту… Все, что я сделал с этой процедурой – это добавил параметр-команду и заменил тип отправляемого пакета на строковый… О эти строки в С++! Кто не работал с указателями, кто не пытался вывести число в строку, тому не понять. Но благодаря примерам и SDK, я все же выяснил, что функция snprintf поддерживается, поэтому вывод форматированного текста (с преобразованием числа в строку) на экран, хоть и не так прост, как в привычных языках высокого уровня, но все же понятен:
channel = 1; // по умолчанию у нас включен 1-ый канал
snprintf(channel_s, sizeof("Channel: 0"), "Channel: %d", channel);
text_layer_set_text(channel_layer, channel_s); /* показываем сообщение при запуске программы */
Я привел только само преобразование и вывод текста, инициализацию текстового слоя полностью можно посмотреть в статье про программирование под Pebble или в исходнике, ссылку на который я приведу в конце статьи.
Попробовав переслать пару статичных команд, и убедившись, что это работает – я перешел к самой сути: акселерометру. Что собственно такое акселерометр? В SDK есть пример работы с ним – часы по тамеру получают значение ускорения для каждой из трех осей и применяют его к паре десятков «дисков», которые имеют условную массу в зависимости от размера, а, следовательно, инерцию. Все это отрисовывается на экране часов.
Диски двигаются при наклоне в ту или иною сторону – все красиво… Но так сразу даже и не понятно, что из этого следует и как это применить на практике. Тогда я взял и тупо вывел данные текущего ускорения по осям в лог!
/* структура для хранения значений ускорения по всем трем осям */
AccelData accel = (AccelData) { .x = 0, .y = 0, .z = 0 };
/* для начала получим текущее значение ускорений */
accel_service_peek(&accel);
APP_LOG(APP_LOG_LEVEL_DEBUG, "x:%d, y:%d, z:%d", accel.x, accel.y, accel.z);
Лирическое отступление: команда вывода в лог APP_LOG(APP_LOG_LEVEL_DEBUG, «SPARTAAA!!!»); встречается в исходниках часто, однако не так очевидно, где же этот самый лог! Ну, во-первых, это кнопочка VIEW LOGS после загрузки приложения на телефон:
А во-вторых, лог можно найти в меню: COMPILATION – VIEW APP LOGS.
Но, вернемся к акселерометру. Медитируя на результаты, я понял, что есть одно ускорение, которое легко диагностировать – это ускорение свободного падения. Именно поэтому наклон проще отследить, чем движение в ту или иную сторону – по сравнению с гравитацией другие импульсы не так уж и значимы… Нет, конечно есть алгоритмы, позволяющие отследить даже то, какую фигуру в воздухе рисует оператор, но у нас приложение начального уровня, поэтому будем брать то, что попроще.
Итак, ускорение свободного падения с точки зрения акселерометра pebble равно 1000 ± 100 пунктов. Думаю именно так и задумывалось при калибровке. Значит порогом срабатывания можно считать 900 при совпадении направления оси и ускорения, и -900 когда ось и вектор ускорения полностью противоположны. Промежуточные значения пока рассматривать не будем.
Создадим структуру для хранения состояния осей, опишем возможные состояния, а так же заведем константу для гравитации:
typedef struct Vector {
int x, y, z;
} Vector;
static Vector current; //текущее состояние
/* Состояния осей акселерометра */
enum{
HYRO_NN = 0, // нейтральное
HYRO_UP = 1, // повернута вверх
HYRO_DN = 2 // повернута вниз
};
#define GRAVITY 900 // Ускорение свободного падения с точки зрения акселерометра pebble 1000 +/- 100
Тогда инициализацию состояния можно было бы прописать как:
if (accel.x > GRAVITY) current.x = HYRO_DN;
else if (accel.x < -GRAVITY) current.x = HYRO_UP;
else current.x = HYRO_NN;
if (accel.y > GRAVITY) current.y = HYRO_DN;
else if (accel.y < -GRAVITY) current.y = HYRO_UP;
else new.y = HYRO_NN;
if (accel.z > GRAVITY) current.z = HYRO_DN;
else if (accel.z < -GRAVITY) current.z = HYRO_UP;
else new.z = HYRO_NN;
Допустим. Однако нам нужен триггер, т.е. событие, на которое можно будет выдать ОДНОКРАТНУЮ реакцию (скажем, подняли руку, или повернули запястье). А значит нам нужно не столько состояние осей, сколько их изменение. Следовательно, нам надо хранить предыдущее измерение, что бы было с чем сравнивать, и обновлять его только если оно изменилось:
static Vector old; //старое
static Vector new; //новое
if (accel.x > GRAVITY) new.x = HYRO_DN;
else if (accel.x < -GRAVITY) new.x = HYRO_UP;
else new.x = HYRO_NN;
if (accel.y > GRAVITY) new.y = HYRO_DN;
else if (accel.y < -GRAVITY) new.y = HYRO_UP;
else new.y = HYRO_NN;
if (accel.z > GRAVITY) new.z = HYRO_DN;
else if (accel.z < -GRAVITY) new.z = HYRO_UP;
else new.z = HYRO_NN;
Теперь мы можем проверить изменилось ли состояние, и если да, то как изменилось. Если так, как задумано, то выполняем действие:
if (old.x!= new.x) { /* если оно изменилось по оси х */
/* проверим, как именно изменилось */
if (new.x == HYRO_UP){ // если руку подняли
snprintf(command_s, sizeof("ON_0"), "ON_%d", channel); // то надо послать команду включения
send_message(command_s); /* посылаем сообщение */
text_layer_set_text(command_layer, command_s); /* показываем сообщение */
}
if (new.x == HYRO_DN){ // если руку опустили
snprintf(command_s, sizeof("OFF_0"), "OFF_%d", channel); // то надо выключить
send_message(command_s); /* посылаем сообщение */
text_layer_set_text(command_layer, command_s); /* показываем сообщение */
}
/* зафиксируем изменение */
old.x = new.x;
}
if (old.y!= new.y) { /* если оно изменилось по оси y */
/* проверим, как именно изменилось */
if (new.y == HYRO_DN){ // если повернули кисть влево
channel++; // то следующий по порядку канал
if (channel == 5) channel = 1; // по кругу
snprintf(channel_s, sizeof("Channel: 0"), "Channel: %d", channel);
APP_LOG(APP_LOG_LEVEL_DEBUG, channel_s);
text_layer_set_text(channel_layer, channel_s); /* показываем сообщение */
}
if (new.y == HYRO_UP){ // если повернули кисть вправо
channel--; // то предыдущий канал
if (channel == 0) channel = 4; // по кругу
snprintf(channel_s, sizeof("Channel: 0"), "Channel: %d", channel);
APP_LOG(APP_LOG_LEVEL_DEBUG, channel_s);
text_layer_set_text(channel_layer, channel_s); /* показываем сообщение */
}
/* зафиксируем изменение */
old.y = new.y;
}
if (old.z!= new.z) { /* если оно изменилось по оси z */
/* пока просто зафиксируем изменение */
old.z = new.z;
}
Пробуем:
Работает!
Эпилог. Поставленная в начале задача выполнена. Думаю многие хотели бы знать, насколько это функционально – переключать свет жестом. Да в общем-то не очень. Практика показывает, что в 80% случаев проще всего пользоваться обычным выключателем. Необходимость удаленного включения/выключения/настройки света может возникнуть только тогда, когда ты, допустим, удобно устроился у телевизора, или лег спать и тебе лень вставать… В таком случае управление с часов удобнее, чем с мобильника (мобильника под рукой может и не быть), однако жесты… Жесты это скорее WOW-эффект. Даже с добавлением «горячих клавиш» от pebblebits (позволяет запустить до четырех приложений не через меню, а нажатием комбинации кнопок), гораздо практичнее было бы грамотно организованное меню, предоставляющее быстрый и интуитивно-понятный доступ к основным функциям освещения. И тем не менее, я считаю этот проект для себя очень удачным. По нескольким причинам. Во-первых, развернутый web-сервер, управляемый GET-запросами – очень классная штука. Он может получать команду не только и не столько от часов, но от чего угодно: от датчиков движения и/или освещенности до какого-то обработчика голосовых команд. Так же по аналогии со скриптом marlight.php, я могу накидать скрипт, выполняющий действия, выходящие далеко за пределы управления освещением. Вполне допускаю, что для большинства здесь присутствующих это азы, но для меня опыт был полезен. Ну и конечно pebble! В ходе проекта я научился строить приложения для часов, которые умеют общаться с внешним миром, что открывает действительно широкие перспективы для их использования. Конечно, мне очень помогли статьи по программированию pebble, которые были на Хабре ранее (а MagicPebble я установил в часы на постоянной основе), но все эти примеры оставляют умные часы вещью в себе, при том, что простейший в общем-то код делает их частью общей «экосистемы» девайсов в доме.
Ссылки:
- Исходник для pebble на GitHub: PebbleToMarlight
- php-скрипт для сервера: marlight_php
- Генератор русской прошивки для pebble с доп. опциями:pebblebits.com
- Хороший набор примеров с разъяснениями:try { work(); } finally { code(); }
- Статьи на хабре по программированию pebble: раз, два, три.
P.S. Пока я писал эту статью, дважды обновилась прошивка pebble. Причем первый раз — с 2.3 на 2.4, что привело к невозможности загружать свои проекты на не обновленные часы, а так-как на pebblebits.com русского варианта не было — обновить до 2.4 означало бы отказаться от основной функции pebble: показывать звонки, сообщения, события и прочая, т.к. в оригинальной прошивке русский текст отображается квадратиками. Однако парни выложили новую платформу через неделю после выхода официальной, за что им глубокий респект!
Автор: Nehc