Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

Elixir+PhoenixとSpread.Viewsでリアルタイムな出勤管理アプリを作ろう

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

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

 本連載では、グレープシティが開発するJavaScriptライブラリ「SpreadJS」の 収録コントロール「Spread.Views」を活用して、IoT時代に役立つさまざまな アプリケーションを作っていきます。今回は、Spread.Viewsのカレンダー機能を使って出勤管理アプリを作ります。

はじめに

 こんにちは、dotstudio株式会社n0bisukeです。前回に引き続き、簡単なコードでさまざまなUIの実装を実現できるライブラリ「Spread.Views」を紹介します。

 前回の記事「ガントチャートもFacebook風もこれ一つでOK! 多彩なUI表現を可能にするSpread.Viewsを使ってみよう」は、Spread.Viewsの機能を包括的に紹介しましたが、今回からは実際に実務で活用できそうな内容を紹介していきます。

今回の内容

 今回は社内の出退勤の管理システムをSpread.Viewsを使って作っていきます。

 構成は以下になります。

  • デバイス側:非接触型ICカードリーダー+Raspberry Pi
  • サーバー側:Elixir/Phoenix
  • クライアント側:Spread.Views(カレンダービュー)

 システムの流れは、社員が登録しているSuicaなどのカードをカードリーダにかざすと、その情報がサーバー側に送られ、クライアント側にリアルタイムに反映される仕組みとなっています。

利用技術の紹介

 実装に入る前に各利用技術の紹介をします。

非接触型ICカードリーダー+Raspberry Pi

 今回は、ソニーが開発している非接触型ICカードリーダー「PaSoRi」を使います。Suicaや電子マネーの残金を知ることができます。

 通常PaSoRiはPCに接続して利用するものですが、今回はPCではなくRaspberry PiにUSBで接続します。Raspberry Piとは、Linuxが動くシングルボードコンピュータです。今回は最新版のRaspberry Pi 3を利用します。

Elixir/Phoenix

 最近リアルタイムウェブを実現するにあたり、Node.js+Socket.ioの構成の代替となる可能性としてElixirが注目を浴びています。ソーシャルゲームの裏側などで利用され始めています。PhoenixはElixirのWebフレームワークにあたります。

Spread.Viewsの「カレンダービュー」機能

 本連載の主役となるSpread.Viewsです。

 「カレンダービュー」という機能があり、簡単な記述でカレンダーを作ることができ、データセットを与えることでカレンダーの日付にマッピングしてくれます。

Web側の実装(1)

 それでは実装に入っていきます。今回はWeb側の実装、次回記事でデバイス側の実装を解説していきます。まずは、ベースとなるPhoenixフレームワークでの開発から始めていきます。

開発環境

 参考までに、筆者の環境を記載します。

  • Phoenix v1.3.0
  • Erlang v9.1.5
  • Elixir v1.5.2
  • Node.js v9.2.0(PhoenixではフロントエンドのビルドツールでNode.jsが使われています)

 ちなみに本番サーバーはUbuntu 16.04、ローカル環境はmacOS high Sierraとなっています。

 Ubuntu上でPhoenixが動くようになるまでの手順は、こちらの記事「Elixir/PhoenixをUbuntu(16.04 on Azure)で動かしてPM2で永続化まで」にまとめていますのでご参照下さい。

Webサービスの雛形を作る

 まずはWebサービスの雛形を作っていきます。

$ mix phx.new myapp --no-ecto

 これで雛形のファイルが作成されます。途中で「Y|n」の入力確認が出たらYをタイプして進みます。myappフォルダができるので移動します。

$ cd myapp

 次にサーバーを起動します。

$ mix phx.server

Compiling 12 files (.ex)
Generated myapp app
[info] Running MyappWeb.Endpoint with Cowboy using http://0.0.0.0:4000
21:49:28 - info: compiled 6 files into 2 files, copied 3 in 1.0 sec

 これでファイルのコンパイルと同時にサーバー起動もしてくれます。

Spread.Viewsのライブラリを読み込む

 Phoenixではフロントエンド開発にBrunchというビルドツールを利用しています。

 assets/jsフォルダとassets/css内にユーザーが作成したJSファイルとCSSファイルを追加し、jQueryや今回使うSpread.Viewsなどの外部ベンダーが作成したライブラリなどをassets/vendorフォルダ内に追加します。

 そうすることで、Brunchがビルドを行い、JSファイルはpriv/static/js/app.jsとしてビルドされ、CSSファイルはpriv/static/css/app.cssとしてビルドされます。

 実際の動作ではビルド後のapp.jsとapp.cssだけを読み込んで使います。

 今回のSpread.Viewsのライブラリはvendorフォルダ内にspreadというフォルダを作成し、その中にcssjsフォルダを作成して管理するようにしました。

 読み込むファイルはこちらの勤務表デモのソースコードを参照しましょう。

 また、jQuryも利用するのでjQueryファイルもvendorフォルダに設置して下さい。

ライブラリの読み込み順番

 Spread.Viewsでは共通のgc.spread.common.10.3.0.min.jsなどを先に読み込み、プラグインのplugins/gc.spread.views.cardlayout.10.3.0.min.jsなどは後に読み込むなどの、ライブラリの読み込み順番が大切です。

 scriptタグで読み込む際は読み込む順番は記述した通りになりますが、Brunchの場合はbrunch-config.jsを編集する必要があります。

 order>beforeに配列で順番を記載することができます。

brunch-config.js
//省略
      order: {
        before: [
          "vendor/spread/css/gc.spread.views.dataview.10.3.0.css",
          "vendor/spread/css/bootstrap-snippet.min.css",
          "vendor/spread/css/plugins/gc.spread.views.cardlayout.10.3.0.css",
          "vendor/spread/css/plugins/gc.spread.views.calendargrouping.10.3.0.css",
          "vendor/jquery.min.js",
          "vendor/spread/js/gc.spread.common.10.3.0.min.js",
          "vendor/spread/js/gc.spread.views.dataview.10.3.0.min.js",
          "vendor/spread/js/locale/gc.spread.views.dataview.locale.ja-JP.10.3.0.min.js",
          "vendor/spread/js/plugins/gc.spread.views.cardlayout.10.3.0.min.js",
          "vendor/spread/js/plugins/gc.spread.views.calendargrouping.10.3.0.min.js",
          "vendor/spread/js/zepto.min.js",
          "vendor/spread/js/license.js",
        ]
      }

//省略

サンプル画像の追加

 出勤表に使う画像をassets/static/imagesに追加しましょう。

 サンプルに合わせてPNGファイルを設置しました。

app.jsの編集

 assets/js/app.jsを編集します。

assets/js/app.js
import "phoenix_html"

// Import local files
//
// Local files can be imported directly using relative
// paths "./socket" or full ones "web/static/js/socket".

import socket from "./socket"

 と書いている部分の下に勤務表デモのJSファイルを追記します。

 サンプルコードの241行目のvar names = ['Troy', 'cvaldes', 'DeepakSharma14', 'hlkhan', 'Jeb', 'luculus', 'john'];部分の配列は先ほど追加したファイル名に当たるので、適宜変更しましょう。

assets/js/app.js
import "phoenix_html"

// Import local files
//
// Local files can be imported directly using relative
// paths "./socket" or full ones "web/static/js/socket".

import socket from "./socket"

var CalendarGrouping;
var dataView;
var monthEventTemplate = '<div><div data-column = "card" class="smallIcon"></div></div>';
var currentArry = [];
var presenter = '<img style ="width:100%;height:100%" src="{{=it.card}}"/>';
var columns = [{
    id: 'name',
    caption: 'name',
    dataField: 'name'
}, {
    id: 'card',
    caption: 'card',
    dataField: 'card',
    presenter: presenter
}, {
    id: 'date',
    caption: 'date',
    dataField: 'date'
}];

$('#btn-next').on('click',next);
$('#btn-previous').on('click',previous);

//after window resize, change the template back
$(window).resize(function() {
    if (dataView) {
        var options = dataView.options;
        var strategy = options.groupStrategy;
        var strategyOptions = strategy.options;
        if (strategyOptions.viewMode === "Month") {
            options.cardHeight = 56;
            options.cardWidth = 56;
            options.rowTemplate = monthEventTemplate;
        }
        dataView.invalidate();
    }
});

const render = () => {
    const sourceData = createData();
    console.log(sourceData)
    CalendarGrouping = new GC.Spread.Views.Plugins.CalendarGrouping({});
    CalendarGrouping.eventLimitClick.addHandler(function(sender, args) {
        var options = sender.options;
        options.cardHeight = 100;
        options.cardWidth = 100;
        options.rowTemplate = '<div><div data-column = "card" class="bigIcon"></div></div>';
    });
    CalendarGrouping.popoverClose.addHandler(function(sender, args) {
        var options = sender.options;
        options.cardHeight = 56;
        options.cardWidth = 56;
        options.rowTemplate = monthEventTemplate;
        sender.invalidate();
    });
    dataView = new GC.Spread.Views.DataView(document.getElementById('grid1'), sourceData, columns, new GC.Spread.Views.Plugins.CardLayout({
        cardHeight: 56,
        cardWidth: 56,
        grouping: {
            field: 'date',
            converter: function(field) {
                return field.toDateString();
            }
        },
        rowTemplate: monthEventTemplate,
        groupStrategy: CalendarGrouping
    }));
    updateTitle();
}

$(document).ready(render); //初期描画

function getMonth(currentDate, monthCount) {
    var year = currentDate.getFullYear();
    var month = currentDate.getMonth() + monthCount;
    var day = currentDate.getDate();

    if (month == 12) {
        month = 0;
        year += 1;
    } else if (month == -1) {
        month = 11;
        year -= 1;
    }

    return new Date(year, month, day, 0, 0, 0);
}

function updateTitle() {
    var options = CalendarGrouping.options;
    var date = options.startDate;
    var title = document.getElementById("title");
    var dateFormatter = new GC.Spread.Formatter.GeneralFormatter('mmmm yyyy');
    title.innerText = dateFormatter.format(date);
}

function previous() {
    var options = CalendarGrouping.options;
    var currentDate = options.startDate;
    currentDate = getMonth(currentDate, -1);
    CalendarGrouping.options.startDate = currentDate;
    updateTitle();
    dataView.invalidate();
}

function next() {
    var options = CalendarGrouping.options;
    var currentDate = options.startDate;
    currentDate = getMonth(currentDate, 1);
    CalendarGrouping.options.startDate = currentDate;
    updateTitle();
    dataView.invalidate();
}

function getDay(currentDate, daysCount) {
    var date = new Date(currentDate);
    var timeSpan = 1000 * 60 * 60 * 24 * (daysCount ? daysCount : 1);
    date.setTime(date.getTime() + timeSpan);

    return date;
}

function createData() {
    var names = ['n0bisuke', 'uko', 'chantoku', 'mao', 'wami', 'chachamaru', 'taka'];
    var data = [];
    var now = new Date();
    var name;
    var factor = 7;
    var date;
    for (var i = 0; i < names.length; i++) {
        for (var j = 0; j < factor; j++) {
            date = getDay(new Date(now.getFullYear(), now.getMonth(), 1), getRandomData(factor * 4));
            name = names[i];
            data.push({
                name: name,
                card: './images/' + name + '.png',
                date: date
            });
        }
        currentArry.length = 0;
    }

    return data;
}

function getRandomData(factor) {
    var dataRandom = Math.floor(Math.random() * factor + 1);
    while (currentArry.indexOf(dataRandom) !== -1) {
        dataRandom = Math.floor(Math.random() * factor + 1);
    }

    currentArry.push(dataRandom);
    return dataRandom;
}

 現時点だとJSコードが荒いので次回の記事で修正を入れますが、一旦このまま進みます。

calender.cssの追加

 先ほどのJSと同様に勤務表デモからCSSファイルを追加します。

 assets/css/calender.cssを追加します。先ほどの説明同様にassetsフォルダ内のCSSはBrunchによってビルドされます。

assets/css/calender.css
* {
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

@font-face {
    font-family: 'spreadview-demo-icon';
    src: url(data:application/font-woff;base64,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) format('woff');
}

.demo-icon {
    font-family: "spreadview-demo-icon";
    font-style: normal;
    display: inline-block;
    text-align: center;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-variant: normal;
    text-transform: none;
}

.icon-left-big:before {
    content: '\e802';
}
/* '' */

.icon-right-big:before {
    content: '\e803';
}
/* '' */

.wrapper {
    height: 1000px;
    position: relative;
    overflow: auto;
}

.gc-grid {
    border: 1px solid #CECECE;
}

.smallIcon {
    width: 50px;
    height: 50px;
    margin: 3px;
}

.smallIcon img {
    border-radius: 50px;
    border: solid 2px #e0e0e0;
}

.bigIcon {
    width: 80px;
    height: 80px;
    margin: 10px;
}

.bigIcon img {
    border-radius: 50px;
    border: solid 2px #e0e0e0;
}

.button-container {
    overflow: hidden;
    margin-bottom: 10px;
}

.dateTextTitle {
    display: inline-block;
    margin-left: 45%;
}

@media only screen and (max-width: 768px) {
    .wrapper {
        height: 820px;
    }
    .dateTextTitle {
        margin-left: 0;
    }
}

Web側の実装(2)

ルーティングの追加

 まずは/shifttableでアクセスできるように処理を追加していきます。

 lib/myapp_web/router.exにルーティングを追加します。

 get "/shifttable", ShifttableController, :shifttableを追記します。

lib/myapp_web/router.ex
//省略

  scope "/", MyappWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    get "/shifttable", ShifttableController, :shifttable #shifttableを追加
  end

//省略

 /shifttableにアクセスがあった時にShifttableControllershifttableメソッドを実行します。

コントローラーの追加

 次にコントローラーの作成です。lib/myapp_web/controllers/shifttable_controller.exを作成します。

defmodule MyappWeb.ShifttableController do
  use MyappWeb, :controller

  def shifttable(conn, _params) do
    render conn, "shifttable.html"
  end
end

 サーバー側では特に処理を入れず、shifttable.htmlを返す処理となっています。

ビューの作成

 次はビューです。

 まずはViewモジュールを作成します。

 lib/myapp_web/views/shifttable_view.exを作り、以下を記述します。

lib/myapp_web/views/shifttable_view.ex
defmodule MyappWeb.ShifttableView do
  use MyappWeb, :view # ビュー関連の機能を使うための宣言
end

 次にテンプレートの作成です。

 lib/myapp_web/tempaltesshifttableフォルダを作成します。

 また、その中にshifttable.html.eexを作成しましょう。

 先ほどのコントローラーでshifttable.htmlを指定した場合、shifttable.html.eexがテンプレートファイルになります。

 このeexファイルにSpread.Viewsのデモコードをもとにbody部分を記述します。

lib/myapp_web/tempaltes/shifttable/shifttable.html.eex
<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 class="wrapper">
        <div class="button-container " id="calendarCommandPanel">
            <div class="dateTextTitle">
                <div id="title" style="font:400 18px arial,sans-serif;text-align: center;">20 Aug 2012-26 Aug 2012
                </div>
            </div>
            <div class="btn-group btns" rol="group" style="float: right">
                <div class="btn btn-default" id="btn-previous">
                    <i class="demo-icon icon-left-big"></i>
                </div>
                <div class="btn btn-default" id="btn-next">
                    <i class="demo-icon icon-right-big"></i>
                </div>
            </div>
        </div>
        <div id="gridContainer" style="height:720px;min-width: 800px">
            <div id="grid1" style="height:100%;position: relative"></div>
        </div>
    </div>

    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>

 JSファイルは<script src="<%= static_path(@conn, "/js/app.js") %>"></script>の部分でSpread.Viewsのライブラリ群も含めてビルドされたJSファイルが読み込まれます。

 また、大元のレイアウトテンプレートはlib/demo_web/templates/layout/app.html.eexに記載されているので、こちらも以下の内容に編集します。

lib/demo_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello Demo!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
  </head>

  <%= render @view_module, @view_template, assigns %>

</html>

 こちらの<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">の記載もJSファイル同様でビルドされたCSSファイルが読み込まれます。

途中経過確認

$ mix phx.server

 でサーバーを起動できます。また、Node.jsなどと同様でCtrl+Cで終了できます。

 この際に[error] Could not start node watcher because script "/home/n0bisuke/phoenixBI/demo/assets/node_modules/brunch/bin/brunch" does not exist. Your Phoenix application is still running, however assets won't be compiled. You may fix this by running "cd assets && npm install".などのエラーが出た場合は、以下のコマンドで再実行しましょう。

cd assets && npm install

 無事に起動できると以下のような表示になります。

[info] Running DemoWeb.Endpoint with Cowboy using http://0.0.0.0:4000

 ブラウザでhttp://localhost:4000/shifttableにアクセスすると、カレンダーが表示されています。

 ランダム表示となる処理が書いてあるので更新するたびに表示が変わります。

Raspberry PiでのICカード読み取り処理

 ここまでで、Webの管理画面のサーバーサイドと表側のUIを作りました。ここからRaspberry Piでカード読み取りをします。

 PaSoRiはいくつか型番がありますが、今回はRC-S380を利用します。別の型番だとエラーが出る場合があるので気をつけましょう。

 PaSoRiは公式にはRaspberry Piに対応していませんが、nfcpyなどサードパーティ製のライブラリが存在します。今回はnfcpyをラップしたnode-nfcpy-idを利用します。

 事前に、Raspberry Pi上でNode.jsが動作するようにセットアップしておきましょう。筆者の環境は以下の通りです。

  • Raspberry Pi 3
  • Raspbian GNU/Linux 8(jessie)
  • Node.js 9.2.0

PaSoRiを使う準備

$ sudo apt-get install python-usb python-pip -y
$ sudo pip install -U nfcpy-id-reader
$ cat << EOF | sudo tee /etc/udev/rules.d/nfcdev.rules
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="06c3", GROUP="plugdev"
EOF
再起動
$ sudo reboot
Raspberry Pi上にプロジェクト作成
$ mkdir shifttable-client
$ cd shifttable-client
$ npm init -y
モジュールのインストール
$ npm i --save node-nfcpy-id

 app.jsを作成し、サンプルプログラムを記述します。

app.js
const NfcpyId = require('node-nfcpy-id').default;
const nfc = new NfcpyId().start();

nfc.on('touchstart', (card) => {
  console.log('Card ID: ' + card.id);

  // card.type is the same value as that of nfcpy.
  // 2: Mifare
  // 3: FeliCa
  // 4: Mifare (DESFire)
  console.log('Card Type: ' + card.type);
});

// If the `mode` is `loop` or `non-loop`, event will occur when the card is removed
nfc.on('touchend', () => {
  console.log('Card was away.');
});

nfc.on('error', (err) => {
  // standard error output (color is red)
  console.error('\u001b[31m', err, '\u001b[0m');
});

 サンプルを動かしてみます。

$ node app.js

 これでICカードの情報を読み取ることができました。セットアップでつまずかなければすんなりいけると思います。

 筆者の場合はライブラリが対応していないPaSoRiの型番を利用していてエラーに悩まされました。利用する場合は型番に気をつけましょう。今回はRC-S380を利用しています。章の冒頭にも書いてますが大事なことなので二回言いました。

Raspberry PiとPhoenixの連携処理

 今回、Phoenixとの通信のやり取りはPhoenix ChannelというWebSocketを抽象化した機能を利用します。Phoenixではこの独自の仕組みを持っていてリアルタイム処理を扱いやすくしてくれています。Node.jsでいうところのSocket.ioに近い仕組みです。

サーバー側でPhoenix Channelのコードを記述

 Phoenix内でPhoenix Channelを扱うための準備です。

 今回はshifttable:lobbyというトピックを作成し、Raspberry Piと通信をします。

 lib/myapp_web/channels/user_socket.exに追記します。

 channel "shifttable:lobby", MyappWeb.ShifttableChannelの行を追加しましょう。

lib/myapp_web/channels/user_socket.ex
defmodule MyappWeb.UserSocket do
  use Phoenix.Socket

  ## Channels
  # channel "room:*", MyappWeb.RoomChannel
  channel "shifttable:lobby", MyappWeb.ShifttableChannel #ここを追記

 これでshifttable:lobbyトピックをMyappWeb.ShifttableChannelモジュールで処理をするという記述が追加されました。

 次はMyappWeb.ShifttableChannelモジュールの中を記述します。lib/myapp_web/channels/shifttable_channel.exを作成しましょう。

 クライアントが接続した時の処理として、join関数を作成し、クライアントから接続があった場合にtouch_nfcイベントを受け付ける関数としてhandle_in関数を作成します。

lib/myapp_web/channels/shifttable_channel.ex
defmodule MyappWeb.ShifttableChannel do
    use MyappWeb, :channel # チャネル関連の機能を使うための宣言

    def join("shifttable:lobby",payload, socket) do
        Process.flag(:trap_exit, true) # 異常時にプロセスが死なない為の設定
        IO.inspect payload
        {:ok, socket}
    end

    def handle_in("touch_nfc", payload, socket) do
        broadcast! socket, "touch_nfc", payload
        {:reply, {:ok, payload}, socket}
    end

end

クライアント側でPhoenix Channelのコードを記述

 assets/js/app.jsを編集します。app.jsの最後に以下を追加しましょう。

assets/js/app.js
// 省略

const chan = socket.channel("shifttable:lobby", {})
chan.join()
    .receive("ignore", () => console.log("auth error"))
    .receive("ok", (messages) => {
        console.log("join ok")
    })
    .receive("timeout", () => console.log("Connection interruption"))

// touch_nfcイベントを受信した時の処理
chan.on("touch_nfc", card => {
    console.log(card);
});

 試しにhttp://localhost:4000/shifttableに再アクセスしてみましょう。

 コンソール側でエラーが出ず、以下のような表示が出れば問題なくWebSocketが接続されています。

[info] JOIN "shifttable:lobby" to MyappWeb.ShifttableChannel
  Transport:  Phoenix.Transports.WebSocket (2.0.0)
  Serializer:  Phoenix.Transports.V2.WebSocketSerializer
  Parameters: %{}
%{}
[info] Replied shifttable:lobby :ok

Raspberry PiからPhoenix Chennelに接続する

 Socket.ioでも他の言語のクライアントライブラリが存在しますが、Phoenix Chennelでも同様に、以下のようなさまざまな言語のクライアントライブラリが存在します。

 今回はNode.jsでPhoenix Channelに接続するクライアントライブラリのphoenix-channelsを利用します。

 「Node.jsでElixir/PhoenixのChannelに接続する」にもまとめています。

 ここからはRaspberry Pi上のNode.jsで作業します。

 先ほどRaspberry Pi側で作成したNFC読み取りのapp.js(shifttable-clientフォルダ)と同じプロジェクトにライブラリをインストールします。

npm i --save phoenix-channels

 同じ階層にclient.jsを作成して接続してみましょう。xx.xx.xx.xxの箇所はPhoenixが動いているマシンのIPアドレスを指定します。

 また、MacBookなどのローカルマシン上でPhoenixのサーバーを立てている場合は、Raspberry PiとMac Bookなどのローカルマシンが同じネットワークに接続していることを確認しましょう。

shifttable-client/client.js
'use strict';

const { Socket } = require('phoenix-channels')
const socket = new Socket("ws://xx.xx.xx.xx:4000/socket");
socket.connect();
const channel = socket.channel("shifttable:lobby", {});

channel.join()
    .receive('ok', resp => console.log(`> joining channel  ${channel.topic}`))
    .receive("error", reason => console.log(`Error joining channel:`, reason));

channel.on("new_msg", msg => console.log(msg));

channel.onError(e => console.log("something went wrong", e))

 実行します。joining channel shifttable:lobbyと表示されれば、無事にRaspberry PiからPhoenix Channelに接続が出来ています。

$ node phoenix.js
> joining channel  shifttable:lobby

ICカード読み取りの処理と繋げる

 ICカードを読み取ったらPhoenix Channelへ情報を送信します。

 Raspberry Pi側のshifttable-client/client.jsを編集して、ICカード読み取りの機能も組み込みます。

shifttable-client/client.js
'use strict';

//NFCリーダーモジュール
const NfcpyId = require('node-nfcpy-id').default;
const nfc = new NfcpyId().start();

const { Socket } = require('phoenix-channels');
const socket = new Socket("ws://xx.xx.xx.xx:4000/
socket");
socket.connect();

let flag = ''; //重複フラグ

const channel = socket.channel("shifttable:lobby", {});
channel.join()
    .receive('ok', resp => console.log(`> joining channel  ${channel.topic}`))
    .receive("error", reason => console.log(`Error joining channel:`, reason));
channel.on("new_msg", msg => console.log(msg));

nfc.on('touchstart', (card) => {
    if(flag === card.id){
        console.log('重複です');
    }else{
        channel.push("touch_nfc", card);
        setTimeout(()=>{
            flag = '';
            console.log('リセット');
        },5*1000);
    }

    flag = card.id;
    console.log(card);
});

nfc.on('touchend', () => {
    console.log('Card was away.');
});

nfc.on('error', (err) => {
    // standard error output (color is red)
    console.error('\u001b[31m', err, '\u001b[0m');
});

channel.onError(e => console.log("something went wrong", e))

 nfc.on('touchstart')のリスナーで、card変数に情報を取得できます。

 channel.push("touch_nfc", card);の部分でPhoenix Channelに情報を送信します。

 また、同じ人が何回もICカードを反応させた場合の重複検知をさせるためにflag変数を使っているのと、setTimeout()で定期的にflagをリセットする処理を書いています。

 これでICカードをリーダーにかざした瞬間にPhoenixのサーバーに情報が飛ぶようになりました。

フロント側のコード改修

 ここまでで、サーバーサイドまでICカードの情報が飛んでくるようになりました。

Spread.ViewsのJSコードをES2015化と整理

 次はフロント側に反映させる必要がありますが、その前に一旦Spread.Viewsの実装部分のコードを整理します。BrunchはES2015を推奨しているため、勤務表デモのJSファイルもモダンな書き方に修正していきます。

 カレンダーの処理を記述するassets/js/calender.jsを作成し、もともとassets/js/app.jsに記述してあったコードを移行させます。

 assets/js/app.jsはかなりシンプルになり、assets/js/calender.jsからclassを読み込んで、静的メソッドのcalender.init()を実行する形になります。

assets/js/app.js
import "phoenix_html"
import socket from "./socket"

import Calender from "./calender"
const calender = new Calender();
calender.init(socket);

 本体となるassets/js/calender.jsを以下のように作成します。

assets/js/calender.js
'use strict';

//private メソッド化
const setWindowAction = Symbol();
const updateTitle = Symbol();
const getMonth = Symbol();
const render = Symbol();
const pushData = Symbol();

class Calender {

    constructor(){
        this.CalendarGrouping = {};
        this.DataView = {};
        this.UserData = [];
        this.monthEventTemplate = `<div><div data-column = "card" class="smallIcon"></div></div>`;
        this.presenter = `<img style ="width:100%;height:100%" src="{{=it.card}}"/>`;
        this.columns = [{
            id: 'name',
            caption: 'name',
            dataField: 'name'
        }, {
            id: 'card',
            caption: 'card',
            dataField: 'card',
            presenter: this.presenter
        }, {
            id: 'date',
            caption: 'date',
            dataField: 'date'
        }];

        this.DSMEMBERS = {
            'xxxxxxxxxxx':'n0bisuke',
            'xxxxxxxxxxx':'uko',
            'xxxxxxxxxxx':'chantoku',
            'xxxxxxxxxxx':'chachamaru'
        };
    }

    init(socket){
    	 /*
    	 あとでPhoenix Channelの記述を追加
    	 */
        $(document).ready(this[render]('init')); //初期描画
        this[setWindowAction](this.CalendarGrouping); //dom系処理を有効に
    }

    //データの追加
    [pushData](name, today = 0){
        const now = new Date();
        if(today === 0) today = now.getDate();
        this.UserData.push({
            name: name,
            card: `./images/${name}.png`,
            date: new Date(now.getFullYear(), now.getMonth(), today)
        });
        this[render]();
    }

    //ページ移動系処理
    [setWindowAction](){
        //リサイズ処理
        const resize = () => {
            if (this.dataView) {
                const options = this.dataView.options;
                const strategy = options.groupStrategy;
                const strategyOptions = strategy.options;
                if (strategyOptions.viewMode === "Month") {
                    options.cardHeight = 56;
                    options.cardWidth = 56;
                    options.rowTemplate = this.monthEventTemplate;
                }
                this.dataView.invalidate();
            }
        }

        const previous = () => move(-1); //前へボタン
        const next = () => move(1); //次へボタン

        const move = (step) => {
            const options = this.CalendarGrouping.options;
            let currentDate = options.startDate;
            currentDate = this[getMonth](currentDate, step);
            this.CalendarGrouping.options.startDate = currentDate;
            this[updateTitle]();
            this.dataView.invalidate();
        }

        $('#btn-next').on('click', next);
        $('#btn-previous').on('click', previous);
        $(window).resize(resize);
    }

    //タイトルの更新
    [updateTitle](){
        const options = this.CalendarGrouping.options;
        const date = options.startDate;
        const $title = document.getElementById("title");
        const dateFormatter = new GC.Spread.Formatter.GeneralFormatter('mmmm yyyy');
        $title.innerText = dateFormatter.format(date);
    }

    //月を取得
    [getMonth](currentDate, monthCount){
        const day = currentDate.getDate();
        let year = currentDate.getFullYear();
        let month = currentDate.getMonth() + monthCount;
        if (month == 12) {
            month = 0;
            year += 1;
        } else if (month == -1) {
            month = 11;
            year -= 1;
        }
        return new Date(year, month, day, 0, 0, 0);
    }

    //レンダリング
    [render](flag = ''){

        this.CalendarGrouping = new GC.Spread.Views.Plugins.CalendarGrouping({});
        this.CalendarGrouping.eventLimitClick.addHandler((sender, args) => {
            const options = sender.options;
            options.cardHeight = 100;
            options.cardWidth = 100;
            options.rowTemplate = '<div><div data-column = "card" class="bigIcon"></div></div>';
        });

        this.CalendarGrouping.popoverClose.addHandler((sender, args) => {
            const options = sender.options;
            options.cardHeight = 56;
            options.cardWidth = 56;
            options.rowTemplate = this.monthEventTemplate;
            sender.invalidate();
        });

        if(this.UserData[0] === undefined) return;
        this.dataView = new GC.Spread.Views.DataView(document.getElementById('grid1'), this.UserData, this.columns, new GC.Spread.Views.Plugins.CardLayout({
            cardHeight: 56,
            cardWidth: 56,
            grouping: {
                field: 'date',
                converter: (field) =>  field.toDateString()
            },
            rowTemplate: this.monthEventTemplate,
            groupStrategy: this.CalendarGrouping
        }));
        this[updateTitle]();
    }
}

export default Calender

constructor

 まず、constructorでは利用する内部だけで利用する変数をプロパティ化して設定しています。

 this.DSMEMBERSはdotstudio社の社員のidを記録しています。

 this.UserDataが勤怠情報の大元となるデータで、ここに誰がいつ出勤したのかが格納されていきます。

initメソッド

 このメソッドは静的メソッドで外部から呼び出します。

 ここが全ての処理の起点となるような形となり後ほどPhoenix Channelの処理を追記する箇所になります。

pushDataメソッド

 もとのコードではcreateDataメソッドgetRandomDataメソッドで初期データを作成していましたが、今回はICカードと連動して、動的にデータを追加していきます。pushDataメソッドではthis.UserDataにデータを追加します。

setWindowActionメソッド

 もとのコードのresizeメソッドpreviousメソッドnextメソッドを吸収しています。

 ウィンドウのリサイズ時の処理、来月を表示するボタン、先月を表示するボタンの処理になります。

updateTitleメソッド

 初期表示時や、月を移動した際の表示タイトルを更新します。

getMonthメソッド

 初期表示時や、月を移動した際に月の数字を再計算します。

renderメソッド

 もとのコードのrenderメソッドをそのまま利用しています。

 初期表示時や、pushDataメソッドでデータが追加された際にカレンダーの再描画を行います。

 これで今回のSpread.Viewsの機能をassets/js/caldender.jsにまとめたので、そのほかの処理を追加するときは同様に新規でファイルを作成してClass分けを行うなど更新しやすくなりました。

Phoenix Channelでカレンダーをリアルタイムに反映

 最終ステップです。

 現状だとICカードを読み取ってもフロント側まで情報は反映されません。フロントのコードにPhoenix Channelのコードをフロントエンドに組み込みます。

 assets/js/calender.jsinitメソッドを編集します。

assets/js/calender.js
//省略

    init(socket){
        const chan = socket.channel("shifttable:lobby", {})
        chan.join()
            .receive("ignore", () => console.log("auth error"))
            .receive("ok", (messages) => {
                messages.forEach((msg) => {
                    if(!isNaN(msg.user) && 0 <= msg.user && msg.user <= 31) this[pushData](msg.body, msg.user);
                })
                console.log("join ok")
            })
            .receive("timeout", () => console.log("Connection interruption"))

        chan.on("touch_nfc", card => {
            const now = new Date();
            const today = now.getDate();
            const name = this.DSMEMBERS[card.id];
            //存在チェック
            if(name === undefined){
                console.log(`${name}は存在しません。`);
                return;
            }

            //重複チェック
            let flag = 0;
            for(let i = 0, len = this.UserData.length; i < len; i++){
                if(this.UserData[i].date.getDate() === today && `${this.UserData[i].name}` === name){
                    console.log(`既に${name}さんは${today}日に出勤登録済みです。`);
                    flag = 1;
                }
            }
            if(flag === 0) this[pushData](name, today);
        });

        $(document).ready(this[render]('init')); //初期描画
        this[setWindowAction](this.CalendarGrouping); //dom系処理を有効に
    }

//省略

 まずはsocket.channel("shifttable:lobby", {})の箇所でshifttable:lobbyトピックに接続します。

 chan.onメソッドではtouch_nfcイベントの待受をします。Raspberry Pi側でICカードリーダから情報が発信されるとこの部分の処理が動き、pushDataメソッドに情報が送られthis.UserDataが更新されます。

 また、この際に同じ人が同じ日に出勤記録を打とうとしてないかの重複チェックと、社員じゃない人や登録していないICカードでリーダーに触れた場合の存在チェックも行っています。

 最後にassets/js/socket.jsを編集します。

assets/js/socket.js
import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()
export default socket

 以上で完成です。

 このようにICカードリーダーにICカード(動画ではNFC機能内臓フィギュアを利用)を近づけると、サーバーにPhoenix Channel経由で情報が飛び、フロント側でもtouch_nfcイベントを取得して情報をカレンダーに反映させます。

デプロイとSpreadJSのライセンスキー

 運用するときには本番環境にデプロイしますが、その際、Spread.Viewsのライセンスキーの登録をする必要があります。

 ライセンスキーの登録をしないでデプロイするとこのように黒い画面が表示されてしまいます。

 ライセンスキーの登録は、グレープシティのライセンス管理システムから行います。利用するドメインを登録してライセンスキーを発行します。

 画面の案内をもとに進めましょう。

 ライセンスキーが取得できたらlicense.jsを編集します。今回の場合はassets/vendor/spread/js/license.jsになります。

 デフォルトでは以下のようになっていますが、GC.Spread.Views.LicenseKeyの箇所にライセンスキーを登録します。

license.js
//Please provide valid license key.
//GC.Spread.Views.LicenseKey = 'Your key here';

//Set GC.Spread.Sheets.LicenseKey becuase there are some demos use GC.Spread.Sheets components
// if (GC.Spread.Sheets) {
//     GC.Spread.Sheets.LicenseKey = 'Your key here';
// }

 以下のように編集しましょう。GC.Spread.Sheets.LicenseKeyは、今回は利用しないのでコメントアウトしたままの状態で問題ないです。

license.js
//Please provide valid license key.
GC.Spread.Views.LicenseKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

//Set GC.Spread.Sheets.LicenseKey becuase there are some demos use GC.Spread.Sheets components
// if (GC.Spread.Sheets) {
//     GC.Spread.Sheets.LicenseKey = 'Your key here';
// }

 更新するとこのようにデプロイした先でも問題無く表示されました。

 URLがしっかりとドメインを指していることが確認できます。

運用

 実際に運用してみましたが、

  • 自分の使っているSuicaなどで出勤登録できて楽しい
  • カレンダー自体はどこからでも確認できるので他の社員の勤怠状況がリモートでも把握できる
  • 見た目が良い

などの感想を得られました。

 今回はSpread.Viewsがフロント側のライブラリなので、できるだけフロント側で処理を書くようにしましたが、実際の運用ではデータの扱いや保存などはサーバー側に任せるケースの方が多いと思うので

  • DBへの情報保存
  • 新規ユーザーの登録フロー
  • Raspberry Pi起動時にプログラムを自動起動

などを追加していくと良いと思います。

まとめ

 いかがでしたでしょうか。今回はSpread.Viewsの活用例としてRaspberry PiとElixir+Phoenixで出勤管理システムを作りました。Spread.Viewsを使うことで開発者が見た目をあまり意識せずにリッチなUIを実現できますし、今回のような実装を行えば外部のライブラリとも連携することができます。

 皆さんも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