Arduino Uno R4 WiFiで RFID を使ったモノの貸し出し記録システムを作ってみた

オフィスや現場では、工具、検査機器、カメラ備品、三脚、鍵、測定器のように、「共有で使うけれど、誰が持ち出したか分かりにくいもの」が意外と多くあります。紙の貸出台帳でも運用はできますが、記入漏れや転記の手間があり、あとから確認したいときに少し不便です。

そこで今回は、Arduino Uno R4 WiFiMFRC522 RFID モジュール を使って、社員カードを先にかざして利用者を認識し、そのあと借りる物に付けたタグをかざして貸し借りを記録する簡易ソリューションを作ってみました。

今回の PoC では、まず ローカルで「誰が」「何を」借りたかをその場で記録して表示することを目的にしています。将来的には、Uno R4 WiFi の無線機能を使って、MQTT やクラウド、Web ダッシュボードへ拡張しやすい形を意識しています。Arduino Uno R4 WiFi は、メイン MCU に Renesas RA4M1、無線機能に ESP32-S3 を搭載しており、Arduino Cloud とも互換性があります。

注意
今回の構成は UID ベースの簡易識別 PoC です。
社員カードそのものに個人情報を書き込むのではなく、読み取った UID とアプリ側の登録情報を対応付ける前提で実装しています。
社内の備品管理や小規模な貸出確認には分かりやすい一方で、高セキュリティ用途ではより厳密な認証・権限設計が必要です。

目次

はじめに:貸した相手と貸した物を紐付けたい

共有備品の管理では、単に「タグを読める」だけでは足りず、誰が借りたのか何を借りたのかを順番に記録できることが重要です。特に、工具や撮影機材、検査用の治具などは、持ち出しの履歴が分かるだけでも運用がかなり楽になります。

そこで今回は、以下のような流れを取ることにしました。

1. まず 社員カード をかざして利用者を認識
2. 次に 物品タグ をかざして対象物を認識
3. その組み合わせで 貸出記録 を作成
4. もし同じ利用者が同じ物品を再度読んだ場合は 返却 として扱う

これにより、1台の RFID リーダーでも、利用者認識 → 物品認識の順番を作ることで、貸し借りの記録を分かりやすく扱えます。

ローカル記録の基本構成

まずは基本となる、RFID で社員カードと物品タグを順に読み取り、OLED とシリアルへ記録結果を表示するシステムを構築しました。

使用した主な部品

・Arduino Uno R4 WiFi
・MFRC522 RFID モジュール
・RFID カード / タグ
・0.96インチ OLED ディスプレイ(SSD1306 系 / I2C)
・ブレッドボード
・ジャンパーピン

Arduino Uno R4 WiFiの特徴

Arduino Uno R4 WiFi は、48MHz 動作の Renesas RA4M1(Arm Cortex-M4、256KB Flash、32KB SRAM)を搭載し、さらに ESP32-S3 による Wi‑Fi / Bluetooth 接続に対応しています。加えて、RTC や Arduino Cloud 互換性も持っており、ローカル PoC から将来のログ送信・可視化まで広げやすい構成です。

今回の貸出記録 PoC では、まずはローカル表示とシリアルログにとどめていますが、将来的には 日時付きログの蓄積、クラウド連携、ダッシュボード表示 といった発展がしやすいのも、このボードを使うメリットです。
RA4M1 - 48MHz Arm Cortex-M4 と LCDコントローラおよびHMI用静電容量式タッチセンサ搭載 32 ビットマイクロコントローラ|Renesas

今回のポイント:社員カード → 物品タグの二段階スキャン

今回の構成で重要なのは、同じ RFID リーダーで「人」と「物」を順番に読むことです。

流れは次の通りです。

・待機中は 社員カード待ち
・登録済み社員カードを読んだら、次は 物品タグ待ち
・登録済み物品タグを読んだら、貸出または返却を判定
・記録後は再び 社員カード待ち に戻る

この方式なら、リーダーを 1 台に抑えつつ、

・誰が借りたか
・何を借りたか
・返却か貸出か

を順番付きで扱えます。

また、今回使う MFRC522 ライブラリ は、RC522 ベースの RFID リーダーを SPI 経由で扱う Arduino 用ライブラリとして提供されています。

開発環境と実装のポイント

開発環境には Visual Studio CodePlatformIO IDE を使いました。PlatformIO では、Arduino Uno R4 WiFi のボード設定として board = uno_r4_wifi、プラットフォームとして renesas-raを使えます。

RFID 読み取りの考え方

今回の PoC では、読み取った UID を以下の 2 種類に分けて扱います。

・社員カード一覧
物品タグ一覧

待機状態が「社員カード待ち」なのか「物品タグ待ち」なのかによって、同じ RFID リーダーでも読み取り対象の意味を切り替えています。

OLED 表示の役割

今回は OLED に以下のような状態を表示します。

Scan employee
User OK / Scan item
BORROW OK
RETURN OK
Unknown card
Held by xxxx

SSD1306 系 OLED を Arduino で使う場合、一般的には Adafruit_SSD1306Adafruit_GFX を組み合わせる構成が使われます。

シリアルログの役割

PoC では、貸出・返却イベントを シリアルモニタに CSV 風の形式で出力します。これにより、

・開発中のデバッグがしやすい
・後で PC 側に取り込みやすい
・将来 MQTT や Web API に差し替えやすい

という利点があります。

配線情報

各モジュールを以下のように配線します。

配線表

モジュール

信号

接続先

補足

MFRC522 RFID

SDA(SS)

Arduino Uno R4 WiFi の D10

SPI の SS として使用します

MFRC522 RFID

SCK

Arduino Uno R4 WiFi の D13

SPI クロックです

MFRC522 RFID

MOSI

Arduino Uno R4 WiFi の D11

SPI 書き込み線です

MFRC522 RFID

MISO

Arduino Uno R4 WiFi の D12

SPI 読み取り線です

MFRC522 RFID

RST

 Arduino Uno R4 WiFi の D9

RFID モジュールのリセットです

MFRC522 RFID

3.3V / GND

Arduino の 3.3V / GND

MFRC522 は 3.3V 系として扱います

OLED (SSD1306系)

SDA

Arduino Uno R4 WiFi の SDA

I2C 通信で使用します

OLED (SSD1306系)

SCL

Arduino Uno R4 WiFi の SCL

I2C 通信で使用します

OLED (SSD1306系)

VCC

3.3V または 5V

モジュール仕様を確認して接続します

OLED (SSD1306系)

GND

Arduino の GND

共通 GND にします

回路上のイメージ

待機中 
 OLED: Scan employee
社員カード認識後 
 OLED: User OKScan item
物品タグ認識後 
 貸出または返却を判定し、OLED とシリアルに結果を表示
記録完了後 
 再び Scan employee に戻る

実際のプログラム

今回のローカル記録版で使用したプログラムは以下の通りです。 
社員カードを先に読み、そのあと物品タグを読んで、貸出・返却を判定します。 
ログはシリアルモニタにも出力されます。

main.cpp

#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <MFRC522.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SS_PIN   10
#define RST_PIN   9

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define OLED_ADDR    0x3C

MFRC522 rfid(SS_PIN, RST_PIN);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

enum ScanState {
  WAIT_EMPLOYEE,
  WAIT_ASSET
};

ScanState scanState = WAIT_EMPLOYEE;

struct Employee {
  byte uid[4];
  const char* name;
};

struct Asset {
  byte uid[4];
  const char* name;
  bool borrowed;
  int borrowerIndex;              // -1 なら未貸出
  unsigned long lastActionMs;
};

// ===== 社員カード登録例 =====
Employee employees[] = {
  {{0xDE, 0xAD, 0xBE, 0xEF}, "SATO"},
  {{0x12, 0x34, 0x56, 0x78}, "YAMADA"}
};
const int employeeCount = sizeof(employees) / sizeof(employees[0]);

// ===== 物品タグ登録例 =====
Asset assets[] = {
  {{0xAA, 0xBB, 0xCC, 0x01}, "TRIPOD", false, -1, 0},
  {{0xAA, 0xBB, 0xCC, 0x02}, "CAMERA_BATT", false, -1, 0},
  {{0xAA, 0xBB, 0xCC, 0x03}, "TOOL_BOX", false, -1, 0}
};
const int assetCount = sizeof(assets) / sizeof(assets[0]);

int activeEmployeeIndex = -1;
unsigned long employeeScanTime = 0;
const unsigned long employeeTimeoutMs = 15000; // 15秒以内に物品タグを待つ
unsigned long logSequence = 0;

bool uidMatches(MFRC522::Uid* uid, const byte targetUid[4]) {
  if (uid->size != 4) return false;

  for (byte i = 0; i < 4; i++) {
    if (uid->uidByte[i] != targetUid[i]) {
      return false;
    }
  }
  return true;
}

int findEmployeeIndex(MFRC522::Uid* uid) {
  for (int i = 0; i < employeeCount; i++) {
    if (uidMatches(uid, employees[i].uid)) {
      return i;
    }
  }
  return -1;
}

int findAssetIndex(MFRC522::Uid* uid) {
  for (int i = 0; i < assetCount; i++) {
    if (uidMatches(uid, assets[i].uid)) {
      return i;
    }
  }
  return -1;
}

void showScreen(const char* line1, const char* line2 = "", const char* line3 = "", const char* line4 = "") {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);

  display.setCursor(0, 0);
  display.println(line1);
  display.setCursor(0, 16);
  display.println(line2);
  display.setCursor(0, 32);
  display.println(line3);
  display.setCursor(0, 48);
  display.println(line4);

  display.display();
}

void showIdleScreen() {
  showScreen("RFID Lending Log", "Scan employee", "", "");
}

void resetToEmployeeScan() {
  scanState = WAIT_EMPLOYEE;
  activeEmployeeIndex = -1;
  employeeScanTime = 0;
  showIdleScreen();
}

void printUid(MFRC522::Uid* uid) {
  Serial.print("UID:");
  for (byte i = 0; i < uid->size; i++) {
    Serial.print(uid->uidByte[i] < 0x10 ? " 0" : " ");
    Serial.print(uid->uidByte[i], HEX);
  }
  Serial.println();
}

void logEvent(const char* action, const char* employeeName, const char* assetName, unsigned long stampMs) {
  logSequence++;
  Serial.print("LOG,");
  Serial.print(logSequence);
  Serial.print(",");
  Serial.print(action);
  Serial.print(",");
  Serial.print(employeeName);
  Serial.print(",");
  Serial.print(assetName);
  Serial.print(",");
  Serial.println(stampMs);
}

void handleEmployeeCard(int employeeIndex) {
  activeEmployeeIndex = employeeIndex;
  scanState = WAIT_ASSET;
  employeeScanTime = millis();

  Serial.print("Employee recognized: ");
  Serial.println(employees[employeeIndex].name);

  showScreen(
    "Employee OK",
    employees[employeeIndex].name,
    "Scan item tag",
    "within 15 sec"
  );
}

void handleAssetTag(int assetIndex) {
  Asset& item = assets[assetIndex];
  const char* employeeName = employees[activeEmployeeIndex].name;
  unsigned long now = millis();

  if (!item.borrowed) {
    // 未貸出 → 貸出
    item.borrowed = true;
    item.borrowerIndex = activeEmployeeIndex;
    item.lastActionMs = now;

    logEvent("BORROW", employeeName, item.name, now);

    Serial.print("BORROW OK: ");
    Serial.print(employeeName);
    Serial.print(" -> ");
    Serial.println(item.name);

    showScreen(
      "BORROW OK",
      employeeName,
      item.name,
      "saved to log"
    );
    delay(1200);
    resetToEmployeeScan();
    return;
  }

  if (item.borrowerIndex == activeEmployeeIndex) {
    // 同じ人が持っている → 返却
    item.borrowed = false;
    item.borrowerIndex = -1;
    item.lastActionMs = now;

    logEvent("RETURN", employeeName, item.name, now);

    Serial.print("RETURN OK: ");
    Serial.print(employeeName);
    Serial.print(" -> ");
    Serial.println(item.name);

    showScreen(
      "RETURN OK",
      employeeName,
      item.name,
      "saved to log"
    );
    delay(1200);
    resetToEmployeeScan();
    return;
  }

  // 他の人が借用中
  const char* holderName = employees[item.borrowerIndex].name;

  Serial.print("DENY: ");
  Serial.print(item.name);
  Serial.print(" is held by ");
  Serial.println(holderName);

  showScreen(
    "Already borrowed",
    item.name,
    "Held by:",
    holderName
  );
  delay(1500);
  resetToEmployeeScan();
}

void setup() {
  Serial.begin(115200);

  SPI.begin();
  rfid.PCD_Init();

  Wire.begin();
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    while (true) {
      // OLED初期化失敗時は停止
    }
  }

  showScreen("RFID Lending Log", "Initializing...", "", "");
  delay(1000);
  showIdleScreen();
}

void loop() {
  // 社員認識後、一定時間物品タグが来なければタイムアウト
  if (scanState == WAIT_ASSET && millis() - employeeScanTime > employeeTimeoutMs) {
    Serial.println("Timeout: item tag was not scanned");
    showScreen("Timeout", "Scan employee", "again", "");
    delay(1000);
    resetToEmployeeScan();
  }

  if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
    return;
  }

  printUid(&rfid.uid);

  if (scanState == WAIT_EMPLOYEE) {
    int employeeIndex = findEmployeeIndex(&rfid.uid);

    if (employeeIndex >= 0) {
      handleEmployeeCard(employeeIndex);
    } else {
      Serial.println("Unknown employee card");
      showScreen("Unknown card", "Not employee", "Scan employee", "again");
      delay(1000);
      resetToEmployeeScan();
    }
  } else {
    int assetIndex = findAssetIndex(&rfid.uid);

    if (assetIndex >= 0) {
      handleAssetTag(assetIndex);
    } else {
      Serial.println("Unknown asset tag");
      showScreen("Unknown tag", "Not registered", "Scan item", "again");
      delay(1000);

      // 社員は有効なままにして、物品タグの再スキャンを待つ
      showScreen(
        "Employee OK",
        employees[activeEmployeeIndex].name,
        "Scan item tag",
        "within 15 sec"
      );
    }
  }

  rfid.PICC_HaltA();
  rfid.PCD_StopCrypto1();
}

platformio.ini

[env:uno_r4_wifi]
platform = renesas-ra
board = uno_r4_wifi
framework = arduino
monitor_speed = 115200

lib_deps =
    miguelbalboa/MFRC522
    adafruit/Adafruit SSD1306
    adafruit/Adafruit GFX Library

PlatformIO では Arduino Uno R4 WiFi に対して platform = renesas-raboard = uno_r4_wifiを指定します。

また、MFRC522 ライブラリは RC522 ベースの RFID リーダーを SPI 接続で扱う Arduino ライブラリです。

待ち受け状態

待ち受け状態

社員証をかざした状態

社員証をかざした状態

レンタルするもののタグをかざした状態

レンタルするもののタグをかざした状態

シリアルモニターには以下のようなログが出力されます。

シリアルモニターログ

RFID を使ったモノの貸し出し記録システム.mp4

RFID 登録方法

今回の PoC では、社員カードと物品タグの登録は「UID を読み取ってソースコードに追加する方式」を採用しました。

登録の流れ

1. 登録したい RFID カード / タグの UID を読み取る
2. シリアルモニタで UID を確認する
3. 社員カードなら employees[] に追加する
4. 物品タグなら assets[] に追加する
5. 再ビルドして Arduino へ書き込む

UID 確認用プログラム

cpp

#include <Arduino.h>
#include <SPI.h>
#include <MFRC522.h>

#define SS_PIN  10
#define RST_PIN 9

MFRC522 rfid(SS_PIN, RST_PIN);

void setup() {
  Serial.begin(115200);
  SPI.begin();
  rfid.PCD_Init();

  Serial.println("Scan RFID card/tag...");
}

void loop() {
  if (!rfid.PICC_IsNewCardPresent()) return;
  if (!rfid.PICC_ReadCardSerial()) return;

  Serial.print("UID:");
  for (byte i = 0; i < rfid.uid.size; i++) {
    Serial.print(rfid.uid.uidByte[i] < 0x10 ? " 0" : " ");
    Serial.print(rfid.uid.uidByte[i], HEX);
  }
  Serial.println();

  rfid.PICC_HaltA();
  rfid.PCD_StopCrypto1();

  delay(1000);
}

社員カード登録例

cpp

Employee employees[] = {
  {{0xDE, 0xAD, 0xBE, 0xEF}, "SATO"},
  {{0x12, 0x34, 0x56, 0x78}, "YAMADA"}
};

物品タグ登録例

cpp

Asset assets[] = {
  {{0xAA, 0xBB, 0xCC, 0x01}, "TRIPOD", false, -1, 0},
  {{0xAA, 0xBB, 0xCC, 0x02}, "CAMERA_BATT", false, -1, 0},
  {{0xAA, 0xBB, 0xCC, 0x03}, "TOOL_BOX", false, -1, 0}
};

登録方式の注意点

この方式は PoC としては分かりやすい一方で、新しい社員カードや物品タグを追加するたびにソースコード更新と再書き込みが必要です。そのため、実運用を考える場合は、次のような発展が考えられます。

・管理者カードで入る 登録モード を作る
・一覧を不揮発メモリへ保存する
・Wi‑Fi 経由で登録情報や貸出状況を管理する

今後の展望(改善案)

今回の試作は、まず「社員カード → 物品タグ」という二段階スキャンで貸し借り記録ができることを主目的にしたシンプルな構成ですが、実運用を考えるとさらに改善の余地があります。

RTC を使った実時刻記録 
 今回は millis() ベースの簡易ログですが、将来的には日時付きの履歴にしたい
Wi‑Fi 連携
 MQTT や Web API で貸出履歴をサーバへ送信する
ダッシュボード表示 
 現在貸し出されている物品一覧を Web で見られるようにする
登録モード 
 社員カードや物品タグを現場で追加・削除できるようにする
権限レベル 
 管理者だけ登録できる、特定物品は特定社員のみ貸出可能、といった制御を加える
ログの永続化
 EEPROM / FRAM / SD カード / クラウド等に履歴を保存する

Arduino Uno R4 WiFi は ESP32-S3 による Wi‑Fi / BluetoothRTC を備えているため、この種の備品管理 PoC をネットワーク型の管理ツールへ発展させやすいボードです。
WiFi/BLE-MCU(RA6シリーズ)
WiFi/BLEモジュール(RRQ61051)

まとめ

今回は、Arduino Uno R4 WiFiMFRC522 RFID モジュール を使って、社員カードを先にかざして利用者を認識し、そのあと物品タグをかざして貸し借りを記録するソリューションを作ってみました。

構成自体はシンプルですが、

・社員カードで利用者を認識する 
・物品タグで対象物を認識する
・貸出 / 返却 / 貸出不可を判定する
・OLED とシリアルに結果を表示する

という流れを通して、共有物の貸し借りをその場で記録できる PoCとしてまとめられる題材でした。

まずはローカルの簡易記録から始め、そこから時刻付きログ、Wi‑Fi 連携、ダッシュボード、登録モードへ広げていける点も、このテーマの面白さだと感じています。

参考リンク

Arduino UNO R4 WiFi 公式ドキュメント
PlatformIO: Arduino Uno R4 WiFi ボード情報
MFRC522 Arduino RFID Library
Adafruit: Monochrome OLED Breakouts / Arduino Library & Examples
RA4M1-48MHz Arm Cortex-M4 と LCDコントローラおよびHMI用静電容量式タッチセンサ搭載 32 ビットマイクロコントローラ
WiFi/BLE-MCU(RA6シリーズ)
WiFi/BLEモジュール(RRQ61051)