イントロダクション
Arduinoの特徴の1つに、オフィシャルなボード以外でも、オフィシャルなボードと同じArduino IDEを利用できるような拡張機能が用意されている、ということがあります。例えば、標準ボードのArduino UnoでプロトタイピングしていたものをAdafruitのTrinket - Miniに置き換えて小型化かつ低価格化する、Feather 32u4 Bluefruit LEに置き換えてBluetooh Low Energyでの通信機能とバッテリ充電機能を拡張すると同時に小型化する、といったことをしつつ同じArduino IDEを使い続けることができるのです。
そうしたオフィシャルなボード以外のハードウェアとして注目されているのがESP8266です。ESP8266はEspressif Systemsが開発した小型で安価なWi-Fiチップで、このチップを採用したモジュールが多数販売されています。その中の1つ、同社によるESP-WROOM-02は日本でも入手しやすく、TELECによる技術基準適合証明・工事設計認証を受けていることから安心して利用できるのが特徴です。このモジュールをプロトタイピングで使いやすいようにするための開発ボードは多数発売されており、その中でも使いやすいと思われるものの1つがスイッチサイエンスによる「ESP-WROOM-02」です。
このモジュールはESP-WROOM-02 Wi-FiモジュールとUSB-シリアル変換IC FT231XS、3.3V出力のレギュレータやリセットスイッチ、動作モード切替用スイッチなどを搭載しているため、ピンヘッダをはんだ付けしてマイクロUSBケーブルを用意するだけですぐに使い始めることができます。ここでは、この開発ボードをArduino IDEから利用できるように設定し、myThingsのIDCFチャンネルを試すところまでを説明します。
準備
以下はArduino LCCによるArduino IDE 1.6.6で動作を確認しています。
ドライバ(Mac OS Xの場合)
Mac OS Xでは、OS X 10.9(Mavericks)以降は標準でFTDIのUSB-シリアル変換チップ用の「簡易」ドライバがインストールされています。Mac OS Xの10.11.3で確認したところ、このドライバ(バージョンは5.0.0)でスケッチをアップロードする際のオートリセットも含めて動作することが確認できていますので、この環境で使用する場合には新たにFTDI版のドライバをインストールする必要はありません。むしろ、FTDI版のドライバをインストールすると、USBポートから抜いた後、リセットするまでそのポートが使えなくなるという問題を確認しています。古い記事ではFTDI版のインストールを推奨しているものがありますので注意しましょう。
ESP8266を使用するための準備
- Arduino IDEを起動する
- Preferences...→Settings→Additional Boards Managers URLsに開発版のボードマネージャー用リンク(Arduino core for ESP8266 WiFi chipのGitHubで「Stable version」として掲載されているもの)「http://arduino.esp8266.com/stable/package_esp8266com_index.json」を入力する
- ツール→マイコンボード→ボードマネージャー...を選択しボードマネージャーを開く
- ボードマネージャーで「esp8266 by ESP8266 Community」を選択し、インストールする
- インストールが終わったらツール→マイコンボードで表示されるメニューからGeneric ESP8266 Moduleを選択する
- ツール→Reset Methodをデフォルトのckからnodemcuに変更する
※この記事は2.0.0で確認していますが将来的に更新されている可能性があります。
以上でArduino IDEでESP8266を使用するための準備は完了です。
ライブラリを使用するための準備
- スケッチ→Include Library...→Manage Libraries...でLibrary Managerを開く
- Library Managerで「PubSubClient by Nick O'Leary」を選択し、インストールする
- 同様に「ArduinoJson by Benoit Blanchon」を選択し、インストールする
- 同様に「Adafruit HDC1000 Library by Adafruit」を選択し、インストールする
- Arduinoフォルダ(Mac OS Xの場合には書類/Arduino)の中にあるlibrariesフォルダを開く
- libraries→PubSubClient→srcと順にたどり、その中にあるPubSubClient.hをテキストエディタで開く
- 23行目のMQTT_MAX_PACKET_SIZEの値を256から1000に変更する
※この記事は2.4.0で確認していますが将来的に更新されている可能性があります。
変更前
// MQTT_MAX_PACKET_SIZE : Maximum packet size
#define MQTT_MAX_PACKET_SIZE 256
変更後
// MQTT_MAX_PACKET_SIZE : Maximum packet size
#define MQTT_MAX_PACKET_SIZE 1000
以上でMQTT用ライブラリPubSubClientを使用するための準備は完了です。
サンプル
以下はmyThingsのIDCFチャンネル用にセットアップしたIDCF CloudサーバにMQTTで接続するサンプル群です。実際のファイルはGitHubから入手してください。
https://github.com/kotobuki/esp8266_examples
Publisher
以下のような形式でIDCFチャンネルサーバに温湿度センサHDC1000から読み取った温度と湿度のデータをpublishするサンプルです。
{
"devices": [
"********-****-****-****-************"
],
"payload": {
"temperature": 24.91,
"humidity": 58.65
}
}
サンプルの中で、config.hの中で「****」のようになっているところはダミーの値ですので、自分のセットアップしたサーバの情報に置き換えます。
// MQTTのクライアントID(ユニークにするためにスケッチをコンパイルした日付と時刻を使用)
const char *mqtt_client_id = "publisher" __DATE__ __TIME__;
// Wi-FiアクセスポイントのSSIDとパスワード
const char *ssid = "********";
const char *password = "********";
// IDCF Cloudに関する設定
const char *server = "210-***-***-***.jp-east.compute.idcfcloud.com";
const char* trigger_1_uuid = "********-****-****-****-************";
const char* trigger_1_token = "********";
const char* action_1_uuid = "********-****-****-****-************";
const char* action_2_uuid = "********-****-****-****-************";
もしtriggerやactionのuuidやtokenが分からない場合には、次のようにサーバにログインして情報を取得します。
ssh root@210.***.***.*** -o PreferredAuthentications=password
...
root@210.***.***.***'s password: パスワード
$ cd ~/iot_apps/meshblu-compose
$ docker-compose run --rm iotutil owner
$ docker-compose run --rm iotutil list
以下がスケッチの本体です。
#include <Wire.h>
#include <ESP8266WiFi.h>
// MQTTを扱うためのライブラリ
#include <PubSubClient.h>
// JSONを扱うためのライブラリ
#include <ArduinoJson.h>
// HDC1000を扱うためのライブラリ
#include <Adafruit_HDC1000.h>
// 設定ファイル
#include "config.h"
WiFiClient wifiClient;
PubSubClient client(wifiClient);
Adafruit_HDC1000 hdc1000 = Adafruit_HDC1000();
// 温度と湿度を格納する変数
float temperature = 0.0;
float humidity = 0.0;
// コールバック関数
void callback(char* topic, byte* payload, unsigned int length) {
// コールバックで必要な処理があればここで
}
void setup() {
// I2Cの通信を開始
// SDA: DIO4
// SCL: DIO5
Wire.begin();
// シリアル通信を開始
Serial.begin(115200);
delay(10);
Serial.println();
Serial.println();
// HDC1000を設定
hdc1000.begin();
}
void loop() {
// Wi-Fiアクセスポイントに接続していなければ接続
if (WiFi.status() != WL_CONNECTED) {
Serial.print("Connecting to ");
Serial.print(ssid);
Serial.println("...");
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
// Wi-Fiアクスポイントへの接続に失敗したら5秒間待ってリトライ
Serial.println("Failed to connect");
delay(5000);
return;
} else {
Serial.print("WiFi connected: ");
Serial.println(WiFi.localIP());
client.setServer(server, 1883);
}
}
// クライアントがICDF Cloudのサーバに接続されていなければ
if (!client.connected()) {
// トリガー1のUUIDとトークンをユーザ名およびパスワードとしてサーバに接続
client.connect(mqtt_client_id, trigger_1_uuid, trigger_1_token);
if (client.connected()) {
Serial.print("MQTT connected: ");
Serial.println(server);
client.setCallback(callback);
} else {
Serial.print("MQTT connection failed: ");
Serial.println(client.state());
delay(5000);
}
} else {
// 既にサーバに接続されていれば通常処理を行う
client.loop();
// クライアントがICDF Cloudのサーバに接続されていたら以下の処理を実行
// 温度と湿度を取得
temperature = hdc1000.readTemperature();
humidity = hdc1000.readHumidity();
// JSONフォーマットでペイロードを用意
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
JsonArray& devices = root.createNestedArray("devices");
devices.add(action_1_uuid);
// devices.add(action_2_uuid);
JsonObject& payload = root.createNestedObject("payload");
payload["temperature"] = temperature;
payload["humidity"] = humidity;
root.prettyPrintTo(Serial);
Serial.println();
char buffer[256];
root.printTo(buffer, sizeof(buffer));
// IDCF CloudにMQTTでデータを送信
bool sent = client.publish("message", buffer);
if (sent) {
Serial.println("Published");
} else {
Serial.println("Failed to publish");
}
Serial.println();
}
delay(10000);
}
Trigger
PublisherのバリエーションでHTTPでtrigger-1をトリガーするサンプルです(config.hはPublisherのサンプルと同一のため省略しています)。
#include <Wire.h>
#include <ESP8266WiFi.h>
// HDC1000を扱うためのライブラリ
#include <Adafruit_HDC1000.h>
// 設定ファイル
#include "config.h"
WiFiClient wifiClient;
Adafruit_HDC1000 hdc1000 = Adafruit_HDC1000();
float temperature = 0.0;
float humidity = 0.0;
void printFormattedFloat(float val) {
char buffer[10];
dtostrf(val, 4, 2, buffer);
Serial.print(buffer);
}
void setup() {
// I2Cの通信を開始
// SDA: DIO4
// SCL: DIO5
Wire.begin();
// シリアル通信を開始
Serial.begin(115200);
delay(10);
Serial.println();
Serial.println();
// HDC1000を設定
hdc1000.begin();
}
void loop() {
// Wi-Fiアクセスポイントに接続していなければ接続
if (WiFi.status() != WL_CONNECTED) {
Serial.print("Connecting to ");
Serial.print(ssid);
Serial.println("...");
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
// Wi-Fiアクスポイントへの接続に失敗したら5秒間待ってリトライ
Serial.println("Failed to connect");
delay(5000);
return;
} else {
Serial.print("WiFi connected: ");
Serial.println(WiFi.localIP());
}
}
// HDC1000から測定値を読み取る
temperature = hdc1000.readTemperature();
humidity = hdc1000.readHumidity();
// 読み取った温度をシリアルにプリント
Serial.print("Temperature: ");
printFormattedFloat(temperature);
Serial.println("ºC");
// もし現在の温度が閾値よりも高ければ以下を実行
if (temperature > threshold) {
Serial.print("Connecting to ");
Serial.println(server);
WiFiClient client;
if (!client.connect(server, 80)) {
Serial.println("Connection failed");
return;
}
// IDCFチャンネルサーバの/data/{trigger-1のuuid}にHTTP POST
String url = "/data/";
url += trigger_1_uuid;
client.print(String("POST ") + url + " HTTP/1.1\r\n" +
"Host: " + server + "\r\n" +
"meshblu_auth_uuid: " + trigger_1_uuid + "\r\n" +
"meshblu_auth_token: " + trigger_1_token + "\r\n" +
"Connection: close\r\n\r\n");
delay(100);
while (client.available()) {
String line = client.readStringUntil('\r');
Serial.print(line);
}
}
delay(5000);
}
Subscriber
myThingsから送信されてくるJSON形式のペイロードは以下のような形式になっています(これは天気・災害チャンネルで本日の天気をトリガーにした例です)。
{"topic":"message","data":{"devices":["********-****-****-****-************"],"payload":"晴時々曇","fromUuid":"********-****-****-****-************"}}
IDCFチャンネルが動作するIDCF Cloudサーバに接続し、MQTTでaction-1をsubscribeすることにより、指定したトリガーが起きたときにメッセージを受け取るサンプルが以下のものです。
// MQTTのクライアントID(ユニークにするためにスケッチをコンパイルした日付と時刻を使用)
const char *mqtt_client_id = "kotobuki" __DATE__ __TIME__;
// Wi-FiアクセスポイントのSSIDとパスワード
const char *ssid = "********";
const char *pass = "********";
// IDCF Cloudに関する設定
const char* action_1_uuid = "********-****-****-****-************";
const char* action_1_token = "********";
IPAddress server(210, ***, ***, ***);
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "config.h"
// メッセージを受け取ったらシリアルにプリント
void callback(char* topic, byte* payload, unsigned int length) {
// PubSubClient.hで定義されているMQTTの最大パケットサイズ
char buffer[MQTT_MAX_PACKET_SIZE];
snprintf(buffer, sizeof(buffer), "%s", payload);
Serial.print("Received: ");
Serial.println(buffer);
// 受け取ったJSON形式のペイロードをデコードする
StaticJsonBuffer<MQTT_MAX_PACKET_SIZE> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(buffer);
if (!root.success()) {
Serial.println("parseObject() failed");
return;
}
const char* parsedPayload = root["data"]["payload"];
if (parsedPayload != NULL) {
Serial.print("payload: ");
Serial.println(parsedPayload);
// ペイロードに応じた処理をここに
}
}
WiFiClient wifiClient;
PubSubClient client(wifiClient);
void setup() {
Serial.begin(115200);
delay(10);
Serial.println();
Serial.println();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.print("Connecting to ");
Serial.print(ssid);
Serial.println("...");
WiFi.begin(ssid, pass);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
// Wi-Fiアクスポイントへの接続に失敗したら5秒間待ってリトライ
delay(5000);
return;
} else {
Serial.print("WiFi connected: ");
Serial.println(WiFi.localIP());
client.setServer(server, 1883);
}
}
// クライアントがサーバに接続されていなければ
if (!client.connected()) {
// アクション1のUUIDとトークンをユーザ名およびパスワードとしてサーバに接続
client.connect(mqtt_client_id, action_1_uuid, action_1_token);
if (client.connected()) {
Serial.print("MQTT connected: ");
Serial.println(server);
client.setCallback(callback);
client.subscribe(action_1_uuid);
} else {
Serial.print("MQTT connection failed: ");
Serial.println(client.state());
delay(5000);
}
} else {
// 既にサーバに接続されていれば通常処理を行う
client.loop();
}
}
トラブルシューティング
再現条件は不明ですが、USBケーブルを抜き差ししているうちに開発ボード上のUSBシリアル変換チップをシリアルポートとして認識できなくなってしまうことがあるようです。USBケーブルを抜いた状態で、アップルメニュー→このMacについて→システムレポート…でUSBを選択した時に「FT231X USB UART」というデバイスが表示されているようであれば該当します。
この時、システムの再起動で解消することもありますが、それでも解消しない場合はAppleのウェブサイトでの説明に従ってNVRAMのリセットを試してみるとよいでしょう。
おわりに
ESP8266およびESP8266用Arduinoコアはホットな話題であり、刻々と変化しています。もし、古くなってしまった内容があればコメント等でお知らせいただけると助かります。