エッジデバイスにデータを書き込むAzure IoT EdgeのPythonモジュールを作成する

# はじめに
横河電機のエッジコントローラe-RT3 Plus F3RP70-2L[^e-RT3]をAzure IoT認定デバイス[^azure]に登録する担当者として、Azure IoT Edgeについて勉強しました。
その内容を数回に分けてご紹介します。
こちらで第5回目の記事となります。
3回目までの記事で、エッジデバイスへのAzure IoT EdgeランタイムのインストールからサンプルのPythonモジュールをデプロイして動作確認する流れを説明しています。

- [第1回目: Ubuntu 18.04にAzure IoT Edgeランタイムをインストールする](https://qiita.com/Yokogawa_Mita/items/f7996d539b29618d7159)
- [第2回目: Azure IoT EdgeランタイムをインストールしたUbuntu 18.04搭載のエッジデバイスをIoT Hubへ接続する](https://qiita.com/Yokogawa_Mita/items/d6525ebd57af03b67cf9)
- [第3回目: Azure IoT EdgeランタイムをインストールしたUbuntu 18.04搭載のエッジデバイスでPythonのサンプルモジュールを作成する](https://qiita.com/Yokogawa_Mita/items/e8a29f9c6682b3156ef2)

また、前回の記事でエッジデバイスのデータを収集しIoT Hubへ送信するPythonモジュールを作成し、デプロイする手順を説明しています。

- [第4回目: Azure IoT EdgeのPythonモジュールでデータ収集してIoT Hubへ送信する](https://qiita.com/Yokogawa_Mita/items/7214feba76ba613b3b79)

さて、今回はUbuntu 18.04搭載のエッジデバイスにデータを書き込むIoT EdgeのPythonモジュールを作成してみます。

AWS IoT Greengrassのインストール手順の紹介は[こちら](https://qiita.com/Yokogawa_Ishino/items/1fb734f091e952a16fea)からどうぞ！

# 環境
**動作確認したデバイス（OS）**

- e-RT3 Plus F3RP70-2L（Ubuntu 18.04 32bit）＋アナログ入出力モジュール
    モジュール構成は以下の通りです。
    - e-RT3 Plus F3RP70-2L（CPUモジュール）
        モジュール一式を制御します。  
        CPUモジュールから各モジュールへアクセスし、書き込みや読み取り等を行えます。
    - F3AD08-6R（アナログ入力モジュール）  
       アナログ入力モジュールは、外部から入力されたアナログデータをデジタルデータに変換します。
    - F3DA04-6R（アナログ出力モジュール）  
        CPUモジュールから入力されたデジタルデータをアナログデータに変換して出力します。  
        今回はこちらのモジュールにデータを書き込みます。
    - F3BU05-0D（ベースモジュール）  
       各モジュールをセットするベースです。  
       電源の供給や、モジュール間の通信はベースモジュールを介して行われます。
    - F3PU20-0S（電源モジュール）  
       電源です。ベースモジュールにセットされます。

    ※写真はチャネル1、2のみ接続していますが、実験時はチャネル1～4を接続しています。

   ![moduleset_n002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682605/2d86a367-a603-70d5-eab9-267fbce72df3.png)
    
    
こちらのデバイスでは `armhf` アーキテクチャのパッケージが動作します。 
また、Windows 10 搭載のPCでモジュールの開発とデバイスの操作を行っています。
（前回までの記事と同様です。）

**IoT EdgeとDockerのバージョン**

```bash:Terminal
$ iotedge --version
iotedge 1.1.1
$ docker --version
Docker version 20.10.5+azure, build 55c4c88966a912ddb365e2d73a4969e700fc458f
```


# ゴール
最終的なゴールは以下の図のような、Azure IoT EdgeランタイムをインストールしたUbuntu 18.04搭載のエッジデバイスでPythonのデータ収集モジュールとデータ書き込みモジュールを作成し、収集モジュールのデータをIoT Hubへ送信し、Power BIとAzure App Serviceで作成したWebで可視化することです。
App ServiceとPower BIでデータを可視化する手順や構成はMicrosoftの公式ドキュメント[^appservicedoc][^powerbidoc]を参考にし、図中のアイコンは[こちら](https://docs.microsoft.com/ja-jp/azure/architecture/icons/)を使用しています。


今回はAzure IoT EdgeランタイムをインストールしたUbuntu 18.04搭載のエッジデバイスで動作する**Pythonのデータ書き込みモジュールを作成し、アナログ出力モジュールへデータを書き込む**ことをゴールとします。

具体的な流れは以下の通りです。

1. Pythonの**IoT Edge書き込みモジュール**を作成し、F3RP70へデプロイする
1. **IoT Edge書き込みモジュール**が**アナログ出力モジュール**のチャネル1～チャネル4に100ミリ秒周期でデータを書き込む
1. 前回デプロイした**IoT Edgeデータ収集モジュール**が**アナログ入力モジュール**の全チャネルのデータを2秒周期で収集し、**IoT Hub**へ送信する
1. **Iot Hub**へ送信されたデータを**Azure IoT Explorer**で確認する


![goal_001-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682605/c854a03d-072a-5a57-77d5-6dae04fee73e.png)



# 準備
今回説明する内容はIoT Edgeモジュールを作成し、デプロイできる環境であることを前提に説明しています。

## 開発環境
開発環境は前回までに準備した環境を使用できます。
準備の手順は基本的にはMicrosoft公式のドキュメント[^doc][^doc2]に従っており、[以前の記事](https://qiita.com/Yokogawa_Mita/items/e8a29f9c6682b3156ef2)でWindows 10の場合を説明しています。


# モジュールの作成
エッジデバイスにデプロイするIoT Edgeモジュールを作成し、Container Registryへプッシュします。
今回実装する機能は以下の通りです。

1. 主な機能
    - アナログ出力モジュールの各出力チャネルに4種類の波形を個別に出力できる       
        sin波、三角波、矩形波、のこぎり波の4種類です。  
        また、アナログ出力モジュールにアクセスするためのライブラリはIoT Edgeモジュールには含めず、CPUモジュール（ホスト）のライブラリをバインドして使用します。
    - 各出力チャネルへの書込み周期は固定で100ミリ秒


1. モジュールツイン
    各出力チャネルに書き込む波形、周期、振幅、オフセットの設定はモジュールツインを使用します。
    各パラメータの説明と設定例は以下の通りです。

    | パラメータ | 内容       |
    | ---------- | ---------- |
    | 1ch - 4ch |  チャネル毎の設定 |
    | form       | 波形       |
    | cycle_sec  | 波形の周期 |
    | amplitude  | 波形の振幅 | 
    | offset     | 波形のオフセット |


    例：チャネル1に周期60秒・振幅2000・オフセット100のsin波、チャネル2に周期120秒・振幅1000・オフセット-100の三角波、チャネル3に周期30秒・振幅500・オフセット0の矩形波、チャネル4に周期40秒・振幅1500・オフセット-500ののこぎり波を書き込む設定
    
```json:モジュールツインの設定
{
    "ert3daout": {
        "ch1": {
            "form": "sin",
            "cycle_sec": 60,
            "amplitude": 2000,
            "offset": 100
        },
        "ch2": {
            "form": "triangle",
            "cycle_sec": 120,
            "amplitude": 1000,
            "offset": -100
        },
        "ch3": {
            "form": "square",
            "cycle_sec": 30,
            "amplitude": 500,
            "offset": 0
        },
        "ch4": {
            "form": "sawtooth",
            "cycle_sec": 40,
            "amplitude": 1500,
            "offset": -500
        }
    }
}
```


## モジュールのプロジェクトの作成

Pythonモジュールのプロジェクトを作成します。
モジュールの作成手順は、基本的には公式ドキュメントの「[チュートリアル:Linux デバイス用の Python IoT Edge モジュールを開発およびデプロイする](https://docs.microsoft.com/ja-jp/azure/iot-edge/tutorial-python-module?view=iotedge-2018-06#create-a-module-project)」に従います。
「モジュール プロジェクトを作成する」内の「新しいプロジェクトを作成する」から「ターゲット アーキテクチャを選択する」までの手順で、Visual Studio Code（VS Code）で新しくプロジェクトを作成します。
以上の手順の詳細が[以前の記事](https://qiita.com/Yokogawa_Mita/items/e8a29f9c6682b3156ef2#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90)の「モジュールの作成」内の「新しいプロジェクトを作成する」から「ターゲット アーキテクチャを選択する」でも紹介されています。

今回は以下の設定でプロジェクトを作成しました。

- Solution Name: Ert3WriteDA
- Module Template: Python Module
- Module Name: Ert3WriteDAModule

また、ターゲットアーキテクチャは `arm32v7` を選択します。



## モジュールのコード
作成したプロジェクトの `main.py` を以下のコードに置き換えて保存します。

```python:main.py
import os
import json
import math
import subprocess
import signal
import datetime
import ctypes
from azure.iot.device import IoTHubModuleClient
from azure.iot.device import Message

DESIRED_KEY = 'desired'
ERT3DAOUT_KEY = 'ert3daout'
FORM_KEY = 'form'
FORM_SIN_TAG = 'sin'
FORM_TRIANGLE_TAG = 'triangle'
FORM_SQUARE_TAG = 'square'
FORM_SAWTOOTH_TAG = 'sawtooth'
CYCLE_SEC_KEY = 'cycle_sec'
AMPLITUDE_KEY = 'amplitude'
OFFSET_KEY = 'offset'
STATUS_KEY = 'status'
CYCLE_SEC_DEFAULT = 60
AMPLITUDE_DEFAULT = 2000
OFFSET_DEFAULT = 0
CHTAG_KEYS = ['ch1', 'ch2', 'ch3', 'ch4', 'ch5', 'ch6', 'ch7', 'ch8']
FAM3DA_CHNUM = {'DA04': 4, 'DA08': 8}
UNIT = 0
SLOT = 3
INTERVAL_SEC = 0.1
LDCONFIGEXEC = 'ldconfig'
M3LIB_PATH = '/usr/local/lib/libm3.so.1'


class DaOut():
    def __init__(self, unit, slot, interval_sec):
        self.__unit = unit
        self.__slot = slot
        self.__interval_sec = interval_sec
        self.__libc = ctypes.cdll.LoadLibrary(M3LIB_PATH)
        self.__libc.getM3IoName.restype = ctypes.c_char_p
        self.__chnum = self.__get_m3da_ch_num()
        self.__counters = [0 for x in range(self.__chnum)]
        self.__configs = {CHTAG_KEYS[x]: {} for x in range(self.__chnum)}
        signal.signal(signal.SIGALRM, self.__signal_handler)
        signal.setitimer(signal.ITIMER_REAL, interval_sec, interval_sec)

    def __get_m3da_ch_num(self):
        namebytes = self.__libc.getM3IoName(
            ctypes.c_int(self.__unit), ctypes.c_int(self.__slot))
        num = 0
        if namebytes is not None:
            num = FAM3DA_CHNUM.get(namebytes.decode(), 0)
        return num

    def __write_m3da_ch_data(self, ch, data):
        short_arr = ctypes.c_short * 1
        ch_data = short_arr(data)

        self.__libc.writeM3IoRegister(
            ctypes.c_int(self.__unit),
            ctypes.c_int(self.__slot),
            ctypes.c_int(ch),
            ctypes.c_int(1),
            ch_data)

    def __da_form_out(self, ch, form, interval, cycle, ampl, offset, count):
        data = 0.0
        count = count % (cycle / interval)

        if form == FORM_SIN_TAG:
            data = ampl * math.sin(math.pi * 2 * interval * count / cycle)

        elif form == FORM_TRIANGLE_TAG:
            data = count * interval * ampl * 4 / cycle
            if ampl * 3 < data:
                data = data - (ampl * 4)
            elif ampl < data:
                data = (ampl * 2) - data

        elif form == FORM_SQUARE_TAG:
            data = ampl
            if ((cycle / interval) / 2) < count:
                data = - data

        elif form == FORM_SAWTOOTH_TAG:
            data = (count * interval * ampl * 2 / cycle) - ampl

        data = data + offset

        self.__write_m3da_ch_data(ch, round(data))

    def __signal_handler(self, signum, frame):
        for id in range(self.__chnum):
            if FORM_KEY in self.__configs[CHTAG_KEYS[id]]:
                self.__da_form_out(
                    id + 1,
                    self.__configs[CHTAG_KEYS[id]][FORM_KEY],
                    INTERVAL_SEC,
                    self.__configs[CHTAG_KEYS[id]].get(CYCLE_SEC_KEY, CYCLE_SEC_DEFAULT),
                    self.__configs[CHTAG_KEYS[id]].get(AMPLITUDE_KEY, AMPLITUDE_DEFAULT),
                    self.__configs[CHTAG_KEYS[id]].get(OFFSET_KEY, OFFSET_DEFAULT),
                    self.__counters[id]
                    )
                self.__counters[id] = self.__counters[id] + 1

    def set_condition(self, desired):
        if ERT3DAOUT_KEY not in desired:
            return {STATUS_KEY: False}

        for id in range(self.__chnum):
            if CHTAG_KEYS[id] not in desired[ERT3DAOUT_KEY]:
                self.__configs[CHTAG_KEYS[id]] = {}
                self.__counters[id] = 0
            elif not self.__configs[CHTAG_KEYS[id]] == desired[ERT3DAOUT_KEY].get(CHTAG_KEYS[id], {}):
                self.__configs[CHTAG_KEYS[id]] = desired[ERT3DAOUT_KEY].get(CHTAG_KEYS[id], {})
                self.__counters[id] = 0

        reported = {STATUS_KEY: True}
        return reported


def da_out():
    module_client = IoTHubModuleClient.create_from_edge_environment()
    module_client.connect()
    twin = module_client.get_twin()

    daout = DaOut(UNIT, SLOT, INTERVAL_SEC)
    reported = daout.set_condition(twin.get(DESIRED_KEY, {}))
    module_client.patch_twin_reported_properties(reported)

    while True:
        reported = daout.set_condition(
            module_client.receive_twin_desired_properties_patch()
        )
        module_client.patch_twin_reported_properties(reported)

    module_client.disconnect()


if __name__ == "__main__":
    subprocess.run([LDCONFIGEXEC])

    da_out()

```


        
## モジュールのプッシュ
`main.py` を書き換えて保存したら、Container Registryへモジュールをプッシュします。

1. VS Codeのエクスプローラから `deployment.template.json` を探して右クリックします。
    表示されるメニューから「Build and Push IoT Edge Solution」を選択します。
    
1. モジュールのビルドとContainer Registryへのプッシュのコマンドが実行されます。
    VS Code下部のターミナルに進行度と実行結果が表示されます。
    
    ※もしProxyの影響でプッシュに失敗した場合は、[以前の記事の補足](https://qiita.com/Yokogawa_Mita/items/e8a29f9c6682b3156ef2#%E8%A3%9C%E8%B6%B3)の内容を確認し、VS CodeとDocker Desktopに設定を行ってください。


# モジュールのデプロイ

モジュールツインの設定とルートの設定手順以外は前回の記事で紹介したものと同じのため、前回の[モジュールのデプロイ](https://qiita.com/Yokogawa_Mita/items/7214feba76ba613b3b79#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)の章を見ながらデプロイしてください。
異なる点のみ説明します。

- モジュールツインの設定
    [モジュールの作成](#モジュールの作成)のモジュールツインの設定例にならったフォーマットで入力します。

- ルートの設定
    このモジュールのデプロイに伴う新たな設定は不要なので何もしません。


## モジュールの動作確認
Azure PortalのIoT Hubでデプロイの成功と動作状況を、Azure IoT ExplorerでIoT Hubへデータを送信できていることを確認します。

### IoT Hubでの確認
「作成」ボタンをクリックすると自動的にモジュールの情報画面に戻ります。
以下の内容を確認します。

- 「IoT Edgeランタイムの応答」が「200 -- OK」であること
- モジュールのリストに表示されているのが「\$edgeAgent」、「\$edgeHub」、「Ert3D2cModule」、デプロイした「Ert3WriteDAModule」であり、ランタイムの状態が全て「running」であること

※変更を加えたモジュールの設定が適用されるまで数分かかります。
もし上記の通りにならない場合は暫く待ち、画面上部の更新ボタンをクリックし最新の情報に更新してから再度確認します。

![iothub_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682605/1714c0cb-2918-bfb8-fed3-3327eb6cc1a7.png)



### Azure IoT Explorerでの確認
前回の記事の[モジュールの動作確認](https://qiita.com/Yokogawa_Mita/items/e8a29f9c6682b3156ef2#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D)の項目と同じ手順で確認できます。
1～4チャネルの値を見ると、今回デプロイしたモジュールの入力が反映されていることが分かります。


```json:Telemetry
{
  "body": {
    "messageID": 336,
    "deviceID": "test_ert3_f3rp70",
    "datetime": "2021-04-07T02:30:53.839398Z",
    "ch1": -358,
    "ch2": 827,
    "ch3": 501,
    "ch4": 413,
    "ch5": 0,
    "ch6": 2,
    "ch7": 5,
    "ch8": 1
  },
  "enqueuedTime": "2021-04-07T02:30:53.847Z",
  "properties": {}
}
```


# 次回
今回はアナログ出力モジュールにデータを書きこむIoT Edgeモジュールを作成し、デプロイしました。
そしてIoT Edgeデータ収集モジュールでアナログ入力モジュールのデータを収集してIoT Hubへ送信し、前回とは異なり値が入力されていることを確認できました。
次回はIoT Hubへ送信されたデータを可視化してみます。

→[次回](https://qiita.com/Yokogawa_Mita/items/e060d8413b65d8ebd166)


# 参考

[^e-RT3]: [リアルタイムOSコントローラ　e-RT3 Plus F3RP70-2L](https://www.yokogawa.co.jp/solutions/products-platforms/control-system/real-time-os-controller/ert3-ai-platform/)
[^azure]: [Azure Certified Device catalog](https://devicecatalog.azure.com/)
[^doc]: [チュートリアル:Linux のデバイス用の IoT Edge モジュールを開発する](https://docs.microsoft.com/ja-jp/azure/iot-edge/tutorial-develop-for-linux?view=iotedge-2018-06)
[^doc2]: [チュートリアル:Linux デバイス用の Python IoT Edge モジュールを開発およびデプロイする](https://docs.microsoft.com/ja-jp/azure/iot-edge/tutorial-python-module?view=iotedge-2018-06)
[^appservicedoc]: [Web アプリで Azure IoT Hub からのリアルタイム センサー データを視覚化する](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-live-data-visualization-in-web-apps)
[^powerbidoc]: [Power BI を使用して Azure IoT Hub からのリアルタイム センサー データを視覚化する](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-live-data-visualization-in-power-bi)