PN532を使ってArduinoでFeliCa学生証を読む方法

# まだidmで消耗してるの？

　会津大学には、至る所に学生証で入室ログを取る装置があるけど、どれも初回は自分の学籍をキーボードから登録する必要があった。
　でもさ、学生証の中に「学籍番号」くらいさすがに保存されてるだろ(笑)
　というわけで、ICタグの識別情報であるidmからじゃなくて、中のバイナリを読み出して学生証のデータを抽出できないか検証してみました！

# PN532の購入方法

Amazonで売っているPN532のモジュールはお高めです。Aitendoから買うといいでしょう。私はそうしました。
http://www.aitendo.com/product/16607

少しでも安く買いたいって人は、Aliexpressでの購入をおすすめします。
ただし、中国からの配送で２週間〜１ヶ月程度かかります。
https://www.aliexpress.com/item/1Set-GREATZT-PN532-NFC-RFID-Wireless-Module-V3-User-Kits-Reader-Writer-Mode-IC-S50-Card/32859551116.html

# PN532の接続

　ご丁寧にも、PN532は３つ(HSU/I2C/SPI)の接続方法を用意しています。
Arduino Unoと接続するわけですが、まあ、一番ラクそうなI2C使うのが妥当でしょう。I2Cなら４箇所のはんだ付けだけで済みます！

SDAはAnalog4ピンに、SCLはAnalog5ピンに接続してください。
![pn532_arduino.png](https://qiita-image-store.s3.amazonaws.com/0/293301/08144511-c681-a8bf-4eef-f74465af968e.png)


ちなみにFaBoを使っている場合は、ピン配置に互換があるのでこんな感じで接続すればいいです。
![pn532_fabo.png](https://qiita-image-store.s3.amazonaws.com/0/293301/a2e2a7b3-4694-e001-731e-83eab8fe168c.png)

# とりあえずデモを動かす

まずは、サンプルソースを以下のサイトから落として、Arduinoのライブラリフォルダに突っ込みます。
https://github.com/elechouse/PN532
そしたらArduinoの`File -> Examples -> PN532 -> FeliCa_card_read`を開きます。
今回はI2C接続ですので、上部のマクロの数字部分どちらも`#if 0` `#elif 0`って感じにして`#else`にあるI2Cが使用されるようにします。

あとは書き込んでシリアルモニタを起動してください。(ボーレートは115200)
こんな感じで読んでくれたら成功です！

```
Hello!
Found chip PN532
Firmware ver. 1.6
Waiting for an FeliCa card...  Found a card!
  IDm:  01 16 04 XX XX XX XX XX
  PMm:  03 32 42 XX XX XX XX XX
  System Code: 809E
Write Without Encryption command 
   Writing current millis ( 00 00 14 1C ) to Block 0 -> error
Read Without Encryption command -> error
Card access completed!

Waiting for an FeliCa card...  
```

# 学籍を取得してみる

それでは、学籍の取得です。
といっても、学籍がどこに保存されてるか分からないので、とりあえずカード内の情報を全部表示してみましょう。

格納箇所の特定にはこのアプリが便利です。
https://play.google.com/store/apps/details?id=at.mroland.android.apps.nfctaginfo&hl=ja

このアプリでカードを読んでポチポチ触ってると、
<img src="https://qiita-image-store.s3.amazonaws.com/0/293301/676f6b1a-4819-7bbe-af13-bad53f9fc34c.png" width="25%"><img src="https://qiita-image-store.s3.amazonaws.com/0/293301/41153075-ff3c-1e35-4eaf-21a9bb533464.png" width="25%"><img src="https://qiita-image-store.s3.amazonaws.com/0/293301/c3515adf-b3da-d6c3-e8d6-b82d12a4d762.png" width="25%">


サービスコードが`0x1A8B`。4ブロックあるのでブロックリストは`0x8000`から`0x8003`まで取得すればいいことがわかります。

というわけでリード周りの行をこんな感じで書き直したのですが

```c

  Serial.print("Read Without Encryption command -> ");
  serviceCodeList[0] = 0x1A8B;
  
  blockList[0] = 0x8000;
  blockList[1] = 0x8001;
  blockList[2] = 0x8002;
  blockList[3] = 0x8003;
  ret = nfc.felica_ReadWithoutEncryption(1, serviceCodeList, 4, blockList, blockData);
  if (ret != 1)
  {
    Serial.println("error");
  } else {
    Serial.println("OK!");
    for(int i=0; i<3; i++ ) {
      Serial.print("  Block no. "); Serial.print(i, DEC); Serial.print(": ");
      nfc.PrintHex(blockData[i], 16);
    }
  }
```
出力結果が`Could not receive response`に。
よくわからないので、blockSizeを1にして学籍そのものが含まれたブロックだけ取得できるように変更すると、
`41 00 1D 07 11 16 04 00 33 19 AF 05 00 00 01 32 30 32 32 30 33 33 31 37 30 FF FF FF FF FF FF`
なぜか最後の6バイトが正しく取得できていません。
もしかしてI2Cの速度の限界かと思い、SPIでつなぎ直してみるとちゃんと読み取りました！！！

というわけで、図のように接続しなおし、コード上部のマクロを`#if 1`に戻しください。ここからはSPIでやりましょう。
![pn532_spi.png](https://qiita-image-store.s3.amazonaws.com/0/293301/a98338ef-2cea-7fe9-5489-a5d8bab0d564.png)

# 学籍を抽出する

せっかくなので、LCDに表示してみました！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/293301/f1d1f8f0-6868-2bd6-324a-589a5ae9b7ce.png)


最終的にできあがったコードはこんな感じです

```c
/**************************************************************************/
/*!
    This example will attempt to connect to an FeliCa
    card or tag and retrieve some basic information about it
    that can be used to determine what type of card it is.

    Note that you need the baud rate to be 115200 because we need to print
    out the data and read from the card at the same time!

    To enable debug message, define DEBUG in PN532/PN532_debug.h

 */
/**************************************************************************/
#include <Arduino.h>

#include <SPI.h>
#include <PN532_SPI.h>
#include <PN532.h>

// include the library code:
#include <Wire.h>
#include <FaBoLCD_PCF8574.h>

// initialize the library
FaBoLCD_PCF8574 lcd;

PN532_SPI pn532spi(SPI, 10);
PN532 nfc(pn532spi);


#include <PN532_debug.h>

uint8_t        _prevIDm[8];
unsigned long  _prevTime;

void PrintHex8(const uint8_t d) {
  Serial.print(" ");
  Serial.print( (d >> 4) & 0x0F, HEX);
  Serial.print( d & 0x0F, HEX);
}

void setup(void)
{
  Serial.begin(115200);
  Serial.println("Hello!");

  nfc.begin();

  uint32_t versiondata = nfc.getFirmwareVersion();
  if (!versiondata)
  {
    Serial.print("Didn't find PN53x board");
    while (1) {delay(10);};      // halt
  }

  // Got ok data, print it out!
  Serial.print("Found chip PN5"); Serial.println((versiondata >> 24) & 0xFF, HEX);
  Serial.print("Firmware ver. "); Serial.print((versiondata >> 16) & 0xFF, DEC);
  Serial.print('.'); Serial.println((versiondata >> 8) & 0xFF, DEC);

  // Set the max number of retry attempts to read from a card
  // This prevents us from waiting forever for a card, which is
  // the default behaviour of the PN532.
  nfc.setPassiveActivationRetries(0xFF);
  nfc.SAMConfig();

  // set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.print("\xb1\xc5\xc0\xc9 \xb6\xde\xb8\xbe\xb7\x3a");
  
  memset(_prevIDm, 0, 8);
}

void loop(void)
{
  uint8_t ret;
  //uint16_t systemCode = 0xFFFF;
  uint16_t systemCode = 0xFE00;
  uint8_t requestCode = 0x01;       // System Code request
  uint8_t idm[8];
  uint8_t pmm[8];
  uint16_t systemCodeResponse;

  // Wait for an FeliCa type cards.
  // When one is found, some basic information such as IDm, PMm, and System Code are retrieved.
  Serial.print("Waiting for an FeliCa card...  ");
  ret = nfc.felica_Polling(systemCode, requestCode, idm, pmm, &systemCodeResponse, 5000);

  if (ret != 1)
  {
    Serial.println("Could not find a card");
    delay(500);
    return;
  }

  if ( memcmp(idm, _prevIDm, 8) == 0 ) {
    if ( (millis() - _prevTime) < 3000 ) {
      Serial.println("Same card");
      delay(500);
      return;
    }
  }

  Serial.println("Found a card!");
  Serial.print("  IDm: ");
  nfc.PrintHex(idm, 8);
  Serial.print("  PMm: ");
  nfc.PrintHex(pmm, 8);
  Serial.print("  System Code: ");
  Serial.print(systemCodeResponse, HEX);
  Serial.print("\n");

  memcpy(_prevIDm, idm, 8);
  _prevTime = millis();

  uint8_t blockData[3][16];
  uint16_t serviceCodeList[1];
  uint16_t blockList[3];

  memset(blockData[0], 0, 16);

  Serial.print("Read Without Encryption command -> ");
  serviceCodeList[0] = 0x1A8B;
  
  blockList[0] = 0x8000;
  blockList[1] = 0x8002;
  blockList[2] = 0x8003;
  ret = nfc.felica_ReadWithoutEncryption(1, serviceCodeList, 3, blockList, blockData);
  if (ret != 1)
  {
    Serial.println("error");
    lcd.setCursor(0, 1);
    lcd.print("ERROR");
  } else {
    Serial.println("OK!");
    for(int i=0; i<3; i++ ) {
      Serial.print("  Block no. "); Serial.print(i, DEC); Serial.print(": ");
      nfc.PrintHex(blockData[i], 16);
    }
    lcd.setCursor(0, 1);
    lcd.print((const char *)blockData[0]);
  }

  // Wait 1 second before continuing
  Serial.println("Card access completed!\n");
  delay(1000);
}
```

これからWi-Fiに接続して、入室ログシステムとか作ろうと思ってますが、今日はとりあえずこのへんで。