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

はじめに

乾電池IoT “MaBeee” を購入しました。
提供されている 公式アプリ でも色々遊べますが、せっかくなので自分でアプリを作って遊んでみました。

MaBeee について

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

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

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

つくったもの

公式アプリでは、声の大きさで電池をコントロールできるモードがあります。
これを応用し、音声認識で特定のワードに反応して電池(+搭載されたおもちゃ: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 もあります)

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

こちら を参考にして、プロジェクトに MaBeeeSDK.framework を追加します。
今回は横着して Embedded Binaries に直接ドラッグアンドドロップで追加しました。

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

import MaBeeeSDK

MaBeee のスキャン・接続

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

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

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

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

MaBeee の制御

MaBeeeDevice クラスの pwmDuty プロパティで MaBeee の出力値を設定することができます。
出力値は 0~100 を指定できますが、今回はシンプルに電池をオンオフできれば良いので、適当に以下のようなコードを書きます。

    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 を利用します。

実装にあたっては、主に以下の記事を参考にさせていただきました :bow:
[iOS 10] SFSpeechRecognizerで音声認識を試してみた

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

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

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

import Speech

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


    // 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を搭載したプラレールを音声アナウンスで制御してみた

感想など

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

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


『 Swift 』Article List