LoginSignup
25
25

More than 1 year has 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.pyINSTALLED_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で、AuthMiddlewareStackURLRouterrealtime_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を使用するのに必要なファイルをダウンロードします。
ダウンロードしたファイルの中のcss/epoch.min.cssjs/epoch.min.jsrealtime_chart_project/static/下に置きます。

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

D3.jsはCDN(Content Delivery Network)上のものを使用します。

<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>

最後に、templatesstaticのディレクトリを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で送信します。設定の方法は「角速度から回転行列を求める - 実装編」を参考にしてください。

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

python manage.py migrate
python manage.py runserver

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

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

まとめ

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

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

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

参考

25
25
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
25