カテゴリー「iOS」の記事

HomebridgeとRaspberry Piを使ってHome Kitの実験をしてみた

TwitterでHomebridgeとRaspberry Piを使ったHome Kitのデモが流れていたので、私も真似をしてやってみました。これ、面白いです! Siriを使って音声でRaspberry PiにつないだLEDをOn/Offできるまでの手順を簡単に示します。

今回の実験はRaspberry Pi 2とRaspbian Wheezyを使っています。Raspbianはずいぶん前にインストールしたものですが、apt-get upgradeで最新の状態にして一連のインストールを行いました。最新のRaspbianはJessieになっていますが、Jessieでは試していません。そのため、最近RasPiを買ってRaspbianをインストールした方には以下の手順は適用できませんのでご注意下さい。

WebIOPiのインストール

今回の実験では、Raspberry PiにLED制御(Raspberry PiのGPIO制御)用のREST APIを作り、Home KitからこのREST APIを叩くことでLEDの制御を行うことにします。そのために、REST APIの提供とGPIOの制御を同時にできるWebIOPiを使用します。WebIOPiのインストールはリンクの手順 に従って行いました。リンクの手順はv0.6.0を使っていますが、執筆時点で最新のv0.7.1を使っています。

WebIOPiのv0.7.1はRaspberry Pi 2に対応していないため、以下の手順でパッチを当ててからビルドします。

wget http://sourceforge.net/projects/webiopi/files/WebIOPi-0.7.1.tar.gz
tar xvzf WebIOPi-0.7.1.tar.gz
cd WebIOPi-0.7.1
wget https://raw.githubusercontent.com/doublebind/raspi/master/webiopi-pi2bplus.patch
patch -p1 -i webiopi-pi2bplus.patch
sudo ./setup.sh

WebIOPiが動くようになったら、LEDをつなぐGPIOポート(今回はGPIO 17を使用)を出力に設定しておきます。

NodeJSとHomebridgeのインストール

このリンクの手順に従って、NodeJSをインストールします。NodeJSをインストールする前にGCC 4.9をインストールする必要があり(Raspberry PiのGCCはバージョンが古くC++14に対応していないため)、Install C++14の手順に従って行います。

リンクの手順ではNodeJSは4.0.0を使っていますが、執筆時点のLTS版であるv4.2.6をインストールしました。私はRaspberry Pi 2を使っているので、ARMv7版を使いました。

続けて、AvahiとHomebridgeをインストールします。

Homebridge Pluginのインストール

HomebridgeをインストールしただけではLEDや電球の制御は行えず、そのためにはプラグインをインストールする必要があります。プラグインはリンクにあるように90近くがあり何を使ったら良いか迷いますが、Raspberry PiのREST APIを叩くことでLEDの制御ができるように、httpベースのプラグインを探しました。今回は、homebridge-httpというプラグインを使います。プラグインのインストールは次のコマンドで行います。

sudo npm install -g homebridge-http

Pluginの設定

インストールしたプラグインをhomebridgeが認識するために、/home/pi/.homebridge ディレクトリにconfig.jsonという名前のファイルを作成します。config.jsonの内容は以下の通りです。

{
    "bridge":
    {
       "name": "Homebridge",
       "username": "B8:27:EB:4D:31:D7",
       "port": 51826,
       "pin": "031-45-154"
    },

    "accessories": [
    {
        "accessory": "Http",
        "name": "部屋のランプ",
        "switchHandling": "yes",
        "http_method": "POST",
        "on_url":      "http://localhost:8000/GPIO/17/value/1",
        "off_url":     "http://localhost:8000/GPIO/17/value/0",
        "status_url":  "http://localhost:8000/GPIO/17/value",
        "service": "Light",
        "brightnessHandling": "no",
        "brightness_url":     "http://localhost/controller/1707/%b",
        "brightnesslvl_url":  "http://localhost/status/100054",
        "sendimmediately": "",
        "username" : "webiopi",
        "password" : "raspberry"                     
     }
  ]
}

設定ファイルのポイントは以下です;

  • bridgeのusernameはMACアドレスの形式ならなんでもよいみたいですが、Raspberry PiのMACアドレスにしています
  • accessoriesのon_rul/off_urlにWebIOPiのREST APIに従ってGPIO制御のurlを記述します。私はGPIO 17番を使っています
  • WebIOPiはusername/passwordを使った認証機能があるため、accessoriesにデフォルトのusername/passwordを設定しています

Homebridgeの起動

homebrigeを起動すると以下のようなメッセージが出れば起動成功です。

*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see 
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see 
Loaded plugin: homebridge-http
Registering accessory 'homebridge-httpstatus.Http'
---
Loaded plugin: homebridge-lockitron
Registering accessory 'homebridge-lockitron.Lockitron'
---
Loaded config.json with 1 accessories and 0 platforms.
---
Loading 1 accessories...
[部屋のランプ] Initializing Http accessory...
Scan this code with your HomeKit App on your iOS device to pair with Homebridge:
                       
    ┌────────────┐     
    │ 031-45-154 │     
    └────────────┘     
                       
Homebridge is running on port 51826.

iPhoneにHome Kitアプリをインストール

iPhoneにHome Kitアプリをインストールします。私はアップルのHomeKit Catalogを使用しました。本アプリはソース形式で配布されているため、Xcodeを使ってビルドする必要があります。ダウンロードにはDevelopperアカウントが必要だと思います。HomeKit Catalog以外にも、App Storeに登録されたHome Kit対応のアプリがあるようですので、HomeKit Catalogがダウンロードできない場合にはApp Storeのアプリを使用可能です。

Home Kitアプリを起動してアクセサリの登録を行い、LEDを制御するための「シーン」を設定します。細かな手順は割愛しますが、アプリを触っていると使い方はわかると思います。

動作イメージ

一連の設定でSiriを使ってRaspberry PiにつないだLEDを音声でOn/Offができるようになります。操作のデモを動画に撮ってYouTubeにアップしました。

iOSからMilkcocoaへMQTT publishする

iOSからMQTTブローカにメッセージをpublishするサンプルを作ってみました。

MQTTブローカにはMilkcocoaというサービスを使いました。フリーのMQTTブローカとしてはtest.mosquitto.orgがありますが、以前に試した際はイマイチ接続が安定していない感じでした。そこで、無償使用も可能な商用サービスであるMilkcocoaを試してみました。Milkcocoaは10万メッセージまでは無償で利用できるため、ちょっとしたお試しは無償利用範囲内だと思います。

Milkcocoaのアカウント作成などは、本記事では割愛します。MilkcocoaにはArudino SDKも提供されており、本SDKを使えばESP-WROOM-02を使って簡単にMilkcocoaサーバーにデータをpublishすることができます。今回は、iOSデバイスからMilkcocoaにデーターのpublishを行いたかったため、iOS用のMQTTライブラリを使ってサンプルを作成しました(Arduino SDKを使うともっと簡潔に記述ができるのですが)。

iOS用MQTTライブラリの準備

iOS用のMQTTライブラリはGoogleで検索するといくつかの候補がヒットします。最近Swiftをお勉強中のため、Objective-CではなくSwiftでプログラミングできるライブラリを選びました。Swiftが使えるMQTTライブラリとしては、上位にヒットするもので以下がありました:

  • CocoaMQTT: SwiftとObjective-C(ソケット関連の処理部分)で書かれたSwift/Objective-Cネイティブなライブラリ
  • Moscapsule: APIはSwiftになっていますが、実態はmosquittoのラッパー

今回は、Swift/Objective-CネイティブなCocoaMQTTを使いました。Webサイトの指示に従って、CocoaPodを使ってライブラリをインストールし、XcodeプロジェクトにBridging-Header.hを登録します。 Bridging-Header.hはCocoaMQTTがSwiftとObjective-Cの両方を使用しているため、SwiftとObjective-C間の連携を行うために必要です。

Swiftのコード

今回作成したコードを以下に示します。iPhoneの加速度センサーを読み取り、1秒毎にMilkcocoa MQTTブローカにメッセージをpublishします。UIとしてiPhoneにも加速度センサーの読み取り値を表示します。

//
//  ViewController.swift
//  mqtt_test
//
//  Created by Todotani on 2016/01/17.
//  Copyright © 2016年 Todotani. All rights reserved.
//

import UIKit
import CoreMotion
import CocoaMQTT

class ViewController: UIViewController, CocoaMQTTDelegate{
    
    @IBOutlet var connectionState: UILabel!
    @IBOutlet var label_accel_X: UILabel!
    @IBOutlet var lable_accel_Y: UILabel!
    
    private struct MqttConstants {
        static let AppID    = "YourAppID"     // Set AppID
        static let ClientId = MqttConstants.AppID
        static let HostName = MqttConstants.AppID + ".mlkcca.com"
        static let UserName = "sdammy"
        static let PassWord = MqttConstants.AppID
        static let Topic    = MqttConstants.AppID + "/Test/push"
    }

    var mqtt: CocoaMQTT!
    var motionManager: CMMotionManager!
    var isConnected: Bool = false {
        didSet{
            if isConnected == true {
                connectionState.text = "Connected"
            } else {
                connectionState.text = "Disconnect"
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        appDelegate.viewController = self
        
        mqtt = CocoaMQTT(clientId: MqttConstants.ClientId, host: MqttConstants.HostName)
        mqtt.username = MqttConstants.UserName
        mqtt.password = MqttConstants.PassWord
        mqtt.keepAlive = 60
        mqtt.delegate = self
        
        motionManager = CMMotionManager()
        motionManager.accelerometerUpdateInterval = 0.1
        
        mqtt.connect()
    }
    
    private var counter = 0
    
    func startUpdate() {
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue()!, withHandler: {(data, error) in
            let accel_X = String(format: "%.4f", data!.acceleration.x)
            let accel_Y = String(format: "%.4f", data!.acceleration.y)
            
            self.label_accel_X.text = accel_X
            self.lable_accel_Y.text = accel_Y
            
            if ((self.counter % 10) == 0) {
                let message = "{\"params\":{\"Accel_X\":\(accel_X),\"Accel_Y\":\(accel_Y)}}"
                self.mqtt.publish(MqttConstants.Topic, withString: message)
            }
            self.counter += 1
        })
    }

    // MARK: CocoaMQTTDelegate
    
    func mqtt(mqtt: CocoaMQTT, didConnect host: String, port: Int) {
        print("didConnect \(host):\(port)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
        print("didConnectAck \(ack.rawValue)")
        if ack == .ACCEPT {
            mqtt.subscribe(MqttConstants.Topic, qos: .QOS0)
            mqtt.ping()
            isConnected = true
            startUpdate()
        }
    }
    
    func mqtt(mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
        print("didPublishMessage with message: \(message.string!)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didPublishAck id: UInt16) {
        print("didPublishAck with id: \(id)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16 ) {
        print("didReceivedMessage: \(message.string!) with id \(id)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didSubscribeTopic topic: String) {
        print("didSubscribeTopic to \(topic)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didUnsubscribeTopic topic: String) {
        print("didUnsubscribeTopic to \(topic)")
    }
    
    func mqttDidPing(mqtt: CocoaMQTT) {
        print("didPing")
    }
    
    func mqttDidReceivePong(mqtt: CocoaMQTT) {
        _console("didReceivePong")
    }
    
    func mqttDidDisconnect(mqtt: CocoaMQTT, withError err: NSError?) {
        _console("mqttDidDisconnect")
    }
    
    func _console(info: String) {
        print("Delegate: \(info)")
    }
}


//
//  AppDelegate.swift
//  mqtt_test
//
//  Created by Todotani on 2016/01/17.
//  Copyright © 2016年 Todotani. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    var viewController: ViewController!

    func applicationDidEnterBackground(application: UIApplication) {
        viewController.mqtt.disconnect()
        viewController.isConnected = false
        viewController.motionManager.stopAccelerometerUpdates()
    }

    func applicationWillEnterForeground(application: UIApplication) {
        viewController.mqtt.connect()
    }

}

注意点

  1. 20行目のAppIDには、Milkcocoa Webサイトで「新しいアプリ」を登録した際に付与されるAppIDを設定します。25行目のTopicは、「AppID/データストア名/push」の書式で記載します。今回の例では、データストア名としてTestを使っています。
  2. CocoaMQTTライブラリを使う際は、CocoaMQTTDelegateプロトコルに定義されているメソッドを実装する必要があります。今回の例ではViewControllerクラスにCocoaMQTTDelegateを適用(adopt)し、77行目以降にdelegateメッソドを記述しています。
  3. CocoaMQTTライブラリは非同期処理を使っており、connect()メッソドを呼ぶとすぐに処理が帰ってきますが、裏で接続処理が続いています。そのため、接続が完了するまではメッセージをpublishしてはいけません。接続が完了するとmqtt:didConnectAckメソッドが呼び出されるため、mqtt:didConnectAckにメッセージの送信開始を記述します。→ ソースの80行目
  4. PublishするメッセージはJSON形式で記述し、”params”をkeyにする必要があります。最初このkeyを書いておらず、送信したメッセージがサーバー側で正しく認識されないため、だいぶ悩みました。X/Y軸加速度をpublishするためのメッセージは以下のように記述します。
    {"params":{"Key_X":"X_Val", “Key_Y":"Y_Val"}}
    今回は70行目の処理で、JSONのひな形文字列内にvalue値を埋め込む汚い処理になっています。Dictionalyを使いKey/Valueを指定することで値を設定しJSONフォーマットにシリアライズする処理も考えたのですが、変換にゴミが入ってしまいうまくいかなかったので手抜きをしています。Arduino SDKはKey/Valueを指定してメッセージのデーターを生成できるのでこちらの方が汎用的です。

終わりに

Swiftが発表されてから長らく様子見だったのですが、サンプルコードも充実してきたのでiOS 9 & Swift 2.0からSwiftのお勉強を始めました。Objective-Cを使ったiOSのプログラミングも殆ど行ってはいないので、覚えた先から忘れているのですが、Objective-CよりSwiftの方がプログラミングもしやすくなってきました。

Milkcocoaはそんなに長時間の接続は試していませんが、フリーのmosquitto.orgより接続が安定しているように思います。今回のサンプルコードでは、publishしたメッセージをローカルにsubscribeしていますが配信の遅延もありません。

参考資料


2017年2月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28        
無料ブログはココログ