はじめに
あるとき、モーションセンサデータをリアルタイムで可視化したいなと思うことがありました。そこで、ネットでリアルタイム可視化ツールについて調べてみると、近年の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`を呼び出してリクエストを処理します。
```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はCDN(Content 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>— 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/