友達の研究室にArduinoで動くカードキーを設置した話

[paiza](https://paiza.jp/) Advent Calender 2021 ([URL](https://adventar.org/calendars/6343)) 8 日目
兼
[木更津高専 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nit_kisarazu) 9 日目
を担当する imaimai17468 です。paizaでは[paizaラーニング](https://paiza.jp/sign_in)の学生アルバイトをしています。

両カレンダーの日付を気にせず登録した結果、ニアピンしてしまったのでこの記事をまとめて充てることにしました。

paiza Advent Calender 2021 の前の記事は xryuseix さんの [paizaの問題文用に作成した文章校正ツールProofLeaderの仕組み](https://qiita.com/xryuseix/items/0328b912d77d2401e8a4) です。
次の記事は malleroid さんの [GPT-3 + GASで作るSlackbot](https://qiita.com/malleroid/items/36def200eadfce51c5f2) です。

木更津高専 Advent Calendar 2021 の前の記事は 
次の記事は rime_math さんの [複素数を利用した三角関数の積分](https://qiita.com/rime_math/items/e96d41ad93bed40ae56a) です。

# 動機（大まかな流れ）
A くんの研究室のボス
`A くん（友人）、研究室のドアにカードキー作ってよ！！`
A くん
`えぇ…`
ぼく
`作る！！！！！！！！`

後は学校に自分の作った何かを残したかったのと、経験を増やすのにいいかなと思ったので作ることにしました。

# 使うもの

| 材料 | 値段 (だいたい) |
|------|------|
| [Arduino Mega 2560](https://www.amazon.co.jp/dp/B06Y4KZV1J/ref=cm_sw_r_tw_dp_XBV1WFBJGND9N8MNKZDT?_encoding=UTF8&psc=1) | 1800 円 |
| [FeliCa リーダー・ライター RC-S620S](https://www.switch-science.com/catalog/353/) | 3700 円 |
| [FeliCa RC-S620S/RC-S730 ピッチ変換基板](https://www.switch-science.com/catalog/1029/) | 550 円 |
| [20x4 LCDディスプレイ (I2C対応のもの) ](https://www.amazon.co.jp/dp/B010SHNMW2/ref=cm_sw_r_tw_dp_152PN1JW38ZY01F82WSW?_encoding=UTF8&psc=1) | 1250 円 |
| [Micro SDカードメモリシールドモジュール](https://www.amazon.co.jp/dp/B010GXAFFU/ref=cm_sw_r_tw_dp_Y8K4MB0Y7258M5019NFM?_encoding=UTF8&psc=1) | 650 円 |
| [リアルタイムクロックモジュール (DS3231)](https://www.amazon.co.jp/dp/B01H5NAFUY/ref=cm_sw_r_tw_dp_N8T62F72Z483KZHA2HNE?_encoding=UTF8&psc=1) | 750 円 |
| [リアルタイムクロックモジュール用のボタン電池](https://www.amazon.co.jp/dp/B07SYYWTQM/ref=cm_sw_r_tw_dp_M56Z6C0EPDW77XHJZPRR) | ものによってまちまち |
| [DCアダプタ](https://www.amazon.co.jp/dp/B0798JTWTD/ref=cm_sw_r_tw_dp_W2RB1H44EPNKQX1FQ5ZR) | 1500 円 |
| [実装したい基板やブレッドボード](https://www.amazon.co.jp/dp/B07DRG2LN2/ref=cm_sw_r_tw_dp_EDYZZWXCN2R8WYD5WRJA) | ものによってまちまち |
| [サーボモーター](https://www.amazon.co.jp/dp/B01LXJ8Y0Z/ref=cm_sw_r_tw_dp_W1WKJ1T3J05R8435JGWS) | 900 円 |
| タクトスイッチとLED | それぞれ何十個セットで 500 円 |
| FeliCaカード | SuicaやFeliCaが使えるスマホがあればプレイスレス |

値段をみてわかると思いますが、ワンチャン**Amazonでカードキーとかスマートロックを買った方が安上がりです。**
経験やらやりがいを含めて等価交換ということで作っていきます。

## 注意点
* Arduino Mega 2560 は互換品で構いません。
* RC-S620Sとピッチ変換基板はフラットケーブルで接続します。URLのものは付属していますが、もっと長いものが欲しい場合は、6ピン0.5mmピッチのAタイプのフラットケーブルを買いましょう。（間違えてBタイプを買ってしまった人）
* LCDのサイズは何でもいいです。ただI2Cのものじゃないと配線がとても面倒くさいので注意してください。
* ボタン電池はCR2032です。LIR2032でもいいですが、これを使うとDS3231が爆発する可能性が何とかで、怖いのでCRのほうにしました。
* 電源にはDCアダプタで9Vを供給します。電池でもいいですが、あまり長持ちしません。
* サーボモーターはSG5010を使いました。トルクが強いやつがいいです。鍵によりますが、少なくともドアノブの鍵はSG90程度では回りません。
* 実装するのは基板でもブレッドボードでもどっちでも構いません。自分は出来栄えが綺麗になるようにシールドとして実装したかったので、KEYSTUDIOのArduino Mega 2560用のPCBボードを買いました。
* シールドとして作る場合は足の長いピンヘッダも用意しましょう。
* タクトスイッチとLEDは、後々FeliCaカードの自動登録などの機能を追加するときに必要になったので付けました。

# 各部品の接続図
![kairozu_v2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481328/02b05ae3-a6b2-c76d-d45f-cae808bcb4ee.png)

これだとどれとどれを接続すればいいのかわかりづらいと思うので以下に表で示します。
タクトスイッチとLEDは動けばどこでもいいです。

| Arduino Mega 2560 | ピッチ変換基板 |
|:-----------------:|:-------------:|
| 5V                | VDD           |
| GND               | GND           |
| TX1(18)           | RXD           |
| RX1(19)           | TXD           |

| Arduino Mega 2560 | LCD           |
|:-----------------:|:-------------:|
| 5V                | VDD           |
| GND               | GND           |
| SDA(20)           | SDA           |
| SC+(21)           | SCL           |

| Arduino Mega 2560 | RTC DS3231    |
|:-----------------:|:-------------:|
| 5V                | VCC           |
| GND               | GND           |
| TX1(18)           | SDA           |
| RX1(19)           | SCL           |

| Arduino Mega 2560 | SDカードリーダーモジュール |
|:-----------------:|:-------------:|
| 5V                | +5            |
| GND               | GND           |
| 50                | MISO          |
| 51                | MOSI          |
| 52                | SCK           |
| 53                | CS            |

これはあくまで概要図であり、実際は上記の通りPCBボードに実装しています。

![20210822_075906054_iOS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481328/8e85e67f-a374-5e2c-2500-0227a9af7504.jpeg)


# 準備
### RC-S620S用のライブラリの準備
RC-S602Sのライブラリはソニーから配布されており、[こちら](http://blog.felicalauncher.com/sdk_for_air/?page_id=2699)からダウンロードできます。
Arduinoのlibrariesフォルダに`RC-S620S`というフォルダをつくり、解凍した中身をぶち込みます。
ただこのライブラリは10年以上前にリリースされたものなので、一部を書き換えます。
RCS620S.cppにある、`Wprogram.h`を`Arduino.h`に変え、`Serial`を`Serial1`に変更します。

| 行  | 前                            | 後                             |
|:---:|:-----------------------------:|:------------------------------:|
| 10  | #include “Wprogram.h”         | #include “Arduino.h”           |
| 312 | Serial.write(data, len);      | Serial1.write(data, len);      |
| 327 | if (Serial.available() > 0) { | if (Serial1.available() > 0) { |
| 328 |	data[nread] = Serial.read();  | data[nread] = Serial1.read();  |
| 338 |	Serial.flush();               | Serial1.flush();               |

Serial1に変える理由としては、Arduino Mega 2560はハードウェアシリアルが複数あり、一つをシリアルモニタに、一つをRC-S620Sに使うとデバッグ作業などがとてもしやすいからです。
Arduino Mega 2560を選んだのにも同様の理由があります。

### その他ライブラリの準備
`LiquidCrystal_I2C.h`や`DS3231.h`などのライブラリは適宜ライブラリマネージャーなどから導入してください。

# 動作の概要
![概要図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481328/ac814621-c6af-ed22-0d83-19fea51ad347.jpeg)
鍵を施錠するまでの大まかな概要図です。
FeliCaカードをタッチすると、RC-S620SからArduino Mega 2560へカードの情報が送られます。
カードの識別には`Idm`という個体番号みたいなやつを使います。
このIdmがSDカードの登録情報にあるIDmを一致したらサーボモータを動かします。
それと同時に、LCDにカード情報を日時を表示し、入退室の記録をSDカードに記録します。

# RTCの時刻合わせ
後述するコードの

```
// RTCの初期化
//  clock.begin();
//  clock.setDateTime(__DATE__, __TIME__);
//  Serial.println("Initialize DS3231");
```

という部分のコメントアウトを外してコンパイルすると、コンパイルした時刻がRTCの時刻にセットされます。
コンパイルした後はまたコメントアウトしてコンパイルしてください。
SDカードに時刻情報を記述する方法も考えましたが、いちいちSDカードを取り出して時刻情報を書いたり消したりするよりはこっちのほうが楽かなと思いこうしました。

# FeliCaカードの登録
![概要図_登録.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481328/7e921d75-4a30-dc76-87c8-5f101aad528e.jpeg)

登録モード用のボタンを押すとカードの登録モードに移行します。
その状態でFeliCaカードをタッチすると、SDカードに既に登録されているIDmを比較し、登録されていない場合は新規に登録します。

### 内側から鍵を開閉できるようにする
もう一つのタクトスイッチは内側から鍵を開閉する用に使います。
というのも、鍵を開閉する機構を内側のドアノブにがん付けするので、内側の鍵は手動で開閉することができません。

# 使用したプログラム

```c++
#include <LiquidCrystal_I2C.h>
#include <SD.h>
#include <RCS620S.h>
#include <avr/pgmspace.h>
#include <Servo.h>
#include <SPI.h>
#include <Wire.h>
#include <DS3231.h>

using namespace std;

String IDms[20];

bool is_enter = true;
int num_felica = 0;

#define COMMAND_TIMEOUT  400
#define POLLING_INTERVAL 1000
#define MOTER_INTERVAL 500
#define SD_CS 53
#define SERVO 3
#define LED_OK 23
#define LED_NG 25
#define BUTTON_CARD 45
#define BUTTON_MOTER 43
#define LCD_I2C 0x27

Servo myservo;
LiquidCrystal_I2C lcd(LCD_I2C, 20, 4);
DS3231 clock;
RTCDateTime dt;
RCS620S rcs620s;
File memory_file, status_file, IDm_file;

void setup() {
  int y = 0, ret = 0, i;
  int ye, mo, da, ho, mi, se;
  char buf[100];
  
  Serial.begin(115200);
  Serial1.begin(115200);
  Serial.println("Serial start");
  
  // ピンモードの設定
  pinMode(BUTTON_CARD, INPUT_PULLUP);
  pinMode(BUTTON_MOTER, INPUT_PULLUP);
  pinMode(LED_OK, OUTPUT);
  pinMode(LED_NG, OUTPUT);

  myservo.attach(SERVO);
  myservo.write(90);
  delay(MOTER_INTERVAL);
  myservo.detach();
  Serial.println("servo init ok");

  // RTCの初期化
//  clock.begin();
//  clock.setDateTime(__DATE__, __TIME__);
//  Serial.println("Initialize DS3231");
  
  // LCDの初期化
  lcd.init();
  Serial.println("LCD init ok");
  
  lcd.backlight();
  lcd.clear();
  
  // SDの初期化
  lcd.setCursor(0, y);
  pinMode(SD_CS, OUTPUT);
  if (!SD.begin(SD_CS)) {
    lcd.print("SD failed");
    Serial.println("SD failed! [SD card]");
    while (1);
  }
  lcd.print("SD init OK");
  Serial.println("SD init OK");
  y++;

  // IDm情報の初期化
  if(SD.exists("idm.txt")){
    // IDmを読み込み
    Serial.println("read idm.txt");
    IDm_file = SD.open("idm.txt");
    while(IDm_file.available()){
      String buffer = IDm_file.readStringUntil('\n');
      IDms[num_felica] = buffer;
      IDms[num_felica].trim();
      num_felica++;
    }
    IDm_file.close();
  }
  Serial.print("num_felica : ");
  Serial.println(num_felica);

  // 読みとったIDm情報を表示 (debug)
  for(int i = 0; i < num_felica; i++){
    Serial.println(IDms[i]);
  }
  
  // RC-S620Sの初期化
  ret = rcs620s.initDevice();
  if(!ret){
    Serial.println("RC-S620S init failed");
    lcd.print("RC-S620S init failed");
    while(1) {}
  }
  rcs620s.timeout = COMMAND_TIMEOUT;
  lcd.setCursor(0, y);
  lcd.print("RC-S620S init ok");
  Serial.println("RC-S620S init ok");
  y++;

  // 初期表示
  delay(2000);
  lcd.clear();
  lcd.setCursor(0, 1);
  lcd.print("Wait FeliCa");
}

void loop() {
  int ret, i;
  String IDm = "";
  String uname, IDm_buf;
  char buf[20], fname[15], date[30];
  RTCDateTime t;
  int button_state_card = 0, button_state_moter = 0;

  // ボタン情報の読み取り
  button_state_card = digitalRead(BUTTON_CARD);
  button_state_moter = digitalRead(BUTTON_MOTER);

  // LED の初期状態
  digitalWrite(LED_OK, HIGH);
  digitalWrite(LED_NG, LOW);

  // サーボモータを動かさない間は開放する
  myservo.detach();

  // 時刻の表示
  t = clock.getDateTime();
  lcd.setCursor(0, 0);
  sprintf(date, "%04d/%02d/%02d %02d:%02d:%02d", t.year, t.month, t.day, t.hour, t.minute, t.second);
  lcd.print(date);
  lcd.setCursor(0, 1);
  lcd.print("Wait FeliCa ");
  lcd.setCursor(0, 2);
  lcd.print("                    ");

  // ボタン押下時の処理
  // カード登録用のボタンが押された時
  if (button_state_card == LOW){
    Serial.println("button card on");
    digitalWrite(LED_NG, HIGH);
    digitalWrite(LED_OK, LOW);

    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Card Registration");
    lcd.setCursor(0, 1);
    lcd.print("Touch the card");

    // カードがタッチされるか、もう一度カード登録ボタンが押されるとループから抜ける
    bool is_touch = false;
    while(!is_touch){
      is_touch = rcs620s.polling();
      button_state_card = digitalRead(BUTTON_CARD);
      if(button_state_card == LOW){
        Serial.println("cancel Registration");
        delay(POLLING_INTERVAL);
        lcd.clear();
        return;
      }
    }
    
    // カード情報を読み取り
    for(i = 0; i < 8; i++){
      sprintf(buf, "%02X", rcs620s.idm[i]);
      IDm += buf;
    }
    Serial.print("IDm = ");
    Serial.println(IDm);

    // IDmをデータベースと比較する
    for(i = 0; i < num_felica; i++){
      if(IDm == IDms[i]){
        lcd.setCursor(0, 3);
        lcd.print("Already registered");
        Serial.print("Already registered, IDm = ");
        Serial.println(IDm);
        delay(POLLING_INTERVAL);
        lcd.clear();
        return;
      }
    }

    // データベースにない場合は登録する
    if(i == num_felica){
      IDm_file = SD.open("idm.txt", FILE_WRITE);
      IDm_file.println(IDm);
      IDm_file.close();
      lcd.setCursor(0, 3);
      lcd.print("Complete");
      Serial.print("Complete registered, IDm = ");
      Serial.println(IDm);
      IDms[num_felica] = IDm;
      num_felica++;
    }

    delay(POLLING_INTERVAL);
    lcd.clear();
  }

  // サーボモーター用のボタンが押された時
  lcd.setCursor(0, 2);
  if (button_state_moter == LOW){
    Serial.println("button morter on");

    // 鍵を内側から開け閉め出来るようにする
    if(is_enter){  // 開→閉
      is_enter = false;
      myservo.attach(SERVO);
      myservo.write(0);
      Serial.println("servo : 0");
      lcd.print(F("unlock      "));
    }else{        // 閉→開
      is_enter = true;;
      myservo.attach(SERVO);
      myservo.write(90);
      Serial.println("servo : 90");
      lcd.print(F("lock        "));
    }

    delay(MOTER_INTERVAL);
  }

  
  // FeliCaのタッチ状態を得る
  ret = rcs620s.polling();
  
  // FeliCaがタッチされた場合
  if(ret){
    sprintf(fname, "%04d%02d%02d", t.year, t.month, t.day);
    strcat(fname, ".csv");
    Serial.println("open " + String(fname));
    
    // IDmを取得する
    for(i = 0; i < 8; i++){
      sprintf(buf, "%02X", rcs620s.idm[i]);
      IDm += buf;
    }

    // IDmをデータベースと比較する
    uname = "Unknown user";
    for(i = 0; i < num_felica; i++){
      if(IDm == IDms[i]){
        uname = IDm;
        break;
      }
    }

    lcd.setCursor(0, 1);

    // 入退室の処理
    if(i == num_felica){  // データベースにないFeliCaがタッチされた場合
      lcd.print(F("Touch       "));
      Serial.print("Unknown FeliCa: IDm = ");
      Serial.println(IDm);
    }else if(is_enter){   // 退室(開→閉)
      is_enter = false;
      lcd.print(F("unlock      "));
      myservo.attach(SERVO);
      myservo.write(0);
      Serial.println("servo : 0");
    }else{                // 入室(閉→開)
      is_enter = true;
      lcd.print(F("lock        "));
      myservo.attach(SERVO);
      myservo.write(90);
      Serial.println("servo : 90");
    }

    // 名前表示
    lcd.setCursor(0, 2);
    lcd.print(F("                    "));
    lcd.setCursor(0, 2);
    lcd.print(uname);

    Serial.print(uname);
    Serial.print(",");
    Serial.print(date);
    if(uname != "Unknown user"){
      Serial.print(",");
      Serial.println((is_enter ? "Enter" : "Leave"));
    }else{
      Serial.println();
    }

        
    // 入退室記録を書き込み
    memory_file = SD.open(fname, FILE_WRITE);
    if (memory_file){
      memory_file.print(uname);
      memory_file.print(",");
      memory_file.print(date);
      if(uname != "Unknown user"){
        memory_file.print(",");
        memory_file.println((is_enter ? "Enter" : "Leave"));
      }else{
        memory_file.println();
      }
    }else{
      Serial.print(fname);
      Serial.println(" File open error");
    }
    memory_file.close();
   
    delay(POLLING_INTERVAL);
  }

  rcs620s.rfOff();
}
```

# 実際の使い方
## 電源ONから使用まで
Arduino Mega2560 の電源を入れます。
SDカードリーダーモジュールとRC-S620Sの初期化が行われます。

```
SD init OK
RC-S620S init ok
```

と表示されたら正常です。

* <code>SD Faild!</code> と表示される
    * SDカードがちゃんと刺さっているか、SDカードリーダーモジュールが基板上でちゃんと接続出来ているか確かめてください。
* <code>SD init OK</code> から動かない
    * RC-S620S のフラットケーブルや基板上の接続を確かめてください。

その後

```
2021/06/21 21:36:14
Wait FeliCa
```

のように時刻と <code>Wait FeliCa</code> の文字列が表示されます。あとは任意のFeliCaカードで鍵の開け閉めを行うことができるはずです。

* 登録されたFeliCaカードの場合
    * 入退室記録に合わせて
 
    ```
    2021/06/21 21:36:14
    Enter
    あなたのIDm
    ```


    ```
    2021/06/21 21:36:14
    Leave
    あなたのIDm
    ```

    と表示されます。
* 登録されていないFeliCaカードの場合

    ```
    2021/06/21 21:36:14
    Touch
    Unknown user
    ```

    と表示されます。IDと名前の登録を行ってください。

## FeliCaカードの登録方法
* カード登録用のボタンを押してください。
* ボタン押すと、黄色LEDが点灯し、LCDに

    ```
    Card Registration
    Touch the card
    ```

    と表示されます。この状態で、Suica等のFeliCaカードをかざしてください。
* そのカードが登録されていない場合

    ```
    Complete
    ```

    と表示され、カードの登録が完了します。
* 既にそのカードが登録されている場合

    ```
    Already registered
    ```

    と表示されます。
* 間違って登録用ボタンを押してしまった場合・キャンセルしたい場合はもう一度ボタンを押してください。
* 間違ったカードを登録してしまった場合は、microSDカードを取りだし、<code>imd.txt</code>にあるカードのIDmを変更・削除してください。

# 各種アタッチメントを作る
ドアノブにサーボモータを取り付けるためのアタッチメントと、Arduino Mega 2560のケースを3DモデルをBlenderでつくり、3Dプリンターで印刷します。
[各種3dモデル](https://github.com/imaimai17468/cardkey/tree/main/cardreader_3dfiles)
ちなみにケースの側面にマグネットを貼ることで扉にくっつくようになってます。

![20210828_034135044_iOS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481328/d58a5530-392a-3d28-6223-462f13a886f8.jpeg)


あとはこれをドアノブにつけて電源をつなげば完成！！

# おわりに
こういうマトモに機能するものを作ったのはこれが初めてなので、とてもいい経験になりました。
友人の研究室のボスや他学科の教授に褒められたりしてとてもうれしいです。