はじめに
乾電池IoT "MaBeee" を購入しました。
提供されている 公式アプリ でも色々遊べますが、せっかくなので自分でアプリを作って遊んでみました。
MaBeee について
NOVARS INC. が発売している乾電池型IoT製品です。
クラウドファンディングによる支援を経て、2016年8月に一般販売が開始されました。
MaBeee(マビー)は、おもちゃ、自分で作った工作、電動歯ブラシなど、乾電池で動く製品をスマホでコントロールできるようになる乾電池型IoT製品です。
スマホを振ったり、傾けたり、声の大きさに反応させたりして、電車のおもちゃを速く走らせたり、ぬいぐるみや工作ロボットを動かしたり、タイマー機能で歯ブラシの時間を知らせてくれたりすることができます。
乾電池型IoT とありますが、実際には単4形電池を BLE(Bluetooth Low Energy)で制御する単3形のデバイスになります。
つくったもの
公式アプリでは、声の大きさで電池をコントロールできるモードがあります。
これを応用し、音声認識で特定のワードに反応して電池(+搭載されたおもちゃ)を制御する 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 の利用は自己責任でお願いします
こちら を参考にして、プロジェクトに 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 (+搭載された機器)のオンオフ制御ができるようになりました!簡単ですね
音声認識
特定ワードの音声に反応させて MaBeee を制御したいので、アプリ内で音声認識させます。
iOS10 から搭載された SpeechFramework を利用します。
実装にあたっては、主に以下の記事を参考にさせていただきました
[iOS 10] SFSpeechRecognizerで音声認識を試してみた
まず、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 がオフ)
が実現できました
なお、SpeechFramework を利用した音声認識は端末・アプリごとに回数制限(具体的な回数は公開されていない)があるため、遊びすぎには注意が必要です
デバッグの様子
感想など
MaBeee SDK は扱いやすく、MaBeee を制御するアプリをサクっと作ることができました。
おかげさまで、電車好きな子どもたち(2歳)も喜んでくれました
MaBeee の代表的なユースケースはおもちゃの制御ですが、アイデア次第でおもちゃ以外にも使えそうです。
また何か思いつけば、適当にアプリを作ってみたいと思います