25
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

リアルタイムチャートをDjango ChannelsとEpoch.jsでつくってみた

Posted at

はじめに

あるとき、モーションセンサデータをリアルタイムで可視化したいなと思うことがありました。そこで、ネットでリアルタイム可視化ツールについて調べてみると、近年のIoTブームもありたくさんのツール、製品が世の中に出ていました。無料のツールだと、「Grafana」などがあります。個人的には「Node-RED」のdashboardも気に入っています。

では、なぜDjangoでリアルタイムチャートを作ろうと思ったのか?
理由は3つです。

  • WebアプリケーションであるためWebブラウザから手軽に利用できる
  • 可視化をちょっと試してみたい、ちょっとカスタマイズしたいとき、言語がPythonだと嬉しい人が多そう(フロントエンド部分は結局jsに頼るのですが。。)
  • 自分の勉強のため

自分の勉強がてら、誰かのお役に立てれば幸いです。

システム構成

先に結論です。完成版はこちらです。

概要
今回のシステムは**Django Channels**を用いて実装します。Django Channelsでは、Channelsを介してメッセージをやり取りすることで、HTTPと似た方法でWebsocketの実装が行えます。(画像:Finally, Real-Time Django Is Here: Get Started with Django Channelsより)

HTTP & Websocket
今回のシステムではブラウザから送られてくるHTTPリクエストの処理はHTTPVIEW)が、センサーから送られてくるデータをブラウザへ送信する処理はWebsocketCOUSUMER)が実行します。

センサとの通信
センサデータはCONSUMERへ送信します。プロトコルで任意です(MQTTやUDPなど)。今回はHASC Loggerというモーションセンサアプリを用いて、加速度をセンシングし、UDPで送信します。

可視化
可視化部分はリアルタイムチャートの実装に特化したライブラリであるEpoch.jsを使用します。

環境構築

今回はWSL(ubuntu-18.04)上で作業していきます(普段はLinux上で作業することが多いので)。
Python環境はpyenvで構築します。anaconda3-5.3.1をインストールします(pyenvのインストール方法は省略します)

pyenv install anaconda3-5.3.1
pyenv global anaconda3-5.3.1

Pythonのバージョンは3.7.0です。

python -V
Python 3.7.0

djangoとchannelsはpipでインストールします(django==3.2.4 channels==3.0.3で動作確認しました)。

pip install django==3.2.4 channels==3.0.3

実装

プロジェクトを作成する

realtime_chart_projectという名前のプロジェクトを作成します。

django-admin startproject realtime_chart_project

一応、動作確認します

cd realtime_chart_project
python manage.py runserver

ブラウザからhttp://127.0.0.1:8000へアクセスします。

アプリケーションを作成

realtime_chartという名前のアプリを作成します。

python manage.py startapp realtime_chart

settings.pyINSTALLED_APPS'realtime_chart'を追加します。

realtime_chart_project/realtime_chart_project/settings.py
INSTALLED_APPS = [
    'realtime_chart',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```

## HTTPリクエスト/レスポンスの実装

はじめにURLを設定していきますDjangoはHTTPリクエストを受け取るとルートのURLの設定から参照して `views.py`を探し`views.py`を呼び出してリクエストを処理します

```realtime_chart_project/realtime_chart/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.chart, name='chart'),
]
```

```realtime_chart_project/realtime_chart_project/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('realtime_chart/', include('realtime_chart.urls')),
    path('admin/', admin.site.urls),
]
```

続いて`views.py`にリクエストを処理するコードを記述しますここではチャートを表示する`chart.html`を返す関数を定義します

```realtime_chart_project/realtime_chart/views.py
from django.shortcuts import render

def chart(request):
    return render(request, 'chart.html', {})
```

`chart.html`の中身は可視化の部分で説明します

## Websocketの実装(Django Channelsの設定)
`settings.py``INSTALLED_APPS``'channels'`を追加します

```settings.py
INSTALLED_APPS = [
    'channels',
    'realtime_chart',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```
またルートのルーティング設定にChannelsを追加するために以下を`settings.py`に追記します

````realtime_chart_project/realtime_chart_project/settings.py
# Channels
ASGI_APPLICATION = 'realtime_chart_project.asgi.application'
```

`consumers.py`を実装します
ここではUDPでセンサからデータを受け取りWebsocketでブラウザにデータを送信する処理が行われています

```realtime_chart_project/realtime_chart/consumers.py
from channels.generic.websocket import WebsocketConsumer
import json
import threading
import time
import random

from socket import socket, AF_INET, SOCK_DGRAM


class SensorConsumer(WebsocketConsumer):

    def connect(self):
        self.accept()
        self.start_publish()

    def disconnect(self, close_code):
        self.stop_publish()

    def start_publish(self):
        self.publishing = True
        self.t = threading.Thread(target=self.publish)
        self.t.start()

    def stop_publish(self):
        self.publishing = False
        self.t.join()

    def publish(self):
        # UDPの設定
        HOST = ''   
        PORT = 4001

        s = socket(AF_INET, SOCK_DGRAM)
        s.bind((HOST, PORT))

        while True:
            # センサーからUDPでデータを受信
            msg, address = s.recvfrom(8192)

            t = int(float(msg.decode('utf-8').split('\t')[0]))
            x = float(msg.decode('utf-8').split('\t')[-1].split(',')[1])
            y = float(msg.decode('utf-8').split('\t')[-1].split(',')[2])
            z = float(msg.decode('utf-8').split('\t')[-1].split(',')[3])

            # Websocketで送信
            if self.publishing == False:
                break
            self.send(text_data=json.dumps([
                {'time': t,'y': x,},
                {'time': t,'y': y,},
                {'time': t,'y': z,},
                ]))

        s.close()

```

`rounting.py`を設定します

```realtime_chart_project/realtime_chart/rounting.py
from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path('ws/', consumers.SensorConsumer.as_asgi()),
]
```

最後にルートのルーティング設定に`realtime_chart.routing`モジュールを追加します`asgi.py``AuthMiddlewareStack``URLRouter``realtime_chart.routing`をインポートし`ProtocolTypeRouter`を以下の形式で`"websocket"`キーに挿入します

```realtime_chart_project/asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import realtime_chart.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'realtime_chart_project.settings')

application = ProtocolTypeRouter({
  "http": get_asgi_application(),
  "websocket": AuthMiddlewareStack(
        URLRouter(
            realtime_chart.routing.websocket_urlpatterns
        )
    ),
})
```

## 可視化の実装
`Epoch.js`を使用するのに必要なファイルを[ダウンロード](https://epochjs.github.io/epoch/)します
ダウンロードしたファイルの中の`css/epoch.min.css``js/epoch.min.js``realtime_chart_project/static/`下に置きます

```
static
├── css
   └── epoch.min.css
└── js
    └── epoch.min.js
```

D3.jsはCDNContent Delivery Network上のものを使用します

```html
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
```

`chart.html`を作成します

```realtime_chart_project/templates/chart.html
<html>
<head>
    <title>graph test</title>
</head>
<body>
    <h1>Real time chart</h1>
    <div id="graph" class="epoch" style="height: 200px;"></div>

    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
    <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
    {% load static %}
    <script type="text/javascript" src="{% static 'js/epoch.min.js' %}"></script>
    <link rel="stylesheet" type="text/css" href="{% static 'css/epoch.min.css' %}">
    <script type="text/javascript">

    const chatSocket = new WebSocket(
        'ws://'
        + window.location.host
        + '/ws/'
    );

    var data = [
        { label: "X-axis", values: [] },
        { label: "Y-axis", values: [] },
        { label: "Z-axis", values: [] },
        ];
    var lineChart = $('#graph').epoch({
        type: 'time.line',
        data: data,
        axes: ['left', 'right', 'bottom'],
    });

    chatSocket.onmessage = function(e) {
        const current = JSON.parse(e.data);
        lineChart.push(current);
    };

    chatSocket.onclose = function(e) {
            console.error('Chat socket closed unexpectedly');
        };

    </script>
</body>
</html>
```

最後に`templates``static`のディレクトリを`settings.py`に追加します

````realtime_chart_project/realtime_chart_project/settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR, 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

STATICFILES_DIRS = (
    [BASE_DIR, 'static']
)
```

# 実行
今回はスマートフォンで観測した3軸加速度データをリアルタイムで可視化するデモを行います

初めにHASC Loggerをスマートフォンにインストールします
HASC Loggerから`[ホストのIP]:4001`へ3軸加速度をUDPで送信します設定の方法は[角速度から回転行列を求める - 実装編](https://qiita.com/yakiimo121/items/1433044a2ca975f0eebe)を参考にしてください

リアルタイムチャートアプリを起動します

```
python manage.py migrate
python manage.py runserver
```

ブラウザから[http://127.0.0.1:8000/realtime_chart](http://127.0.0.1:8000/realtime_chart)へアクセスします

グラフがリアルタイムで表示されたら成功です

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Django ChannelsとEpoch.jsでセンサデータをリアルタイムに表示するチャート作ってみましたDjango Channelsを使用することでHTTPと同じくらい簡単にWebsocketを実装出来ましたEpoch.jsも学習コストが低く使いやすかったです <a href="https://t.co/oflpVucoVk">pic.twitter.com/oflpVucoVk</a></p>&mdash; yakiimo121 (@yakiimo121) <a href="https://twitter.com/yakiimo121/status/1406586719856594949?ref_src=twsrc%5Etfw">June 20, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# まとめ
Django ChannelsとEpoch.jsを用いてリアルタイムチャートを作成しましたDjango Channelsを使用することでHTTPと同じくらい簡単にWebsocketを実装出来ましたEpoch.jsも学習コストが低く使いやすかったです

ただしEpoch.jsでも100Hzのモーションセンサデータをリアルタイムで描画するのは困難でした遅延が発生したりそれにともないキューからデータがこぼれたりしますさすがにWebベースの限界なのでしょうかそれとも他のライブラリを使えば解決できるのでしょうか??もちろん温度データを可視化する場合などではもっとサンプリングレートが低くていいので問題ありません

今後は機能を拡張していきたいと思いますデータの保存機能MQTTへの対応デザインをもっとかっこよくするなどですかね

# 参考
- chart
    - https://qiita.com/shiro-kuma/items/0607e01a19e093fdb631
    - https://4009.jp/post/archives/20
- Websocket
    - https://www.keicode.com/script/html5-websocket-1.php
    - https://pypi.org/project/gevent-websocket/
    - https://pypi.org/project/django-gevent-websocket/
- Django Channels
    - https://docs.djangoproject.com/ja/3.2/intro/tutorial01/
    - https://channels.readthedocs.io/en/latest/tutorial/index.html
    - https://qiita.com/massa142/items/cbd508efe0c45b618b34#groups
    - https://qiita.com/ekzemplaro/items/a6b81bd1d181fdd0cc24
    - http://engmng.blog.fc2.com/blog-entry-110.html
    - https://kivantium.hateblo.jp/entry/2020/05/02/151321
    - https://blog.heroku.com/in_deep_with_django_channels_the_future_of_real_time_apps_in_django
- Epoch
    - https://qiita.com/okoppe8/items/d8d8bc4e68b1da4a0a36
    - https://syncer.jp/d3js
    - http://epochjs.github.io/epoch/
25
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?