ロゴ ロゴ

Grafana+InfluxDBでデータを可視化してみよう Part2

まえがき

Grafana+InfluxDBでデータを可視化してみよう

いきなりリンクから始まりました。どうもウグイスです。

このブログは前回のブログの続きなのでとりあえず初手にリンクを貼ってみました。時間がある人はそっちから読んできてください。

時間がない人にために前回やったことを3行でまとめると

Grafana+InfluxDBをラズパイ400のDocker上で動かした

Docker上のGrafanaとInfluxDBを接続した

GrafanaでInfluxDBのデータをグラフ表示した

InfluxDBのクライアントライブラリ(Kotlin)を使ってテストデータを送信した

って感じです。

今回は

  1. ESP32からデータを受け取ってInfluxDBにデータを渡すサーバ(Kotlin製)作り
  2. ESP32からデータを送信する

の2本立てで行こうかなと考えてます。(Grafana+InfluxDBのブログは試験的に、作業をしながらブログを書くのはありかなしかを実験しているので、この段階では確定ではない)

一応、前回のブログのイラストで説明すると下のようになります。

どうですかね、わかりやすいといいのですが

あと、今回Grafanaあんまり関係ないじゃんというツッコミはなしで

ktorプロジェクトの作成と実行テスト

お詫び

さて、まずは前回のブログについてちょっと訂正があります。

環境作りのWindowsPCの欄でKtorプラグインを導入と書きましたが、どうにも廃止されて推奨されないみたいなのでなしでお願いします。(前回のブログにも加筆修正を加えておきます)

閑話

サーバーの言語はkotlin、フレームワークはktorを使うのですが、選択の経緯を書いてなかったのでここで書いてみます。

私が取っている授業でチーム開発をするという物がありました。

そこで題材として、サーバーと通信させてデータを受け取って表示するAndroidアプリを作ることになりました。

サーバーとクライアントの言語を統一しようぜ、となりサーバーをkotlinで開発することになったわけです。

作成前にうちのサークルの同世代でweb関連とかに詳しいtofuさんに相談したところ

 私「…というわけで、kotlinでサーバープログラムを書くんだけど、いいフレームワークとかある?」

 中略(どういうことをやりたいかとかをやり取りした)

 tofu「それくらいのやりたいことならktorとかかな」

という回答をもらったので、ktorで作りました。

私はそれまでサーバープログラムを書いたことがなく、これ以外のフレームワークを使ったことが無いので今回もktorを採用している感じです。

閑話休題

本題に入りましょう。お詫びに書いた通りプラグインは非推奨のようなので

ktorGenerator

を使ってプロジェクトを作ります。

名前を適当に決めます。

Jsonでデータの受け取りをしたいので、プラグインとしてkotlinx.serializationを入れてください。

下のGenerate Projectを押せばzipファイルがダウンロードされます。

これを適当なフォルダに展開して、IntelliJ IDEAで開きます。

大体こんな感じになってれば大丈夫だと思います。

早速実行しましょう。fun main()の横に再生ボタンがあるのでそれをクリックして実行してみてください。

ログの部分がこんな感じになれば成功です。以下のリンクにブラウザからアクセスしてください

http://127.0.0.1:8080

Hello World!と表示されていれば成功です。

他の確認方法としてcurlを使うこともできます。

コマンドプロンプトを開いて、以下のコマンドを実行

curl 127.0.0.1:8080

同じくHello World!となれば成功です。

簡単な解説

ネットに死ぬほど情報がありますし、何番煎じかわかりませんが、解説します。

前回から引き続きJavaってなってますがkotlinです。(相談したんですが、修正は難しいらしいです。)

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting()
        configureSerialization()
    }.start(wait = true)
}

embeddedServerってのでサーバーの設定をしてます。embeddedServerの返り値に.start()をすると実行される感じです。

embeddedServerの中括弧の中でルーティング等の設定をするのですが、サンプルコードではこれが別のファイルに切り出されてます。

com.example.pluginsにRouting.ktがあります。

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

これの意味は単純でパス無しのGETリクエストが来たら、Hello World!を返すというだけです。

embeddedServerの中括弧の中でconfigureRouting()と呼ぶことで、これの中身が設定される感じです。

あくまで切り出しなので、中身をembeddedServerの中括弧の中に書いても同じように実行できます。

サーバープログラムを作っていく

どういうものを作るのか

いきなり作業から入ってもいいのですが、どういう機能を持たせるのかをまとめておこうと思います。

  1. /house/sensor/thermohygro でPOSTリクエストを受け付ける
  2. POSTリクエストが来たら、ボディのJsonをデシリアライズする
  3. 現在時間の情報を付加してInfluxDBに登録する

大体こんな感じで作っていこうと思ってます。

ライブラリを入れる

前回のブログとほぼ同じです。

build.gradle.ktsのdependenciesブロックに

implementation("com.influxdb:influxdb-client-kotlin:6.3.0")

これを追加します。

多分、dependenciesブロックの中はこんな感じになるはず(中身の順序は割とどうでもいいので、書き足す場所は任意です)

dependencies {
    implementation("io.ktor:ktor-server-content-negotiation-jvm:ktor_version")
    implementation("io.ktor:ktor-server-core-jvm:ktor_version")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:ktor_version")
    implementation("ch.qos.logback:logback-classic:logback_version")
    implementation("com.influxdb:influxdb-client-kotlin:6.3.0")
    testImplementation("io.ktor:ktor-server-tests-jvm:ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

作っていく

pluginsの中にRouting.kt,Serialization.ktがあると思います。

ただ、今回は単純なものを作るので設定の切り出しを2つに分ける意味がないです。Serialization.ktを削除します。

main関数とRouting.ktの関数を以下のように変えます。

main関数

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting()
    }.start(wait = true)
}

Routing.kt

fun Application.configureRouting() {
    install(ContentNegotiation) {
        json()
    }

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

前準備はこんな感じでしょうか。ここからは本格的に作っていきます。

dataClassを用意する

とりあえず、必要なデータクラスを用意します。

以下の二つを作ります

  1. esp32から送られてくるJsonをデシリアライズするためのデータクラス
  2. InfluxDBに送るためのデータクラス

まずは1番からThermoHygroDataというデータクラスを作成します。内容は以下

ThermoHygroData

package com.example.dataclass

@kotlinx.serialization.Serializable
data class ThermoHygroData(
    val name: String,
    val temp: Double,
    val humidity: Double
)

データクラスの前に@kotlinx.serialization.Serializableを付けることで、いい感じでJsonを処理できるようになります。

nameにはどのarduinoから送信されてるかを入れる予定。

tempは温度、humidityは湿度です。

次に2番、ThermoHygroInfluxDataというデータクラスを作成します。

ThermoHygroInfluxData

package com.example.dataclass

import com.influxdb.annotations.Column
import com.influxdb.annotations.Measurement
import java.time.Instant

@Measurement(name = "ThermoHygroSensor")
data class ThermoHygroInfluxData(
    @Column(tag = true) val tagKey: String,
    @Column val temp: Double,
    @Column val humidity: Double,
    @Column(timestamp = true) val time: Instant
)

これは前回のブログとほぼ同じですね。今回は値が2つ入っています。

ルーティングとかを設定する。

Routing.kt

fun Application.configureRouting() {
    val org    = "yourOrganization"
    val bucket = "yourBucket"
    val url    = "yourURL"
    val token  = "yourToken"

    install(ContentNegotiation) {
        json()
    }

    routing {
        post("/house/sensor/thermohygro") {
            try {
                //Jsonデータを受け取りデータクラスに格納
                val thermoHygroData = call.receive<ThermoHygroData>()
                //現在時刻の情報を付加したデータクラスの作成
                val thermoHygroInfluxData = ThermoHygroInfluxData(
                    tagKey   = thermoHygroData.name,
                    temp     = thermoHygroData.temp,
                    humidity = thermoHygroData.humidity,
                    time     = Instant.now()
                )

                //InfluxDBに接続
                val client   = InfluxDBClientKotlinFactory.create(url,token.toCharArray(),org,bucket)
                val writeApi = client.getWriteKotlinApi()

                //データの書き込み
                runBlocking {
                    writeApi.writeMeasurement(thermoHygroInfluxData, WritePrecision.NS)
                }

                client.close()

                call.respondText("success")
            }
            catch (e: Exception) {
                call.respond(HttpStatusCode.BadRequest,"failed")
            }
        }
    }
}

面倒なので、例外が出たら全部BadRequestを返すようにしました。(良い子の皆さんはマネしないでください)

organizationとかtokenとかは自分のものを使ってください。

実行確認

とりあえず、これで動くはずなので実行します。実行手順はさっきと同じです。

テストですが、今回はPOSTなのでブラウザではなくcurlで確認します。

とりあえず、JSONファイルを適当なフォルダに作ってください。私はファイル名をsensor.jsonにしました。

sensor.json

{
    "name":"test",
    "temp":25.1,
    "humidity":70.5
}

JSONファイルを作ったフォルダーでコマンドプロンプトを起動し、以下のコマンドを実行

curl -X POST -H "Content-Type: application/json" -d "@sensor.json" "127.0.0.1:8080/house/sensor/thermohygro"

successと帰ってきたら成功です。InfluxDBにデータが入っていることも確認してください。

ちょっと修正

今は、URLやらをコードに直接書いています。

まあ個人使用するプログラムであって、誰かに使ってもらうものでもないので、これで完成

としてもいいのですが、修正します。

ビルドの時だけURLを変えるのが面倒というのが理由です。(作成はwindowsPCで実行はラズパイ400上なのでURLが変わる)

Q.じゃあどうするの?

A.Jsonでセッティングファイルを作る

ちなみにJsonにした理由は、Kotlinx.serializationが入っているからです

セッティングファイルのパスをコマンドライン引数で渡す実装で行きます

セッティングファイルを受け取るためのデータクラスを作ります。

SettingData.kt

package com.example.dataclass

@kotlinx.serialization.Serializable
data class SettingData(
    val org: String,
    val bucket: String,
    val url: String,
    val token: String
)

Main関数ではコマンドライン引数を受け取って、Jsonのデシリアライズを行います。

Main関数

fun main(args: Array<String>) {
    if(args.isEmpty()) {
        println("引数にセッティングファイルのパスを入れる(セッティングファイルはJsonファイルで以下の形式)")
        println("{")
        println("""  "org": 組織名"""")
        println("""  "bucket": バケット名""")
        println("""  "url": InfluxDBのURL""")
        println("""  "token": InfluxDBのToken""")
        println("}")
        return
    }

    val settingData: SettingData
    try {
        val filePath   = args[0]
        val jsonString = File(filePath).readText()
        settingData = Json.decodeFromString(jsonString)
    }
    catch (e: Exception) {
        println(e)
        return
    }

    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting(settingData)
    }.start(wait = true)
}

Routing.ktに関しては一部しか変更がないのでほとんど省略します。

Routing.kt

fun Application.configureRouting(settingData: SettingData) {
    val org    = settingData.org
    val bucket = settingData.bucket
    val url    = settingData.url
    val token  = settingData.token

    中略
}

Jsonファイルはこんな感じ

{
  "org": "yourOrganization",
  "bucket": "yourBucket",
  "url": "yourURL",
  "token": "yourToken"
}

これで完成ですね。

サーバープログラムをビルドしよう

さてこれでソースコード自体は完成です。ただこのままだとラズパイ上に持っていくのが面倒です。

なのでビルドして単一の実行ファイルを生成します。

ビルドツールとして、GradleShadowを使います。

これは、依存関係とかいろいろまとめて一つの実行ファイルとしてビルドしてくれます。

そのファイル一つあれば、JAVAが動く環境でならどこでも動かせます。便利ですね。

ではbuild.gradle.ktsのpluginブロックに以下の1行を追記

id("com.github.johnrengelman.shadow") version "7.1.2"

多分、pluginブロックはこんな感じになると思います。

plugins {
    application
    kotlin("jvm") version "1.7.10"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.7.10"
    id("com.github.johnrengelman.shadow") version "7.1.2"
}

Gradleの変更を読み込んだあと、以下の画像の選択されている部分をダブルクリックでビルドされると思います。

その後、プロジェクトフォルダのbuild/libsに.jarファイルが生成されていると思います。

これでビルド完了です。

ラズパイ400で実行する

ファイルの送信

ラズパイ400で実行するためにはビルドしたファイルを転送しないといけません。

ラズパイにはちゃんとUSBポートがあるので、USBメモリで転送もできます。

ただ、プログラムにミスが有るたびにUSBを刺したり抜いたりは面倒です。

というわけでSSHで送りましょう。

  1. TeraTermを使います。ない人はインストールしてください。
  2. ラズパイのSSHをオフってる人はオンにしてください。
  3. TeraTermを起動しホストの部分にラズパイのipアドレスを入れます。
  4. ユーザ名とパスワードを入力して接続します。
  5. 送りたいファイルをドラッグアンドドロップで送信できます

以上、完了です。送るのは.jarファイルとセッティングファイルです。

実行する

ここからはラズパイ側から操作します。

まずはセッティングファイルのURLを以下のように変更

http://127.0.0.1:8086

プログラムがあるディレクトリに移動し、以下のコマンドを実行

java -jar .jarファイル名 セッティングファイルのパス

ちなみにセッティングファイルのパスは同一ディレクトリにあればファイル名だけで通ります。

windowsからデータを送って動いているか確認します。

以下のコマンドを実行。ラズパイのipアドレス部分は自分の環境に合わせてください。

curl -X POST -H "Content-Type: application/json" -d "@sensor.json" {ラズパイのipアドレス}:8080/house/sensor/thermohygro"

successが帰ってくれば成功です。


お疲れ様です。ここまででラズパイサイドの実装は終了となります。あとはESP32だけですね。

もうひと踏ん張りです。

ESP32でサーバーに情報を送信する

さて、ESP32ですね。開発環境については以下のブログを参照してください。(別に自分で書くのが面倒とかではない)

【新歓ブログリレー】ESP32でボタンが押された回数をサーバーに送信する

まあ、一つ注意するとすればドライバが入っていないと当然ESP32が認識されない点ですね。

では、↑のブログを読んでarduinoIDEで開発できる準備ができたとして話を進めます。

配線

とりあえず、配線とかでつないでいきます。材料は以下の通り

  1. ESP32
  2. DHT20
  3. ジャンパワイヤーとか、ブレッドボードとか

下の写真の感じで作りました。

一応、言葉で説明すると、DHT20を正面から見た時左から順に、3V3,21,GND,22に接続しています。当たり前ですが配線をミスると動かないので注意。

プログラム

プログラムは適当に書きました。delay()で1分待機して、送信している感じです。送信部分は関数にして処理を分けました。

もうちょいちゃんと書こうかとも思ったんですが、まあ動けばいいかという感じです。

#include <Wire.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "DHT20.h"

#define DHT20ADDRESS 0x38

DHT20 DHT(&Wire);

const char *ssid = "YourSSID";
const char *password = "YourPassword";

void setup() {
  Serial.begin(115200);
  while(!DHT.begin()) {
    Serial.println("failed");
    delay(1000);
  }
  delay(2000);

  WiFi.begin(ssid, password);

  while(WiFi.status() != WL_CONNECTED) {
    Serial.println("Waiting for WI-Fi connection....");
    delay(500);
  }
  Serial.println("Connected to Wi-Fi");
}

void loop() {
  // put your main code here, to run repeatedly:
  int status = DHT.read();
  Serial.print(status);
  Serial.print(",");
  Serial.print(DHT.isConnected());
  Serial.print(",");
  Serial.print(DHT.getHumidity(),1);
  Serial.print(",");
  Serial.println(DHT.getTemperature(),1);
  if(status == DHT20_OK && sendData(DHT.getTemperature(),DHT.getHumidity())) {
    Serial.println("sendOK");
    delay(60000);
  }
  else {
    Serial.println("sendFailed");
    delay(1000);
  }
}

bool sendData(double temperature, double humidity) {
  HTTPClient http;
  bool ret;
  String tag = "myroom";
  String body = "{\"name\":\"" + tag + "\",\"temp\":" + String(temperature) + ",\"humidity\":" + String(humidity) + "}";
  String server = "http://サーバのIPアドレス:8080/house/sensor/thermohygro";

  http.begin(server);
  http.addHeader("Content-Type","application/json; charset=utf8");

  int httpCode = http.POST(body);
  if(httpCode >= 0) {
    String body = http.getString();
    if(body.indexOf("success") >= 0) {
      ret = true;
    }
    else{
      ret = false;
    }
  }
  else {
    ret =  false;
  }

  return ret;
}

Wifiのssidとパスワード、ラズパイのIPアドレスは自分の環境に合わせて要書き換え。

一応、説明するとsendData()がデータの送信部分で、成功でtrue、失敗でfalseを返します。

bodyはJsonです。ソースコードにべちゃ書きしてますが、Arduino言語だと書きにくい。Kotlinみたいにいちいちエスケープしなくてもよいような書き方があればいいのですが。

loop()の中では

  1. DHTのデータの読み込み
  2. DHTが読み込めてれば、データの送信を試みる。
  3. 成功なら、1分待機。失敗なら1秒待機。
  4. 最初に戻る

といった感じの処理になっています。

setup()では、DHTの初期化と、WiFiへの接続を行っています。

適当にESP32に給電して動かすと、InfluxDBにデータが入ると思います。

ちなみに、ESP32を増やす場合は、sendData()内のtagを書き換えてください。どのESP32のデータなのかが分からなくなるので。

Grafanaで表示する

完成品

とりあえず完成品を見せます。(以下の写真)

これは2日分のデータを表示させています。

温度は緑色、湿度は青色で描画されるようにしてみました。

作り方

ダッシュボードから、AddPanelをして二つ作ります。

一つ目は温度にします。

設定画面のクエリに以下のFluxのスクリプトを書きます。

from(bucket: "room_sensors")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "ThermoHygroSensor")
  |> filter(fn: (r) => r["_field"] == "temp")
  |> filter(fn: (r) => r["tagKey"] == "myroom")

2つ目は湿度です。

設定画面のクエリに以下のFluxのスクリプトを書きます。

from(bucket: "room_sensors")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "ThermoHygroSensor")
  |> filter(fn: (r) => r["_field"] == "temp")
  |> filter(fn: (r) => r["tagKey"] == "myroom")

そして、色とかを自分好みにいじくったら完成です。

あとがき

割とHow toが分かりやすいように書いたつもりですが、わかりやすかったでしょうか?

実はこのブログはPart1を書いた後すぐに書き始めているので、執筆に4か月くらいかかっています。

本当は夏休みには完成させたかったのですが、DHT20がうまく動かず(多分配線ミス)ずっと詰まってたり。単純に忙しくて(芝浦祭とか)後回しになってました。

なので、ESP32以降の部分はあっさりとしてます。

これで作ろうとしていたものは完成しました。

ですが、Grafana+InfluxDBの記事はもう一本上げようと思います。

理由なんですが、少し触っていなかった機能が残っているからです。

一応、触ろうと思っているのは

  1. Grafanaのダッシュボードの自動生成
  2. PCの負荷計測
  3. InfluxDBのarduinoクライアントライブラリを使う
  4. InfluxDBのHTTPAPIを使う

くらいですかね。結構多いので、分割するかもしれないです。

ちなみに、作業しながらブログを書くのは、ありかなと思いました。全部完成させてからだと、写真等の資料の用意の手間がかかるので、その分だけ楽です。

ただ、途中で詰まると下書きが放置されてもったいない感じになるのがネックでした。結局ものによるという結論です。

長いブログにお付き合いいただきありがとうございます。

では、また次のブログでお会いしましょう。

コメント入力

関連サイト