Arduino :: Lecture & TIPs

[Arduino] 근거리 무선 통신 기술, NFC 와 ESP8266 을 이용한 선불교통카드 잔액 조회기

외국인 친구들이 한국에 여행 차 방문하는 경우가 종종 생깁니다. 상황이 되면 제가 친구들을 따라다니며 가이드를 해 주는데 외국인 친구들이 한국에 들렀을 때 대중교통을 가장 저렴하면서도 편하게 이용할 수 있는 방법 중 하나가 바로 편의점에서 선불 교통카드를 구매한 후 충전하여 사용하는 방법입니다. 그러나, 가장 많은 불편함을 느끼며 제게 이야기 해주는 것 중 하나가 선불 교통카드의 잔액 확인 방법이 까다롭다는 것인데요. 대중교통 이용 시에 카드를 직접 버스 내 교통카드 단말기에 찍어보지 않는 이상, 혹은 버스 정류소 앞에서 버스 정보 안내기에 마련된 잔액 확인 기능으로만 잔액을 확인 할 수 있고, 앱도 모두 한글화되어 있어 실시간 잔액 조회가 까다롭다는 점에서 아쉽다는 의견들이 많았는데요. 특히 앱을 사용하기 위해서는 회원 가입 절차와 실명 인증을 모두 거쳐야 했던 관계로 외국인 친구들이 사용하기에 상당한 어려움과 불편함을 느끼곤 했습니다.

Tmoney 교통 카드와 서비스 사용 설명서. ⓒ happybono.

앞서 언급해드린 불편함들도 존재함은 물론이고, 외국인들이 수 많은 여행지를 다니면서 잔액을 그 때 그 때 기억하기도 어려운 상황이라 손쉽게 잔액 확인을 할 수 있도록 조회 할 수 있는 잔액 조회 기기를 제작해보기로 결심했습니다. 개인정보 유출 문제가 심각한 요즈음 실태에 따라 앱을 설치해 사용하기를 꺼려하시는 분들이 제작하여 사용하거나, 스마트폰을 아직 보유하고 있지 않으면서도 후불 교통카드 사용 빈도가 높은 초등학생, 중학생들에게 혹은 한국을 종종 방문하는 외국인 친구들에게 만들어 선물하면 실용적이면서도 유용하게 활용할 수 있는 프로젝트입니다.

준비물

선불 교통 카드 잔액 조회기 제작을 위해 필요한 모듈들을 준비했습니다.

제일 먼저, NFC 카드 인식을 위한 PN532-v3 모듈이 필요합니다. PN532-v3 모듈에 대해 궁금하시면 아래의 게시글 을 정독해보시면 되겠습니다.

[Arduino] PN532 NFC / RFID Controller 와 ESP8266-12E

PN532 V3 Controller NFC / RFID 규격의 신호로 전송되는 정보를 읽기 위한 모듈로 범용적으로 사용되고 있는 부품 중 하나가 PN532 입니다. 현재 판매되고 있는 버전의 대부분은 “버전 (v) 3” 으로 스위치가 컨트롤러 상단에 배치되어, 이를 사용해 입출력 모드의 선택과 변경이 용이하도록 설계된 모듈입니다. 세 가지 (SPI, I2C, UART (High Speed UART)) 의 입출력 방식을 지원합니다. […]

PN532 NFC 카드 인식 모듈. ⓒ happybono.

그 다음 0.96 인치 I2C 방식으로 연결되는 SSD1306 (OLED 화면) 부품NodeMCU ESP8266-E12 모델의 MCU 칩셋준비하였습니다.

NodeMCU ESP8266-E12 MCU 칩셋. ⓒ happybono.
128 × 96 픽셀 크기 규격의 SSD1306 OLED. ⓒ happybono.

제작 결과물

최종 선불교통카드 잔액 조회기의 모습으로 아래 동영상과 같이 동작하며, 배터리가 내장되도록 설계하지 않았기에, 외장 배터리를 ESP8266-12E MCU 에 탑재된 Micro-USB 단자에 연결 후 사용합니다.

제작 방법

40 포인트 정도의 납땜이 필요합니다. 전체적인 회로도는 아래와 같습니다.

Tmoney 선불교통카드 잔액 조회기를 제작하기 위한 전체적인 회로도. ⓒ happybono.

추후 부품 재활용이 용이하도록, 각 모듈들에 Pin header (핀 헤더) 를 납땜하여 고정한 후 F-F Jumper Cable (점퍼 케이블) 을 이용해 연결한 모습입니다. (케이스의 경우 직접 설계 및 제작하여 3D 프린터로 출력된 결과물 현재 여유 공간이 많기에 이를 개선하여 재설계하면, 휴대용의 목적에 맞게 소형화 할 수 있습니다.)

필요한 모든 부품들의 회로를 연결하고, 케이스를 직접 설계한 후 3D 프린터로 출력하여 각 부품들을 적절한 위치에 고정시켰다. ⓒ happybono.

소스 코드를 활용하기 위해서는 두 개의 라이브러리를 추가해주셔야 합니다. 전체 과정 및 라이브러리는 하단의 링크에서 다운로드 하실 수 있습니다.

PN532 NFC 센서와 모듈과 관련된 라이브러리https://www.github.com/adafruit/Adafruit-PN532 를 통해 다운로드 가능하시며, OLED 디스플레이 모듈과 관련된 라이브러리https://www.github.com/ThingPulse/esp8266-oled-ssd1306 를 통해 다운로드 하실 수 있습니다.

선불 교통 카드 잔액 조회기를 만드는 과정에서 연구한 전체 내용과 작성한 소스코드 는 아래와 같습니다.

  
//
//    FILE: K-TransitCardBalance.ino
//  AUTHOR: Jaewoong Mun (happybono@outlook.com)
// CREATED: November 19, 2019
//
// Released to the public domain
//

#include <Arduino.h>
#include "Adafruit_PN532.h"
#include "SSD1306.h"

#define OLED_RESET -1   // S/W Reset. (OLED 를 소프트웨어 적으로 초기화하는 코드입니다. 사용되지는 않습니다.)

// To use the PN532 module in SPI mode, Firstly changing the switch,
// then a total of 6 wires must be connected, including VCC and GND pins.
// Afterward, define the connection pins specified based on the NodeMCU ESP8266-12E.
// PN532 모듈을 SPI 모드로 사용하기 위해서는 스위치를 변경한 후 
// VCC, GND 핀을 포함하여 총 6개의 선 연결이 필요합니다.
// NodeMCU ESP8266-12E 기준으로 지정한 연결 핀들을 입력해줍니다.
#define PN532_SCK  (D5) //
#define PN532_MOSI (D7) //
#define PN532_SS   (D0) //
#define PN532_MISO (D6) //

// To use the 'I2C mode', the two pins below must be connected.
// I2C 모드를 이용하기 위해서는 아래 2 개의 핀 연결이 필요합니다.
#define PN532_IRQ   (2)  // Not used.
#define PN532_RESET (0)  // Not connected by default on the NFC Shield.

//--------------------------------------------------------------
// Global variables
//--------------------------------------------------------------

SSD1306 display(0x3c, D2, D1); // Object declaration for OLED screen display. (OLED 화면 표시용 객체입니다.)
Adafruit_PN532 nfc(PN532_SS);  // Object declaration for NFC capabilities. (NFC 통신용 객체입니다.)

//--------------------------------------------------------------
// Setup function
//--------------------------------------------------------------

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

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH); // Disables a dedicated LED light on the ESP8266 chipset. (내장된 식별 LED 를 끕니다.)
  Serial.println("Start NFC");

  nfc.begin();
  uint32_t versiondata = nfc.getFirmwareVersion();
  if (! versiondata) {
    Serial.print("Didn't find PN53x board"); // Reboot if the PN532 is not recognized. (PN532 를 인식하지 못한 경우 재부팅합니다.)
    while (1);
  }

  // If the PN532 is connected and recognized, display information related to the chipset on the console log.
  // PN532 가 연결되어 인식되면 콘솔 로그에 칩셋 관련 정보를 표시합니다.
  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);

  // Connect Oled as an I2C method, and its address is 0x3c.
  // Initializing the OLED.
  // OLED 를 I2C 방식으로 연결하시면, 그 주소는 0x3c 로 지정되어 있습니다.
  // OLED 를 초기화 합니다.
  display.init();
  display.flipScreenVertically();
  
  // RFID 
  // Configure board to read RFID tags.
  // RFID 태그를 읽을 수 있도록 보드를 구성합니다.
  nfc.SAMConfig();
  Serial.println("Waiting for an ISO14443A Card ...");
  display.display(); 
  delay(2000);

  display.clear();
  display.setFont(ArialMT_Plain_10);
  display.drawString(0, 0, "T-Money Balance Checker");
  display.drawString(1, 17, "To check the balance,");
  display.drawString(1, 29, "please tag your prepaid");
  display.drawString(1, 41, "T-Money card to this device.");
  display.display();
}

//--------------------------------------------------------------
// loop function
//--------------------------------------------------------------

char  card_id[16+1] = "";  // Card No. (16-bytes) 카드 일련번호
char  date_issued[8+1]=""; // Date issued. (8-bytes) 카드 최초 발행일
char  card_issuer = 0x00;  // Card issuer  카드 발행사
char  card_type = 0x00;    // Card type    카드 종류

void loop(){
  uint8_t success;
  uint8_t responseLength = 64;
  success = nfc.inListPassiveTarget();
  if (success) {
    Serial.println("Found something!");
    // This code is for reading the serial number or issued date of the T-money card. 
    // 티머니 카드의 일련 번호 또는 발급 날짜를 읽기 위한 코드입니다.
    uint8_t cardInfo[responseLength];
    uint8_t cardNumSize = 0;
    uint8_t selectApdu[] = { 0x00, 0xA4, 0x00, 0x00, 0x02, 0x42, 0x00 };
    success = nfc.inDataExchange(selectApdu, sizeof(selectApdu), cardInfo, &responseLength);
    if (success) {
      Serial.print("responseLength: "); Serial.println(responseLength);
      nfc.PrintHexChar(cardInfo, responseLength);
      if (responseLength >= 24) {
        
        CharToHex( cardInfo + 13, card_id, 3);   // Card No. (카드 일련번호)
        // CharToHex( cardInfo + 8, card_id, 8); 
        CharToHex( cardInfo + 21, date_issued, 3);  // Date issued. (카드 최초 발행일)
        // CharToHex( cardInfo + 21, date_issued, 4);  
        card_issuer = cardInfo[7];   // Card issuer (카드 발행사)
        card_type = cardInfo[29];  // Card type (카드 종류)
        Serial.print("card number : "); nfc.PrintHexChar(cardInfo + 8, 8);
        Serial.print("date issued : "); nfc.PrintHexChar(cardInfo + 21, 4);
        Serial.print("card issuer : ");  Serial.println(issuer_corps( (int)card_issuer) );
        Serial.print("card type : "); Serial.println(user_type((int)card_type) );
      }
    }

    // Used for balance inquiry purposes.
    // When the inquiry is complete, the remaining balance will be display on the OLED screen.
    // 잔액 조회 용도로 사용되는 코드입니다.
    // 조회가 완료되면 남은 잔액을 OLED 에 표시합니다.
    uint8_t balance[responseLength];
    uint8_t balanceApdu[] = { 0x90, 0x4C, 0x00, 0x00, 0x04 } ;
    success = nfc.inDataExchange(balanceApdu, sizeof(balanceApdu), balance, &responseLength);
    if (success) {
      Serial.print("responseLength: "); Serial.println(responseLength);
      nfc.PrintHexChar(balance, responseLength);
      if (responseLength >= 4) {
        char fpsbuf[32] = ""; // It is used to convert numbers into strings and display them on the screen. (숫자를 문자열로 변환하여 화면에 출력하는 용도로 사용됩니다.)
        uint32_t credit = balance[0] * 256 * 256 * 256 +  balance[1] * 256 * 256 +  balance[2] * 256 + balance[3];
        display.clear();
        memset(card_id + 0,'x',2);  // For extra security, only the last 4 digits of the card's serial number are displayed. (보안성 강화를 위해 카드의 일련번호 맨 끝 4 자리만 표시합니다.)
        display.drawString(0, 0, card_id);
        display.drawString(41, 0, date_issued);
        display.drawString(83, 0, user_type((int)card_type));
        dtostrf((float)credit, 10, 0, fpsbuf);
        display.setFont(ArialMT_Plain_24);
        display.drawString(0, 11, fpsbuf);
        display.setFont(ArialMT_Plain_10);
        if (credit < 1500) {
          display.drawString(0, 32, "The balance on your");
          display.drawString(0, 42, "card is critically low.");
          display.drawString(0, 52, "Please recharge it NOW.");      
          } else if (credit < 3500) {
          display.drawString(0, 32, "You have less than");
          display.drawString(0, 42, "3500 won on your card.");
          display.drawString(0, 52, "Please recharge it ASAP.");
          } else {
          display.drawString(0, 34, "Have a great day and");
          display.drawString(0, 46, "safe travel ! * ' - ' *");
          }
        display.display();
        delay(5000);
        Serial.print("Balance left on this card : "); Serial.println(credit);
      }
    }
  }
  else {
  display.clear();
  display.setFont(ArialMT_Plain_10);
  display.drawString(0, 0, "T-Money Balance Checker");
  display.drawString(1, 17, "To check the balance,");
  display.drawString(1, 29, "please tag your prepaid");
  display.drawString(1, 41, "T-Money card to this device.");
  nfc.SAMConfig();
  display.display();
  }
  delay(100);
}

void CharToHex(uint8_t *ch, char* szHex, int len)
{
 unsigned char saucHex[] = "0123456789ABCDEF";
 for (int  i=0; i<len; i++) {
 szHex[i*2+0] = saucHex[ch[i] >> 4];
 szHex[i*2+1] = saucHex[ch[i] & 0xF]; 
 }
}

char* issuer_corps( int idx ) {
  switch(idx) {
   case 1:  return "KFTC"; // Korea Financial Telecomunications & Clearings Institute
   case 2:  return "A-Cash";
   case 3:  return "Mybi";
   case 5:  return "V-Cash";
   case 6:  return "Mondex Korea";
   case 7:  return "Korea Expressway Corp.";
   case 8:  return "T-Money";
   case 9:  return "Korail";
   case 11:  return "EB Card";
   case 12:  return "Seoul Metropolitan Bus Association";
 }
  return "Unspecified"; 
}

char* user_type( int idx ) {
  switch(idx) {
   case 1:  return "Regular";
   case 2:  return "Student";
   case 3:  return "Teenager";
   case 4:  return "Senior";
   case 5:  return "Disabled";
 }
  return "Card Type : Unspecified"; 
}

사실 최근에 연구한 모듈과 내용을 다시금 정리하고 디스플레이를 추가한 결과물이긴 합니다. 개발 도중 선불교통카드에 APDU 방식의 명령어로 요청을 보낸 후 데이터를 받아와야 하는데, 해당 정보가 극히 적었던 관계로 보내야 하는 명령어나 코드에 대한 정보를 찾기가 상당히 까다로웠었습니다. 이 게시글에서 관련 명령어는 한 줄의 소스 코드로 포함되어 있지만, 해당 코드는 수일간 검색하고 연구해서 구현에 성공한 코드입니다.

고맙습니다.

1 comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: