Как мы сделали умнее наш настольный футбол и себя

в 15:53, , рубрики: angular.js, arduino, DIY, node.js, программирование микроконтроллеров

Как мы сделали умнее наш настольный футбол и себя - 1

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

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

Предыстория

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

И вот, однажды, когда путаница с очередью всем порядком надоела, нам в голову пришла идея:

Как мы сделали умнее наш настольный футбол и себя - 2

  • А давайте сделаем электронную очередь!
  • И чтобы стол сам голы считал!
  • И мог определить кто из нас круче!
  • И смски пусть присылает, что освободился!

И все разбежались гуглить.

День 1

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

День 2

Как мы сделали умнее наш настольный футбол и себя - 3

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

Как мы сделали умнее наш настольный футбол и себя - 4

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

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

Arduino:

Как мы сделали умнее наш настольный футбол и себя - 5

Как мы сделали умнее наш настольный футбол и себя - 6

Лазеры:

Как мы сделали умнее наш настольный футбол и себя - 7

Как мы сделали умнее наш настольный футбол и себя - 8

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

  • Режимы игры 1x1, 2x2.
  • Уровни игроков.
  • Коллекционирование достижений игроков.
  • Ведение личных и командных рейтингов.
  • Звуковое сопровождение игры.

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

Как мы сделали умнее наш настольный футбол и себя - 9

Как мы сделали умнее наш настольный футбол и себя - 10

Как мы сделали умнее наш настольный футбол и себя - 11

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

  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;

//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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js