Arduino x Ruby で某クイズの早押しシステムを構築する

会社の同僚の結婚式二次会の余興で、何を血迷ったか、かの有名な○ルトラクイズをそのまんま再現してしまいました。
その決勝戦で使用した早押しシステムについてです。
初めてArduinoを触ってみました。

## どんなシステム？

`「ニューヨークへ行きたいかー！？」`

このフレーズは有名ですね。
このクイズ番組で使われているの早押しシステムを、番組の象徴でもあるクイズハットと合わせて構築しました。

仕組みとしては`Arduinoを使ってサーボモーターとボタンを制御し、ボタンが押されたらサーボモーターを回転させて回答権があることを示すポップアップを立ち上げる`、というものです。


（なお、この企画の総監督は@debilityさんです。
PHPカンファレンス福岡2017にて予選クイズにおけるシステムの仕組みをLTしております。)
https://www.slideshare.net/debility/2017-76849790

## 早押しに必要なモノ

一言に早押しといっても、意外に考えないといけないことは多いです。


1.  ボタンに反応してポップアップを上げる
2.  ポップアップが上がっている状態では、他の回答者のボタンは無効
3.  ボタンに反応して音を鳴らす
4.  回答に応じて正解・不正解の音を鳴らす
5.  回答が終了したらポップアップを下げる（リセット）

これらの制御が必要です。

## Rubyを使って回答の判定やハットをコントロールする
上記1,2に関してはArduinoだけで完結できそうですが、3や4の音を鳴らす制御ができるのかどうか。
なるべくコストをかけずに行いたかったので、Arduinoを接続した先のPC上のRubyでプログラムを書いて音を鳴らすようにしました。また、5のリセットも合わせてRubyで制御することにします。なお、Rubyにしたのは私が得意だからです。（こういった場合の定石ではないかもしれませんが）

## 全体像
<img width="537" alt="スクリーンショット 2017-06-21 0.53.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/30229/604802c8-daab-9242-88dd-16d47ad5b2f7.png">


こんな感じで、ハード側の制御はArduino、その他コントローラーの役割をPC(Ruby)にもたせます。

## Arduinoで早押しとハットのポップアップを制御する

1,2に関する、Arduinoのコードは以下の通りです。


```Arduino
#include <Servo.h> // サーボモータの制御用

Servo servoRed; // 赤ハットサーボ
Servo servoGreen; // 緑ハットサーボ
int servoRedPin = 11; // 赤ハットが反応するピン
int servoGreenPin = 8; // 緑ハットが反応するピン

int redButtonPin = 3; // 赤ボタンのピン
int greenButtonPin = 5; // 緑ボタンのピン

boolean running = false; // 回答中かどうか

int servoAngle = 90; // サーボの初期角度
char ch = ''; // シリアルからの文字読み取り

void setup() {
  Serial.begin(9600);

  // サーボの適用
  servoGreen.attach(servoGreenPin);
  servoRed.attach(servoRedPin);

  // サーボモーターを初期位置に（ポップアップが下がっている）
  servoGreen.write(90);
  servoRed.write(90);

  pinMode(3,INPUT);
}

void loop() {
  // 赤ボタンを反応させる（緑ハットが有効の時は反応しない）
  if (digitalRead(redButtonPin) != HIGH && !running) {
    running = true;
    Serial.println(12);
    servoRed.write(0); // 赤ハットのポップアップが立ち上がる
  }
  // 緑ボタンを反応させる（赤ハットが有効の時は反応しない）
  if (digitalRead(greenButtonPin) != HIGH && !running) {
    running = true;
    Serial.println(12);
    servoGreen.write(0); // 緑ハットのポップアップが立ち上がる
  }
}

```

## Rubyでボタンに反応して音を鳴らす
ボタンを押した時に音を鳴らすための、Rubyによるコードです。

```Ruby
require 'serialport'
require 'open3'
require 'io/console'

# arduinoポート
serial_port = "/dev/tty.usbmodem1421"
serial_bps = 9600
data_bits = 8
stop_bits = 1
parity = SerialPort::NONE

sp = SerialPort.new(serial_port, serial_bps, data_bits, stop_bits, parity)

# SEの設定
on_filename = "/xxx/button.mp3"

@i = nil

loop do
  line = sp.gets
  if line.size > 0
    # 入力検知でピンポン音
    @i = spawn("mpg123 -C #{on_filename}")

    # 他のmpg123プロセスを消す
    `ps aux | grep mpg123`.split("\n").each do |row|
      next if row.include?("grep") || row.include?(@i.to_s)
      pid = row.split[1]
      Process.detach pid.to_i
      `kill -9 #{pid} > /dev/null 2>&1`
    end
  end
end

```
Arduino側から、何らかのInputがあればそれに反応して音を鳴らします。
なお、音は `mpg123` を使って鳴らしています。これを使った理由は簡単に音源を鳴らすものがこれぐらいしか見つけきれなかったため。なお、mpg123を単純に実行するとプロセスが起動したままとなり、やればやるほどプロセスが溜まっておかしくなるので、1回毎にプロセスをkillしています。

## Rubyでキーボード入力に応じて回答状態をリセットする&色んな音を鳴らす
これだけだけでは、クイズの回答が1回のみというシビアなルールになってしまうので、回答が完了したらリセットさせないといけません。また、正解なら「ピンポン」、不正解なら「ブー」などの効果音を流すことも必要。これらの制御はキーボードコマンドで行います。

```Ruby
# arduinoポート
serial_port = "/dev/tty.usbmodem1421"
serial_bps = 9600
data_bits = 8
stop_bits = 1
parity = SerialPort::NONE

sp = SerialPort.new(serial_port, serial_bps, data_bits, stop_bits, parity)

# SE
ok_filename = "/xxx/correct.mp3" #正解の音
ng_filename = "/xxx/buzzer.mp3" #不正解の音
thinking_filename = "/xxx/clock.mp3" #シンキングタイムの音

# プロセスID
@i = nil

# キー入力受付
while c = STDIN.getch
  if c == ?\C-c # 実行中はCtr+Cで抜けることができなくなるので、明示的に書く
    begin
      Process.detach @i
      Process.kill(:TERM, @i)
    rescue => e
    end
    exit
  end

  if c.size > 0 && !@i.nil?
    begin
      Process.detach @i
      Process.kill(:TERM, @i)
    rescue => e
    end
  end

  if c == "p"
    # 正解
    @i = spawn("mpg123 -C #{ok_filename}")
  elsif c == "o"
    # 不正解
    @i = spawn("mpg123 -C #{ng_filename}")
  elsif c == "i"
    # シンキング
    @i = spawn("mpg123 -C #{thinking_filename}")
  elsif c == "r"
    # リセット通知
    sp.write('r') # Arduinoへ通知
  end
end
```
こちらも音を鳴らすためにmpg123を使っています。

また、
「r」の入力があればArduino側はそれに反応してポップアップを下げるために、以下のコードをArduinoのloop関数内に追加。

```Arduino
  ch = Serial.read();
  if(ch == 'r'){
    // サーボの位置をリセット
    servoGreen.write(90);
    servoRed.write(90);
    running = false;
  }
```


これで、早押しクイズシステムができあがりました。

## さいごに
Arduinoを使ってみたくて電子工作っぽいことをしましたが、ハードを作ることに楽しさを覚えました。Rubyに頼ってしまった部分が多いにありますが、次はArduinoで完結するような何かを作ってみたいものです。

<font color="#FFFFFF">ウルトラクイズ 早押しクイズ</font>