LoginSignup
20
8

More than 1 year has passed since last update.

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

Last updated at Posted at 2017-02-04

はじめに

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

MaBeee について

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

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

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

つくったもの

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

なお、普段モバイル開発はしていないので、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:

20
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
8