[Из песочницы] Как мы сделали умнее наш настольный футбол и себя

5d6ec5b0f8484b21b1bcab77335ab0e0.jpgЧтобы сделать умный настольный футбол, нам понадобится:

обычный глупый настольный футбол — 1шт.; контроллер Arduino — 1шт.; лазер — 2шт.; фоторезистор — 2шт.; несколько заинтересованных людей, свободные выходные. ПредысторияВ нашей компании большинство сотрудников не прочь сыграть партейку в настольный футбол. Скорее даже очень любят и, понятно, одной партией дело не ограничивается. Поэтому на обеде и вечером возле стола собирается толпа ждущих своей очереди айтишников.И вот, однажды, когда путаница с очередью всем порядком надоела, нам в голову пришла ИДЕЯ!

b65354f90df6472f9a718c4e8c512361.jpg

А давайте сделаем электронную очередь! И чтобы стол сам голы считал! И мог определить кто из нас круче! И смски пусть присылает, что освободился! И все разбежались гуглить. День 1 e6a639c77367491d89938c3ccadb9220.jpgВ пятницу вечером группа единомышленников-футболопоклонников собралась возле виновника торжества — стола — на совещание. Поделились нагугленным, определились с основными требованиями и технологиями, распределили роли, повертели в руках выпрошенный у начальства микроконтроллер.

День 2 2bf216c8347849cbb75711710118f50e.jpgПервым делом в субботу утром развинтили стол. Чтобы научить его отслеживать забитые голы, прицепили 2 лазера и 2 фоторезистора на ворота и контроллер Arduino посередине. Систему придумали такую: когда в область между лазером и фоторезистором попадает мяч, контроллер фиксирует изменение напряжения на сенсоре. Так, изменение напряжения является следствием изменения сопротивления на фоторезисторе. Принципиальная схема изображена ниже.

image

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

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

Arduino:

26ef3656f7db45a1997c5b8198391806.jpg

3d4776e9fb6846139705bc32d13ffe56.jpg

Лазеры:

806f4744b6254a6c878eb998bfe2ef6f.jpg

2f75d100ab854ba882dea4117928c6e1.jpg

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

Режимы игры 1×1, 2×2; Уровни игроков; Коллекционирование достижений игроков; Ведение личных и командных рейтингов; Звуковое сопровождение игры. Надо сказать, нам крупно повезло, что в настольный футбол любит резаться и наша креативная дизайнер. Поэтому к обеду у нас на руках уже были симпатичные мокапы. Забегая вперед, покажем что из них получилось:

557bd07567d544db8216fd22f8a7df87.PNG

893bf61048564992ab2a190207dc2ec0.PNG

50066fe73898435db10ef0804bb9de2a.PNG

Разработка велась параллельно по трем веткам:

Клиентская сторона — Angular.js, Bootstrap.Создали основные страницы приложения, оформили дизайн, реализовали взаимодействие с сервером через Rest API и Socket.io. Адаптировали верстку под мобильные устройства. Серверная сторона — Node.js, Socket.io, MongoDB.Создали структуру проекта, разработали модель данных, настроили взаимосвязь между клиентом и сервером, разграничение по правам доступа. Реализовали логику по расчёту статистики, коллекционированию достижений, ведению рейтингов. Сделали оповещение клиента о возникающих событиях при помощи Socket.io. Взаимосвязь между Arduino и сервером.Написали прослойку между контроллером и сервером. Тут надо заметить, что мы решили совместить приятное с полезным. Поэтому выбирали технологии малознакомые участникам проекта, чтобы заодно прокачать скилы.В общем-то, писать подробнее о первом и втором пунктах смысла нету. Несмотря на то, что разработка этих частей заняла большую часть времени, никаких сверхзадач тут не стояло, все было достаточно тривиально. Поэтому перейдем к самому вкусному — взаимодействию между сервером и нашим умным столом.

Конечно же, было бы правильнее организовать беспроводную передачу данных между Adruino и сервером, используя wi-fi или bluetooth модули для взаимодействия с сервером. Или даже использовать Raspberry Pi как сервер для нашего приложения. Но у нас не было ни первого, ни второго, ни третьего, зато был компот старый компьютер, который все еще мог послужить нам в качестве сервера. Поэтому наш сервер соединен со столом при помощи USB кабеля, и все общение между Arduino и сервером происходит через COM-порт.

Arduino получает с порта сигналы о включении/выключении лазеров и, в свою очередь, отправляет сигналы о зафиксированных голах на сервер.

Скетч для Arduino //Pin for white gate laser #define WHITE_LED_PIN 13 //Pin for blue gate laser #define BLUE_LED_PIN 8 //Pin for white gate photoresistor #define WHITE_LDR_PIN A0 //Pin for blue gate photoresistor #define BLUE_LDR_PIN A1

//Commands to arduino: // 1 — start game const int START_COMMAND = 1; // 2 — stop game const int STOP_COMMAND = 2;

//Commands from arduino: // 'GOAL: WHITE' — goal to white gate // 'GOAL: BLUE' — goal to blue gate const char GOAL_PREFIX[] = «GOAL:»; const char WHITE_TEAM_NAME[] = «WHITE»; const char BLUE_TEAM_NAME[] = «BLUE»; // 'LISTENING' — waiting for commands const char LISTENING_MESSAGE[] = «LISTENING»; // 'START' — game is started const char START_GAME_MESSAGE[] = «START»; // 'CALIBRATION' — sensors are in calibration const char CALIBRATION_MESSAGE[] = «CALIBRATION»; // 'STOP' — game is stopped const char STOP_GAME_MESSAGE[] = «STOP»;

//Timeout after which game will be automatically stopped //15 minutes — 900000 ms const long INACTIVITY_TIMEOUT = 900000;

//Minumum deviation that is necessary to count a goal //E.g if deviation is 1.1 it means that when value on photoresistor exceeds calibrated maximum at least on 10%, goal will be counted const int MINIMUM_SENSOR_DEVIATION = 1.1;

//Time in milliseconds for photoresistors calibration const long CALIBRATION_TIME = 1000;

//Delay in milliseconds after goals to avoid multiple counting of the same goal const long DELAY_AFTER_GOALS = 1000;

//Delay in milliseconds to avoid interference on LDR right after lasers are 'ON' const long DELAY_BEFORE_CALIBRATION = 200;

//Maximum values on photoresistors after calibration. int maxWhiteSensorValue = 0; int maxBlueSensorValue = 0;

//Is game currently running boolean running = false; //Milliseconds from last activity (from game start or from last goal) long lastActivityTime = 0;

void setup () { Serial.begin (9600); pinMode (WHITE_LED_PIN, OUTPUT); pinMode (BLUE_LED_PIN, OUTPUT); Serial.println (LISTENING_MESSAGE); }

void loop () { if (running) { if (millis () — lastActivityTime >= INACTIVITY_TIMEOUT) { //Stop the game because of inactivity stopTheGame (); } else { checkFootballGate (WHITE_LDR_PIN, maxWhiteSensorValue, WHITE_TEAM_NAME); checkFootballGate (BLUE_LDR_PIN, maxBlueSensorValue, BLUE_TEAM_NAME); } } else { //If game isn’t running, check serial port for incoming commands int serialValue = Serial.parseInt (); if (serialValue == START_COMMAND) { startTheGame (); } } }

//Turn on lasers and calibrate the photoresistors void startTheGame () { digitalWrite (WHITE_LED_PIN, HIGH); digitalWrite (BLUE_LED_PIN, HIGH); //Delay to avoid interference on LDR right after lasers are 'ON' delay (DELAY_BEFORE_CALIBRATION); Serial.println (CALIBRATION_MESSAGE); calibrateSensors (); running = true; lastActivityTime = millis (); Serial.println (START_GAME_MESSAGE); }

void stopTheGame () { Serial.println (STOP_GAME_MESSAGE); digitalWrite (WHITE_LED_PIN, LOW); digitalWrite (BLUE_LED_PIN, LOW); running = false; }

//Measuring of maximum values on photoresistors during calibrationTime period void calibrateSensors () { maxWhiteSensorValue = 0; maxBlueSensorValue = 0; long startMillis = millis (); while (millis () — startMillis < CALIBRATION_TIME) { int whiteSensorValue = analogRead(WHITE_LDR_PIN); int blueSensorValue = analogRead(BLUE_LDR_PIN); // record the maximum sensor value if (whiteSensorValue > maxWhiteSensorValue) { maxWhiteSensorValue = whiteSensorValue; } if (blueSensorValue > maxBlueSensorValue) { maxBlueSensorValue = blueSensorValue; } } }

void checkFootballGate (int ldrPin, int maxSensorValue, const char *teamName) { int sensorValue = analogRead (ldrPin); //If sensorValue is exceeds maxValue at least on configured minimum deviation (which means that light flow is interrupted) if (sensorValue >= (maxSensorValue * MINIMUM_SENSOR_DEVIATION)) { Serial.print (GOAL_PREFIX); Serial.println (teamName); lastActivityTime = millis (); checkForStopCommand (); delay (DELAY_AFTER_GOALS); } }

//Check serial port for stop game command void checkForStopCommand () { int serialValue = Serial.parseInt (); if (serialValue == STOP_COMMAND) { stopTheGame (); } } Контроллер на серверной стороне var Arduino = function () {

var self = this; // constans block self.LISTENING_MESSGAE = «LISTENING»; self.STOP_GAME_MESSAGE = «STOP»; …

var portIsReady = true; // init SerialPort var serialPort = new SerialPort (self.PORT_NUMBER, { parser: serialport.parsers.readline (»\r\n»), baudrate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false }, true); // open connection and listening port serialPort.on (self.PORT_OPEN_COMMAND, function () { serialPort.on (self.PORT_RECEIVE_DATA_COMMAND, function (arduinoMessage) { if (arduinoMessage === self.LISTENING_MESSGAE) { // arduino is ready self.emit (self.ARDUINO_READY_COMMAND, arduinoMessage); } else if (arduinoMessage === self.STOP_GAME_MESSAGE) { // stop command or timeout stop self.emit (self.ARDUINO_IS_STOPPED, arduinoMessage); } else if (arduinoMessage === self.GOAL_WHITE_MESSAGE || arduinoMessage === self.GOAL_BLUE_MESSAGE) { // goal in white gate (point for blue team) self.emit (self.ARDUINO_GOAL, arduinoMessage); } }); });

serialPort.on (self.PORT_CLOSE_COMMAND, function () { portIsReady = false; }); serialPort.on (self.PORT_ERROR_COMMAND, function () { portIsReady = false; }); self.on (self.ARDUINO_READY_COMMAND, function () { portIsReady = true; }); self.on (self.ARDUINO_START_COMMAND, function () { if (portIsReady) { serialPort.write (self.START_GAME_COMMAND); } }); self.on (self.ARDUINO_STOP_COMMAND, function () { if (portIsReady) { serialPort.write (self.STOP_GAME_COMMAND); } }); self.start = function () { self.emit (self.ARDUINO_START_COMMAND); };

self.stop = function () { self.emit (self.ARDUINO_STOP_COMMAND); }; }; Здесь мониторим порт, к которому подключён Arduino. При получении команды, генерируем то или иное событие. Для запуска и остановки Arduino у нас есть две специальных функции start и stop, которые управляют включением и выключением лазеров.Пример обработки событий var GameController = function () { … Arduino.on (Arduino.ARDUINO_GOAL, function (team) { goal (team); });

Arduino.on (Arduino.ARDUINO_IS_STOPPED, function () { _stop (currentGame, true); });

… } Таким образом, к концу второго дня мы получили рабочую базовую функциональность клиента и сервера и готовую прослойку по взаимодействию с Arduino.

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

Наконец, все собрано, подключено, запуск — заработало! Приступили к функциональному тестированию. Ладно-ладно, играли в футбол, чего уж там…Парочка багфиксингов, небольшой допил и …PROFIT! Умный футбол готов.

[embedded content]

Итог В результате получился прототип высокотехнологичного настольного футбола, который самостоятельно фиксирует и считает забитые голы, ведет рейтинг игроков, формирует очередь и, вообще, делает наш отдых намного удобнее и интереснее! А еще мы отлично провели время и повысили свои скилы, конечно.Надеемся, статья была хоть сколько-нибудь полезной и вдохновит вас на собственные эксперименты. Всем удачи!

© Habrahabr.ru