Azure IoT Hubにjava+MQTTでテレメトリを送信
Azure側
まずは無料アカウントを作る
自動で課金されることはないって言うけど、クレジットカード番号を要求されるのは心理的ハードルが高いよなぁ...と長いこと保留にしていたが、本業に差し障るので覚悟を決める。
IoT Hubを作成する
ポータルからサクッと作る。
- IoT Hub名はグローバルでユニークにしないといけないみたい。
- 領域は自分に近い場所として「East Asia」を選択する。「Japan East」が選択肢にあることにはデプロイしてから気づいた。
- 学習用なので、価格とスケールティアは「F1: Freeレベル」で良いはずだ。
IoT Hubにデバイスを登録する
こちらもポータルからサクッと作る。
- デバイスIDを適当に入れる。
- その他の項目は画面の通りにする。
クライアント側
Azure SDKを使えば手っ取り早いのだけど、MQTTも勉強しなくちゃいけないので、Eclipse Paho Java Clientを使って作る。
pom.xml
以下をdependencies
に追加。
<dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.0</version> </dependency>
App.java
SHARED_ACCESS_KEY
は自動生成されたデバイスの主キーを指定。ポータルで登録したデバイスを参照すると取得できる。
package com.example.mqtt; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; public class App implements MqttCallback { private final static String IOT_HUB_NAME = "your-own-iot-hub.azure-devices.net"; private final static String DEVICE_ID = "device0001"; private final static String SHARED_ACCESS_KEY = "c2hhcmVkX2FjY2Vzc19rZXk="; public static void main(String[] args) { try { new App().run(); } catch (Exception e) { e.printStackTrace(); } } public void run() throws MqttException, InterruptedException { final String endpoint = String.format("ssl://%s:8883", IOT_HUB_NAME); final MqttClient client = new MqttClient(endpoint, DEVICE_ID); final MqttConnectOptions connOpts = new MqttConnectOptions(); final String username = String.format("%s/%s/?api-version=2020-09-30", IOT_HUB_NAME, DEVICE_ID); final long expiryTime = (System.currentTimeMillis() / 1000L) + 30; final String sasToken = SASToken.getSASToken(String.format("%s/devices/%s", IOT_HUB_NAME, DEVICE_ID) , SHARED_ACCESS_KEY, expiryTime); connOpts.setUserName(username); connOpts.setPassword(sasToken.toCharArray()); client.connect(connOpts); client.setCallback(this); final MqttMessage message = new MqttMessage("{\"test\":\"value\"}".getBytes()); client.publish("devices/" + DEVICE_ID + "/messages/events/" , message); System.out.println("Published message."); Thread.sleep(1000L); client.disconnect(); System.exit(0); } public void connectionLost(Throwable cause) { System.out.println("Lost connection."); } public void messageArrived(String topic, MqttMessage message) throws Exception { System.out.println("Received message from topic " + topic + ": " + message); } public void deliveryComplete(IMqttDeliveryToken token) { System.out.println("Delivered message."); } }
SASToken.java
Azure IoT Hubのドキュメントに載ってたサンプルを流用。この選択が後に悲劇を生むことになる...
package com.example.mqtt; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Base64.Encoder; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class SASToken { public static String getSASToken(String resourceUri, String key, long expiryTime) { String expiry = Long.toString(expiryTime); String sasToken = null; try { // String stringToSign = URLEncoder.encode(resourceUri, "UTF-8") + "\n" + expiry; String stringToSign = resourceUri + "\n" + expiry; String signature = getHMAC256(key, stringToSign); // sasToken = "SharedAccessSignature sr=" + URLEncoder.encode(resourceUri, "UTF-8") + "&sig=" sasToken = "SharedAccessSignature sr=" + resourceUri + "&sig=" + URLEncoder.encode(signature, "UTF-8") + "&se=" + expiry; } catch (Exception e) { e.printStackTrace(); } return sasToken; } private static String getHMAC256(String key, String input) { Mac sha256_HMAC = null; String hash = null; try { sha256_HMAC = Mac.getInstance("HmacSHA256"); // SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256"); byte[] decodedDeviceKey = Base64.getDecoder().decode(key.getBytes()); SecretKeySpec secret_key = new SecretKeySpec(decodedDeviceKey, "HmacSHA256"); sha256_HMAC.init(secret_key); Encoder encoder = Base64.getEncoder(); hash = new String(encoder.encode(sha256_HMAC.doFinal(input.getBytes("UTF-8")))); } catch (Exception e) { e.printStackTrace(); } return hash; } }
動作確認
プログラムを実行する。上手くいったようだが出力結果が地味すぎる。
Published message. Delivered message.
ポータルで受け取ったメッセージを簡単に見れないの?とあちこち探したが、見つけられず。この辺はあまり興味もないし、次回の課題にしよう。とりあえずダッシュボードのグラフにメッセージがカウントされたので、良しとするか。
ハマったポイント
SASトークンの仕様と生成サンプルについては、こちらの公式ドキュメントを参考にした。
「リソースURIをURLエンコードした文字列」を得るのにサンプルではURLEncoder.encode()
を使っているが、これで「/」を「%2F」にエンコードするのがNGだったようだ。とりあえずURLエンコードが必要な文字は他にないので、暫定策として処理を省いて上手く動いた。原因を突き止めるのに、結局SDKを使ったクライアントを作って動かす羽目になったよ。やれやれ。