Pythonで収集したデータをInfluxDBに格納してGrafanaで可視化する

# はじめに
[前回の記事](https://qiita.com/Yokogawa_Ishino/items/84abfe8a2e37d843717d)ではPythonを使ってModbus TCP通信でデータを収集する方法を紹介しました。
今回は収集したデータをエッジデバイスにインストールしたInfluxDBに格納し、Grafanaで可視化する方法を紹介します。
エッジデバイスのみで手軽に装置のデータを可視化することができます。

# 構成
以下の構成でデータの収集～モニタリングを行います。

1. データ収集
    Modbus TCP通信を行ってエッジデバイスが装置からデータを収集します。

1. データ蓄積
    収集したデータをInfluxDBに蓄積します。
    
1. 可視化
    InfluxDBに蓄積したデータをGrafanaを使って可視化します。
    
1. モニタリング
    PCでエッジデバイスのGrafanaにアクセスして結果をモニタリングします。
![kousei.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/15b84825-bd7e-03f6-f4e1-71d568afa94a.png)

# 動作確認済みデバイス
動作確認済デバイス（OS）

- e-RT3 Plus F3RP70-2L[^ert3]（Ubuntu 18.04 32bit）
- Raspberry Pi 4 Model B （Ubuntu Server 20.04 32bit）

これらのデバイスでは armhf アーキテクチャのパッケージが動作します。

# 準備
## Grafanaのインストール
エッジデバイスにGrafanaをインストールします[^grafana]。

必要なパッケージをインストールします。

```bash
sudo apt install -y apt-transport-https
sudo apt install -y software-properties-common wget
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
```

リポジトリを追加してGrafanaをインストールします。

```bash
echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
```
```bash
sudo apt update
sudo apt install grafana
```

サービスを起動して状態を確認します。

```bash
sudo systemctl daemon-reload
sudo systemctl start grafana-server
sudo systemctl status grafana-server
```

以下のような表示があれば成功です。

```bash
username@ubuntu:~$ sudo systemctl status grafana-server
* grafana-server.service - Grafana instance
   Loaded: loaded (/usr/lib/systemd/system/grafana-server.service; disabled; ven
   Active: active (running) since Fri 2021-12-24 07:38:20 UTC; 14s ago
     Docs: http://docs.grafana.org
 Main PID: 2756 (grafana-server)
    Tasks: 12 (limit: 2366)
   CGroup: /system.slice/grafana-server.service
           `-2756 /usr/sbin/grafana-server --config=/etc/grafana/grafana.ini --p

Dec 24 07:38:32 ubuntu grafana-server[2756]: t=2021-12-24T07:38:32+0000 lvl=info
Dec 24 07:38:32 ubuntu grafana-server[2756]: t=2021-12-24T07:38:32+0000 lvl=info
...
```
>**Note**
>エッジデバイスがproxy環境下にある場合は[proxy設定](https://qiita.com/Yokogawa_Mita/items/f7996d539b29618d7159#proxy%E8%A8%AD%E5%AE%9A)が必要です。


## InfluxDBのインストール
エッジデバイスにInfluxDBをインストールします。

PythonからInfluxDBを操作するためのパッケージをインストールします。

```bash
python3 -m pip install influxdb
```

InfluxDBをインストールします[^influxdb]。
リポジトリを追加して、インストールを行います。

```bash
curl -s https://repos.influxdata.com/influxdb.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/influxdb.gpg
export DISTRIB_ID=$(lsb_release -si); export DISTRIB_CODENAME=$(lsb_release -sc)
echo "deb [signed-by=/etc/apt/trusted.gpg.d/influxdb.gpg] https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list
```
```bash
sudo apt update
sudo apt install influxdb
```

サービスを起動して状態を確認します。

```bash
sudo systemctl unmask influxdb.service
sudo systemctl start influxdb
sudo systemctl status influxdb
```

以下のような表示があれば成功です。

```bash
username@ubuntu:~$ sudo systemctl status influxdb
* influxdb.service - InfluxDB is an open-source, distributed, time series databa
   Loaded: loaded (/lib/systemd/system/influxdb.service; enabled; vendor preset:
   Active: active (running) since Fri 2021-12-24 07:51:02 UTC; 6s ago
     Docs: https://docs.influxdata.com/influxdb/
  Process: 4191 ExecStart=/usr/lib/influxdb/scripts/influxd-systemd-start.sh (co
 Main PID: 4192 (influxd)
    Tasks: 9 (limit: 2366)
   CGroup: /system.slice/influxdb.service
           `-4192 /usr/bin/influxd -config /etc/influxdb/influxdb.conf

Dec 24 07:51:01 ubuntu influxd-systemd-start.sh[4191]: ts=2021-12-24T07:51:01.75
Dec 24 07:51:01 ubuntu influxd-systemd-start.sh[4191]: ts=2021-12-24T07:51:01.75
...
```

# データ収集
PyModbus[^pymodbus]を使用したPythonプログラムで装置からデータを取得します。
今回は装置の代わりにPC上でModbusサーバーを起動します。

>**Note**
>PyModubsを利用したデータ収集の詳細については[以前の記事](https://qiita.com/Yokogawa_Ishino/items/84abfe8a2e37d843717d)をご覧ください。

## Modbusサーバーの準備
PC上でModbusサーバーを起動します。
定期的に値を書き換えるために、[updating_server.py](https://github.com/riptideio/pymodbus/blob/v2.5.2/examples/common/updating_server.py)(ライセンスは[こちら](https://github.com/riptideio/pymodbus/blob/v2.5.2/LICENSE))を一部変更したプログラムを使用します。
今回は保持レジスタに2つの時系列データを、コイルに1つの2値データを格納します。

関数`updating_writer`を以下のコードに変更します。

```python
import random
def updating_writer(a):
    context = a[0]
    slave_id = 0x00

    # コイル1への書き込み
    fx = 1
    address = 0x01
    values = [bool(random.getrandbits(1))]
    log.debug("coil1: " + str(bool(random.getrandbits(1))))
    context[slave_id].setValues(fx, address, values)

    # 保持レジスタ2への書き込み
    fx = 3
    address = 0x02
    values = context[slave_id].getValues(fx, address, count=1)
    values[0] +=  random.randint(-10, 10)
    if values[0] > 100:
        values[0] = 100
    elif values[0] < 0:
        values[0] = 0
    log.debug("holding2: " + str(values[0]))
    context[slave_id].setValues(fx, address, values)

    # 保持レジスタ3への書き込み
    fx = 3
    address = 0x03
    values = context[slave_id].getValues(fx, address, count=1)
    values[0] +=  random.randint(-5, 5)
    if values[0] > 50:
        values[0] = 50
    elif values[0] < 0:
        values[0] = 0
    log.debug("holding3: " + str(values[0]))
    context[slave_id].setValues(fx, address, values)
```

プログラム下部の以下のコードを書き換えます。

```python
# 変更前
StartTcpServer(context, identity=identity, address=("localhost", 5020))
# 変更後
StartTcpServer(context, identity=identity, address=("", 5020))
```

必要なパッケージをインストールしてプログラムを起動します。

```bash
python -m pip install twisted pymodbus
python updating_server.py
```



## エッジデバイスでのデータ収集
エッジデバイスで以下のプログラム`client.py`を実行してサーバーのデータを5秒周期で収集します。
`<IP_ADDRESS_OF_SERVER_PC>`はModbusサーバーを起動しているPCのIPアドレスで置き換えてください。

```python:client.py
#!/usr/bin/env python
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
import time

# Modbusサーバーに接続
mbclient = ModbusClient('<IP_ADDRESS_OF_SERVER_PC>', port=5020)
mbclient.connect()

while True:
    try:
        # コイル1の値読み取り
        rr = mbclient.read_coils(1, 1)
        co1 = rr.bits[0]

        # 保持レジスタ2,3の値読み取り
        rr = mbclient.read_holding_registers(2, 2)
        hr2 = rr.registers[0]
        hr3 = rr.registers[1]

        print("coil1: {0}, holding2: {1}, holding3: {2}".format(co1, hr2, hr3))

        # 5秒スリープ
        time.sleep(5)

    except KeyboardInterrupt:
        # 切断
        mbclient.close()
        break
        
    except Exception as e:
        print(e)
```

プログラムを起動して、以下のような出力があれば成功です。

```bash
username@ubuntu:~$ python3 client.py
coil1: False, holding2: 89, holding3: 12
coil1: True, holding2: 85, holding3: 13
coil1: True, holding2: 78, holding3: 14
...
# Ctrl+Cで終了
```

>**Note**
>このプログラムによる周期的なデータ収集は簡易的なものであり、周期は正確ではありません。
>より正確な周期でデータ収集を行いたい方は他の方法をご検討ください。

# データ蓄積
エッジデバイスで収集したデータをInfluxDBに蓄積します。

## データベースの準備
InfluxDBを起動してデータベースを作成します。
今回は`sampleDB`という名前でデータベースを作成します。

```bash
sudo systemctl start influxdb
```
```bash
username@ubuntu:~$ influx
Connected to http://localhost:8086 version 1.8.10
InfluxDB shell version: 1.8.10
> CREATE DATABASE sampleDB
> exit
```
## データベースへの格納
収集したデータを作成したデータベースに格納します。
[InfluxDBのサンプルプログラム](https://influxdb-python.readthedocs.io/en/latest/examples.html)を参考にして[エッジデバイスでのデータ収集](#エッジデバイスでのデータ収集)で作成した`client.py`に、データを格納するコードを追加します。
追加後のコードは以下のようになります。

```python:client.py
#!/usr/bin/env python
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from influxdb import InfluxDBClient
from datetime import datetime
import time

# Modbusサーバーに接続
mbclient = ModbusClient('<IP_ADDRESS_OF_SERVER_PC>', port=5020)
mbclient.connect()

# データベースに接続
dbclient = InfluxDBClient(host='localhost', port=8086, database='sampleDB')

while True:
    try:
        # コイル1の値読み取り
        rr = mbclient.read_coils(1, 1)
        co1 = rr.bits[0]

        # 保持レジスタ2,3の値読み取り
        rr = mbclient.read_holding_registers(2, 2)
        hr2 = rr.registers[0]
        hr3 = rr.registers[1]

        print("coil1: {0}, holding2: {1}, holding3: {2}".format(co1, hr2, hr3))

        # データベースへの書き込み
        json_body = [
            {
                "measurement": "sample_measurement",
                "time": datetime.utcnow(),
                "fields": {
                    "coil_1": co1,
                    "holding_register_2": hr2,
                    "holding_register_3": hr3
                }
            }
        ]
        print("Write points: {0}".format(json_body))
        dbclient.write_points(json_body)

        # 5秒スリープ
        time.sleep(5)

    except KeyboardInterrupt:
        # 切断
        mbclient.close()
        dbclient.close()
        break
    
    except Exception as e:
        print(e)
```

>**Note**
>データベースへデータを格納する度にSDカードへの書き込みが発生します。
>SDカードへの高頻度な書き込みを避けたい場合は、RAMディスクやネットワークディスクなど適当なストレージを用意し、InfluxDBの設定で格納先として指定してください。

プログラムを起動して数十秒待ちます。

```bash
python3 client.py
# Ctrl+Cで終了
```

エッジデバイスで以下のコマンドを実行してテーブルの中身を表示します。以下のようにデータが格納されていたら成功です。

```bash
username@ubuntu:~/modbus$ influx
Connected to http://localhost:8086 version 1.8.10
InfluxDB shell version: 1.8.10
> USE sampleDB
Using database sampleDB
> SELECT * FROM "sample_measurement" LIMIT 3
name: sample_measurement
time                coil_1 holding_register_2 holding_register_3
----                ------ ------------------ ------------------
1641876882000000000 false  23                 13
1641876887000000000 false  16                 14
1641876892000000000 true   15                 13
```

# 可視化とモニタリング
InfluxDBに蓄積したデータをGrafanaで可視化します。

エッジデバイスで以下のコマンドを実行してGrafanaを起動します。

```bash
sudo systemctl start grafana-server
```

PCで`http://<IP_ADDRESS_OF_EDGE_DEVICE>:3000`にアクセスしてGrafanaを開きます。

ログイン画面が開きます。ユーザー名とパスワードの初期設定値はどちらも`admin`です。必要に応じて変更してください。
![modbus-grafana-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/28803d44-f231-0c0c-d316-2cf1e9792c61.png)

## データベースの登録
Grafanaに[データベースの準備](#データベースの準備)で作成した`sampleDB`を登録します。

左端の歯車のアイコンをクリックしてConfiguration > Data Sourcesと進み、「Add data source」をクリックします。
![modbus-grafana-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/b019a051-0fe9-fcd7-a905-228b4e6232b6.png)

InfluxDBをクリックします。
![modbus-grafana-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/575b362f-e236-59c2-ce75-b57ae19cd40a.png)

設定画面が表示されます。以下の項目を入力し、「Save & test」をクリックします。

| 項目     | 設定値                |
| -------- | --------------------- |
| URL      | http://localhost:8086 |
| Database | sampleDB              |

![modbus-grafana-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/aca19178-c46b-81f1-23fe-3bc15d9a1792.png)

![modbus-grafana-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/92e9dead-1dc5-7d09-a489-1263babb2dc7.png)

## 時系列データの可視化
ダッシュボードを作成し、収集した時系列データをグラフ化します。
今回は保持レジスタ2の値を生データの折れ線グラフ、保持レジスタ3の値を1分毎の平均値の棒グラフで可視化します。

左端の+アイコンをクリックしてCreate > Dashboardと進み、「Add a new panel」をクリックします。
![modbus-grafana-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/d55689ce-a27e-d534-9252-372b5e912ff2.png)

保持レジスタ2(`holding_register_2`)のグラフの設定を以下の画面のように設定を行い、「Apply」をクリックします。
![modbus-grafana-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/d45c8bec-32f4-716f-7a24-34a01ff6f64e.png)

>**Note**
>鉛筆のマークをクリックし以下のクエリ文を入力することでクエリの設定を行うこともできます。
>`SELECT "holding_register_2" FROM "sample_measurement" WHERE $timeFilter`
>![modbus-grafana-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/8025c7cf-1a1b-7133-6a6e-dd7e0ecf1e36.png)

「Add panel」のアイコンをクリックして同様に保持レジスタ3(`holding_register_3`)の棒グラフを作成します。以下の画面のように設定を行います。
![modbus-grafana-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/51c1cc4c-29f1-05ee-9f07-d1a7f8c27011.png)

>**Note**
>以下のクエリ文でクエリの設定を行うこともできます。
>`SELECT mean("holding_register_3") FROM "sample_measurement" WHERE $timeFilter GROUP BY time(1m)`

## 2値データの可視化
コイル1の最新の状態をtrue/falseで表示する可視化を行います。

「Add panel」のアイコンをクリックしてパネルを追加し、右側のメニューから「Stat」を選択して以下のように設定を行います。
![modbus-grafana-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/6c12a6a2-d7d0-0d8a-a6aa-cc2ba6a09687.png)

>**Note**
>以下のクエリ文ででクエリの設定を行うこともできます。
>`SELECT "coil_1" FROM "sample_measurement" WHERE $timeFilter`

すべての作業が完了すると以下のような画面になります。「Save dashboard」をクリックしてダッシュボードを保存します。
![modbus-grafana-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/00088523-d3be-c498-d691-8a0ceec6a416.png)

# まとめ
今回はPythonを使ったModbus通信によるデータ収集からInfluxDBでのデータ蓄積、Grafanaによる可視化までをエッジデバイス上で行いました。
クラウド等の外部のシステムに接続せずにローカルで可視化を行うことができるので、手軽に可視化を行いたいときに便利です。是非ご活用ください。


[^ert3]: [産業用AIプラットフォーム | 横河電機株式会社](https://www.yokogawa.co.jp/solutions/products-platforms/control-system/real-time-os-controller/ert3-ai-platform/)
[^pymodbus]: [GitHub - riptideio/pymodbus: A full modbus protocol written in python](https://github.com/riptideio/pymodbus)
[^grafana]: [Install on Debian/Ubuntu | Grafana Labs](https://grafana.com/docs/grafana/latest/installation/debian/)
[^influxdb]: [Install InfluxDB OSS | InfluxDB OSS 1.8 Documentation](https://docs.influxdata.com/influxdb/v1.8/introduction/install/)