/

ThingSpeak によるグラフ表示

最新版
無線 LAN 子機として振る舞い、MathWorks のサービス ThingSpeak を使って温湿度データをグラフ化するためのサンプルスケッチ spot-thingspeak の解説です。

ソースコードの入手

GitHub リポジトリ monowireless/spot-thingspeak から入手できます。

システムの概要

spot-thingspeak サンプルは、TWELITE ARIA から受信したデータを HTTP GET リクエストとして ThingSpeak のサーバへ送信し、グラフとして表示できるようにします。

表示例

表示例

開発に必要なもの

環境整備

ThingSpeak の設定

  1. ThingSpeak のサイトへアクセスし、MathWorks のアカウントを作成します
  2. “Channel” を作成し、次のように設定します。
Channelの設定例

Channelの設定例

  1. “API Keys” タブにある、16文字の “Write API key” を控えておきます

IDE とツールチェインの導入

Arduino IDE 1.x による開発環境の構築方法 をご覧ください。

プロジェクトファイルの入手

  1. GitHub (monowireless/spot-thingspeak) から Zip ファイルをダウンロードします
  2. Zip ファイルを展開し、フォルダ名を spot-thingspeak-main から spot-thingspeak に変更します
  3. Arduino のスケッチブックの保存場所(Arduino IDE 環境設定に記載。例:C:\Users\foo\Documents\Arduino)に spot-thingspeak フォルダを配置します

ユーザ設定の変更

Arduino IDE 上部のタブから config.h を開き、Wi-Fi の SSID と パスワードを設定してください。WPA2-PSK ネットワークを想定しています。

また、先ほど控えた 16文字の “Write API key” についても設定してください。必要に応じて、ルート証明書も書き換えてください。

プロジェクトファイルの書き込み

ESP32 へのスケッチの書き込み方法 をご覧ください。

TWELITE ARIA の設定と起動

  1. TWELITE ARIA の設定を変更し、TWELITE ARIA モード送信間隔を30秒以上(例:60秒)とします(サーバへの過負荷を避けるため)
  2. CR2032 電池を投入し、TWELITE ARIA を起動します

スケッチ

Arduino スケッチ spot-thingspeak.ino および config.h の解説です。

ライブラリのインクルード

Arduino および ESP32 公式ライブラリ

5-7行目では、Arduino および ESP32 の公式ライブラリをインクルードしています。

#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <WiFi.h>
ヘッダファイル内容備考
Arduino.hArduino の基本ライブラリ省略できる場合もあるが念のため記載
WiFiClientSecure.hESP32 で SSL通信を行う
WiFi.hWi-Fiを扱う省略できる場合もあるが念のため記載

MWings ライブラリ

13行目では、MWings ライブラリをインクルードしています。

#include <MWings.h>

ユーザ設定の定義

16行目では、config.h をインクルードしています。

#include "config.h"

データ型の定義

19-26行目では、子機から受信したデータを保管しておく構造体の型を定義しています。

struct DataFromAria {
    uint32_t serialId;
    uint8_t logicalId;
    uint16_t supplyVoltage;
    uint8_t linkQuality;
    int16_t temp100x;
    uint16_t humid100x;
};
名称内容
serialIdシリアルID
logicalId論理デバイスID
supplyVoltage電源電圧
linkQualityLQI
temp100x100倍された温度
humid100x100倍された湿度

ここでは、TWELITE ARIA を使用します。

config.h

再起動間隔の定義

config.h の4行目では、ESP32 の再起動間隔を指定しています。

const uint32_t REBOOT_INTERVAL = 21600; // seconds

ここでは、21600秒=6時間としています。

TWELITE 設定の定義

config.h の7-8行目では、TWELITE SPOT に搭載された TWELITE 親機に適用する設定を定義しています。

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
名称内容
TWE_CHTWELITE の 周波数チャネル
TWE_APPIDTWELITE の アプリケーション ID

Wi-Fi 設定の定義

config.h の11-12行目では、TWELITE SPOT に搭載された ESP32 に適用するWi-Fi 設定を定義しています。

const char* WIFI_SSID = "YOUR SSID";
const char* WIFI_PASSWORD = "YOUR PASSWORD";
名称内容
WIFI_SSID接続するネットワークの SSID
WIFI_PASSWORD接続するネットワークのパスワード

ThingSpeak の Write API key

config.h の15行目では、ThingSpeak の “Channel” の “Field” へデータを追加するために必要な “Write API key” を定義しています。

ルート証明書

config.h の18-41行目では、Google Chrome を使って api.thingspeak.com から取得したルート証明書の中身を記述しています。必要に応じて書き換えてください。

const char *CA_CERT = R"(
-----BEGIN CERTIFICATE-----
中略
-----END CERTIFICATE-----
)";

ホストの設定

config.h の43-44行目では、ホストの設定を定義しています。

const char *SERVER_HOST = "api.thingspeak.com";
const uint16_t SERVER_PORT = 443;
名称内容
SERVER_HOSTサーバのホスト名
SERVER_PORTサーバのポート番号

各種定数の定義

config.h の46行目からは、各種定数を定義しています。

const int QUERIES_MAX_LENGTH = 128;         // bytes (without \0)
const int32_t CONNECT_TIMEOUT = 10;     // seconds
const uint32_t RECONNECT_MIN_INTERVAL = 5; // seconds
// According to thingspeck free limitations, SEND_MIN_INTERVAL is set for 20s.
const uint32_t SEND_MIN_INTERVAL = 20; // seconds
const uint32_t REQUEST_TIMEOUT = 10;   // seconds
名称内容
QUERIES_MAX_LENGTHクエリ文字列の最大長(ヌル文字含まず)
CONNECT_TIMEOUTサーバへの接続時のタイムアウト
RECONNECT_MIN_INTERVALWi-Fiアクセスポイントへ再接続する際の最短間隔
SEND_MIN_INTERVALリクエスト間隔の最短間隔
REQUEST_TIMEOUTリクエストからレスポンスまでのタイムアウト

ピン番号の定義

spot-thingspeak.ino の29-34行目では、ピン番号を定義しています。

static const int RST_PIN = 5;
static const int PRG_PIN = 4;
static const int LED_PIN = 18;

static const int8_t RX1_PIN = 16;
static const int8_t TX1_PIN = 17;
名称内容
RST_PINTWELITE の RST ピンが接続されているピンの番号
PRG_PINTWELITE の PRG ピンが接続されているピンの番号
LED_PIN基板上の ESP32 用 LED が接続されているピンの番号
RX1_PINTWELITE の RX1 ピンが接続されているピンの番号
TX1_PINTWELITE の TX1 ピンが接続されているピンの番号

グローバルオブジェクトの宣言

37行目では、グローバルオブジェクトを宣言しています。

static WiFiClientSecure client;
名称内容
clientHTTPS通信のインタフェース

グローバル変数の宣言

40-41行目では、グローバル変数を宣言しています。

static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
名称内容
LatestDataFromAriaTWELITE ARIA から受信した最新のデータ
IsThereNewDataFromAriaTWELITE ARIA から新たなデータを受信したことを示すフラグ

関数プロトタイプの宣言

44-54行目では、関数プロトタイプを宣言しています。

void anotherLoopForTWELITE();
名称内容
anotherLoopForTWELITETWELITEのデータを処理するためのループ関数
void initTWELITE();
void initWiFi();
名称内容
initTWELITETWELITEの初期化関数
initWiFiWi-Fiの初期化関数
void onAppAriaPacket(const ParsedAppAriaPacket& packet);
名称内容
onAppAriaPacketTWELITE ARIA からデータを受信した際のコールバック関数
void sendAriaData(const DataFromAria& data)
名称内容
sendAriaDataTWELITE ARIAのデータを HTTP GET リクエストにのせて送る関数

setup()

57-74行目では、全体の初期化を行います。

void setup() {
    Serial.begin(115200);

    initTWELITE();
    initWiFi();

    // Attach another loop function for TWELITE
    // Note: Core 0 is also used for the WiFi task, which priority is 19 (ESP_TASKD_EVENT_PRIO - 1)
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForTWELITE()", 8192, nullptr, 18, nullptr,
        0); // Priority is 18 (lower than WiFi)
}

xTaskCreatePinnedToCore() により、loop() とは別のタスクを登録しています。

下記の部分はキャプチャのない無名関数です。不要なグローバル空間の汚染を避けることができます。

        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },

loop()

77-99行目は、主となるループ処理です。

HTTP リクエストの処理、Wi-Fi 切断時の再接続処理、定期リセットの処理を行います。

void loop() {
    static uint32_t lastTimeReconnected = 0;
    if (WiFi.status() == WL_CONNECTED) {
        // Regular operations
        // Check for new data
        if (IsThereNewDataFromAria) {
            IsThereNewDataFromAria = false; // Clear first; data is updated on another thread
            DataFromAria data = LatestDataFromAria; // Now, the buffer is open for incoming data
            sendAriaData(data);
        }
    } else if (millis() - lastTimeReconnected > RECONNECT_MIN_INTERVAL * 1000) {
        // Lost connection, reconnect periodically
        Serial.println("Disconnected. Reconnecting to WiFi...");
        WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
        lastTimeReconnected = millis();
    }

    // Reboot every x interval
    if (millis() > REBOOT_INTERVAL * 1000) {
        Serial.println("Rebooting...");
        ESP.restart();
    }
}

anotherLoopForTWELITE()

102-104行目は、TWELITE のためのループ処理です。

データの受信と解釈を逐次行うため、ブロッキング処理を含む loop() とは別のタスクとしています。

void anotherLoopForTWELITE() {
    Twelite.update();
}

initTWELITE()

107-114行目は、TWELITE の初期化処理です。

TWELITE SPOT に搭載された TWELITE を指定された設定で起動し、パケット受信時のコールバック関数を登録しています。

void initTWELITE() {
    Serial2.begin(115200);
    if (Twelite.begin(Serial2, LED_PIN, RST_PIN, PRG_PIN, TWE_CHANNEL, TWE_APP_ID)) {
        Serial.println("Started TWELITE.");
    }
    // Attach event handlers to process packets
    Twelite.on(onAppAriaPacket);
}

initWiFi()

117-144行目は、Wi-Fi の初期化処理です。

接続されない場合は、5秒置きに再接続を試みます。

void initWiFi() {
    Serial.print("\nConnecting to the WiFi network ");
    Serial.print(WIFI_SSID);
    Serial.println("...");

    // Begin
    WiFi.mode(WIFI_STA);
    WiFi.setAutoReconnect(true);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    // Wait for connection
    Serial.print("Connecting.");
    while (WiFi.status() != WL_CONNECTED) {
        static int count = 0;
        Serial.print('.');
        delay(500);
        // Retry every 5 seconds
        if (count++ % 10 == 0) {
            WiFi.disconnect();
            WiFi.reconnect();
            Serial.print('!');
        }
    }
    Serial.println("\nConnected!");

    // Set Root CA certificate
    client.setCACert(CA_CERT);
}

onAppAriaPacket()

147-157行目には、TWELITE ARIA からデータを受信した際の処理を記述しています。

ここでは HTTP の送信処理を行わず、グローバル変数へセットしています。 グローバル変数へセットしたデータは、別のタスクで sendAriaData() によって処理します。

void onAppAriaPacket(const ParsedAppAriaPacket& packet)
{
    // Store data
    LatestDataFromAria.serialId = packet.u32SourceSerialId;
    LatestDataFromAria.logicalId = packet.u8SourceLogicalId;
    LatestDataFromAria.supplyVoltage = packet.u16SupplyVoltage;
    LatestDataFromAria.linkQuality = packet.u8Lqi;
    LatestDataFromAria.temp100x = packet.i16Temp100x;
    LatestDataFromAria.humid100x = packet.u16Humid100x;
    IsThereNewDataFromAria = true;
}

sendAriaData()

160-220行目は、TWELITE ARIA のデータを HTTP GET リクエストのクエリ文字列にセットして送信する関数です。

サーバへの過度な負荷を防ぐため、高頻度でパケットが到着した際には送信をスキップしています。

void sendAriaData(const DataFromAria& data)
{
    static uint32_t lastTimeRequested = 0;
    if (millis() - lastTimeRequested > SEND_MIN_INTERVAL * 1000 or lastTimeRequested == 0) {
        Serial.println("Connecting to the server...");
        if (not client.connect(SERVER_HOST, SERVER_PORT, CONNECT_TIMEOUT * 1000)) {
            Serial.println("Connection failed!");
        } else {
            Serial.println("Connected to the server!");

            // Make a query string for the Channel on the ThingSpeak
            char queries[QUERIES_MAX_LENGTH+1];
            snprintf(queries, sizeof(queries),
                     "api_key=%s&field1=%s&field2=%s&field3=%s&field4=%d",
                     // Write API key for the Channel
                     WRITE_API_KEY,
                     // Field 1: Temperature
                     String(data.temp100x / 100.0f, 2),
                     // Field 2: Humidity
                     String(data.humid100x / 100.0f, 2),
                     // Field 3: Supply Voltage
                     String(data.supplyVoltage / 1000.0f, 2),
                     // Field 4: Link Quality
                     data.linkQuality);

            // Send a request
            client.println(String("GET https://") +
                           SERVER_HOST +
                           String("/update?") +
                           queries +
                           String(" HTTP/1.1"));
            client.println("Accept: */*");
            client.println(String("Host: ") + SERVER_HOST);
            client.println("Connection: close");
            client.println();
            uint32_t timeSentRequest = millis();

            // Handle a response
            while (client.connected()) {
                String line = client.readStringUntil('\n');
                if (line == "\r") {
                    Serial.print("Index (if succeeded): ");
                    break;
                }
                if (millis() - timeSentRequest > REQUEST_TIMEOUT * 1000) {
                    Serial.println("Request was timed out");
                    break;
                }
            }
            while (client.available()) {
                char c = client.read();
                Serial.write(c);
            }
            client.stop();
            Serial.println("");
        }
        lastTimeRequested = millis();
    } else {
        Serial.println("Requests are too frequently; skip.");
    }
}