2017/08/24
来訪者向け受付システムを作った話
経緯
2017年7月22日、弊社は神泉町から南平台 (住所は道玄坂) に引っ越しました。 〒150-0043 東京都渋谷区道玄坂1丁目16-10 渋谷DTビル 7F 新しいオフィスの入り口にはインターホンが付いておらず、来訪者のための呼び鈴に相当するものを何かしら用意する必要がありました。そこで社内で検討した結果、以下の 3 案が上がりました。- 一般的なドアホン
- 電話機
- iPad を使った受付システム
要件
時間もリソースも限られているので最小限な要件を作りました。- 来訪者が iPad をタッチする
- 待合室、執務室双方にチャイム音が鳴る
- Slack に来訪者が来た旨を通知する
- 来訪者の顔をキャプチャして Slack に通知する
- 来訪者にスタッフを呼び出している旨を伝える
システム構成
まず試したのは、iPad と Bluetooth スピーカーを使った構成です。 この構成には 3 つの問題がありました。- iPad のスピーカーで音が出せない
- Bluetooth 接続が途切れる
- 音声信号が乱れる
1. iPad のスピーカーで音が出せない
Bluetooth スピーカーを執務室に設置し、iPad の音声出力を Bluetooth スピーカーに向けた場合、待合室に設置した iPad からは音が出なくなるため、来訪者は呼び出しが正常に行われたことを視覚のみでしかフィードバックを得られません。実際に体験してみると、聴覚によるフィードバックもあった方が安心感が得られることがわかりました。2. Bluetooth 接続が途切れる
実装方法の問題かも知れませんが、一旦 Bluetooth スピーカーに接続し、オーディオファイルを再生したのち、しばらくすると Bluetooth スピーカーの接続が自動的に切れる現象に遭遇しました。Bluetooth スピーカーとの接続を監視してみると、おおよそ10〜15分程度で接続が解除されることがわかりました。これについては解決策がわからなかったため、30 秒毎に超音波を鳴らして接続をキープするという方法を取ることにしました。3. 音声信号が乱れる
空っぽの新オフィスで実験している時は問題なかったのですが、実際に什器が設置され、人が入り、Wi-Fi が飛ぶようになると Bluetooth で飛ばした音声信号が乱れる現象に遭遇しました。これについては解決の糸口がすら見出せずに終わりました。 以上の 3 点から、システム構成を考え直すことにしました。 Raspberry Pi を経由することで、無線通信は API 呼び出しのみ、音声信号は有線で接続という構成を作りました。 構成がやや複雑になったものの、初期の構成で問題になった 3 点はいずれも解消します。レシピ
材料
ハードウェア
- iPad
- Raspberry Pi 3
- Bluetooth スピーカー
- iPad スタンド
ソフトウェア
- オーディオファイル
- API サーバー
- iPad アプリ
1. オーディオファイルファイルを作る
一般的なドアホンのようなチャイム音のオーディオファイルについて、初めはフリー素材を探してみました。ところがいざやってみるとちょうど良いフリーのオーディオファイルを探すというのは案外面倒でした。そこで Ableton Live を使い手元で作ってしまうことにしました。ピアノロールで MIDI を書く
前オフィスのドアホンの音を耳コピしてみたところ、メジャースケールで3度、1度の順で鳴らせば良さそうなことがわかりました。なんとなく気分でC#メジャースケールを採用することにしました。 音色はシンプルにサイン波を使います。立ち上がり (Attack) が速くて、余韻 (Release) を少し長め (3秒) にすると良いです。 出来たものを 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 アプリを作る
まずは簡単な画面イメージを描きました。 iPad 以下に必要な機能は以下です。- 表示周りの実装
- 音声ファイルの再生
- 静止画のキャプチャ (ただしステルス)
- 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アプリの作成は完了です。