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() } }
注意点
- 20行目のAppIDには、Milkcocoa Webサイトで「新しいアプリ」を登録した際に付与されるAppIDを設定します。25行目のTopicは、「AppID/データストア名/push」の書式で記載します。今回の例では、データストア名としてTestを使っています。
- CocoaMQTTライブラリを使う際は、CocoaMQTTDelegateプロトコルに定義されているメソッドを実装する必要があります。今回の例ではViewControllerクラスにCocoaMQTTDelegateを適用(adopt)し、77行目以降にdelegateメッソドを記述しています。
- CocoaMQTTライブラリは非同期処理を使っており、connect()メッソドを呼ぶとすぐに処理が帰ってきますが、裏で接続処理が続いています。そのため、接続が完了するまではメッセージをpublishしてはいけません。接続が完了するとmqtt:didConnectAckメソッドが呼び出されるため、mqtt:didConnectAckにメッセージの送信開始を記述します。→ ソースの80行目
- 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していますが配信の遅延もありません。
参考資料