aDSC_8213

来訪者向け受付システムを作った話

S2, 技術部

経緯

2017年7月22日、弊社は神泉町から南平台 (住所は道玄坂) に引っ越しました。
〒150-0043 東京都渋谷区道玄坂1丁目16-10 渋谷DTビル 7F

新しいオフィスの入り口にはインターホンが付いておらず、来訪者のための呼び鈴に相当するものを何かしら用意する必要がありました。そこで社内で検討した結果、以下の 3 案が上がりました。

  1. 一般的なドアホン
  2. 電話機
  3. iPad を使った受付システム

当初は 1 の一般的なドアホンを購入して付ければ良いかというような話になっていました。しかし、オフィス移転という好機にそれでは面白くないだろうと思い直し、カッとなって iPad を使った受付システムを自作することにしました。

要件

時間もリソースも限られているので最小限な要件を作りました。

  1. 来訪者が iPad をタッチする
  2. 待合室、執務室双方にチャイム音が鳴る
  3. Slack に来訪者が来た旨を通知する
  4. 来訪者の顔をキャプチャして Slack に通知する
  5. 来訪者にスタッフを呼び出している旨を伝える

以前のオフィスでは一般的なドアホンを使っていたため、まずはドアホンの代わりとなるシステムを作ることにしました。来訪者に目的を選ばせたり、担当者を選ばせたりする機能も検討しましたが、今回は要件から外しました。

システム構成

まず試したのは、iPad と Bluetooth スピーカーを使った構成です。

s2-reception_structure2

この構成には 3 つの問題がありました。

  1. iPad のスピーカーで音が出せない
  2. Bluetooth 接続が途切れる
  3. 音声信号が乱れる

1. iPad のスピーカーで音が出せない

Bluetooth スピーカーを執務室に設置し、iPad の音声出力を Bluetooth スピーカーに向けた場合、待合室に設置した iPad からは音が出なくなるため、来訪者は呼び出しが正常に行われたことを視覚のみでしかフィードバックを得られません。実際に体験してみると、聴覚によるフィードバックもあった方が安心感が得られることがわかりました。

2. Bluetooth 接続が途切れる

実装方法の問題かも知れませんが、一旦 Bluetooth スピーカーに接続し、オーディオファイルを再生したのち、しばらくすると Bluetooth スピーカーの接続が自動的に切れる現象に遭遇しました。Bluetooth スピーカーとの接続を監視してみると、おおよそ10〜15分程度で接続が解除されることがわかりました。これについては解決策がわからなかったため、30 秒毎に超音波を鳴らして接続をキープするという方法を取ることにしました。

3. 音声信号が乱れる

空っぽの新オフィスで実験している時は問題なかったのですが、実際に什器が設置され、人が入り、Wi-Fi が飛ぶようになると Bluetooth で飛ばした音声信号が乱れる現象に遭遇しました。これについては解決の糸口がすら見出せずに終わりました。

以上の 3 点から、システム構成を考え直すことにしました。

Raspberry Pi を経由することで、無線通信は API 呼び出しのみ、音声信号は有線で接続という構成を作りました。

s2-reception_structure

構成がやや複雑になったものの、初期の構成で問題になった 3 点はいずれも解消します。

レシピ

材料

ハードウェア

  1. iPad
  2. Raspberry Pi 3
  3. Bluetooth スピーカー
  4. iPad スタンド

iPad と Raspberry Pi は社内に転がっていたものを使用。Bluetooth スピーカーは Amazon で Anker の SoundCore 2 (5,000 円くらい) を購入しました。一番お金がかかったのは 意外なことに iPad スタンドでしたが、オウルテック社のiPadフロアスタンド を購入しました。

ソフトウェア

  1. オーディオファイル
  2. API サーバー
  3. iPad アプリ

ソフトウェアは全て自作します。ここからが本題。

1. オーディオファイルファイルを作る

一般的なドアホンのようなチャイム音のオーディオファイルについて、初めはフリー素材を探してみました。ところがいざやってみるとちょうど良いフリーのオーディオファイルを探すというのは案外面倒でした。そこで Ableton Live を使い手元で作ってしまうことにしました。

ピアノロールで MIDI を書く

前オフィスのドアホンの音を耳コピしてみたところ、メジャースケールで3度、1度の順で鳴らせば良さそうなことがわかりました。なんとなく気分でC#メジャースケールを採用することにしました。

piano-roll

音色はシンプルにサイン波を使います。立ち上がり (Attack) が速くて、余韻 (Release) を少し長め (3秒) にすると良いです。

synth

出来たものを Live から .wav で書き出し、iOS で再生するために .caf ファイルも作っておきます。.caf ファイルへの変換は Mac に入っている (要 Xcode?) afconvert を使います。

% afconvert -f caff -d LEI16 sound.wav sound.caf 

以上でオーディオファイルの作成は完了です。

2. API サーバーを作る

Perl と Dwarf (Web Application Framework) を使って JSON API を作成します。サーバーは Raspberry Pi 3 です。Raspberry Pi を使うのが初めてだったので、OS は標準の raspbian を使い、Perl もプリインストールされているものをそのまま使います。

必要な CPAN モジュールは Carton でインストールします。あらかじめ libssl-dev だけ apt-get でインストールしました。

作った API は一つだけです。呼び出されたらオーディオファイルをバックグラウンドで再生するという非常にシンプルな API を用意しました。

package App::Controller::Api::Visitor;
use Dwarf::Pragma;
use parent 'App::Controller::ApiBase';
use Dwarf::DSL;
use Class::Method::Modifiers;

after will_dispatch => sub {
    self->validate(
        mention => [qw//],
        sound   => [[DEFAULT => 'sound'], qw/NOT_BLANK/, [CHOICE => qw/sound test/]],
    );
};

sub post {
    my $sound = param('sound');
    my $path = c->base_dir . "/assets/$sound.wav";

    my $cmd = conf('/audio_player/cmd');
    if ($cmd) {
        system "$cmd $path&"
    }

    return {
    };
}

1;

シンプルな API サーバーなので、Apache や nginx も使わず、Plack のスタンドアローンサーバーを systemd で常駐する形で稼働させます。

[Unit]
Description = S2 Reception API Server

[Service]
ExecStart=/home/pi/Desktop/s2-reception/app/script/start_server.sh
Restart=always
Type=simple

[Install]
WantedBy=multi-user.target

以上でAPIサーバーの作成は完了です。

3. iPad アプリを作る

まずは簡単な画面イメージを描きました。

screen-image

iPad 以下に必要な機能は以下です。

  1. 表示周りの実装
  2. 音声ファイルの再生
  3. 静止画のキャプチャ (ただしステルス)
  4. API に POST

1. 表示周りの実装

Storyboard で画面イメージと同じ画面構成を作り、Segue で遷移を定義します。呼び出し完了画面を表示したら 10 秒で元の画面に戻すためのタイマーを実装しました。

var timer:Timer?

override func viewDidAppear(_ animated: Bool) {
    if let t = timer {
        t.invalidate()
    }

    timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { [unowned self] (timer) in
        self.performSegue(withIdentifier: "unwindToViewControllerSegue", sender: nil)
    }
}

override func viewWillDisappear(_ animated: Bool) {
    if let t = timer {
        t.invalidate()
    }
}

2. 音声ファイルの再生

AVAudioPlayer を使い、オーディオファイルを再生します。

var player:AVAudioPlayer!

func initAudio() {
    if let url = Bundle.main.url(forResource: "sound", withExtension: "caf") {
        do {
            player = try AVAudioPlayer(contentsOf: url)
            player.prepareToPlay()
            player.numberOfLoops = 0
            player.volume = 1.0
        }
        catch {
            print(error)
        }
    }
}

func playSound() {
    if let p = self.player {
        p.currentTime = 0
        p.play()
    }
}

3. 静止画のキャプチャ (ただしステルス)

受付が行われるタイミングで画像を撮影します。ただし、「カシャッ」という音が鳴ると来客を不穏な気持ちにさせるので、ステルスで撮影を行います。詳しい実装は割愛しますが、AVFoundation を使い動画のフレームを AVCaptureVideoDataOutput で取り出し、来客が呼び出しボタンを押したタイミングで動画のフレームを静止画に変換する方法を使いました。

4. API に POST

URLSession で API 呼び出しを実装します。

class func postToAPI() {
    let url = URL(string: "http://reception-sound-rpi.s2factory.co.jp:11022/api/visitor")!
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config)

    var request = URLRequest(url: url)
    request.httpMethod = "POST"

    let task = session.dataTask(with: request, completionHandler: {
        (data, response, error) in
    })

    task.resume()
}

画像の POST に必要なマルチパートのデータは愚直にテキストを積んでいく実装を行いました。

var data = Data()
data.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
data.append("Content-Disposition: form-data; name=\"channels\"\r\n".data(using: String.Encoding.utf8)!)
data.append("\r\n".data(using: String.Encoding.utf8)!)
data.append("#reception\r\n".data(using: String.Encoding.utf8)!)

data.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"reception.jpg\"\r\n".data(using: String.Encoding.utf8)!)
data.append("Content-Type: image/jpeg\r\n".data(using: String.Encoding.utf8)!)
data.append("\r\n".data(using: String.Encoding.utf8)!)
data.append(UIImageJPEGRepresentation(image, 0.7)!)
data.append("\r\n".data(using: String.Encoding.utf8)!)

data.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)

以上でiPadアプリの作成は完了です。

雑感

引っ越して約1ヶ月。作ったシステムは今のところ安定して稼働しています。今後の改善案として、画像認識技術などを使って「誰が来た」という情報を自動で判別するようなことに挑戦してみたいです。現状 Slack には「誰か来たよ」といった程度の情報しか流せていないのですが、もう少し具体的に「佐川さんが来たよ」「ヤマトさんが来たよ」という情報を流せると良さそうです。一般的に来訪者に「目的」を選ばせる形で実現させるものが多いかと思いますが、来訪者の負担にせずに自動的にシステムで解決する、と少しだけ未来を感じられそうです。

また、今回のシステム制作の過程で Bluetooth オーディオは少し距離が離れると難しいということが体感出来たのは良い収穫でした。

ちょっとした社内システムを実際に作ってみることで技術習得を行うという手法はあらゆる面でオススメです。