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

WEBアプリ はじめの一歩

これまで「WEBアプリでRFIDシステム はじめの一歩」ということで、リーダライタのデータの検知データを保存、表示するものを紹介させていただきました。

UHF帯リーダライタの活用のため、はじめにちゃんと検知できるかどうか確認する作業があります。検知予定のものが全部よめているか、検知したくない予定外のものが検知してしまっていないかといったことをテストするわけです。このとき、予定内・外のICタグが、どのアンテナでどのくらい検知できているか確認したくなります。今回は予定を消し込むタイプのビューワを紹介します。読み取り状況の確認などで活用できます。

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

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

今回のシステム想定例

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

リーダライタ設定

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

送信先設定

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

連続モード設定

何度もテストを行うのであれば、重複チェックは無効とするか、リセットを数秒程度でリセットする設定がよいでしょう。 アンテナ毎の検知回数やRSSI傾向など確認するためには、重複チェック機能はいっそ無効のほうがよいでしょう。 ただデータが大量に送信されてきますので、ファイルサイズが大きくなります。大きくなりすぎると、ブラウザが動かなくなりますので注意してください。

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

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

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

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

検知結果の確認

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

>>データを受信するスクリプト@「test.asp」は前の「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.以下のファイルの内容を「test8.html」というファイルで保存(UTF-8)

※test.html以外に、「test.asp」(⇒その1ページ)と「clear2log.asp」(⇒その3ページ)も必要です。

2.csvファイルと同じディレクトリ、今回の例であれば、c:\inetpub\wwwroot\test ディレクトリにおく。※「test8.html」「clear2log.asp」「test.asp」の3つのファイルが必要です。

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

4.対象csvファイル名欄に、例:data20230515.csv といった対象ファイル名を入力して、[表示開始]をクリック。(dataのあとはyyyymmddの日付のファイル名です)

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

test8.html のソースコード

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
    <title>CSVデータViewer8</title>
    <style>
        body {
            font-family: "BIZ UDゴシック", sans-serif;
            margin: 20px; /* 全体の余白 */
        }
        table {
            border-collapse: collapse;
        }
        th, td {
            border: 1px solid black;
            padding: 5px;
        }
        tr:nth-child(even) {
            background-color: #f2f2f2;
        }
        p#uniqueProducts {
            font-size : 2em;
        }
        .sort-button {
            margin-left: 5px;
            font-size: 0.8em;
            padding: 2px 5px;
        }
        .cleared-row { /* 新しく追加 */
            background-color: #A9A9A9 !important; /* 暗めのグレー、!importantで偶数行の背景色を上書き */
        }
        .red-text {
            color: red;
        }
        #modeButtons {
            margin-bottom: 10px;
        }
        #modeDisplay {
            font-size: 1.2em;
            font-weight: bold;
            margin-bottom: 10px;
        }
        #summaryInfo { /* ユニークなタグ数と予定数、予定外数の表示エリア */
            display: flex;
            align-items: center;
            gap: 20px; /* 各情報の間のスペース */
        }
        #summaryInfo p {
            margin: 0; /* pタグのデフォルトマージンをリセット */
        }
    </style>
</head>
<body>
<h1>CSVデータViewer8</h1>
    <div id="modeButtons">
        <button id="detectionModeButton">検知モード</button>
        <button id="clearanceModeButton">消込モード</button>
        <button id="revertToExpectedButton" style="display:none;">予定表示にもどす</button>
    </div>
    <p id="modeDisplay">現在のモード: 検知モード</p>
    対象CSVファイル:<input type=text id="csvFile" value="">
    <input type="file" id="fileInput" accept=".csv" style="display: none;">
    <button id="startButton">表示開始</button>
    <button id="clearAndStartButton">CSVファイルクリア to logファイル&表示開始</button>
    <button id="clearButton">CSVファイルクリア to logファイル</button>
    <button id="stopButton" style="display : none;">表示停止</button>
    <button id="resetButton">ソート順リセット</button>
    <button id="saveButton">テーブル保存</button>
    <button id="clearDispButton">画面表示クリア</button>
    <button id="loadTableButton">テーブル読み込み</button>
    <p id="message"></p>
    <div id="summaryInfo">
        <p id="uniqueProducts">ユニークなtag数: 0</p>
        <p id="expectedCount" style="display:none;">予定数: 0/0</p>
        <p id="unexpectedCount" style="display:none;">予定外数: 0</p>
    </div>
    <table id="dataTable">
        <thead>
            <tr>
                <th>削除</th>
                <th>PC<button class="sort-button" data-column="1" data-order="asc">▲</button><button class="sort-button" data-column="1" data-order="desc">▼</button></th>
                <th>EPC<button class="sort-button" data-column="2" data-order="asc">▲</button><button class="sort-button" data-column="2" data-order="desc">▼</button></th>
                <th>ant0<button class="sort-button" data-column="3" data-order="asc">▲</button><button class="sort-button" data-column="3" data-order="desc">▼</button></th>
                <th>ant1<button class="sort-button" data-column="4" data-order="asc">▲</button><button class="sort-button" data-column="4" data-order="desc">▼</button></th>
                <th>ant2<button class="sort-button" data-column="5" data-order="asc">▲</button><button class="sort-button" data-column="5" data-order="desc">▼</button></th>
                <th>ant3<button class="sort-button" data-column="6" data-order="asc">▲</button><button class="sort-button" data-column="6" data-order="desc">▼</button></th>
                <th>全アンテナ合計<button class="sort-button" data-column="7" data-order="asc">▲</button><button class="sort-button" data-column="7" data-order="desc">▼</button></th>
                <th>RSSI最小値<button class="sort-button" data-column="8" data-order="asc">▲</button><button class="sort-button" data-column="8" data-order="desc">▼</button></th>
                <th>RSSI最大値<button class="sort-button" data-column="9" data-order="asc">▲</button><button class="sort-button" data-column="9" data-order="desc">▼</button></th>
                <th>RSSI平均値<button class="sort-button" data-column="10" data-order="asc">▲</button><button class="sort-button" data-column="10" data-order="desc">▼</button></th>
                <th>初回検知時刻<button class="sort-button" data-column="11" data-order="asc">▲</button><button class="sort-button" data-column="11" data-order="desc">▼</button></th>
                <th>最新検知アンテナ<button class="sort-button" data-column="12" data-order="asc">▲</button><button class="sort-button" data-column="12" data-order="desc">▼</button></th>
                <th>最新RSSI<button class="sort-button" data-column="13" data-order="asc">▲</button><button class="sort-button" data-column="13" data-order="desc">▼</button></th>
                <th>最新検知時刻<button class="sort-button" data-column="14" data-order="asc">▲</button><button class="sort-button" data-column="14" data-order="desc">▼</button></th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
<script>
const startButton = document.getElementById('startButton');
const clearAndStartButton = document.getElementById('clearAndStartButton');
const stopButton = document.getElementById('stopButton');
const resetButton = document.getElementById('resetButton');
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');
const expectedCountDisplay = document.getElementById('expectedCount');
const unexpectedCountDisplay = document.getElementById('unexpectedCount');
const detectionModeButton = document.getElementById('detectionModeButton');
const clearanceModeButton = document.getElementById('clearanceModeButton');
const modeDisplay = document.getElementById('modeDisplay');
const loadTableButton = document.getElementById('loadTableButton');
const fileInput = document.getElementById('fileInput');
const revertToExpectedButton = document.getElementById('revertToExpectedButton');
let intervalId;
let groupedData = {};
let csvFile = "";
let currentSortColumn = -1;
let currentSortOrder = '';
let originalTableRows = []; // 検知モード表示、またはテーブル読み込み時の全データ保持
let currentMode = 'detection'; // 'detection' or 'clearance'
let initialEPCsInClearanceMode = new Set(); // 消込モード開始時のEPCを保持
let detectedEPCsInClearanceMode = new Set(); // 消込モード中に検知されたEPCを保持
let initialTableStateForRevert = []; // 消込モードに切り替える直前のテーブル状態(PCとEPCのみ)を保持
let tableLoadedAfterClearanceModeSwitch = false; // 消込モード開始後にテーブル読み込みが行われたか
//
//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 => {
            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],
                            initialExistence: false
                        };
                    }
                    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];
                    if (currentMode === 'clearance') {
                        detectedEPCsInClearanceMode.add(epc);
                    }
                });
                const tableBody = dataTable.querySelector('tbody');
                // 消込モードの場合、既存の行を保持して更新、そうでない場合は全クリア
                if (currentMode === 'clearance') {
                    // 既存の行を保持するためのMap
                    const existingRows = new Map();
                    Array.from(tableBody.querySelectorAll('tr')).forEach(row => {
                        const epc = row.cells[2].textContent;
                        existingRows.set(epc, row);
                    });
                    // 新しいデータを元にテーブルを更新
                    Object.entries(dataMap).forEach(([epc, data]) => {
                        let row = existingRows.get(epc);
                        if (!row) {
                            // 新しいEPCの場合は新規行を作成
                            row = document.createElement('tr');
                            row.insertCell().innerHTML = '<button onclick="deleteRow(this)">削除</button>';
                            for (let i = 0; i < 14; i++) { // PCから最新検知時刻までのセル
                                row.insertCell();
                            }
                            tableBody.appendChild(row);
                            row.dataset.epc = epc; // EPCをデータ属性として保持
                        }
                        // 行のデータを更新
                        // PCとEPCは消込モード開始時に残しているので更新しない
                        // 新規で追加された行のPC/EPCは更新
                        if (row.cells[1].textContent === '') {
                            row.cells[1].textContent = data.pc;
                        }
                        if (row.cells[2].textContent === '') {
                            row.cells[2].textContent = epc;
                        }
                        
                        row.cells[3].textContent = data.counts['0'];
                        row.cells[4].textContent = data.counts['1'];
                        row.cells[5].textContent = data.counts['2'];
                        row.cells[6].textContent = data.counts['3'];
                        row.cells[7].textContent = Object.values(data.counts).reduce((a, b) => a + b, 0);
                        row.cells[8].textContent = data.min;
                        row.cells[9].textContent = data.max;
                        row.cells[10].textContent = (data.count > 0 ? data.sum / data.count : 0).toFixed(2);
                        row.cells[11].textContent = data.start;
                        row.cells[12].textContent = data.recentantno;
                        row.cells[13].textContent = data.recentrssi;
                        row.cells[14].textContent = data.end;
                        // スタイル適用
                        if (initialEPCsInClearanceMode.has(epc)) {
                            if (detectedEPCsInClearanceMode.has(epc)) {
                                row.classList.add('cleared-row'); // 背景色を変更
                                row.classList.remove('red-text');
                            }
                        } else {
                            row.classList.add('red-text');
                            row.classList.remove('cleared-row'); // 新規タグは背景色を適用しない
                        }
                    });
                    // 消込終了時刻のチェック
                    let allInitialEPCsDetected = true;
                    if (initialEPCsInClearanceMode.size > 0) {
                        initialEPCsInClearanceMode.forEach(epc => {
                            if (!detectedEPCsInClearanceMode.has(epc)) {
                                allInitialEPCsDetected = false;
                            }
                        });
                        if (allInitialEPCsDetected && !modeDisplay.textContent.includes('消込終了時刻')) {
                            modeDisplay.textContent += `   消込終了時刻: ${new Date().toLocaleString()}`;
                        }
                    }
                } else { // 検知モード
                    tableBody.innerHTML = '';
                    const newRows = [];
                    Object.entries(dataMap).forEach(([epc, data]) => {
                        const row = document.createElement('tr');
                        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;
                        newRows.push(row);
                    });
                    originalTableRows = [...newRows];
                    if (currentSortColumn !== -1) {
                        sortAndDisplayTable(currentSortColumn, currentSortOrder, newRows);
                    } else {
                        newRows.forEach(row => tableBody.appendChild(row));
                    }
                }
            }
            message.textContent = "更新時刻: " + new Date().toLocaleString();
            uniqueProducts.textContent = `ユニークなtag数: ${Object.keys(dataMap).length}`;
            updateSummaryCounts(); // 集計情報を更新
        })
        .catch(error => {
            console.error('エラー:', error);
        });
}
// テーブルをソートして表示する関数
function sortAndDisplayTable(columnIndex, order, rowsToSort) {
    const tableBody = dataTable.querySelector('tbody');
    const rows = rowsToSort || Array.from(tableBody.querySelectorAll('tr'));
    rows.sort((a, b) => {
        const aValue = a.cells[columnIndex].textContent;
        const bValue = b.cells[columnIndex].textContent;
        const numA = Number(aValue);
        const numB = Number(bValue);
        if (!isNaN(numA) && !isNaN(numB)) {
            return order === 'asc' ? numA - numB : numB - numA;
        } else {
            return order === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
        }
    });
    tableBody.innerHTML = '';
    rows.forEach(row => tableBody.appendChild(row));
    currentSortColumn = columnIndex;
    currentSortOrder = order;
}
// CSV形式に変換して保存する関数
function saveTableAsCsv() {
  const rows = dataTable.querySelectorAll("tr");
  let csvContent = "data:text/csv;charset=utf-8,";
  
  // ヘッダー行を追加 (削除ボタン列は除外)
  const headerCells = dataTable.querySelector('thead tr').cells;
  let headerData = [];
  for (let i = 1; i < headerCells.length; i++) {
      let text = headerCells[i].innerText.replace(/"/g, '""').split('\n')[0]; // ソートボタンのテキストを除去
      if (text.includes(",") || text.includes("\"") || text.includes("\n")) {
          text = "\"" + text + "\"";
      }
      headerData.push(text);
  }
  csvContent += headerData.join(",") + "\r\n";
  // データ行を追加 (削除ボタン列は除外)
  rows.forEach(row => {
    const rowData = [];
    const cells = row.querySelectorAll("td"); // 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_${new Date().toISOString().slice(0,19).replace(/T|:/g,'-')}.csv`); // ファイル名に日時を追加
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
// clear2log.asp を呼び出す関数
async function callClear2log() {
    csvFile = document.getElementById('csvFile').value;
    try {
        const response = await fetch('clear2log.asp?csvFile=' + csvFile);
        if (!response.ok) {
            throw new Error('log追加処理に失敗しました');
        }
        console.log('logファイル追加処理完了');
        message.textContent = "CSVファイルをログに保存しました: " + new Date().toLocaleString();
    } catch (error) {
        console.error('エラー:', error);
        message.textContent = "log追加処理中にエラーが発生しました: " + error.message;
    }
}
// 集計情報を更新する関数
function updateSummaryCounts() {
    if (currentMode === 'clearance') {
        let detectedExpectedCount = 0;
        let unexpectedDetectedCount = 0;
        detectedEPCsInClearanceMode.forEach(epc => {
            if (initialEPCsInClearanceMode.has(epc)) {
                detectedExpectedCount++;
            } else {
                unexpectedDetectedCount++;
            }
        });
        expectedCountDisplay.textContent = `予定数: ${detectedExpectedCount}/${initialEPCsInClearanceMode.size}`;
        unexpectedCountDisplay.textContent = `予定外数: ${unexpectedDetectedCount}`;
        expectedCountDisplay.style.display = 'inline';
        unexpectedCountDisplay.style.display = 'inline';
    } else {
        expectedCountDisplay.style.display = 'none';
        unexpectedCountDisplay.style.display = 'none';
    }
}
// ボタンイベントリスナー
// 定期読み込み開始
startButton.addEventListener('click', async () => {
    csvFile = document.getElementById('csvFile').value;
    if (currentMode === 'clearance') {
        // 消込モード時の処理
        modeDisplay.textContent = `現在のモード: 消込モード   消込開始時刻: ${new Date().toLocaleString()}`;
        
        // 画面の表のPCとEPC以外のデータをクリア
        const tableBody = dataTable.querySelector('tbody');
        initialEPCsInClearanceMode.clear();
        detectedEPCsInClearanceMode.clear();
        Array.from(tableBody.querySelectorAll('tr')).forEach(row => {
            // PC (cells[1]) と EPC (cells[2]) は残し、それ以外をクリア
            for (let i = 3; i < row.cells.length; i++) {
                row.cells[i].textContent = '';
            }
            row.classList.remove('cleared-row', 'red-text'); // スタイルをリセット
            initialEPCsInClearanceMode.add(row.cells[2].textContent); // 現在のEPCを初期EPCとして記録
        });
        revertToExpectedButton.style.display = 'inline'; // 消込モード開始時にボタン表示
    } else {
        // 検知モード時の処理
        modeDisplay.textContent = `現在のモード: 検知モード`;
        initialEPCsInClearanceMode.clear();
        detectedEPCsInClearanceMode.clear();
        revertToExpectedButton.style.display = 'none'; // 検知モードではボタン非表示
    }
    // 共通の処理:CSV読み込み開始
    fetchData();
    intervalId = setInterval(fetchData, 1000);
    startButton.style.display = 'none';
    clearAndStartButton.style.display = 'none';
    clearButton.style.display = 'none';
    stopButton.style.display = 'inline';
    updateSummaryCounts(); // 集計情報を更新
});
// CSVファイルクリア to logファイル&表示開始ボタン
clearAndStartButton.addEventListener('click', async () => {
    await callClear2log(); // clear2log.asp を呼び出す
    // その後、startButtonの処理を実行
    csvFile = document.getElementById('csvFile').value;
    if (currentMode === 'clearance') {
        modeDisplay.textContent = `現在のモード: 消込モード   消込開始時刻: ${new Date().toLocaleString()}`;
        const tableBody = dataTable.querySelector('tbody');
        initialEPCsInClearanceMode.clear();
        detectedEPCsInClearanceMode.clear();
        Array.from(tableBody.querySelectorAll('tr')).forEach(row => {
            for (let i = 3; i < row.cells.length; i++) {
                row.cells[i].textContent = '';
            }
            row.classList.remove('cleared-row', 'red-text');
            initialEPCsInClearanceMode.add(row.cells[2].textContent);
        });
        revertToExpectedButton.style.display = 'inline'; // 消込モード開始時にボタン表示
    } else {
        modeDisplay.textContent = `現在のモード: 検知モード`;
        initialEPCsInClearanceMode.clear();
        detectedEPCsInClearanceMode.clear();
        revertToExpectedButton.style.display = 'none'; // 検知モードではボタン非表示
    }
    fetchData();
    intervalId = setInterval(fetchData, 1000);
    startButton.style.display = 'none';
    clearAndStartButton.style.display = 'none';
    clearButton.style.display = 'none';
    stopButton.style.display = 'inline';
    updateSummaryCounts(); // 集計情報を更新
});
// 定期読み込み停止
stopButton.addEventListener('click', () => {
    clearInterval(intervalId);
    startButton.style.display = 'inline';
    clearAndStartButton.style.display = 'inline';
    clearButton.style.display = 'inline';
    stopButton.style.display = 'none';
});
// リセットボタン
resetButton.addEventListener('click', () => {
    clearInterval(intervalId);
    startButton.style.display = 'inline';
    clearAndStartButton.style.display = 'inline';
    clearButton.style.display = 'inline';
    stopButton.style.display = 'none';
    currentSortColumn = -1;
    currentSortOrder = '';
    const tableBody = dataTable.querySelector('tbody');
    tableBody.innerHTML = '';
    originalTableRows.forEach(row => tableBody.appendChild(row.cloneNode(true)));
    // モード表示をリセット
    modeDisplay.textContent = `現在のモード: ${currentMode === 'detection' ? '検知モード' : '消込モード'}`;
    initialEPCsInClearanceMode.clear();
    detectedEPCsInClearanceMode.clear();
    updateSummaryCounts(); // 集計情報を更新
    revertToExpectedButton.style.display = currentMode === 'clearance' ? 'inline' : 'none'; // 消込モードなら表示
    tableLoadedAfterClearanceModeSwitch = false; // リセットでフラグもリセット
});
// CSVファイル to logファイル追加
clearButton.addEventListener('click', callClear2log);
// 画面クリア
clearDispButton.addEventListener('click', () => {
    const tableBody = dataTable.querySelector('tbody');
    tableBody.innerHTML = '';
    originalTableRows = [];
    initialEPCsInClearanceMode.clear();
    detectedEPCsInClearanceMode.clear();
    updateSummaryCounts(); // 集計情報を更新
    revertToExpectedButton.style.display = 'none'; // 画面クリア時は非表示
    initialTableStateForRevert = []; // 画面クリア時に状態もクリア
    tableLoadedAfterClearanceModeSwitch = false; // 画面クリアでフラグもリセット
});
// CSV形式に変換して保存
saveButton.addEventListener('click', saveTableAsCsv);
//表示されたtableから削除ボタンで行削除
function deleteRow(btn) {
    const row = btn.parentNode.parentNode;
    const epcToDelete = row.cells[2].textContent; // 削除される行のEPCを取得
    row.parentNode.removeChild(row);
    
    // originalTableRows からも削除
    originalTableRows = originalTableRows.filter(originalRow => originalRow.cells[2].textContent !== epcToDelete);
    // initialTableStateForRevert からも削除
    initialTableStateForRevert = initialTableStateForRevert.filter(item => item.epc !== epcToDelete);
    // initialEPCsInClearanceMode からも削除
    initialEPCsInClearanceMode.delete(epcToDelete);
    // detectedEPCsInClearanceMode からも削除
    detectedEPCsInClearanceMode.delete(epcToDelete);
    uniqueProducts.textContent = `ユニークなtag数: ${dataTable.querySelector('tbody').children.length}`;
    updateSummaryCounts(); // 集計情報を更新
}
// ソートボタンのイベントリスナー
document.querySelectorAll('.sort-button').forEach(button => {
    button.addEventListener('click', (event) => {
        const columnIndex = parseInt(event.target.dataset.column);
        const sortOrder = event.target.dataset.order;
        sortAndDisplayTable(columnIndex, sortOrder);
    });
});
// モード切り替えボタンのイベントリスナー
detectionModeButton.addEventListener('click', () => {
    if (currentMode !== 'detection') {
        if (intervalId) {
            clearInterval(intervalId);
            startButton.style.display = 'inline';
            clearAndStartButton.style.display = 'inline';
            clearButton.style.display = 'inline';
            stopButton.style.display = 'none';
            message.textContent = "表示を停止しました。";
        }
        currentMode = 'detection';
        modeDisplay.textContent = '現在のモード: 検知モード';
        // 消込モードで追加されたスタイルをリセット
        const tableBody = dataTable.querySelector('tbody');
        Array.from(tableBody.querySelectorAll('tr')).forEach(row => {
            row.classList.remove('cleared-row', 'red-text');
        });
        // 検知モードに戻る際にテーブルをクリアし、再度読み込み(必要であれば)
        tableBody.innerHTML = '';
        originalTableRows = []; // 検知モードではoriginalTableRowsは常に最新のCSVデータとなるためクリア
        initialEPCsInClearanceMode.clear();
        detectedEPCsInClearanceMode.clear();
        updateSummaryCounts(); // 集計情報を更新
        revertToExpectedButton.style.display = 'none'; // 検知モードではボタン非表示
        initialTableStateForRevert = []; // モード切り替えで状態もクリア
        tableLoadedAfterClearanceModeSwitch = false; // モード切り替えでフラグもリセット
    }
});
clearanceModeButton.addEventListener('click', () => {
    if (currentMode !== 'clearance') {
        if (intervalId) {
            clearInterval(intervalId);
            startButton.style.display = 'inline';
            clearAndStartButton.style.display = 'inline';
            clearButton.style.display = 'inline';
            stopButton.style.display = 'none';
            message.textContent = "表示を停止しました。";
        }
        currentMode = 'clearance';
        modeDisplay.textContent = '現在のモード: 消込モード';
        
        // 消込モードに切り替わる際にPCとEPC以外のデータをクリア
        const tableBody = dataTable.querySelector('tbody');
        // initialTableStateForRevert に現在のテーブルの状態を保存(PCとEPCのみ)
        initialTableStateForRevert = Array.from(tableBody.querySelectorAll('tr')).map(row => {
            return {
                pc: row.cells[1].textContent,
                epc: row.cells[2].textContent
            };
        });
        Array.from(tableBody.querySelectorAll('tr')).forEach(row => {
            // PC (cells[1]) と EPC (cells[2]) は残し、それ以外をクリア
            for (let i = 3; i < row.cells.length; i++) {
                row.cells[i].textContent = '';
            }
            row.classList.remove('cleared-row', 'red-text'); // スタイルをリセット
        });
        // 関連する状態変数をクリア
        initialEPCsInClearanceMode.clear();
        initialTableStateForRevert.forEach(item => initialEPCsInClearanceMode.add(item.epc));
        detectedEPCsInClearanceMode.clear();
        updateSummaryCounts(); // 集計情報を更新
        revertToExpectedButton.style.display = 'inline'; // 消込モードではボタン表示
        tableLoadedAfterClearanceModeSwitch = false; // モード切り替え時にフラグをリセット
    }
});
// テーブル読み込みボタンのイベントリスナー
loadTableButton.addEventListener('click', () => {
    fileInput.click(); // 隠されたファイル選択inputをクリック
});
fileInput.addEventListener('change', (event) => {
    const file = event.target.files[0];
    if (!file) {
        return;
    }
    const reader = new FileReader();
    reader.onload = (e) => {
        const csvText = e.target.result;
        const tableBody = dataTable.querySelector('tbody');
        tableBody.innerHTML = ''; // 現在のテーブルをクリア
        originalTableRows = []; // originalTableRowsもクリア
        const lines = csvText.split('\n');
        // ヘッダー行をスキップ (もしCSVにヘッダーがある場合)
        const dataLines = lines.slice(1); 
        dataLines.forEach(line => {
            const columns = line.split(',');
            if (columns.length >= 2) { // 最低でもPCとEPCが含まれていることを確認
                const row = document.createElement('tr');
                row.insertCell().innerHTML = '<button onclick="deleteRow(this)">削除</button>';
                row.insertCell().textContent = columns[0].trim(); // PC
                row.insertCell().textContent = columns[1].trim(); // EPC
                // ant0より右の列はクリアして追加
                for (let i = 0; i < 12; i++) { // ant0から最新検知時刻まで (インデックス3から)
                    row.insertCell().textContent = ''; // 列は空で追加
                }
                tableBody.appendChild(row);
                originalTableRows.push(row); // 読み込んだ行をoriginalTableRowsにも追加
            }
        });
        message.textContent = `「${file.name}」を読み込みました。`;
        uniqueProducts.textContent = `ユニークなtag数: ${tableBody.children.length}`;
        
        // 消込モードで表示開始する場合に備え、読み込んだEPCを初期リストに設定
        if (currentMode === 'clearance') {
            initialEPCsInClearanceMode.clear();
            Array.from(tableBody.querySelectorAll('tr')).forEach(row => {
                initialEPCsInClearanceMode.add(row.cells[2].textContent);
            });
            tableLoadedAfterClearanceModeSwitch = true; // テーブル読み込みが行われたことを示すフラグを立てる
        }
        updateSummaryCounts(); // 集計情報を更新
        revertToExpectedButton.style.display = currentMode === 'clearance' ? 'inline' : 'none'; // 消込モードなら表示
    };
    reader.readAsText(file);
});
// 予定表示にもどすボタンのイベントリスナー
revertToExpectedButton.addEventListener('click', () => {
    if (currentMode === 'clearance') {
        const tableBody = dataTable.querySelector('tbody');
        tableBody.innerHTML = ''; // 現在のテーブルをクリア
        
        let rowsToDisplay = [];
        if (tableLoadedAfterClearanceModeSwitch) {
            // [テーブル読み込み]が行われていたら、その内容に戻す
            rowsToDisplay = [...originalTableRows]; // originalTableRowsは[テーブル読み込み]で更新されているはず
            message.textContent = "テーブルを「テーブル読み込み」の内容に戻しました。";
        } else {
            // [テーブル読み込み]が行われていなければ、消込モードに切り替える直前の状態に戻す
            initialTableStateForRevert.forEach(item => {
                const row = document.createElement('tr');
                row.insertCell().innerHTML = '<button onclick="deleteRow(this)">削除</button>';
                row.insertCell().textContent = item.pc;
                row.insertCell().textContent = item.epc;
                // ant0より右の列はクリアして追加
                for (let i = 0; i < 12; i++) { // ant0から最新検知時刻まで (インデックス3から)
                    row.insertCell().textContent = '';
                }
                rowsToDisplay.push(row);
            });
            message.textContent = "テーブルを消込モード切り替え前の内容に戻しました。";
        }
        // 実際のテーブルに表示
        rowsToDisplay.forEach(row => {
            const clonedRow = row.cloneNode(true); // 行をクローンして追加
            // ant0より右の列をクリア
            for (let i = 3; i < clonedRow.cells.length; i++) {
                clonedRow.cells[i].textContent = '';
            }
            clonedRow.classList.remove('cleared-row', 'red-text'); // スタイルをリセット
            tableBody.appendChild(clonedRow);
        });
        // 関連する状態をリセット
        initialEPCsInClearanceMode.clear();
        Array.from(tableBody.querySelectorAll('tr')).forEach(row => {
            initialEPCsInClearanceMode.add(row.cells[2].textContent);
        });
        detectedEPCsInClearanceMode.clear();
        updateSummaryCounts();
        modeDisplay.textContent = `現在のモード: 消込モード`; // 消込終了時刻表示をリセット
        tableLoadedAfterClearanceModeSwitch = false; // フラグをリセット
    }
});
// 初期表示時に集計情報を更新
updateSummaryCounts();
</script>
</body>
</html>

test8.htmlの画面例

機能説明

【検知モード】(デフォルト)

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

[CSVファイルクリア to log ファイル&表示開始] : 対象CSVファイルをクリアしてから表の更新の開始

[テーブル保存] : 表をローカルパソコンにダウンロード保存します

[画面表示クリア] : 表示されているテーブルの内容をクリアします

【消込モード】

基本的に検知モードと、同じです。検知したら、背景が濃いグレーになります。

※予定外は赤字で表示されます。

[テーブル読み込み] : 予定となるデータを読み込みします。

[予定表示にもどす] : 検知モードをクリックしたとき、あるいはテーブル読み込みを行ったときの予定の内容に表の内容をもどします

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

表頭の▲▼クリックで昇順降順にソートできます。

まとめ

UHF帯RFIDは、HF帯にくらべて、遠方の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通過方向検知装置