2015.11.19
- 人×技術
D3.jsベースのEpochを使ってリアルタイムチャートを描いてみる
こんにちは。情報可視化の学習のために買った本を読み切る前に紛失したイシイです。
今回は、IntelのEdisonのアドオンボードの9 Degrees of Freedom Blockから取得できる9軸(加速度、ジャイロ、地磁気)センサーの値を、Socket通信でデータを受け取ってリアルタイムでグラフ表示させてみようと思います。
背景
今回取り組んでみた背景として、センサーから取得できる値を参照する際に、コンソールログ上では見づらいし、log4jsなどでログをファイル出力してから後で見るより、リアルタイムで値を見て確認した方が効率的と思われたので、リアルタイムチャートを作成してみることにしました。
リアルタイムのグラフ描画にあたって
リアルタイムチャートの描画には、D3.jsベースのEpochというライブラリを使用してみました。リアルタイムチャートのチュートリアルを見つつ、今回は瞬間瞬間の値を見るためのゲージタイプと、ヒストリーを追えるラインチャートタイプを使ってみました。データの流れとしては、9軸センサー → UDP → app.js(Node.js) → Socket通信 → HTML(Epoch) という感じです。今回のエントリーで表示させているのはひとまず取得したデータのうちの加速度の値のみです。
リアルタイムチャートの様子
なにはともあれ、動いてる様子を。
手前側の9軸センサー付きのEdisonを傾けると、それに応じてゲージのメーターが変わっているのがわかります。また、ラインチャートの方では傾けたタイミングから数秒遅れたタイムラインになっていて、ヒストリーデータが表示されています。これで、なんとなくその瞬間の値と、履歴データがほぼリアルタイムで確認できます。
仕組み解説
仕組みについては、上記のデータの流れのうち、app.js以降のみを見ていきます。
app.js
9軸センサーの値をUDPで送るC++のプログラムは、a.outという名前で実行ファイルを生成しています。Nodeで動かしているapp.jsでは、このa.outからUDPで受け取ったデータを各センサー毎にデータ格納しつつ、express.staticを使ってNodeで立てたWebサーバーのpublicディレクトリにあるindex.htmlにセンサーデータをSocket通信で送っています。なお、内部でセンサーデータを取り扱いやすく成形しているedison9dofクラスについては、流れには直接は影響ないので、解説は割愛します。
//UDP from 9DOF Sensor
var PORT = 12345;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
//Keypress Event Setup
var keypress = require('keypress');
keypress(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();
//Graph SetUp
var express = require('express');
var app = express();
var http_server = require('http').Server(app);
var socketio = require('socket.io')(http_server);
http_server.listen(3000);
app.use(express.static(__dirname + '/public'));
//Sensor Value
var Sensor = require('./edison9dof');
var Accel = new Sensor(0,0,0);
var Gyro = new Sensor(0,0,0);
var Mag = new Sensor(0,0,0);
var execFile = require('child_process').execFile,
childProcess;
childProcess = execFile('./9DOF/a.out');
server.on('message', function (message, remote) {
var data = message.toString('utf8').replace(/¥0/g, '').split(":");
Accel.setXYZ(data[1], data[3], data[5]);
Gyro.setXYZ(data[7], data[9], data[11]);
Mag.setXYZ(data[13], data[15], data[17]);
//For Graph
socketio.emit("paramData", {ax:Accel.getX(), ay:Accel.getY(), az:Accel.getZ(), gx:Gyro.getX(), gy:Gyro.getY(), gz:Gyro.getZ(), mx:Mag.getX(), my:Mag.getY(), mz:Mag.getZ()});
});
server.bind(PORT, HOST);
// Keypress Event
process.stdin.on('keypress', function (ch, key) {
if (key.name === 'x') {
childProcess.kill();
process.stdin.pause();
process.exit();
}
});
index.html
http://EdisonのIPアドレス:3000でブラウザアクセスすると表示されるように設定したindex.htmlでは、app.jsからSocket通信で受け取った9軸センサーのデータ(加速度:Ax〜Az,ジャイロ:Gx〜Gz,地磁気:Mx〜Mz)を1秒ごとに取得して各チャートのデータ配列に突っ込み、描画を行っています。
<!DOCTYPE html>
<html>
<head>
<title>Socket通信で受け取ったデータのリアルタイムチャート</title>
<script type="text/javascript" src="./jquery-2.1.4.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="./d3.min.js"></script>
<script src="./epoch.min.js"></script>
<script src="./data.js"></script>
<link rel="stylesheet" type="text/css" href="./epoch.min.css">
</head>
<body>
<style>
.gauges .epoch {
display: inline-block;
}
</style>
<h3 id="gauge" style="margin-left: 10px">Accelerater Gauge</h3>
<div class="gauges">
<div id="gauge-ax" class="epoch gauge-medium"></div>
<div id="gauge-ay" class="epoch gauge-medium"></div>
<div id="gauge-az" class="epoch gauge-medium"></div>
</div>
<h3 id="line" style="margin-left: 10px">Accelerater Line</h3>
<div id="timeline-ax" class="epoch"></div>
<div id="timeline-ay" class="epoch"></div>
<div id="timeline-az" class="epoch"></div>
<script>
var socketData = {Ax:0, Ay:0, Az:0, Gx:0, Gy:0, Gz:0, Mx:0, My:0, Mz:0};
//Gause Chart Run
$(function() {
var data = new GaugeData();
var accelProperty = { type: 'time.gauge', value: 0, format: function(v) { return Math.abs((v*100).toFixed(2)) + '%'; } };
var charts = [
$('#gauge-ax').epoch(accelProperty), $('#gauge-ay').epoch(accelProperty), $('#gauge-az').epoch(accelProperty),
];
function updateCharts() {
var c = 0;
for (var pname in socketData) {
charts[c].update(Math.abs(socketData[pname]));
c++;
if(c >= charts.length) {
return;
}
}
}
setInterval(updateCharts, 1000);
});
//Line Chart Run
$(function() {
var data = new RealTimeData(1);
var dataProperty = {type: 'time.line', ticks: { time: 10, right: 5, left: 5 }, margins: { top: 10, right: 50, bottom: 30, left: 50 }, height: 150, data: data.history(60), axes: ['left', 'bottom', 'right']};
var charts = [
$('#timeline-ax').epoch(dataProperty), $('#timeline-ay').epoch(dataProperty), $('#timeline-az').epoch(dataProperty),
];
function updateCharts() {
var c = 0;
for (var pname in socketData) {
charts[c].push(data.next(socketData[pname]));
c++;
if(c >= charts.length) {
data.nextFrame();
return;
}
}
}
setInterval(updateCharts, 1000);
});
//Socket Data Receive
$(function() {
var url = "xxx.xxx.xxx.xxx:3000";
var socket = io.connect(url);
socket.on("paramData", function (d) {
socketData = {Ax:d.ax, Ay:d.ay, Az:d.az, Gx:d.gx, Gy:d.gy, Gz:d.gz, Mx:d.mx, My:d.my, Mz:d.mz};
});
});
</script>
</body>
</html>
data.js
data.jsは、サンプルコードの作りだとラインチャートを複数表示させようとしたときに、それぞれのタイムラインが約1秒づつズレてしまう仕組みになってしまっていて、今回は各センサー値のX軸・Y軸・Z軸データは同一タイミングのタイムラインで縦に並べたかったのでそこを修正しつつ、表示させるデータがサンプル用のランダム値だったところも変更しています。
(function() {
/*
* Class for generating real-time data for the area, line, and bar plots.
*/
var RealTimeData = function(layers) {
this.layers = layers;
this.timestamp = ((new Date()).getTime() / 1000)|0;
this.start_timestamp = 0;
};
RealTimeData.prototype.history = function(entries) {
var history = [];
for (var k = 0; k < this.layers; k++) {
history.push({ values: [] });
}
this.start_timestamp = this.timestamp;
var t = this.start_timestamp;
for (var i = 0; i < entries; i++) {
for (var j = 0; j < this.layers; j++) {
history[j].values.push({time: t, y: 0});
}
t++;
}
return history;
};
RealTimeData.prototype.next = function(v) {
var entry = [];
for (var i = 0; i < this.layers; i++) {
entry.push({ time: this.timestamp, y: v});
}
return entry;
}
RealTimeData.prototype.nextFrame = function() {
this.timestamp++;
return ;
}
window.RealTimeData = RealTimeData;
/*
* Gauge Data Generator.
*/
var GaugeData = function() {};
window.GaugeData = GaugeData;
})();
感想
今回は参考としてEdisonの9軸センサーの値を使いましたが、本家のD3.jsに比べて痒いところに手は届かないものの、簡易的(といってもdata.jsをいじることになり、やや量は増えてしまいましたが)にリアルタイムチャートを使うことができるので、リアルタイムにグラフ化させたりする他、用途に応じてデバッグシーンなどでも使えそうです。