乾電池型IoT"MaBeee"を使って簡単なおもちゃ制御アプリを作ってみる

## はじめに

乾電池IoT ["MaBeee"](http://mabeee.mobi/) を購入しました。
提供されている [公式アプリ](https://itunes.apple.com/jp/app/mabeee-kontororu/id1104665648) でも色々遊べますが、せっかくなので自分でアプリを作って遊んでみました。

## MaBeee について

NOVARS INC. が発売している乾電池型IoT製品です。
クラウドファンディングによる支援を経て、2016年8月に一般販売が開始されました。

> MaBeee(マビー)は、おもちゃ、自分で作った工作、電動歯ブラシなど、乾電池で動く製品をスマホでコントロールできるようになる乾電池型IoT製品です。
> スマホを振ったり、傾けたり、声の大きさに反応させたりして、電車のおもちゃを速く走らせたり、ぬいぐるみや工作ロボットを動かしたり、タイマー機能で歯ブラシの時間を知らせてくれたりすることができます。 

乾電池型IoT とありますが、実際には単4形電池を BLE（Bluetooth Low Energy）で制御する単3形のデバイスになります。


## つくったもの

公式アプリでは、[声の大きさで電池をコントロールできるモード](https://www.youtube.com/watch?v=cD6buSTW1I8)があります。
これを応用し、音声認識で特定のワードに反応して電池（+搭載されたおもちゃ:train2:）を制御する iOS アプリを作ってみました。

https://github.com/amotz/MabeeeAllAboard

なお、普段モバイル開発はしていないので、Swift はほぼ初心者です。あしからず。

## 開発環境

Xcode 8.2
Swift 3.0
iOS 10.2 iPhone6

## つくりかた
### MaBeee SDK のインポート

まだ公式にはサポートされていませんが、MaBeee を iOS で利用するための SDK が GitHub に公開されています。

https://github.com/novars-jp/MaBeeeiOSSDK
（[Android向けSDK](https://github.com/novars-jp/MaBeeeAndroidSDK) もあります）

<font color="Crimson">※上記サイトにも記載されていますが、2017/2/4 現在、MaBeee SDK は公式にリリース・サポートされていません。SDK の利用は自己責任でお願いします</font> :pray:

[こちら](https://github.com/novars-jp/MaBeeeiOSSDK/wiki/%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88) を参考にして、プロジェクトに `MaBeeeSDK.framework` を追加します。
今回は横着して `Embedded Binaries` に直接ドラッグアンドドロップで追加しました。

あとは、以下のようにインポートすれば MaBeee SDK が扱えます。

```swift
import MaBeeeSDK
```
### MaBeee のスキャン・接続

`MaBeeeScanViewController` という設定用の ViewController が提供されているので、そのまま利用します。

```swift
  @IBAction func tappedSettingsButton(_ sender: UIButton) {
    let vc = MaBeeeScanViewController()
    vc.show(self)
  }
```

紐付けられたボタンをタップすると、公式アプリ同様の MaBeee スキャン・接続画面が開きます。

まず MaBeee 全体を管理する `MaBeeeApp`クラスのインスタンスが生成され、`MaBeeeDevice` クラスのインスタンスが Bluetooth で検出された MaBeee ごとに生成されます。

### MaBeee の制御

`MaBeeeDevice` クラスの [pwmDuty プロパティ](https://github.com/novars-jp/MaBeeeiOSSDK/wiki/MaBeeeDevice#pwmduty)で MaBeee の出力値を設定することができます。
出力値は 0～100 を指定できますが、今回はシンプルに電池をオンオフできれば良いので、適当に以下のようなコードを書きます。

```swift
    fileprivate func startMaBeee() {
        setMaBeeePWMDuty(100)
    }

    fileprivate func stopMaBeee() {
        setMaBeeePWMDuty(0)
    }

    fileprivate func setMaBeeePWMDuty (_ pwmDuty :Int32) {
        // スキャン・接続画面で接続された MaBeee の出力値を設定する
        for device in MaBeeeApp.instance().devices() {
            device.pwmDuty = pwmDuty
        }
    }
```

これで MaBeee （+搭載された機器）のオンオフ制御ができるようになりました！簡単ですね :grinning: 

### 音声認識

特定ワードの音声に反応させて MaBeee を制御したいので、アプリ内で音声認識させます。
iOS10 から搭載された [SpeechFramework](https://developer.apple.com/reference/speech) を利用します。

実装にあたっては、主に以下の記事を参考にさせていただきました :bow:
[[iOS 10] SFSpeechRecognizerで音声認識を試してみた](http://dev.classmethod.jp/smartphone/iphone/try-ios10-speech-recognizer/)

まず、`Info.plist` にマイクと音声認識の利用目的を記述します。

```xml:Info.plist
<dict>
    ...
    <key>NSMicrophoneUsageDescription</key>
    <string>音声認識のためにマイクを利用します。</string>
    <key>NSSpeechRecognitionUsageDescription</key>
    <string>Mabeeeを操作するために音声を認識します。</string>
    ...
</dict>
```

処理を書く前に、コード内で SpeechFramework をインポートしておきます。

```swift
import Speech
```

これで準備が整ったので、音声認識周りのコードを書いてみます。

```swift

    // MARK: Properties
    private let startWords = ["ドクターイエロー", "出発進行"]
    private let stopWords = ["停車します"]
    
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private let audioEngine = AVAudioEngine()

    // MARK: Actions
    @IBAction func tappedRecButton(_ sender: UIButton) {
        if audioEngine.isRunning {
            stopRecording()
            recButton.isEnabled = false
            recButton.setImage(UIImage(named: "Mic")!, for: UIControlState())
            speechLabel.text = ""
            balloonImage.isHidden = true
        } else {
            try! startRecording()
            recButton.setImage(UIImage(named: "Pause")!, for: UIControlState())
        }
    }

    // MARK: Speech functions
    fileprivate func requestRecognizerAuthorization() {
        SFSpeechRecognizer.requestAuthorization { authStatus in
            OperationQueue.main.addOperation { [weak self] in
                guard let `self` = self else { return }
                
                switch authStatus {
                case .authorized:
                    self.recButton.isEnabled = true
                case .denied:
                    self.recButton.isEnabled = false
                case .restricted:
                    self.recButton.isEnabled = false
                case .notDetermined:
                    self.recButton.isEnabled = false
                }
            }
        }
    }
    
    fileprivate func startRecording() throws {
        refreshTask()
        
        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(AVAudioSessionCategoryRecord)
        try audioSession.setMode(AVAudioSessionModeMeasurement)
        try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
        
        recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        
        guard let inputNode = audioEngine.inputNode else { fatalError("Audio engine has no input node") }
        guard let recognitionRequest = recognitionRequest else { fatalError("Unable to created a SFSpeechAudioBufferRecognitionRequest object") }
        
        recognitionRequest.shouldReportPartialResults = true
        
        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in
            guard let `self` = self else { return }
            
            var isFinal = false
            
            if let result = result {
                let resultString = result.bestTranscription.formattedString
                self.speechLabel.text = resultString
                
                // 認識結果が指定単語であれば MaBeee を制御
                if self.startWords.contains(resultString){
                    self.startMaBeee()
                } else if self.stopWords.contains(resultString){
                    self.stopMaBeee()
                }
                isFinal = result.isFinal
            }
            
            if error != nil || isFinal {
                self.audioEngine.stop()
                inputNode.removeTap(onBus: 0)
                
                self.recognitionRequest = nil
                self.recognitionTask = nil
                
                self.speechLabel.text = ""
                self.recButton.isEnabled = true
            }
        }
        
        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.recognitionRequest?.append(buffer)
        }
        
        try startAudioEngine()
    }
    
    fileprivate func stopRecording() {
        audioEngine.stop()
        recognitionRequest?.endAudio()
    }
    
    fileprivate func refreshTask() {
        if let recognitionTask = recognitionTask {
            recognitionTask.cancel()
            self.recognitionTask = nil
        }
    }
    
    fileprivate func startAudioEngine() throws {
        audioEngine.prepare()
        try audioEngine.start()
        
        balloonImage.isHidden = false
        speechLabel.text = "アナウンスしてください"
    }
    
    // MARK: SFSpeechRecognizerDelegate
    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        if available {
            recButton.isEnabled = true
        } else {
            recButton.isEnabled = false
        }
    }
```



雑に解説すると、録音ボタンをタップしたら音声入力（アナウンス）を開始し、入力された音声の認識結果を画面に表示します。
文字列が指定した単語に合致していれば、MaBeee をオンオフします。

これで

- **「出発進行」**あるいは**「ドクターイエロー」**とアナウンスすれば発車（MaBeee がオン）
- **「停車します」**とアナウンスすれば停車（MaBeee がオフ）

が実現できました :train2: 

なお、SpeechFramework を利用した音声認識は端末・アプリごとに回数制限（具体的な回数は公開されていない）があるため、遊びすぎには注意が必要です :fist: 

## デバッグの様子

[![MaBeeeを搭載したプラレールを音声アナウンスで制御してみた](http://img.youtube.com/vi/HX1aOEDfA9k/0.jpg)](http://www.youtube.com/watch?v=HX1aOEDfA9k)
（クリックすると動画が見れます）

## 感想など

MaBeee SDK は扱いやすく、MaBeee を制御するアプリをサクっと作ることができました。
おかげさまで、電車好きな子どもたち（2歳）も喜んでくれました :boy_tone1: 

MaBeee の代表的なユースケースはおもちゃの制御ですが、アイデア次第でおもちゃ以外にも使えそうです。
また何か思いつけば、適当にアプリを作ってみたいと思います :battery: