Чтобы сделать умный настольный футбол, нам понадобится:
- обычный глупый настольный футбол — 1шт.,
- контроллер Arduino — 1шт.,
- лазер — 2шт.,
- фоторезистор — 2шт.,
- несколько заинтересованных людей,
- свободные выходные.
Предыстория
В нашей компании большинство сотрудников не прочь сыграть партейку в настольный футбол. Скорее даже очень любят и, понятно, одной партией дело не ограничивается. Поэтому на обеде и вечером возле стола собирается толпа ждущих своей очереди айтишников.
И вот, однажды, когда путаница с очередью всем порядком надоела, нам в голову пришла идея:
- А давайте сделаем электронную очередь!
- И чтобы стол сам голы считал!
- И мог определить кто из нас круче!
- И смски пусть присылает, что освободился!
И все разбежались гуглить.
День 1
В пятницу вечером группа единомышленников-футболопоклонников собралась возле виновника торжества – стола – на совещание. Поделились нагугленным, определились с основными требованиями и технологиями, распределили роли, повертели в руках выпрошенный у начальства микроконтроллер.
День 2
Первым делом в субботу утром развинтили стол. Чтобы научить его отслеживать забитые голы, прицепили 2 лазера и 2 фоторезистора на ворота и контроллер Arduino посередине. Систему придумали такую: когда в область между лазером и фоторезистором попадает мяч, контроллер фиксирует изменение напряжения на сенсоре. Так, изменение напряжения является следствием изменения сопротивления на фоторезисторе. Принципиальная схема изображена ниже.
Несмотря на предельную простоту системы, пришлось все же столкнуться с некоторыми проблемами. Во-первых, изменение освещения в помещении с футбольным столом могло вызывать ложно-положительные срабатывания датчиков. Во-вторых, особо сильные вибрации стола во время игры могли привести к механическим повреждениям компонентов системы.
Первую проблему устранили перекалибровкой фоторезистора при каждом старте игры. Вторая решилась еще проще – c помощью отвертки, суперклея и, конечно же, синей изоленты все компоненты системы были надежно зафиксированы.
Arduino:
Лазеры:
Параллельно начали работу над программной составляющей проекта. Первым делом конкретизировали требования:
- Режимы игры 1x1, 2x2.
- Уровни игроков.
- Коллекционирование достижений игроков.
- Ведение личных и командных рейтингов.
- Звуковое сопровождение игры.
Надо сказать, нам крупно повезло, что в настольный футбол любит резаться и наша креативная дизайнер. Поэтому к обеду у нас на руках уже были симпатичные мокапы. Забегая вперед, покажем что из них получилось:
Разработка велась параллельно по трем веткам:
- Клиентская сторона — 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 получает с порта сигналы о включении/выключении лазеров и, в свою очередь, отправляет сигналы о зафиксированных голах на сервер.
//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("rn"),
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! Умный футбол готов.
Итог
В результате получился прототип высокотехнологичного настольного футбола, который самостоятельно фиксирует и считает забитые голы, ведет рейтинг игроков, формирует очередь и, вообще, делает наш отдых намного удобнее и интереснее. А еще мы отлично провели время и повысили свои скилы, конечно.
Надеемся статья была хоть сколько-нибудь полезной и вдохновит вас на собственные эксперименты. Всем удачи!
Автор: oss