💭

Private LoRa通信を利用したIoTシステムを実装する〜データ格納&Web表示編〜

2022/03/25に公開

はじめに

Private LoRa通信を利用したIoTシステム実装の続きです.

前回の記事では,エッジデバイスからゲートウェイに集約したデータをデータベースに格納するパターンとしてApacheとMySQLを利用した手法を説明しました.

今回の記事では,集めたセンサデータをWebアプリケーション上でグラフにして表示するところまでをやってみます.今回はWebアプリケーションとしてDjangoを利用したアプリケーションを開発して,このアプリケーションでPOSTデータの受け取りも行います.

IoTシステムを実装手順

以下が本記事でIoTシステムを構築する手順です.今回の記事では【パターン2】の部分を解説していきます.

  • 【共通部分】

    1. 温湿度センサとAdafruit Metro M4とES920LR2でエッジデバイスを作成する
    2. エッジデバイスからPrivate LoRa通信ゲートウェイにデータを集約する
  • 【パターン1】Apache + MySQLでセンサデータをデータベースに格納する

    1. Private LoRa通信ゲートウェイのEthernet通信設定を行う
    2. データサーバがPrivate LoRa通信ゲートウェイから受け取ったデータをデータベースに格納する
  • 【パターン2】Django + MySQLでセンサデータをデータベースに格納し,Webアプリケーション上に表示する

    1. Private LoRa通信ゲートウェイのEthernet通信設定を行う
    2. Django REST frameworkでセンサデータをPOSTできるようにする
    3. DjangoのWebアプリケーション上でセンサデータをプロットする

この【パターン2】では,Djangoと呼ばれるPythonを活用したWebフレームワークを用いてWebアプリケーションを開発します.

また,Private LoRa通信ゲートウェイからサーバにPOSTされるセンサデータはDjango REST frameworkでデータ取得用のAPIを実装して,データの格納を行えるようにします.

ゲートウェイのEthernet通信設定を行う

まず,ES920GWX2のEthernet通信設定を行い,Private LoRa通信ゲートウェイがサーバへ接続およびデータPOSTできるようにします.

ここは前回の記事と共通している部分が多いので,不要な説明は読み飛ばして頂いても大丈夫です.前回の記事との差分は,データのPOST先の設定が異なる点にありますので,そこにはご注意ください.

ターミナルエミュレータの準備

ES920GWX2のEthernet通信設定は,前回の記事のPrivate LoRa通信設定時と同様にターミナルエミュレータを利用して行います.前回触れた内容と重複しますので,必要があれば以下のトグルから説明を表示して,それを参考に準備を進めてください.

Windowsをお使いの方の場合

Windows環境ではターミナルエミュレータとして,「Teraterm」を使用します.以下の記事を参考にインストールをお願い致します.

インストールが完了したら,以下の手順でTeratermの設定を変更します.

  1. ES920GWX2とPCをUSBケーブルで接続します.
  2. ES920GWX2の電源をONにします.(このとき電源供給を行うアダプタは純正のものを使用するようにしてください)
  3. Teratermを起動します.
  4. 「シリアル」からES920LRGWが接続されているポートを選択します.
  5. メニューバーの「設定」→「端末」を選択します.
  6. 「改行コード」にある「送信(M)」を「CR+LF」に変更し,ローカルエコーにチェックを入れます.
  7. メニューバーの「設定」→「シリアルポート」を選択します.
  8. 「スピード(E)」を「115200」に変更します.
  9. Teraterm内でEnterキーを入力し,「>COM」と出力されれば完了です.
Macをお使いの方の場合

Mac環境ではターミナルエミュレータとして「Coolterm」を使用します.以下の記事からインストールをお願い致します.

インストールが完了したら,以下の手順でCooltermの設定を変更します.

  1. ES920GWX2とPCをUSBケーブルで接続します.
  2. ES920GWX2の電源をONにします.(このとき電源供給を行うアダプタは純正のものを使用するようにしてください)
  3. Cooltermを起動します.
  4. メニューバーの「Connection」→「Options...」を選択します.
  5. 「Serial Port」の「Port」をES920LRGWが接続されているポートに接続し,「Baudrate」を「115200」に変更します.
  6. 「Terminal」の「Enter Key Emulation」が「CR+LF」になっていることを確認し,「Local Echo」にチェックを入れます.
  7. 「Connect」をクリックします.
  8. Coolterm内でEnterキーを入力し,「>COM」と出力されれば完了です.

ES920GWX2の設定を変更する

ここから,ES920GWX2がサーバへアクセス可能とするための設定を行なっていきます.

まず,ES920GWX2をEthernet通信でネットワークに接続します.そのためにはES920GWX2のEthernetポートのIPアドレスやサブネットマスク,デフォルトゲートウェイを適切に設定する必要があります.

上記の設定はそれぞれ,「dhcp」,「ipaddr」,「subnet」,「gateway」コマンドで設定変更可能です.適切な設定値は実装環境によって異なりますので,ES920GWX2 取扱説明書を参考にしながら設定を行います.

筆者の場合は,このES920GWX2を大学のプライベートネットワーク内で利用しました.そのため,設定手順としてはまず,「mac?」コマンドでES920GWX2のMACアドレスを確認し,そのMACアドレスをプライベートネットワーク内に登録することでIPアドレスの割り当てを行いました.このとき,サブネットマスクとデフォルトゲートウェイの情報も得られました.最後にES920GWX2のDHCP設定をOFFにして,IPアドレスなどの登録情報を入力しました.

ゲートウェイをネットワークに接続できる設定にしたところで,次はデータのPOST先を決めましょう.

本記事で使用するサーバのIPアドレスを<ServerIP>であるとします.そして,今回の記事では,ES920GWX2のPOST先設定を以下のように設定します.(この部分が前回の記事と異なります

設定項目 コマンド 設定内容
サーバ名の設定 servern <ServerIP>
サーバディレクトリの設定 serverd /uplink/data_post/
サーバポート番号の設定 serverp 8000

サーバとの通信接続を確認する

前節のEthernet通信設定でちゃんとES920GWX2とサーバが接続されるようになったかを確認します.

本記事では,通信疎通の確認はサーバ側から行います.また今回の記事で利用しているサーバのOSは「Ubuntu Server 20.04 LTS」です.(CentOS等でも進め方は一緒だと思います)

まず,みなさんのPCとサーバを接続します.接続の方法は特に指定はありませんが,個人的にはSSH接続が手っ取り早いと思います.(サーバの22番ポートが開放されていない場合はSSH接続できない場合があるのでご注意ください)

$ ssh <ユーザ名>@<ServerIP>

サーバへのSSH接続が完了したら,pingコマンドを利用してサーバとES920GWX2の通信疎通を確認しましょう.このとき,ES920GWX2のIPアドレスを入力する必要がありますが,このIPアドレスはES920GWX2に対して「ipaddr?」コマンドを入力することで確認することができます.(ES920GWX2 取扱説明書を参考に確認できます)

ES920GWX2に割り当てられているIPアドレスが確認できたら,SSH接続しているサーバ内で以下のコマンドを入力します.(このときES920GWX2のIPアドレスを<GatewayIP>であるとしてます.)

$ ping <GatewayIP>

【接続できている場合のレスポンス例】
64 bytes from <GatewayIP>: icmp_seq=1 ttl=64 time=0.476 ms
64 bytes from <GatewayIP>: icmp_seq=2 ttl=64 time=0.255 ms
:

【接続できていない場合のレスポンス例】
From <ServerIP> icmp_seq=9 Destination Host Unreachable
From <ServerIP> icmp_seq=10 Destination Host Unreachable
:

ES920GWX2とサーバの接続確認ができたら,ゲートウェイのEthernet通信設定は完了です.

Webアプリケーションを開発する

ここから,Private LoRa通信ゲートウェイからのPOSTデータを受け取り,かつそのデータをグラフを表示できるようにするWebアプリケーションを開発します.

今回の記事では,集約したセンサデータをグラフ上にプロットするところまでを説明しますが,このDjangoのWebアプリケーションはデータ分析など,他にも様々な使い方ができるので,本記事を読んだあとに勉強するとIoTシステムで提供できる価値が広がると思います.

環境構築

まず,SSH接続でサーバへアクセスし,以下のコマンドで本記事のWebアプリケーションを開発するためのディレクトリを作成します.ディレクトリ作成場所は特に指定はありませんが,筆者は/home/<ユーザ名>/のディレクトリ内で開発しました.

$ cd <任意のディレクトリ>
$ mkdir test_iot_system

次にDjangoのWebアプリケーションを開発するための仮想環境を構築します.仮想環境構築はこちらが参考になります.

$ cd test_iot_system
$ mkdir web_app_env
$ python3 -m venv web_app_env

仮想環境の構築が完了したら,この仮想環境に入ります.

$ source web_app_env/bin/activate
↓ こんな感じになります
(web_app_env)$

そして,この仮想環境にDjangoのWebアプリケーション時に必要なライブラリをインストールしていきます.必要なライブラリは複数あるのでrequirements.txtにまとめておいて,一気にインストールするのが楽です.本記事ではライブラリの後にバージョンを指定していますが,これをなしにすると最新のバージョンがインストールされます.

requirements.txt
Django==3.2.0
djangorestframework==3.12.3
(web_app_env)$ touch requirements.txt
(web_app_env)$ pip install --upgrade pip
(web_app_env)$ pip install -r requirements.txt 

ここまでで,DjangoのWebアプリケーションを開発する環境の構築は完了です.

Djangoプロジェクトの作成

次はDjangoのプロジェクトを作成します.以下のコマンドでDjangoのプロジェクトを作成しましょう.

(web_app_env)$ mkdir web_app
(web_app_env)$ cd web_app
(web_app_env)$ django-admin startproject config .

すると以下のようなファイル構成になります.

.
├── config
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

Djangoを学習されたことがない方は上記のコマンドでDjangoプロジェクトの「設定ファイル」と「実行ファイル」が作成されたとザックリ理解して頂けると良いです.(本記事では,Djangoの詳しい説明は省きますね)

データ格納用アプリケーションを作成する

さて,上記までで作成したDjangoプロジェクトはまだアプリケーションが実装されておらず,特に機能は持っていない状況です.

この章ではPrivate LoRa通信ゲートウェイからPOSTされたデータを受け取るアプリケーションを実装します.

アプリケーション作成手順

まずは「uplink」アプリケーションを生成します.

(web_app_env)$ python manage.py startapp uplink

すると以下のような「uplink」ディレクトリとファイルが生成されます.

.
├── config (省略)
├── manage.py
└── uplink
    ├── admin.py
    ├── apps.py
    ├── __init__.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py

アプリケーションが作成できたら,settings.pyファイルのINSTALLED_APPSにこのアプリケーションを追加します.ついでにREST Frameworkも追加しておきます.また,DjangoプロジェクトにアクセスできるようにALLOW_HOSTも編集しておきます.そして,Djangoプロジェクトで使用する言語を日本語にして,時刻も東京の時刻にしてもらいます.

web_app/config/settings.py
- ALLOWED_HOSTS = []
+ ALLOWED_HOSTS = ['<ServerIP>']

INSTALLED_APPS = [
    ...
+   'uplink.apps.UplinkConfig',
+   'rest_framework',
    ...
]

- LANGUAGE_CODE = 'en-us'
- TIME_ZONE = 'UTC'
+ LANGUAGE_CODE = 'ja'
+ TIME_ZONE = 'Asia/Tokyo'

また,uplinkアプリケーションのURLを定義します.

web_app/config/urls.py
- from django.urls import path
+ from django.urls import path, include

urlpatterns = [
 +   path('uplink/', include('uplink.urls')),
 ]

次に,ゲートウェイからPOSTされる温湿度センサデータのモデルを定義します.client_ipaddrに関しては必須ではありませんが,POSTしてくるゲートウェイのIPアドレスがわかれば便利な場合(一つのIoTシステムで複数のゲートウェイを設置する場合など)もあるので,今回の記事では追加しておきます.

web_app/uplink/models.py
from django.db import models
from django.utils import timezone
from django.core.validators import MinLengthValidator

class TempHmdData(models.Model):
    """
    温湿度センサ値のデータモデルクラス

             create_at: データPOST時刻
                  temp: 温度データ
                   hmd: 湿度データ
    private_lora_ownid: Private LoRa通信のOWNID設定
         client_ipaddr: データをPOSTしたクライアントのIPアドレス
    """
    class Meta:
        db_table="temp_hmd_data"
        verbose_name='TempHmdData'
        verbose_name_plural='TempHmdData'

    create_at = models.DateTimeField(
        verbose_name='create_at',
        blank=False,
        null=False,
        editable=False,
        default=timezone.now,
    )

    temp = models.DecimalField(
        verbose_name='temp',
        max_digits=5,
        decimal_places=2,
        blank=False,
        null=True,
        default='00.00',
    )

    hmd = models.DecimalField(
        verbose_name='hmd',
        max_digits=5,
        decimal_places=2,
        blank=False,
        null=True,
        default='00.00',
    )

    private_lora_ownid = models.CharField(
        verbose_name='OWNID',
        max_length=4,
        validators=[MinLengthValidator(4)],
        blank=False,
        null=False,
        default='FFFF',
        help_text='入力必須項目です.4桁の16進数アドレスを入力してください.',
    )

    client_ipaddr = models.GenericIPAddressField(
        verbose_name='Client IP address',
        blank=False,
        null=False,
        default='0.0.0.0',
        editable=False,
    )

さらに,adminファイルに上記のモデルを追加します

web_app/uplink/admin.py
from django.contrib import admin

from .models import TempHmdData

@admin.register(TempHmdData)
class TempHmdDataAdmin(admin.ModelAdmin):
    list_display = (
        'create_at',
        'temp',
        'hmd',
        'private_lora_ownid',
        'client_ipaddr',
    )

次はPOSTデータを受け取るURLを定義します.urls.pyはアプリケーション生成時に作成されないファイルなので,ご自分で生成をお願いします.

web_app/uplink/urls.py
from django.urls import path
from . import views

app_name = 'uplink'
urlpatterns = [
    path('data_post/', views.CreateData.as_view(), name='data_post'),
]

次にシリアライザを定義します.このserializer.pyも自動で生成されないファイルなので,こちらもご自分で生成してください.

web_app/uplink/serializer.py
from rest_framework import serializers

from .models import TempHmdData

class TempHmdDataSerializer(serializers.ModelSerializer):
    client_ipaddr = serializers.IPAddressField()

    class Meta:
        model = TempHmdData
        fields = (
            '__all__'
        )
        read_only_fields = ('create_at', 'client_ipaddr', )

そして,次はViewsを定義します.

web_app/uplink/views.py
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser

from django.utils import timezone

from .serializer import TempHmdDataSerializer

import urllib

class CreateData(APIView):
    parser_classes = (MultiPartParser, FormParser, )

    def post(self, request, format=None):
        # POSTされたデータ 
        post_data = request.body.decode()
        # POSTされたデータをdict型に変換
        payload = urllib.parse.parse_qs(post_data.strip())
        # POSTクライアント
        client_ipaddr = request.META.get('REMOTE_ADDR')
        # POSTデータのペイロードをシリアライズするために整形
        data = {
            'create_at': timezone.localtime(timezone.now()),
            'temp': float(int(payload['temp'][0])/100),
            'hmd': float(int(payload['hmd'][0])/100),
            'private_lora_ownid': str(payload['id'][0]),
            'client_ipaddr': client_ipaddr,
        }
        # シリアライザにかける
        serializer = TempHmdDataSerializer(data=data)
        # シラアライズの成否で処理を変更
        if serializer.is_valid():
            serializer.save()
            print('●Valied data post.')
            print(serializer.validated_data)
            return Response(serializer.validated_data, status=status.HTTP_201_CREATED)
        # エラーを吐いたら詳細を見せてもらう
        print('●Data post is invalied.')
        print(serializer.errors)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

最後にデータベースのマイグレーションをします.

(web_app_env)$ python manage.py makemigrations
(web_app_env)$ python manage.py migrate

データ格納確認

データ格納用アプリケーションuplinkが作成できたので,ちゃんとデータがPOSTされるかを確認しましょう.

データPOSTの確認はDjangoのAdminページで行いますので,そこにアクセスできるスーパーユーザアカウントを作っておきます.

(web_app_env)$ python manage.py createsuperuser

そして,Djangoプロジェクトを実行します.

(web_app_env)$ python manage.py runserver 0.0.0.0:8000

上記のコマンドを実行して●Valid data post.と表示されるようになれば,データの格納に成功しています.

また,<ServerIP>:8000/adminにアクセスし,admin画面のTempHmdDataを選択することでPOSTされたデータを確認することもできます.


※こちらの画像では取得したIPアドレスを隠していますが,取得可能です

グラフプロット用アプリケーションを作成する

エッジデバイスからの温湿度センサデータをデータベースに格納できたところで,次はWebアプリケーション上でグラフにしてプロットします.

アプリケーション作成手順

まずは「plot」アプリケーションを生成します.

(web_app_env)$ python manage.py startapp plot

生成できたら,plotアプリケーションを設定ファイルのINSTALLED_APPSに追加し,urlpatternsにURLの登録も行いましょう.ついでにテンプレートファイルをまとめた「template」ディレクトリを作成します.(個人的におすすめのファイル構成です)

web_apps/config/settings.py
+ import os

INSTALLED_APPS = [
    ...
+   'plot.apps.PlotConfig',
    ...
]

TEMPLATES = [
    {
        ...
-        'DIRS': [],
+        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        ...
    },
]
(web_app_env)$ mkdir templates

↓ ザックリこういうファイル構成になります
.
├── config
├── db.sqlite3
├── manage.py
├── plot
├── templates
└── uplink
web_app/config/urls.py
urlpatterns = [
 +   path('plot/', include('plot.urls')),
 ]

そして,グラフをプロットするURLを定義します.このファイルは先ほどと同様ご自分での生成が必要です.

web_app/plot/urls.py
from django.urls import path
from . import views

app_name = 'plot'
urlpatterns = [
    path('temp_hmd_data/<str:ownid>', views.PlotTempHmdData.as_view(), name='plot_temp_hmd_data'),
]

また,グラフをプロットするビューを定義します.

web_app/plot/views.py
from django.shortcuts import render
from django.views.generic import ListView

from uplink.models import TempHmdData

from datetime import datetime

class PlotTempHmdData(ListView):
    template_name = 'plot/temp_hmd_data.html'
    model = TempHmdData

    def get_queryset(self, **kwargs):
        """
        TempHmdDataモデルデータから最新の20個を抽出する
        """
        ownid = self.kwargs.get('ownid', '0000')

        queryset = super().get_queryset(**kwargs).filter(private_lora_ownid=ownid).order_by('-create_at')[:20]
        queryset = [
            {
                'create_at': data.create_at.strftime('%H:%M:%S'),
                'temp': float(data.temp),
                'hmd': float(data.hmd)
             } for data in queryset
        ]
        queryset.reverse()

        return queryset

次に,グラフをプロットするテンプレート(HTMLファイル)を定義します.

web_app/templates/plot/temp_hmd_data.html
<!DOCTYPE html>
<html>
  <head>
    <title>温湿度プロット</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script>
  </head>

  <body>
    <canvas id="temp_chart"></canvas>
    <canvas id="hmd_chart"></canvas>
  </body>

  <script>
  var temp_ctx = document.getElementById('temp_chart');
  var hmd_ctx = document.getElementById('hmd_chart');

  var queryset = {{ object_list | safe }};
  var labels = [];
  var temp_data = [];
  var hmd_data = [];

  for (var i = 0; i < queryset.length; i++) {
    labels.push(queryset[i]['create_at']);
    temp_data.push(queryset[i]['temp']);
    hmd_data.push(queryset[i]['hmd']);
  }
  console.log(labels);
  console.log(temp_data);
  console.log(hmd_data);
  
  var plot_temp_data = {
      labels: labels,
      datasets: [
        {
          label: '温度データ',
          data: temp_data,
          borderColor: 'rgba(100, 255, 100, 1)',
          fill: false,
        },
      ]
  };

  var plot_hmd_data = {
      labels: labels,
      datasets: [
        {
          label: '湿度データ',
          data: hmd_data,
          borderColor: 'rgba(100, 100, 255, 1)',
          fill: false,
        },
      ]
  };
  
  var options = {};
  
  var temp_chart = new Chart(temp_ctx, {
      type: 'line',
      data: plot_temp_data,
      options: options
  });

  var hmd_chart = new Chart(hmd_ctx, {
      type: 'line',
      data: plot_hmd_data,
      options: options
  });

  </script>
</html>

ここまででプロット用のアプリケーションが完成しました.

アプリケーションが完成したので,Djangoプロジェクトを実行します.

(web_app_env)$ python manage.py runserver 0.0.0.0:8000

<ServerIP>:8000/plot/temp_hmd_data/1000にアクセスすると温度データと湿度データのそれぞれがWebアプリケーション上にプロットされてますね!

温度データプロット例

湿度データプロット例

以上で,今回の記事の目的は達成です.
みなさんお疲れ様でした!

おわりに

今回までの記事で,みなさんは以下のIoTシステムのフローを実装することができました.

  1. マイコンを使って温湿度センサからセンサデータを取得する
  2. 取得したセンサデータをPriate LoRa通信を使って発信し,ゲートウェイに集約する
  3. ゲートウェイに集約したデータをデータベースに格納する
  4. データベースに格納されたデータをWebアプリケーション上にプロットする

以上のシステムを開発したみなさんは簡単なIoTシステムを実装できるようになったと思います.

今回のIoTシステムはあくまで「センサデータをプロットすること」のみを目的として実装しましたが,本来のIoTシステムであれば,データベースに格納されたデータを分析し,ユーザにとって価値のある情報に変換してWebアプリケーション上に表示するなどの応用が必要となります.

今回の記事が,ユーザに届けるためのIoTシステムを開発するみなさんの助けになれば幸いです.

また,みなさんの貴重なご意見,ご感想をお待ちしております.

参考

【ゼロからわかる】Teratermのインストールと使い方
CoolTerm ダウンロードしてインストールします | Mac
ES920GWX2 取扱説明書
venv: Python 仮想環境管理 - Qiita
【Django】REST frameworkでプロジェクトにデータPOSTしてくるクライアントのIPアドレスをDBに格納する - Qiita
Chart.js を使って作る折れ線グラフのシンプルなサンプル

Private LoRa通信関連の記事シリーズ

Private LoRa通信モジュールに関する記事をいくつか書いておりますので,興味がございましたら合わせてご覧ください.

https://zenn.dev/masaoguchi/articles/81905f45c37f23

https://zenn.dev/masaoguchi/articles/4f56abc05bae83

https://zenn.dev/masaoguchi/articles/a33746a576e21f

https://zenn.dev/masaoguchi/articles/34bc99e9fcd431

https://zenn.dev/masaoguchi/articles/56b735207daf6e

Discussion