WEBアプリでRFIDシステム はじめの一歩 その3

WEBアプリ はじめの一歩

以前、「WEBアプリでRFIDシステム はじめの一歩」「WEBアプリでRFIDシステム はじめの一歩 その2」を掲載いたしました。もうすこし、一歩すすめて集計表を表示・ダウンロードするようちょっと工夫したものを紹介します。

リーダライタからのデータ送信されてきたデータを受信するIISのスクリプトは「WEBアプリでRFIDシステム はじめの一歩」のほうをご覧ください。

IISはWindows11にも搭載されており、Windows10でも11でも基本的に同じように使えます。

RFIDシステムは、導入時にうまく読み取りできるか、ICタグやアンテナの向きなど、数日間設置してみてテストすることも多いです。そんなとき手軽に読み取りの確認や、RSSI値の平均や、アンテナ毎の検知回数を確認するといった用途にも活用できるでしょう。

今回のシステム想定例

今回の想定システムイメージです。

リーダライタ設定

MRU-F7100JP, MRU-F5100JPやFRU-F4025Plus等のリーダライタ側の設定です。詳しくは各々の製品のマニュアルを参照してください。

送信先設定

項目内容
プロトコルhttp
メソッドpost
IPアドレス/ホスト名192.168.209.100
パス名/test/test.asp
フォーマットquery
ポート番号80
Keep-Alive有効

連続モード設定

デフォルト設定のままでも大丈夫ですが、同じデータが送信されるのを止めたい場合は、重複チェック機能の設定で同じICタグの送信を削減できます。

動作モード変更(読み取りスタート)

動作モードを「連続モード」に変更します。

動作モードを「連続モード」に変更すると、リーダライタはICタグの検知をはじめます。ICタグを検知するとそのデータを、上の送信先設定で設定したWEBサーバ(192.168.209.100)に送信します。

その他、細かいアンテナ設定や送信データ設定などは割愛します。

結果確認

動作モードを連続モードに変更すると、ICタグの読み取りと、検知したICタグデータの送信が行われ、C:\inetpub\wwwroot\test ディレクトリに、csvファイルが生成されます。

>>データを受信するスクリプトは前回「WEBアプリでRFIDシステム はじめの一歩」をご覧ください。

今回は日付をファイル名にするようなスクリプトですので、2023年5月1日であればdata20230501.csv といった名称のcsvファイルが生成され、同じ日であれば、最後にデータは追加されていきます。

EXCELやメモ帳

csvファイルですので、マイクロソフトのEXCELや、Windowsのメモ帳アプリで内容を確認できます。

生成されるcsvファイル中のデータ例   ファイルネーム: data20230501.csv

※CSVの内容は1番目の項目が、サーバで付加した日付で、2番目の項目の「TAG」以降がリーダライタから送信されてきたデータです。データの内容・意味につきましてはリーダライタのマニュアルを参照してください。

2023/05/01 17:00:54,TAG,1,20180101090003660,3000,1,-516,E2806894000050028A4304AF,1
2023/05/01 17:00:54,TAG,1,20180101090003714,3000,1,-510,E2806894000040029142A520,1
2023/05/01 17:00:54,TAG,1,20180101090003769,3000,1,-516,E2806894000050028A4304AF,1

参考:csvファイルを集計して閲覧するWEBページ

取得されたデータの活用は、実運用時にはいわゆる業務システムの部分ですが、以下、簡単に確認していただくためにcsvファイルにたまったデータを集計して表示するWEBページ例を示します。

例:

1.以下の2つのファイルの内容を「test4.html」「clear2log.asp」というファイルで保存(UTF-8)

2.csvファイルと同じディレクトリ、今回の例であれば、c:\inetpub\wwwroot\test ディレクトリにおく。

3.ブラウザのアドレス欄に http://localhost/test/test4.html あるいは   http://192.168.209.100/test/test4.html と入力して開く。

4.対象csvファイル名欄に、例:data20230515.csv といった対象ファイル名を入力して、[表示開始]をクリック。

これで1秒おきに画面が更新され、csvファイルにたまったデータを集計したアンテナ毎の検知数やRSSI値、PC、EPC等が集計表示されます。

test4.html のコード

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
    <title>CSVデータViewer4</title>
    <style>
        table {
            border-collapse: collapse;
        }
        th, td {
            border: 1px solid black;
            padding: 5px;
        }
        tr:nth-child(even) {
            background-color: #f2f2f2;
        }
        p#uniqueProducts {
            font-size : 2em;
        }
    </style>
</head>

<body>
<h1>CSVデータViewer4</h1>
    対象CSVファイル:<input type=text id="csvFile" value=""> 
    <button id="startButton">表示開始</button>
    <button id="stopButton" style="display : none;">表示停止</button>
    <button id="saveButton">テーブル保存</button> 
    <button id="clearDispButton">画面表示クリア</button>
    <button id="clearButton">CSVファイルクリア to logファイル</button>
    <p id="message"></p>
    <p id="uniqueProducts"></p>

    <table id="dataTable">
        <thead>
            <tr>
                <th>削除</th>
                <th>PC</th>
                <th>EPC</th>
                <th>ant0</th><th>ant1</th><th>ant2</th><th>ant3</th><th>全アンテナ合計</th>
                <th>RSSI最小値</th>
                <th>RSSI最大値</th>
                <th>RSSI平均値</th>
                <th>初回検知時刻</th>
                <th>最新検知アンテナ</th><th>最新RSSI</th>
                <th>最新検知時刻</th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>


<script>

const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
const saveButton = document.getElementById('saveButton');
const clearButton = document.getElementById('clearButton');
const clearDispButton = document.getElementById('clearDispButton');
const dataTable = document.getElementById('dataTable');
const message = document.getElementById('message');
const uniqueProducts = document.getElementById('uniqueProducts');

let intervalId;
let groupedData = {};

let csvFile = "";

//
//CSVファイルを読み込みテーブル表示
//
function fetchData() {
    fetch(csvFile, { cache: 'no-store' }) // キャッシュを使用しない
        .then(response => {
          if (!response.ok) {
            message.textContent = "対象CSVファイルが読み取りできません: " + new Date().toLocaleString();
            throw new Error('Network response was not ok');
          }
          return response.text();
        })
        .then(data => {
            // CSVファイルをパース、集計
            data = data.trim(); // 最後の改行を削除
            const dataMap = {};
            const rows = data.split('\n');
            if( rows.length > 1 ) { // データ有無チェック
                rows.forEach(row => {
                    const columns = row.split(',');
                    const epc = columns[7];
                    const antno = parseInt(columns[8]);
                    const rssi = parseFloat(columns[6]);
                    if (!dataMap[epc]) {
                        dataMap[epc] = {
                            pc: "",
                            counts: { 0: 0, 1: 0, 2: 0, 3: 0 },
                            min: Infinity,
                            max: -Infinity,
                            sum: 0,
                            count: 0,
                            start: columns[0],
                            recentant: 0,
                            recentrssi: 0,
                            end: columns[0]
                        };
                    }
                    dataMap[epc].pc = columns[4];
                    dataMap[epc].counts[antno]++;
                    dataMap[epc].min = Math.min(dataMap[epc].min, rssi);
                    dataMap[epc].max = Math.max(dataMap[epc].max, rssi);
                    dataMap[epc].sum += rssi;
                    dataMap[epc].count++;
                    dataMap[epc].recentantno = antno;
                    dataMap[epc].recentrssi = rssi;
                    dataMap[epc].end = columns[0];
                 });

                // 集計データをtableに表示
                const tableBody = dataTable.querySelector('tbody');
                tableBody.innerHTML = '';
                Object.entries(dataMap).forEach(([epc, data]) => {
                    const row = tableBody.insertRow();
                    row.insertCell().innerHTML = '<button onclick="deleteRow(this)">削除</button>';
                    row.insertCell().textContent = data.pc;
                    row.insertCell().textContent = epc;
                    for (const antno of ['0', '1', '2', '3']) {
                        row.insertCell().textContent = data.counts[antno];
                    }
                    row.insertCell().textContent = Object.values(data.counts).reduce((a, b) => a + b, 0);
                    row.insertCell().textContent = data.min;
                    row.insertCell().textContent = data.max;
                    row.insertCell().textContent = (data.count > 0 ? data.sum / data.count : 0).toFixed(2);
                    row.insertCell().textContent = data.start;
                    row.insertCell().textContent = data.recentantno;
                    row.insertCell().textContent = data.recentrssi;
                    row.insertCell().textContent = data.end;
                });
            }
            message.textContent = "更新時刻: " + new Date().toLocaleString();
            uniqueProducts.textContent = `ユニークなtag数: ${Object.keys(dataMap).length}`;
        })
        .catch(error => {
            console.error('エラー:', error);
        });
}



// ボタンイベントリスナー

// 定期読み込み開始
startButton.addEventListener('click', () => {
    csvFile = document.getElementById('csvFile').value; // 対象CSVファイル

    intervalId = setInterval(fetchData, 1000); //1秒毎更新 もしも更新間隔を変更したい場合は、ここの数値 単位ms を1000から変えてください
    startButton.style.display = 'none';
    stopButton.style.display = 'inline';
});

// 定期読み込み停止
stopButton.addEventListener('click', () => {
    clearInterval(intervalId);
    startButton.style.display = 'inline';
    stopButton.style.display = 'none';
});

// CSVファイル to logファイル追加
clearButton.addEventListener('click', () => {
    fetch('clear2log.asp?csvFile=' + csvFile)
        .then(response => {
            if (!response.ok) {
                throw new Error('log追加処理に失敗しました');
            }
            const tableBody = dataTable.querySelector('tbody');
            tableBody.innerHTML = '';
            console.log('logファイル追加処理完了');
        })
       .catch(error => console.error('エラー:', error));
});

// 画面クリア
clearDispButton.addEventListener('click', () => {
    // tableクリア
    const tableBody = dataTable.querySelector('tbody');
    tableBody.innerHTML = '';
});


// CSV形式に変換して保存
saveButton.addEventListener('click', () => {
  const rows = dataTable.querySelectorAll("tr");
  // CSV形式の文字列を作成
  let csvContent = "data:text/csv;charset=utf-8,";
  rows.forEach(row => {
    const rowData = [];
    const cells = row.querySelectorAll("td, th"); // thも考慮
    // 左端の削除セルを除外して行作成
    for (let i = 1; i < cells.length; i++) {
      let text = cells[i].innerText.replace(/"/g, '""');
        if (text.includes(",") || text.includes("\"") || text.includes("\n")) {
          text = "\"" + text + "\"";
        }
      rowData.push(text);
    }
    csvContent += rowData.join(",") + "\r\n";
  });

  // ダウンロードリンクを作成
  const encodedUri = encodeURI(csvContent);
  const link = document.createElement("a");
  link.setAttribute("href", encodedUri);
  link.setAttribute("download", "table_data.csv.txt");
  document.body.appendChild(link); // 必要に応じてbodyに追加
  link.click(); // リンクをクリックしてダウンロード開始
  document.body.removeChild(link); // リンクを削除

});

//表示されたtableから削除ボタンで行削除
function deleteRow(btn) {
  const row = btn.parentNode.parentNode; // ボタンの親要素(td)の親要素(tr)を取得
  row.parentNode.removeChild(row);       // 行を削除
}


</script>
</body>
</html>

clear2log.asp のコード

<%
' *.csvファイルを*.csv.logに追記し、*.csvを空にする
'  追記の際に空行を追加

On Error Resume Next ' エラー発生時に次の行へ進む
' 引数csvFileで指定したファイルを読み込み
csv_path = Server.MapPath(Request.QueryString("csvFile"))
Set fso = Server.CreateObject("Scripting.FileSystemObject")
Set file = fso.OpenTextFile(csv_path, 1)
strData = file.ReadAll
file.Close

If Err.Number = 0 Then

  ' 最後の文字が改行でない場合、改行を追加 追記時に改行が不要な場合は以下3行削除
  If Right(strData, 1) <> vbCrLf Then
    strData = strData & vbCrLf
  End If

' .csvの内容を.csv.logに追記
  log_path = Server.MapPath(Request.QueryString("csvFile") & ".log")
  Set file = fso.OpenTextFile(log_path, 8, True)
  file.Write strData
  file.Close

  Set file = fso.OpenTextFile(csv_path, 2)
  file.Write ""
  file.Close

End If

%>

test4.htmlの画面例

機能説明

[表示開始][表示停止] : 画面の更新の開始・停止

[テーブル保存] : 表示されたテーブルの集計表をCSVでダウンロードします。不要な行は左端の[削除]で削除できます。

[画面表示クリア] : テーブルの内容をクリアします。

[CSVファイルクリア to logファイル] : データがたまった対象CSVファイルの内容を<対象CSVファイル名.log >のファイルに追記し、対象CSVファイルはクリアします。

まとめ

ICタグを製品やパレットなどに貼付し、アンテナの前を通過させ、そのデータを検知して、進捗や入庫・出庫など管理をおこなうといったケースも多いかと思います。電波の強さやアンテナの設置によっては、余計なICタグを検知してしまったり、十分に目的のICタグが検知できなかったりという場合があります。ですので、はじめに設置・調整で電波の出力値や、アンテナの位置・向きを試行錯誤する必要があります。

FRU/MRUシリーズの場合、設置・調整中のテストで、ここでやったようなRSSI値や検知回数など確認して、適切なRSSIフィルタや、回数フィルタを設定することで、余計なデータを上位に送信してしまう可能性を減らせます。

ここでは「連続モード」としましたが、センサー連携の「トリガモード」等でも同様にデータを収集し、集計を行うことができます。

当社がホームページで公開しておりますツールの「mrurecv」などでも画面である程度は確認可能ですし、「mrurecv」はある程度のCSVデータの出力が可能です。CSVデータがあればEXCELで集計ができます。

また、こういったWEBアプリを発展させて、業務システムとすることも可能です。その場合、実運用では、単純なcsvファイルの生成でなく、データベースに格納するようなシステムにしたり、負荷を考えたシステムにする必要がありますので、WEBアプリに長けたシステム会社様にご相談ください。今回は単純にCSVファイルに追記していって、それを集計・表示しているだけですが、多くの場合、読み取りデータは、オンプレであればSQL Server 、Oracle、PostgreSQLといったデータベースにまずは格納します。オンプレなサーバに加えて、多拠点であれば、さらにクラウドを活用するのもよくやられる手法です。WEBサーバもnginxやApache、言語もpython、PHP、Ruby、JAVA、その他各種のフレームワークの活用が一般的です。

※今回の記事の内容はあくまでも実験・テスト用です。記事の利⽤により、万が⼀利⽤者に何らかの損害が⽣じても当社は責任を負わないものとさせていただきます。利⽤者の責任においてご活⽤くださいますよう、お願い申し上げます。

固定式リーダライタ MRU-F7100JP
UHF帯RFIDリーダライタ

固定式、ハンディ、ゲート型、トンネル型など各種取り揃え

HF帯RFIDリーダライタ

卓上タイプ、タッチパネル端末など各種取り揃え

パッケージシステム

RFIDシステムのスモールスタートをサポート

ICタグ・ICカード

金属対応タグ、リネンタグ、耐熱タグなど各種取り揃え


RFID所在/通過管理スタートアップ
RFID持ち出し管理スタートアップ
RFID通過方向検知装置