Pull to refresh

Как нас било током на 1 апреля

Reading time 9 min
Views 68K
На день дурака в этом году мы с коллегами решили сделать чуть больше, чем просто шутку. Мы придумали интерактивный формат с трансляцией из нашего офиса и дали возможность всем желающим пощекотать наших добровольцев небольшим электрическим разрядом буквально в прямом эфире.



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

Тут мы расскажем о том как работала техническая часть этого мероприятия.

Задача


На странице слайдер из трёх видеопотоков, под каждым из которых кнопки like от Facebook и VK. Нажатие на любую из них должно вызывать замыкание соответствующего слайду реле.

Реализация вкратце


  • Нажатие на like-кнопку вызывает ajax-запрос на сервер. На сервере принимающий скрипт собирает очередь “лайкнутых” кадров из слайдера;
  • На компьютере, к которому подключена плата Arduino, запущено Qt приложение. Оно раз в секунду обращается к сайту (идея с тем, чтобы держать одно соединение прогорела из-за недостаточно стабильного подключения в офисе) и получает список “лайкнутых” слайдов;
  • Это же приложение полученные номерки слайдов пересылает через последовательный порт на Arduino, которая согласно полученным номеркам на 1200 мс включает реле;

Отдельные благодарности Arduino, Qt и замечательной библиотеке QextSerialPort.

Реализация подробнее



Синей изоленты не оказалось из-за чего пришлось использовать неправославную черную изоленту.

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

Займёмся вставкой нескольких кнопок и отловом лайков.

Вконтакте


Читаем мануал vk.com/dev/widget_like, создаём три блока:
<div class="vk vk1"><div id="vk1"></div></div>
<div class="vk vk2"><div id="vk2"></div></div>
<div class="vk vk3"><div id="vk3"></div></div>

И инициализируем их:
<scriрt>
	VK.Widgets.Like('vk1', {pageImage:'http://site.ru/i/vk.png', pageTitle:'Разряди обстановку — еб*ни сеошника током!', pageUrl:'http://electro.eggo.ru/?1', width:80,type:'mini'}, 100);
	VK.Widgets.Like('vk2', {pageImage:'http://site.ru/i/vk.png', pageTitle:'Разряди обстановку — еб*ни сеошника током!',pageUrl:'http://electro.eggo.ru/?2', width:80,type:'mini'}, 200);
	VK.Widgets.Like('vk3', {pageImage:'http://site.ru/i/vk.png', pageTitle:'Разряди обстановку — еб*ни сеошника током!',pageUrl:'http://electro.eggo.ru/?3', width:80,type:'mini'}, 300);
</scriрt>

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

Теперь отлов лайков. Документация говорит, что можно подписаться на событие widgets.like.liked. Но параметров функции-обработчика не описано и как понять, какую кнопку нажали — не очень понятно. Через отладчик удалось выяснить, что есть два параметра a и b, но пользоваться недокументированными возможностями чревато. Помним про законы Мерфи. За сим, заводим глобальную переменную “номер слайда”, по которой и отсылаем на сервер данные о том, какое реле должно сработать (и кого, соответственно, из наших звезд ударить током). К маньякам, которые быстро-быстро листают и лайкают мы применим страусовый алгоритм, то есть ничего делать не будем. Итак, js начинает обретать следующий вид:
/* инициализация */
var current_slide = 1;
function sendLike(type)
{
	$.post('/sn/snh.php', {action:'like', socnet:type}, function(reply){});
}
VK.Observer.subscribe("widgets.like.liked", function f(a, b)
{
    sendLike(current_slide);
});

На листалку слайдов туда-сюда ставим функции, меняющие переменную current_slide на соответвующее текущему кадру значение.

Facebook


Теперь вспоминаем про Facebook. Не люблю эту соцсеть, но как говорится, на вкус и цвет все фломастеры разные. В документации
developers.facebook.com/docs/plugins/like-button
developers.facebook.com/docs/reference/javascript/FB.Event.subscribe
находим возможность подписаться на событие edge.create и пробуем его запользовать:
$(window).load(function(){
	FB.Event.subscribe('edge.create', function(targetUrl) {
    		sendLike( current_slide );
	});
});
Сами кнопки вставляем строчкой вида:
 <fb:like href="site.ru/?1" layout="button_count"  show_faces="false" width="160" height="40" action="like" colorscheme="light"></fb:like>
...
 <fb:like href="site.ru/?2" layout="button_count"  show_faces="false" width="160" height="40" action="like" colorscheme="light"></fb:like>
...
 <fb:like href="site.ru/?3" layout="button_count"  show_faces="false" width="160" height="40" action="like" colorscheme="light"></fb:like>

get-параметр служит только для того, чтобы счётчики около кнопок работали независимо друг от друга.
Сохраняем, грузим страницу и тестируем кнопки. Ура, данные уходят на сервер. Пока что, в никуда.


На фото данные уже пришли куда надо и загорелась лампочка.

Серверная часть


В виду отсутствия большого количества времени и наличия небольшой лени, серверная часть приняла следующий вид:
1. ajax-сервер, который принимает отчёты о лайках по http и передаёт их по сокету на процесс-демон, который уже хранит в себе все неотправленные на исполняющую часть лайки и пересылает их по запросу с клиента.
2. процесс-демон, который представляет из себя сервер на сокетах и понимает два варианта запроса: “отдай налайканное” и “добавь лайк в очередь”.

Первый пункт решается кратко и понятно:
ajax-сервер
/sn/snh.php
error_reporting(0);
if ($_REQUEST['action'] == 'like' && intval($_REQUEST['socnet'])) {
	$address = "127.0.0.1";
	$port = 13666;

	/* Create a TCP/IP socket. */
	$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
	if ($socket === false) {
die();
	} 
	$result = socket_connect($socket, $address, $port);
	if ($result === false) {
		die();
	} 

	socket_write($socket, 'add '.trim(strval($_REQUEST['socnet']))."\n");
	$_SESSION['count']++; // потом пригодится
	echo $_SESSION['count'];
	socket_close($socket);
}

“Демон” чуть позамороченнее, ибо он и поумнее
Демон
srv.php
<?php

error_reporting(E_ALL);
echo "starting...\n";
$address = "0.0.0.0";
$port = 13666;

$sock = socket_create(AF_INET, SOCK_STREAM, 0);
echo "socket ok\n";

socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);

if(!socket_bind($sock, $address, $port)) {
	socket_close($client);
	die('Could not bind to address');
}

echo "bind ok\n";
socket_listen($sock);
echo "listen ok\n";

$events = array();

echo "accepting...\n";
socket_set_nonblock($sock);
ini_set('log_errors', false);

$notify_socket = array();
while (true) {
	if ($client = @socket_accept($sock)) {
    	if (is_resource($client)) {
    	echo "connected client\n";
        	$command = trim(socket_read($client, 32, PHP_NORMAL_READ));
        	if (strpos($command, "get") !==false) {
            	echo "send events\n";
            	$n = -1;
            	$str = '';
            	while (++$n < 5 && count($events) > 0) {
                	$str .= array_shift($events);
            	}
            	echo "\nsend events:".$str."\n\n";
            	if (strlen($str) > 0) {
                	socket_write($client, $str."\n");
            	} else {
                	socket_write($client, "none\n");
            	}
        	} else if (strpos($command, "add")===0) {
            	$what = explode(" ", $command);
            	if (intval($what[1])) {
                	socket_write($client, "ok\n");
                    	$events[]=(int)$what[1];
                    	echo "n/c. events: [".implode(" ", $events)."]\n";
            	}
        	}
        	socket_shutdown($client);
        	socket_close($client);
        	echo "end of chat\n";
    	}//if valid connection
	}//if accepted
}//while true;

socket_shutdown($sock);
socket_close($sock);

Так-с, теперь запускаем…
screen php -f srv.php

И переходим к части, которая будет работать с Arduino. В виду отсутствия ethernet shield, делаем desktop-приложение, которое будет работать модулем сопряжения демона на сервере и Arduino.

Так как я линуксоид, а “клиентские” компы будут работать под Windows, то для разработки этого модуля берём Qt + Qt Creator. Он может сделать интерфейс, работает с сетью, да и вообще штука хорошая и кроссплатформенная. Для работы с последовательным портом стандартной библиотеки нет, но есть замечательный проект QextSerialPort: code.google.com/p/qextserialport который уже давно мной используется в своих целях.

Ставим Q-всё, делаем новый проект, добавляем модуль network, файлы QextSerialPort и клепаем простенький интерфейс: текстовое поле для ввода названия порта и кнопка “соединиться”, которая запускает бесконечный процесс запроса новых лайков у сервера и пересылке всего ответа на arduino.

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


Главное орудие пыток.

Arduino


Скетч крайне прост. Читаем из последовательного порта цифру и подаём HIGH на соответствующий пин. И через 1200мс выключаем обратно.

Код Arduino
#define LED_1  A0
#define LED_2  A1
#define LED_3  A2

void setup()
{
  pinMode(LED_1, OUTPUT);
  pinMode(LED_2, OUTPUT);
  pinMode(LED_3, OUTPUT);
  Serial.begin(9600);
}

void LEDoff()
{
  digitalWrite(LED_1, LOW);
  digitalWrite(LED_2, LOW);
  digitalWrite(LED_3,  LOW);
}//sub

void loop()
{
 if (Serial.available() > 0) {
   char ch;
   while (Serial.available()) {
 	ch=Serial.read();
 	if (ch == '1') {
   	digitalWrite(LED_1, HIGH);
 	} else if(ch == '2') {
   	digitalWrite(LED_2, HIGH);
 	} else if (ch == '3') {
   	digitalWrite(LED_3, HIGH);
 	}
  }//while
  delay(1200);
LEDoff();
 }//if avail
}//sub

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

Включение на 1200мс выбрано опытным путём, этого значения оказалось достаточно, чтобы китайская зажигалка для плиты гарантированно “раскачалась” выдать искру.


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

Железная часть на Arduino


Берём Arduino Uno, prototype shield, три реле, проводки, обрезок витой пары, 3 китайские электрозажигалки и планку PLS-40/PBS-40 для соединений. Пока греется паяльник надеваем шилд на arduino, отрезаем сантиметров 15 витой пары и нарезаем планки разъёмов на 3 по 3. Витая пара оказалась медной, но моножильной, что несколько расстроило, так как я привык к домашним (уже почившим) запасам, где проводки были многожильными и мягкими. Паяем соединительные провода, подключаем реле к Arduino и проводком проверяем работоспособность реле. Щёлк-щёлк-щёлк. Работает.


Сама Arduino с установленной планкой.

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

Втыкаем Arduino в комп, заливаем скетч, запускает Qt-клиента, пробуем. Ядовитое трещание искр от зажигалок показывает, что заработало.

Прекрасно понимая, что добрые люди могут спокойно “зажарить” подопытных, а батарейка типоразмера D просто разрядится, то параллельно управляющим контактам каждого реле через 1КОм резистор включаем светодиод, вынесенный на длинном проводе на верхний край каждого монитора. Учитывая, что зажигалка “раскачивается” почти секунду, эта сигнализация идёт даже раньше щелчка реле и искры с зажигалки где-то на полсекунды.


Батарейка смогла продержаться целый день.

Настройка видео.


Видеотрансляция была реализована через youtube с помощью Live events. Так как один канал умеет только одну трансляцию одновременно, то пришлось сделать три подтверждённых канала. Плюс надо в браузер поставить расширение Google hangouts. И то и другое занимает не так уж много времени.
Итак, программная часть готова, камеры установлены. Открываем на трёх компьютерах наших артистов youtube, запускаем три трансляции. Копируем на них ссылки для показа и вставляем их на нашу страничку с электроуправлением. Главное не напутать в соответствии камер и реле. Ну и не забыть волшебную фразу “не закрывать это окошко!”.
Ограничение на длину трансляции в 8ч обошли просто: по истечении этих самых 8ч остановили старые и запустили новые трансляции на оставшийся час(шоу длилось 9ч). А артисты получили пять минут отдыха.


За 15 минут до старта.

Проверка боем


В процессе эксплуатации выяснилось, что нашлись хитрые пользователи, которые догадались, что можно ставить like, потом нажимать dislike и так до бесконечности.

Мы так и не определились баг это или фича, но всё-таки, получив лулзов от пользователей и фонтаны радости и репостов, мы эту дыру слегка прикрыли. У нас три потока, у каждого можно поставить like в двух соцсетях. Итого 6 максимум для “неклинического” садиста. Даём небольшой запас на поиграться и на значении 9 начинаем показывать картинку с надписью “Садист”. Да, возможность играться дальше мы оставляем.


Итоги


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

Скриншот сайта, как оно выглядело в живую
Тыц

UPD Видео трансляции. Видеоотчет о мероприятии монтируется.
www.youtube.com/watch?v=G52Rfq6wrDk
www.youtube.com/watch?v=S4dwTEVqZIc
www.youtube.com/watch?v=p0ABXUK3tWo
Tags:
Hubs:
+31
Comments 21
Comments Comments 21

Articles