Як ми зробили розумніші наш настільний футбол і себе



Щоб зробити розумний настільний футбол, нам знадобиться:

  • звичайний дурний настільний футбол — 1шт.,
  • контролер Arduino — 1шт.,
  • лазер — 2шт.,
  • фоторезистор — 2шт.,
  • кілька зацікавлених людей,
  • вільні вихідні.

Передісторія
У нашій компанії більшість співробітників не проти зіграти партійку в настільний футбол. Швидше навіть дуже люблять і, зрозуміло, однією партією справа не обмежується. Тому на обід і увечері біля столу збирається натовп чекають своєї черги айтішників.

І ось, одного разу, коли плутанина з чергою всім набридла, нам в голову прийшла ідея:



  • А давайте зробимо електронну чергу!
  • І щоб стіл сам голи вважав!
  • І міг визначити хто з нас крутіший!
  • І смски нехай присилає, що звільнився!


І всі розбіглися гуглити.

1 День
У п'ятницю ввечері група однодумців-футболопоклонников зібралася біля винуватця торжества — столу — на нараду. Поділилися нагугленным, визначилися з основними вимогами та технологіями, розподілили ролі, повертели в руках випрошені у начальства мікроконтролер.

День 2


Першим ділом в суботу вранці развинтили стіл. Щоб навчити його відстежувати забиті голи, причепили 2 лазера і 2 фоторезистора на ворота і контролер Arduino посередині. Систему придумали таку: коли в область між лазером і фоторезистором потрапляє м'яч, контролер фіксує зміна напруги на сенсорі. Так, зміна напруги є наслідком зміни опору на фоторезисторе. Принципова схема зображена нижче.



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

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

Arduino:





Лазери:





Паралельно почали роботу над програмної складової проекту. Першим ділом конкретизували вимоги:
  • Режими гри 1x1, 2x2.
  • Рівні гравців.
  • Колекціонування досягнень гравців.
  • Ведення особистих і командних рейтингів.
  • Звуковий супровід гри.


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







Розробка велася паралельно з трьох гілок:
  1. Клієнтська сторона — Angular.js, Bootstrap.
    Створили основні сторінки програми, оформили дизайн, реалізували взаємодію з сервером через Rest API і Socket.io. Адаптували верстку під мобільні пристрої.
  2. Серверна сторона — Node.js, Socket.io, MongoDB.
    Створили структуру проекту, розробили модель даних, налаштували взаємозв'язок між клієнтом і сервером, розмежування прав доступу. Реалізували логіку розрахунку статистики, колекціонування досягнень, ведення рейтингів. Зробили сповіщення клієнта про виникаючі події за допомогою Socket.io.
  3. Взаємозв'язок між 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;

//Game Is 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 value sensor
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 configured on minimum deviation (which means that flow light 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 stop timeout
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! Розумний футбол готовий.



Підсумок
В результаті вийшов прототип високотехнологічного настільного футболу, який самостійно фіксує і вважає забиті голи, веде рейтинг гравців, формує чергу і, взагалі, робить наш відпочинок набагато зручніше і цікавіше. А ще ми відмінно провели час і підвищили свої скіли, звичайно.

Сподіваємося стаття була хоч скільки-небудь корисною і надихне вас на власні експерименти. Всім удачі!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.