Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

Spread.ViewsとRaspberry Pi+ESP32搭載ボードNefry BTでインフルエンザ対策のBIツールを作る

IoT時代の救世主! SpreadJSで作るデータ可視化アプリ 第3回

  • LINEで送る
  • このエントリーをはてなブックマークに追加

 本連載では、グレープシティが開発するJavaScriptライブラリ「SpreadJS」の 収録コントロール「Spread.Views」を活用して、IoT時代に役立つさまざまなアプリケーションを作っていきます。今回は、Spread.Viewsのスパークライン機能を使って、オフィス内の温湿度データなどを可視化するインフルエンザ対策BIツールを作ってみます。

 こんにちは、dotstudio株式会社n0bisukeです。前回はSpread.ViewsのカレンダーUIを利用して、出退勤の管理システムを作成しました。今回はIoTデータのモニタリングを行うためのBIツールを作成してみます。

IoT界隈のBIツール

 データが大事ということは色々な業界で言われ続けていることです。IoTの世界で何が嬉しいかというと、世の中の今まで誰も取ろうとしてなかったデータすらも低コストでセンサリングできるようになったことが挙げられます。

 その結果、これまでに無いくらいの膨大なデータが集まりやすくなりました。膨大なデータをそのまま扱おうとすると、人間にとって見づらく扱いにくいものとなってしまいます。この流れから最近では、センサリングしたデータの可視化に注目が集まっています。

 自分で可視化ツールを作成するのも良いですが、開発コストが大きいため、簡単に扱えるWebサービスも増えてきています。

  • freeboard
  • ThingSpeak
  • Fastsensing
  • Ambient

 このようなサービスの需要があるようにBIツール市場は注目されています。

 一方で、既存のWebサービスだと機能が少ない、ユーザーにとって必要な表示機能が足りていない、カスタマイズが難しいなどといった側面がありました。

 今回はカスタマイズ自在な自前のBIツールをSpread.Viewsを利用して素早く作っていきます。

どのような可視化にするか

 どんなものを作るかイメージするときは既存のUIを見て、自分が作りたいものに近しい例がないか確認すると良いでしょう。その点で Spread.Viewsはデモが豊富に揃っているのが嬉しいです。Spread.Viewsのデモサイトをチェックしてみましょう。デモサイトの左上のメニューを開くとさまざまなデモが確認できます。

 今回はスパークラインのサンプルがデータ可視化に良さそうだったので、このサンプルとデータを組み合わせてみたいと思います。

BIツールの設計(どんなBIツールにするか)

 まずはどんなBIツールを作るのかを考えます。

 今回は最近オフィスが乾燥して風邪が流行ってきているので、オフィスの環境データをモニタリングするインフルエンザ対策BIツールを作成します。

  • 温度のモニタリング
  • 湿度のモニタリング
    • 湿度が低かったら加湿器をつけるように促す
  • CO2濃度のモニタリング
    • CO2濃度が高ければ窓を開けるように促す

利用アーキテクチャ

 社内向けのシステムかつ、社内にいる人がみるツールとなるため、公開サーバーにはせず、Raspberry Pi上のローカルでホスティングします。また、Raspberry PiにはNode.jsをインストールしてMQTTのブローカー(サーバー)にします。

 センサーの値はESP32搭載のマイコンボードNefry BTを利用し、MQTTのPublisherとして実装します。

  • BIサーバー:Raspberry Pi 3
    • Node.js v9.4.0
    • MQTT ブローカー
    • Spread.Views
  • エージェント: Nefry BT
    • 温度湿度センサー
    • Co2センサー
    • MQTT Publiser

Node.jsでサーバー構築

 それでは実際に作っていきます。まずは、Node.jsでWebアプリを作るにあたり一般的なフレームワークであるExpressを使ってWebサーバーを構築します。

 ただ作るのだと面白みがないので、今回はチャレンジ要素としてExpressの5系を導入してみました。

 執筆時点(2018年1月)ではまだα版ですが、今後5系がデファクトになる将来を見越して使ってみます。実運用で利用する人は4系を使った方が安心できると思います。

mkdir ds_office_bi
cd ds_office_bi
npm init -y
npm i --save express@5.0.0-alpha.3
touch server.js

 server.jsに以下を記述します。

server.js
`use strict`;

const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Hello World');
});

app.listen(3000);

 サーバーを起動してみます。

node server.js

 http://localhost:3000/にアクセスするとHello Worldと表示されます。

フロント側の静的ファイルの準備

 今のままでは文字列を返すだけのWebサイトなので、ここにSpread.Viewsのコードをホスティングしていきましょう。

 まずはindex.htmlを作成します。

index.html
<html>
    <head>
        <meta charset="utf-8">
        <title>dotstudioオフィス環境のBIツール</title>
    </head>
    <body>
        <h1>dotstudioオフィス環境のBIツール</h1>
    </body>
</html>

 server.jsなどと同じ階層に作成するのでこのような構成になります。

$ ls

index.html        package-lock.json
node_modules      package.json      server.js

 次にserver.jsのres.send()res.sendFile()に変更します。

 res.sendFile()メソッドではindex.htmlのパスを指定するとindex.htmlの中身を展開してくれます。

server.js
`use strict`;

const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
})

app.listen(3000);

 ちなみにExpress4系まではres.sendfile()でしたが、5系ではres.sendFile()とキャメルケースに変更されるようです。

 server.jsを修正したら、Ctrl+cでサーバーを落としてから再度node server.jsコマンドでサーバーを起動しましょう。

 http://localhost:3000にアクセスするとindex.htmlの内容が表示されていることを確認できます。

 次にCSSの利用です。publicフォルダを作成してそこに静的ファイルを設置します。

mkdir public
mkdir public/css
mkdir public/js
touch public/css/main.css

 publicフォルダ内にcssとjsフォルダ、cssフォルダ内にmain.cssファイルを作成しました。

 main.cssには確認用に背景を青くするコードを記述します。

public/css/main.css
body{
    background-color: blue
}

 index.html側では/css/main.cssを指定して呼び出します。

index.html
<html>
    <head>
        <meta charset="utf-8">
        <title>dotstudioオフィス環境のBIツール</title>
        <link rel="stylesheet" href="/css/main.css">
    </head>
    <body>
        <h1>dotstudioオフィス環境のBIツール</h1>
    </body>
</html>

 サーバー側ではapp.use(express.static(__dirname + '/public'));を利用して、静的サイトはpublicフォルダを参照する形にします。

server.js
`use strict`;

const express = require('express');
const app = express();

app.use(express.static(__dirname + '/public'));
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});

app.listen(3000);

 これで下準備は完了です。

Spread.Viewsのコードをホスティング

 index.htmlを以下に書き換えます。パスを少し編集した程度で基本はサンプルコードのままです。

index.html
<!doctype html>
<html style="height:100%;font-size:14px;">

<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" type="text/css" href="css/gc.spread.views.dataview.10.3.0.css">
    <script src="js/gc.spread.common.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.dataview.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.dataview.locale.ja-JP.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.gridlayout.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.sparkline.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/license.js" type="text/javascript"></script>
    <script src="js/sales_details_data.js" type="text/javascript"></script>
    <style>
        * {
            -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
        }
    </style>
</head>

<body style="margin:0;position:absolute;top:0;bottom:0;left:0;right:0;font-size:14px;user-select:none;-webkit-user-select: none;overflow:hidden;">
    <div id="grid1" style="height:100%"></div>
    <script type="text/javascript">
        var standard = 3000;

        function renderWinlosssparkline(data, container) {
            var newData = [];
            newData.push(
                data.may - standard,
                data.june - standard,
                data.july - standard,
                data.aug - standard,
                data.sept - standard,
                data.oct - standard
            );
            var winlossSparkline = new GC.Spread.Views.Plugins.Sparkline.WinlossSparkline({
                values: newData,
                setting: {
                    axisColor: '#0C0A3E',
                    markersColor: '#FED766',
                    negativeColor: '#FED766',
                    seriesColor: '#995D81',
                    displayXAxis: true,
                    showFirst: true,
                    showHigh: true,
                    showLast: true,
                    showLow: true,
                    showNegative: true,
                    showMarkers: true
                }
            });
            winlossSparkline.paint(container);
        }

        var lineSparklineSetting = {
            highMarkerColor: '#995D81',
            lowMarkerColor: '#FED766',
            markersColor: 'black',
            seriesColor: '#009FB7',
            showMarkers: true,
            showHigh: true,
            showLow: true,
            lineWeight: 2,
            minAxisType: 2,
            maxAxisType: 2,
            manualMax: 25000,
            manualMin: 0
        };
        var columnSparklineSettings = {
            highMarkerColor: '#995D81',
            lowMarkerColor: '#FED766',
            markersColor: 'black',
            seriesColor: '#009FB7',
            showMarkers: true,
            showHigh: true,
            showLow: true,
            minAxisType: 2,
            maxAxisType: 2,
            manualMax: 25000,
            manualMin: 0
        };
        var lineSparklineFormula = '=LINESPARKLINE("may,june,july,aug,sept,oct", "", "' + JSON.stringify(lineSparklineSetting).replace(/\"/g, '') + '")';
        var columnSparklineFormula = '=COLUMNSPARKLINE("may,june,july,aug,sept,oct", "", "' + JSON.stringify(columnSparklineSettings).replace(/\"/g, '') + '")';
        var smallDevice = screen.width <= 480;
        var colWidthSmall = smallDevice ? 125 : '*';
        var colWidthLarge = smallDevice ? 125 : '2*';

        var columns = [{
            id: 'salesPerson',
            caption: 'Sales Person',
            dataField: 'Salesperson',
            width: colWidthSmall
        }, {
            id: 'may',
            caption: 'May',
            dataField: 'May',
            dataType: 'number',
            format: '$#,##'
        }, {
            id: 'june',
            caption: 'June',
            dataField: 'June',
            dataType: 'number',
            format: '$#,##'
        }, {
            id: 'july',
            caption: 'July',
            dataField: 'July',
            dataType: 'number',
            format: '$#,##'
        }, {
            id: 'aug',
            caption: 'Aug.',
            dataField: 'Aug',
            dataType: 'number',
            format: '$#,##'
        }, {
            id: 'sept',
            caption: 'Sept.',
            dataField: 'Sept',
            dataType: 'number',
            format: '$#,##'
        }, {
            id: 'oct',
            caption: 'Oct.',
            dataField: 'Oct',
            dataType: 'number',
            format: '$#,##'
        }, {
            id: 'trend',
            caption: 'LineSparkline',
            width: colWidthLarge,
            dataField: lineSparklineFormula
        }, {
            id: 'sales',
            caption: 'ColumnSparkline',
            width: colWidthLarge,
            dataField: columnSparklineFormula,
            visible: !smallDevice
        }, {
            id: 'winloss',
            caption: 'WinlossSparkline',
            width: colWidthLarge,
            asyncRender: renderWinlosssparkline,
            visible: !smallDevice
        }, ];

        dataView = new GC.Spread.Views.DataView(document.getElementById('grid1'), data, columns,
            new GC.Spread.Views.Plugins.GridLayout({
                rowHeight: 32,
                allowAsyncRender: isTouchDevice()
            }));

        function isTouchDevice() {
            return window.PointerEvent || window.MSPointerEvent || ('ontouchstart' in document.documentElement);
        }

        //focus data.view by default
        document.querySelector('#grid1').focus();
    </script>
</body>

</html>

 利用するJSファイルとCSSファイルをpublic/jspublic/cssに設置します。

 ファイルは以下となっています。

  • gc.spread.views.dataview.10.3.0.css
  • gc.spread.common.10.3.0.min.js
  • gc.spread.views.dataview.10.3.0.min.js
  • gc.spread.views.dataview.locale.ja-JP.10.3.0.min.js
  • gc.spread.views.gridlayout.10.3.0.min.js
  • gc.spread.views.sparkline.10.3.0.min.js
  • license.js
  • sales_details_data.js

 あらかじめ前回までの記事を参考に、Spread.Viwsのファイルをダウンロードしておきましょう。

 先ほどと同様にhttp://localhost:3000にアクセスすると、表とグラフが表示されます。

 サンプルが優秀でコピペだけでできてしまいましたね。

MQTTブローカーの実装

 ここまででそれっぽいUIを作ることができました。次にセンサーデバイスとの連携をMQTTを利用して実装します。

 語弊を恐れずにいうと、MQTTはデバイスとデバイスでリアルタイムに情報を送受信できるプロトコルです。

 MQTTは「Nefry BT(ESP32)でMQTTを使ってみよう Subscribe編」で紹介しているように、ブローカー(Broker)と呼ばれるサーバーの役割が必要です。

 MQTTブローカーはNode.jsでも実装できます。

 今回はMoscaというライブラリを利用してMQTTブローカーを立てます。

npm i --save mosca

 server.jsにMQTTブローカーの記述を追記します。

server.js
'use strict';

const express = require("express");
const http = require('http');
const mosca = require('mosca');

const HTTP_PORT = 3000;
const MQTT_PORT = 1883;

const app = express();
const httpServer = http.createServer(app);
const broker = new mosca.Server({port: MQTT_PORT});

broker.on('ready', () => console.log('Server is ready.'));
broker.on('clientConnected', client => console.log('broker.on.connected.', 'client:', client.id));
broker.on('clientDisconnected', client => console.log('broker.on.disconnected.', 'client:', client.id));
broker.on('subscribed', (topic, client) => console.log('broker.on.subscribed.', 'client:', client.id, 'topic:', topic));
broker.on('unsubscribed', (topic, client) => console.log('broker.on.unsubscribed.', 'client:', client.id));
broker.on('published', (packet, client) => {
    if (/\/new\//.test(packet.topic))return;
    if (/\/disconnect\//.test(packet.topic))return;
    console.log('broker.on.published.', 'client:', client.id);
});

app.use(express.static(__dirname + '/public'));
app.get('/',(req,res) => res.sendFile(__dirname + '/index.html'));

broker.attachHttpServer(httpServer);
httpServer.listen(HTTP_PORT);

 実行してみましょう。

$ node server.js
Server is ready.

 エラーが出なければ問題無いです。

 broker.on('published')のコールバック関数の箇所がエッジデバイスからデータが送信されたら反応します。

エージェント(Nefry BT)側の配線

 今回はGroveの温湿度センサーを利用し、温度と湿度を測ります。また、CO2濃度を測るためにMH-Z19センサーを利用します。

 Grove温湿度センサーはNefry BTのGroveコネクタD2に接続し、MH-Z19センサーはGND、Vin、Tx、RxをそれぞれNefry BTのGND、5v、D3、D4に配線しましょう。

エージェント(Nefry BT)側の実装

 Nefry BTでエージェント側の実装をします。Nefry BTでプログラムを書いて利用開始するまでの手順はNefry BTの始め方を参照して下さい。

 今回はセンサリングした値をMQTTで送信するPublisherプログラムです。

 Arduino向けのMQTTライブラリをknolleary/pubsubclientからインストールします。

 以下のコードを書き込めば接続できますが、ブローカーのIPアドレスが必要となるのでRaspberry Pi上でifconfigなどのコマンドを実行し、IPアドレスを調査し、#define URLの箇所に適宜記載して下さい。

publish.ino
#include <Nefry.h>
#include <PubSubClient.h>

WiFiClient httpsClient;
PubSubClient mqttClient(httpsClient);

#define TOPIC "n0bisuke"
#define QOS 0
#define URL "192.168.179.2" //Raspberry PiのIPアドレスを指定
#define PORT 1883

#include "DHT.h"
#define DHTPIN D2     // what digital pin we're connected to
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

HardwareSerial Serial1(1);
byte cmd_conc[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
byte cmd_zero[9] = {0xFF,0x01,0x87,0x00,0x00,0x00,0x00,0x00,0x78};
byte res[9];
int  co2 = 0;

void callback(char* topic, byte* payload, unsigned int length);
void errorReport();
bool verify(byte packet[]);

void setup() {
  Nefry.enableSW();
  Nefry.setLed(0, 0, 0);

  mqttClient.setServer(URL, PORT);
  mqttClient.setCallback(callback);

  Serial.begin(115200);
  Serial.println("DHTxx test!");
  dht.begin();

  // Serial1 RX : Nefry D3 (ESP32 IO19) --- CO2 Sensor TX
  // Serial1 TX : Nefry D4 (ESP32 IO18) --- CO2 Sensor RX
  Serial1.begin(9600, SERIAL_8N1, 19, 18);
  delay(10*1000);
}

void loop() {
  delay(2000);

  // Calibration Co2 sensor
  if (Nefry.readSW()) {
    Serial1.write(cmd_zero, 9);
    Serial.println("---Calibration---");
    delay(100);
  }

  /**
   * 温度湿度
   */
  float h = dht.readHumidity(); //湿度
  float t = dht.readTemperature(); //温度
  if (isnan(h) || isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    return;
  }

  /**
   * co2
   */
  // Send command to get co2 concentration
  Serial1.write(cmd_conc, 9);
  delay(10);
  // Receive packet
  if (Serial1.readBytes(res, 9) == 9) {
    // verify checksum
    if (verify(res)) {
      /*
      * 012345678
      * SCHL----ck
      */
      int resHigh = (int)res[2];
      int resLow  = (int)res[3];
      co2 = (resHigh<<8) + resLow;
    } else {
      // invalid packet
      co2 = -1;
    }
  } else {
    // received strange length of bytes
  }

  /**
   * MQTT
   */
  if(!mqttClient.connected()) {
    if (mqttClient.connect(TOPIC)) {
        Serial.println("MQTT Connected.");
    }
    else {
      errorReport();
    }
  }else{
    char buffer[70];
    sprintf(buffer, "{\"humidity\":%f,\"temperature\":%f,\"co2\":%d}", h, t, co2);
    mqttClient.publish(TOPIC, buffer);
    Serial.println("Published");
  }

  /**
   * 表示
   */
  Serial.print("Humidity: ");
  Serial.print(h);
  Serial.print(" %\t");
  Serial.print("Temperature: ");
  Serial.print(t);
  Serial.print(" C\t");
  Serial.print("CO2: ");
  Serial.println(co2);

  mqttClient.loop();
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
}

void errorReport(){
  Serial.print("Failed. Error state = ");

  switch (mqttClient.state()) {
    case MQTT_CONNECT_UNAUTHORIZED:
      Serial.println("MQTT_CONNECT_UNAUTHORIZED");
      break;
    case MQTT_CONNECT_BAD_CREDENTIALS:
      Serial.println("MQTT_CONNECT_BAD_CREDENTIALS");
      break;
    case MQTT_CONNECT_UNAVAILABLE:
      Serial.println("MQTT_CONNECT_UNAVAILABLE");
      break;
    case MQTT_CONNECT_BAD_CLIENT_ID:
      Serial.println("MQTT_CONNECT_BAD_CLIENT_ID");
      break;
    case MQTT_CONNECT_BAD_PROTOCOL:
      Serial.println("MQTT_CONNECT_BAD_PROTOCOL");
      break;
    case MQTT_CONNECTED:
      Serial.println("MQTT_CONNECTED");
      break;
    case MQTT_DISCONNECTED:
      Serial.println("MQTT_DISCONNECTED");
      break;
    case MQTT_CONNECT_FAILED:
      Serial.println("MQTT_CONNECT_FAILED");
      break;
    case MQTT_CONNECTION_LOST:
      Serial.println("MQTT_CONNECTION_LOST");
      break;
    case MQTT_CONNECTION_TIMEOUT:
      Serial.println("MQTT_CONNECTION_TIMEOUT");
      break;
  }

  delay(5000); // Wait 5 seconds before retrying
}

bool verify(byte packet[]) {
  byte checksum = 0;
  for (int i=1; i<8; i++) {
    checksum += packet[i];
  }
  checksum = 0xFF - checksum;
  checksum += 0x01;
  if (packet[8] == checksum) {
    return true;
  } else {
    return false;
  }
}

 Arduino IDEからNefry BTに書き込めば完成ですが、Nefry BTとRaspberry Piは同じWi-Fiに接続していることを確認しましょう。

 これでデータの確認をしてみましょう。

 問題なければ2秒おきのデータがNode.jsのブローカー側で確認できます。

 ちなみにFailed to read from DHT sensor!と表示されてうまく湿度の値が取れてない箇所がありますが、これは利用しているセンサー(DHT11)の対応範囲が湿度20%~90%となっており、湿度が下回りすぎて上手くデータが取得できていないという衝撃的な事実でした。

 dotstudioオフィスが乾燥しすぎなので加湿器を増やそうと思います。

ブラウザへのデータのつなぎこみ

 MQTT.jsを利用して、Brokerに送られたデータをブラウザ側で受け取ります。クライアントサイドにSocket.ioの接続部分を追記します。

index.html
・
省略
・
・
    <script src="https://unpkg.com/mqtt@2.15.1/dist/mqtt.min.js"></script>
    <script>
        const client = mqtt.connect();
        client.subscribe("n0bisuke");

        client.on('message', (topic, payload) => {
           console.log(topic,payload.toString('utf-8'));
        });
    </script>
・
・
省略
・

 ブラウザで確認してみると、ブラウザのコンソールにセンサーの値が届いていることが分かります。

Spread.Viewsへのデータのつなぎこみ

 読み込んでいたsales_details_data.jsに以下のようなデータが入っています。

var data =[{"Salesperson":"Albertson, Kathy","May":"3947","June":"557","July":"3863","Aug":"1117","Sept":"8237","Oct":"8690"},{"Salesperson":"Allenson, Carol","May":"4411","June":"1042","July":"9355","Aug":"1100","Sept":"10185","Oct":"18749"},
・
・
(省略)
・

 このdata変数の形式に合わせて湿度、温度、CO2濃度のデータを入れ込みます。

index.html
<!doctype html>
<html style="height:100%;font-size:14px;">

<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" type="text/css" href="css/gc.spread.views.dataview.10.3.0.css">
    <script src="js/gc.spread.common.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.dataview.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.dataview.locale.ja-JP.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.gridlayout.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/gc.spread.views.sparkline.10.3.0.min.js" type="text/javascript"></script>
    <script src="js/license.js" type="text/javascript"></script>
    <script src="js/sales_details_data.js" type="text/javascript"></script>
    <script src="https://unpkg.com/mqtt@2.15.1/dist/mqtt.min.js"></script>
    <script>
        data = [
            {
                "環境データ":"湿度%",
                "6秒前":"75",
                "5秒前":"40",
                "4秒前":"23",
                "3秒前":"32",
                "2秒前":"51",
                "1秒前":"15"
            },
            {
                "環境データ":"温度℃",
                "6秒前":"75",
                "5秒前":"40",
                "4秒前":"23",
                "3秒前":"32",
                "2秒前":"51",
                "1秒前":"15"
            },
            {
                "環境データ":"CO2濃度_(×0.01)",
                "6秒前":"7.5",
                "5秒前":"4.0",
                "4秒前":"2.3",
                "3秒前":"3.2",
                "2秒前":"5.1",
                "1秒前":"1.5"
            },
        ];

        let humidity = [];
        let temperature = [];
        let co2 = [];

        const client = mqtt.connect();
        client.subscribe("n0bisuke");
        client.on('message', (topic, payload) => {
            console.log(topic,payload.toString('utf-8'));
            const json = JSON.parse(payload.toString('utf-8'));

            humidity.unshift(json.humidity);
            data[0]["6秒前"] = humidity[5];
            data[0]["5秒前"] = humidity[4];
            data[0]["4秒前"] = humidity[3];
            data[0]["3秒前"] = humidity[2];
            data[0]["2秒前"] = humidity[1];
            data[0]["1秒前"] = humidity[0];

            temperature.unshift(json.temperature);
            data[1]["6秒前"] = temperature[5];
            data[1]["5秒前"] = temperature[4];
            data[1]["4秒前"] = temperature[3];
            data[1]["3秒前"] = temperature[2];
            data[1]["2秒前"] = temperature[1];
            data[1]["1秒前"] = temperature[0];

            co2.unshift(json.co2 * 0.01); //値の範囲を調整
            data[2]["6秒前"] = co2[5];
            data[2]["5秒前"] = co2[4];
            data[2]["4秒前"] = co2[3];
            data[2]["3秒前"] = co2[2];
            data[2]["2秒前"] = co2[1];
            data[2]["1秒前"] = co2[0];

            dataView.invalidate();
            dataView = new GC.Spread.Views.DataView(
                document.getElementById('grid1'), 
                data,
                columns,
                new GC.Spread.Views.Plugins.GridLayout({
                    rowHeight: 100,
                    allowAsyncRender: isTouchDevice()
                })
            );

            const $humidityDom = document.getElementsByClassName("r0")[0];
            if(json.humidity < 30){
                $humidityDom.style.color = "red";
            }else{
                $humidityDom.style.color = "black";
            }
        });

    </script>
    <style>
        * {
            -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
        }
    </style>
</head>

<body style="margin:0;position:absolute;top:0;bottom:0;left:0;right:0;font-size:14px;user-select:none;-webkit-user-select: none;overflow:hidden;">
    <div id="grid1" style="height:100%"></div>
    <script>
        const lineSparklineSetting = {
            highMarkerColor: '#995D81',
            lowMarkerColor: '#FED766',
            markersColor: 'black',
            seriesColor: '#009FB7',
            showMarkers: true,
            showHigh: true,
            showLow: true,
            lineWeight: 2,
            minAxisType: 2,
            maxAxisType: 2,
            manualMax: 100,
            manualMin: 0
        };
        const columnSparklineSettings = {
            highMarkerColor: '#995D81',
            lowMarkerColor: '#FED766',
            markersColor: 'black',
            seriesColor: '#009FB7',
            showMarkers: true,
            showHigh: true,
            showLow: true,
            minAxisType: 2,
            maxAxisType: 2,
            manualMax: 100,
            manualMin: 0
        };
        const lineSparklineFormula = '=LINESPARKLINE("6秒前,5秒前,4秒前,3秒前,2秒前,1秒前", "", "' + JSON.stringify(lineSparklineSetting).replace(/\"/g, '') + '")';
        const columnSparklineFormula = '=COLUMNSPARKLINE("6秒前,5秒前,4秒前,3秒前,2秒前,1秒前", "", "' + JSON.stringify(columnSparklineSettings).replace(/\"/g, '') + '")';
        const smallDevice = screen.width <= 480;
        const colWidthSmall = smallDevice ? 125 : '*';
        const colWidthLarge = smallDevice ? 125 : '2*';

        const columns = [
            {
                id: '環境データ',
                caption: '環境データ',
                dataField: '環境データ',
                width: colWidthSmall
            }, {
                id: '6秒前',
                caption: '6秒前',
                dataField: '6秒前',
                dataType: 'number',
                format: '#.##'
            }, {
                id: '5秒前',
                caption: '5秒前',
                dataField: '5秒前',
                dataType: 'number',
                format: '#.##'
            }, {
                id: '4秒前',
                caption: '4秒前',
                dataField: '4秒前',
                dataType: 'number',
                format: '#.##'
            }, {
                id: '3秒前',
                caption: '3秒前.',
                dataField: '3秒前',
                dataType: 'number',
                format: '#.##'
            }, {
                id: '2秒前',
                caption: '2秒前.',
                dataField: '2秒前',
                dataType: 'number',
                format: '#.##'
            }, {
                id: '1秒前',
                caption: '1秒前.',
                dataField: '1秒前',
                dataType: 'number',
                format: '#.##'
            }, {
                id: 'trend',
                caption: 'LineSparkline',
                width: colWidthLarge,
                dataField: lineSparklineFormula
            }, {
                id: 'sales',
                caption: 'ColumnSparkline',
                width: colWidthLarge,
                dataField: columnSparklineFormula,
                visible: !smallDevice
            },
        ];

        const isTouchDevice = () => window.PointerEvent || window.MSPointerEvent || ('ontouchstart' in document.documentElement);

        dataView = new GC.Spread.Views.DataView(
            document.getElementById('grid1'), 
            data,
            columns,
            new GC.Spread.Views.Plugins.GridLayout({
                rowHeight: 100,
                allowAsyncRender: isTouchDevice()
            })
        );

        //focus data.view by default
        document.querySelector('#grid1').focus();
    </script>
</body>

</html>

 データは配列データになっていたので、その形式に合わせる形で作っています。

 データはデバイスごとに送信されてきてco2humidity-temperatureなどのトピック名ごとにイベントが発火します。データが送られて来たタイミングでグラフは再描画をし、閾値よりも湿度が低ければ赤い文字にしてアラートを出します。

動作の確認

 それでは実際にブラウザを確認してみます。

 このような感じで、温度と湿度、CO2濃度の値をリアルタイムに表示し、アラートするBIツールを作ることができました。

まとめ

 いかがでしたでしょうか。Spread.Viewsを利用することでIoTデータの可視化を行うことが出来ました。

 BIツールはオンラインサービスが多いですが、細かいところまで機能が充実していない、かといって一からグラフなどの実装を自分たちでやるのはタスクが多すぎる。そういった場合にSpread.Viewsを使って自作BIツールを作るのは選択肢としてはアリだと思います。

 また今回は分かりやすさを優先し簡単なデータ例でご紹介しましたが、使用電気料、物流データ、入退出データなどを利用したBIツールにもカスタマイズできます。

 皆さんも是非Spread.Viewsを使ってお手軽データ可視化を試してみて下さい。

 それでは!

  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 菅原のびすけ(dotstudio株式会社)(スガワラ ノビスケ)

     日本最大規模のIoTコミュニティ「IoTLT」主催。岩手県立大学大学院ソフトウェア情報学研究科を卒業後、株式会社LIGでWebエンジニアとして入社し、Web開発に携わる。2016年にdotstudio株式会社を立ち上げ、今はIoT領域を中心に活動している。JavaScript Roboticsコミ...

All contents copyright © 2005-2018 Shoeisha Co., Ltd. All rights reserved. ver.1.5