ラズパイとA/D変換基板「ADPi」を用いて赤外線温度センサ値を取得、SORACOM Harvest/Lagoonで可視化、アラート送信

メカトラックス社のラズベリーパイ用高精度A/D変換モジュール「[ADPi Pro](https://mechatrax.com/products/adpi/)」を使ってセンサーから温度情報を取得し、メカトラックス社のラズベリーパイ用3G通信モジュール「[3GPi](https://mechatrax.com/products/3gpi/)」を使って、取得したデータをSORACOM Harvestにアップロードして可視化し、SORACOM Lagoon を使用して温度監視します。温度計測は、ADPi Proに接続したCalex 赤外線温度センサ「[PC21MT-1](https://jp.rs-online.com/web/p/ir-infrared-temperature-sensors/0553343/) 」を使用します。
今回作成する温度計測プログラムは、計測した温度情報をGUIツールキット「Qt」のPython版「PyQt」を使用して、ラズベリーパイのデスクトップ画面にグラフィック表示し、同時に3GPiを使って、計測した温度情報をSORACOM Harvestにアップロードして可視化します。
また、SORACOM Lagoon を使用して、計測した温度をダッシュボートのグラフに表示し、あらかじめ設定した温度閾値を超えた際のアラート通知メールを送信します。なお、センサ（PC21MT-1）による計測は、ホットプレートを熱源としその温度を一定時間ごとに計測します。

今回使用するADPi Pro、3GPiをRaspberry Pi 3にマウントし、 PC21MT-1をADPi Proに接続した画像を次に示します。

![PC21MT00.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/b7fcef08-679b-0515-b850-2e8dd618238b.jpeg)

3GPiから転送された温度情報を、SORACOM Lagoon のダッシュボードのグラフに表示する流れを次に示します。3GPiに実装されたSORACOM Airから温度情報が、キャリアの閉域網を使ってSORACOM プラットフォームのSORACOM Harvestに転送され、Harvestにひも付けられたLagoonから参照されます。

![Harvest.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/437ebe29-a991-5c7f-95c8-afa68af6e814.jpeg)

# セットアップ
Raspberry Pi 3にADPi Proを取り付け、その上に3GPiに取り付けます。そして、ADPi ProのターミナルブロックにPC21MT-1を接続します。3GPiで使用するSIMカードは、SORACOM Airを使用します。また、センサーで取得した温度情報は、SORACOM プラットフォームのSORACOM Harvestで収集され、SORACOM Lagoonのダッシュボードでグラフ表示やアラート通知メールを送信できるようにセットアップします。

## PC21MT-1の仕様
PC21MT-1の仕様を次に示します。

- センサータイプ：MA 出力信号
- 応答時間：240 ms
- 精度：1 %
- ケーブル長：1m
- 最低検出温度：0°C
- 最大検出温度：+250°C

## ADPi Proと PC21MT-1との接続
センサー温度を測定するために、PC21MT-1に抵抗「250Ω」を接続します。ADPi Proのch1のVoutをONの状態にして、ADPi Proのch2を計測します。ADPi Proのch1のVoutがセンサー温度の電源になります。

- 計測対象温度：ADPi ch1
   - Vout　⇔　PWR+
   - Vin+　⇔　OP+
   - Vin-　⇔　OP-
   - GND　⇔　(未接続)

- センサー温度：ADPi ch2
   - Vout　⇔　(未接続)
   - Vin+　⇔ PWR-
   - Vin-　⇔　抵抗
   - GND　⇔　抵抗

ADPi Proと PC21MT-1との接続画像を次に示します。

![PC21MT接続.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/3221b068-d7de-3708-670f-dbff024859a1.jpeg)


## 3GPiのSIMカードの登録
3GPiのSIMカードには、IoT向きの従量課金のSORACOM Airを利用します。3GPiにSIMカードを設定し、SORACOM のユーザーコンソールを利用して、設定したSIMカードを登録します。SIMカードの登録方法の詳細ついては、「[メカトラックス3GPＩでSORACOM Airをつなぐ](https://tomosoft.jp/design/?p=7053)」を参照してください。

## SORACOM Harvestのセットアップ
SORACOM のユーザーコンソールを利用して、3GPiのSIMカード（SORACOM Air）とでグループを作成してHarvest機能をONに設定します。Harvestのセットアップ方法の詳細ついては、「[SORACOM Harvestに保存したデータをLagoonで表示](https://tomosoft.jp/design/?p=11875)」を参照してください。

## SORACOM Lagoonのセットアップ 
SORACOMユーザーコンソールから「ダッシュボードの作成・共有」を選択し、SORACOM Lagoonの利用を開始します。SORACOM Lagoon consoleを利用して、ダッシュボードを作成し、グラフとアラートを作成します。グラフには、センサー温度と計測対象温度を折れ線グラフで表示し、アラートは計測対象温度が100℃になるとアラート通知メールが送信されるように設定します。Lagoonのセットアップ方法の詳細ついては、「[SORACOM Harvestに保存したデータをLagoonで表示](https://tomosoft.jp/design/?p=11875)」を参照してください。

# 温度計測プログラムの作成
Python3により作成し、デスクトップ画面のGUIの作成はPyQtを使用します。温度計測プログラムは、全体を制御するメインモジュール「adpimain.py」、PC21MT-1から温度情報を取得する温度計測モジュール「pc21mt.py」、Qt Designerを使って作成したパネルモジュール「adpipanel.py」、PyQtによる温度カラーバー表示モジュール「adpidisplay.py」で構成します。

全体を制御するメインモジュール「adpimain.py」を次に示します。

- 温度計測プログラムを実行するとmain関数が呼び出され、温度カラーバー表示モジュール「adpidisplay.py」に制御が移ります。
- sigmoid関数とcolor_bar_rgb関数は、温度カラーバーを表示するために使用します。
- upload_temp関数は、SORACOM Harvestへ温度情報を転送するときに呼び出します。

<h4>adpimain.py</h4>

```python
# -*- coding: utf-8 -*- 
import sys
import numpy as np
import json
import requests

from PyQt5.QtWidgets import (QApplication)

import adpidisplay

gain = 30
offset_x = 0.2
offset_green = 0.7


def sigmoid(x, p_gain=1, p_offset_x=0):
    return (np.tanh(((x + p_offset_x) * p_gain) / 2) + 1) / 2


def color_bar_rgb(x):
    x = (x * 2) - 1
    red = sigmoid(x, gain, -1 * offset_x)
    blue = 1 - sigmoid(x, gain, 1 * offset_x)
    green = sigmoid(x, gain, 1 * offset_green) + (1 - sigmoid(x, gain, -1 * offset_green))
    green -= 1.0
    return red * 256, green * 256, blue * 256


def upload_temp(time, temp_sensor, temp_target):
    # JSONを整形
    json_data = {"time": time, "temp_sensor": temp_sensor, "temp_target": temp_target}
    encode_json_data = json.dumps(json_data)
    # SORACOM Harvestに送信
    print(
        requests.post('http://harvest.soracom.io', data=encode_json_data, headers={'Content-Type': 'application/json'}))


def main():
    app = QApplication(sys.argv)
    panel = adpidisplay.Panel()
    panel.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
```

## 温度計測モジュール
外部（計測対象）温度については、0mVが入力されると0℃、50mVが入力されると250℃となります。 内部（センサ）温度については250Ωで電圧変換した結果、1Vが0℃、5Vで250℃となります。従って、計測対象温度とセンサー温度の計算式は次のようになります。なお、Tが温度、Vが電圧（mv）を示します。

- 計測対象温度：ADPi ch1
T = 5 / V

- センサ温度：ADPi ch2
T = 0.0625 * (V - 1000)

「[SPI /IIOサブシステムを用いたラズベリーパイ用高精度A/D変換モジュール「ADPi Pro」による計測方法](https://qiita.com/TomoSoft/items/3d9fa6f8db3daa0e5b8d)」の「SPIを用いたADPi ProのPythonスクリプトの作成」をベースにして、PC21MT-1から入力したアナログ電圧値を温度変換する機能を追加します。PC21MT-1から温度情報を取得する温度計測モジュール「pc21mt.py」を次に示します。

- get_temp関数により、PC21MT-1から入力したアナログ電圧値を温度変換します。変数「sensor」にセンサー温度、変数「target」に計測対象温度をそれぞれ保存します。

<h4>pc21mt.py</h4>

```python
# -*- coding: utf-8 -*-

import spidev
import smbus
import adpi
import sys
from time import sleep
from datetime import datetime


class Pc21mt(object):
    RAW_OFFSET = (1 << 23)
    RAW_SCALE = (
        0.000596040,
        0.000298020,
        0.000149010,
        0.000074500,
        0.000037250,
        0.000018620,
        0.000009310,
        0.000004650,
    )

    sensor_unit = 0.0625
    target_unit = 250.0 / 50.0

    """コンストラクタ"""

    def __init__(self):
        spibus = 0
        spics = 0
        eeprombus = 1
        eepromaddr = 0x57
        gpioaddr = 0x27
        spi = spidev.SpiDev()
        i2c = smbus.SMBus(eeprombus)
        spi.open(spibus, spics)
        spi.mode = 0b11
        spi.max_speed_hz = 1000000
        self.ad = adpi.ADPiPro(spi, i2c, eepromaddr, gpioaddr)
        self.out12v()

    def v2k(self, rate, val):
        for k, v in rate.items():
            if v == val:
                return k

    def out12v(self):
        c = self.ad.adc.channel["1"]
        self.ad.set_output(c, 1)

    def single_conversion(self, ch):
        c = self.ad.adc.channel[ch]
        g, _ = self.ad.read_configuration()
        self.ad.write_configuration(g, c)
        _, r = self.ad.read_mode()
        self.ad.write_mode(self.ad.adc.mode['single'], r)
        rate = self.v2k(self.ad.adc.rate, r)
        while True:
            sleep(2 * 1.0 / float(rate))
            if not self.ad.read_status() & 0x80:
                break

        raw = self.ad.read_data()
        return raw, g

    #    ch1:Target Temperature
    #    ch2:Sensor Temperature
    def get_temp(self):
        raw1, g1 = self.single_conversion("2")
        # T = 0.0625 * (V - 1000)
        sensor = (Pc21mt.RAW_SCALE[g1] * (raw1 - Pc21mt.RAW_OFFSET) - 1000.0) * Pc21mt.sensor_unit
        raw2, g2 = self.single_conversion("1")
        target = Pc21mt.RAW_SCALE[g2] * (raw2 - Pc21mt.RAW_OFFSET) * Pc21mt.target_unit
        return sensor, target
```


## PyQtによるGUI画面の作成
ラズベリーパイのデスクトップ画面に表示するGUI作成にはPyQtを使用します。GUI画面自身はパソコン上でQt Designerを使用して作成し、変換プログラムを使用してPyQtのコードに変換します。また、温度情報は、テキストおよび温度カラーバーに変換して表示します。

### Qt DesignerによるGUI画面の作成
パソコン上で動作するQt Designerによりラズベリーパイのデスクトップ画面に表示するGUIを作成します。GUIウィジェットとして、温度カラーバーを表示する「Graphics View」、経過時間や各温度を表示する「Text Edit」を使用します。Qt Designerで作成したGUI画面を次に示します。

![qtgui.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/7bfbad7c-5316-3be8-0048-c4cd04de6f99.jpeg)

Qt Designerを使って作成したGUIを、変換プログラムを使ってPythonプログラムに変換します。モジュール名をパネルモジュール「adpipanel.py」とします。Qt Designerを使って作成したコードをPyQtコードに変換する方法は、「[Qt DesignerによるPyQt5のGUIプログラム作成](https://tomosoft.jp/design/?p=9067)」を参照してください。

### PyQtによる温度カラーバー表示モジュール作成
計測した温度情報は、サーモグラフィのように温度の高低を色で表現した温度カラーバーで表示します。温度カラーバーでは、温度が低い順に 青→水色→緑→黄色→赤 となります。温度カラーバーの作成方法については「[PyQt5による温度カラーバーの作成](https://tomosoft.jp/design/?p=11549)」を参照してください。

PyQtによる温度カラーバー表示モジュール「adpidisplay.py」を次に示します。

- Panelクラスのインスタンスメソッドで、Qt Designerで作成したGUI画面を表示し、画面左側に温度カラーバーで表示される温度に対する色の凡例を表示し、1秒ごとにイベントを発生させて、get_temp_interval関数を呼び出します。
- get_temp_interval関数は、disp_thermo関数を呼び出し、disp_thermo関数は、PC21MT-1から入力した温度情報の取得するget_temp関数や、SORACOM Harvestへの温度情報の転送するupload_temp関数を呼び出します。また、取得した計測対象温度を温度カラーバーに変換して画面に表示します。

<h4>adpidisplay.py</h4>

```python
import sys
from datetime import datetime

from PyQt5.QtCore import (QTimer, Qt)
from PyQt5.QtWidgets import (QApplication, QWidget, QDialog,
                             QLabel, QPushButton, QGraphicsLineItem,
                             QGraphicsView, QGraphicsScene, QGraphicsItem)
from PyQt5.QtGui import (QBrush, QPen, QColor)

from adpipanel import Ui_Dialog_panel
import adpimain
import pc21mt


def get_temp():
    Panel.instance.disp_thermo()


class Panel(QWidget):
    comp_temp = [10, 20, 30]
    comp_size = [30, 20, 10]
    comp_unit = [2, 1, 0.5]

    # カラーバーコード
    # [0]：　最低温度(0℃
    # [999]：最高温度(250℃)
    rgb_data = []

    scene_temp = [0] * 64
    timer = QTimer()
    instance = 0

    def __init__(self):
        # super() でスーパークラスのインスタンスメソッドを呼び出す
        #        super(MainWindow, self).__init__(parent)
        # super().__init__()
        super().__init__()
        Panel.instance = self
        self.ui = Ui_Dialog_panel()
        self.ui.setupUi(self)
        self.view_bar = self.ui.graphicsView_bar
        self.view_temp = self.ui.graphicsView_temp
        self.scene_bar = QGraphicsScene()

        Panel.rgb_data = [adpimain.color_bar_rgb(x * 0.001) for x in range(1000)]
        for i in range(80):
            # QPenの設定
            color = 80 - i
            pen = QPen(
                QColor(Panel.rgb_data[color * 12][0], Panel.rgb_data[color * 12][1], Panel.rgb_data[color * 12][2]),
                1,
                Qt.SolidLine,
                Qt.RoundCap,
                Qt.RoundJoin)
            self.scene_bar.addLine(0, i, 30, i, pen)

        self.view_bar.setScene(self.scene_bar)
        self.sensor = pc21mt.Pc21mt()

        self.save_target = []
        for i in range(320):
            self.save_target.append(0)

        self.time = 0
        Panel.timer.timeout.connect(get_temp)
        Panel.timer.start(1000)

    def button_stop(self):
        print("button_stop")
        Panel.timer.stop()
        self.sensor.close()

    def disp_thermo(self):
        self.time += 1
        temp_sensor, temp_target = self.sensor.get_temp()
        print("time:{} temp_sensor:{} temp_target:{}".format(self.time, temp_sensor, temp_target))

        if temp_target < 0:
            return
        elif temp_target > 250:
            return
        else:
            pass

        temp_target = int(temp_target)
        temp_sensor = int(temp_sensor)
        adpimain.upload_temp(self.time, temp_sensor, temp_target)

        self.ui.textEdit_time.setText("{}秒".format(self.time))
        self.ui.textEdit_sensor.setText("{}℃".format(temp_sensor))
        self.ui.textEdit_target.setText("{}℃".format(temp_target))
        self.ui.textEdit_time.setAlignment(Qt.AlignRight)
        self.ui.textEdit_sensor.setAlignment(Qt.AlignRight)
        self.ui.textEdit_target.setAlignment(Qt.AlignRight)

        del self.save_target[0]
        self.save_target.append(temp_target)

        self.scene_temp = QGraphicsScene()

        for i in range(320):
            temp = self.save_target[i]

            color = int((temp / 250) * 999)
            pen = QPen(
                QColor(Panel.rgb_data[color][0], Panel.rgb_data[color][1], Panel.rgb_data[color][2]),
                1,
                Qt.SolidLine,
                Qt.RoundCap,
                Qt.RoundJoin)
            self.scene_temp.addLine(i, 0, i, 80, pen)

        self.view_temp.setScene(self.scene_temp)
```
## SORACOM Harvestへの温度情報の転送
SORACOM Harvestへの温度情報の転送は、計測対象温度とセンサ温度をJson形式に変換して、SORACOMのエンドポイント「`http://harvest.soracom.io`」にHttp Postします。実際に使用するPythonコードは、全体を制御するメインモジュール「adpimain.py」のupload_temp関数を使用します。


# 温度計測の実施
ラズベリーパイのデスクトップ画面で、ターミナルから次のコマンドを入力して作成した温度計測プログラムを実行します。温度計測プログラムが実行されると、熱源であるホットプレートのスイッチをオンにし、適当なところでスイッチをオフにし、温度の変化を確認します。

```
$ sudo python3 adpimain.py 
libEGL warning: DRI2: failed to authenticate
time:1 temp_sensor:27.07907511500001 temp_target:26.106552
<Response [201]>
time:2 temp_sensor:27.085706060000007 temp_target:26.085690600000003
<Response [201]>
time:3 temp_sensor:27.089841087500005 temp_target:26.008205400000005
<Response [201]>
time:4 temp_sensor:27.08443947500001 temp_target:26.017146000000004
<Response [201]>
time:5 temp_sensor:27.073785260000008 temp_target:26.0409876
<Response [201]>
　　　　　　　　　　　　　・・・
　　　　　　　　　　　　　・・・
```


## 温度計測の実施環境
温度計測される熱源はホットプレートを使用します。温度計測実施時の様子を、次の画像により示します。PC21MT-1は、PC21MT-1用スタンドにより固定され、ホットプレートに向けられます。またPC21MT-1はADPi Proに接続します。

![PC21MT01.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/5ef21198-9380-a746-0354-d6755754dcf8.jpeg)


## PyQtによる温度カラーバーの表示
温度計測プログラムを実行すると、ラズベリーパイのデスクトップ画面に次のGUIが表示されます。プログラムを実行してからの経過時間を「経過時間」、PC21MT-1から取得したセンサ温度と計測対象温度をそれぞれ「センサ温度」と「計測対象温度」に表示します。現在の温度を温度カラーバーの表示エリアの右側に表示してリアルタイムで更新します。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/124811/ab650c55-aa0c-6d62-5656-d70d7b0f3c1a.png)

ホットプレートの電源をオンにすると、ホットプレートの温度上昇が次のようにデスクトップ画面の温度カラーバーで表示されます。ホットプレートの電源をオフにすると、ホットプレートの温度が下がっていることがわかります。

![measure.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/b0bb7059-3698-54df-a41f-1cdf8344a10f.jpeg)



## Harvestでの収集データ
3GPiを使用してSORACOM Harvestに転送した温度情報のグラフ表示を次に示します。途中でホットプレートの電源をオフにしたため、温度が下がっているデータが取得できています。

![Harvest1.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/3135db13-cf06-14ef-c5d9-4dfcccd0fe39.jpeg)

同様に、表形式での表示を次に示します。

![Harvest2.jpg](https://qiita-image-store.s3.amazonaws.com/0/124811/b061dad9-da59-6b82-770d-214174931703.jpeg)


## SORACOM Lagoonのダッシュボードの表示とアラーム通知
SORACOM Harvestに収集された温度情報に基づき表示したLagoonのダッシュボードのグラフを次に示します。時刻「8：33」頃にホットプレートの電源をオフにしました。温度が下がっていることが確認できます。

![Harvest3.JPG](https://qiita-image-store.s3.amazonaws.com/0/124811/9a1be2ce-ca8e-63b9-2068-81d5f7c1a892.jpeg)

計測された温度が100℃を超えたとき、次のアラート通知メールを受信しました。

![Harvest4.JPG](https://qiita-image-store.s3.amazonaws.com/0/124811/a2926258-1268-df34-7246-95b26e008af2.jpeg)