はじめに
あるとき、モーションセンサデータをリアルタイムで可視化したいなと思うことがありました。そこで、ネットでリアルタイム可視化ツールについて調べてみると、近年の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リクエストの処理はHTTP(VIEW
)が、センサーから送られてくるデータをブラウザへ送信する処理はWebsocket(COUSUMER
)が実行します。
センサとの通信
センサデータは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.py
のINSTALLED_APPS
に'realtime_chart'
を追加します。
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
を呼び出してリクエストを処理します。
from django.urls import path
from . import views
urlpatterns = [
path('', views.chart, name='chart'),
]
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
を返す関数を定義します。
from django.shortcuts import render
def chart(request):
return render(request, 'chart.html', {})
chart.html
の中身は可視化の部分で説明します。
Websocketの実装(Django Channelsの設定)
settings.py
のINSTALLED_APPS
に'channels'
を追加します。
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
に追記します。
# Channels
ASGI_APPLICATION = 'realtime_chart_project.asgi.application'
consumers.py
を実装します。
ここではUDPでセンサからデータを受け取り、Websocketでブラウザにデータを送信する処理が行われています。
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
を設定します。
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"
キーに挿入します。
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.css
とjs/epoch.min.js
をrealtime_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
を作成します。
<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
に追加します。
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も学習コストが低く使いやすかったです。 pic.twitter.com/oflpVucoVk
— yakiimo121 (@yakiimo121) June 20, 2021
まとめ
Django ChannelsとEpoch.jsを用いてリアルタイムチャートを作成しました。Django Channelsを使用することで、HTTPと同じくらい簡単にWebsocketを実装出来ました。Epoch.jsも学習コストが低く使いやすかったです。
ただし、Epoch.jsでも100Hzのモーションセンサデータをリアルタイムで描画するのは困難でした。遅延が発生したり、それにともないキューからデータがこぼれたりします。さすがにWebベースの限界なのでしょうか?それとも他のライブラリを使えば解決できるのでしょうか??もちろん、温度データを可視化する場合などでは、もっとサンプリングレートが低くていいので問題ありません。
今後は機能を拡張していきたいと思います。データの保存機能、MQTTへの対応、デザインをもっとかっこよくするなどですかね。
参考
- chart
- 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