티머니머니
최근 티머니 교통카드 관련 기능을 구현하였는데, 생각보다 NFC 스펙이 어려운 편은 아니었다. 충전기 데모를 만드는 중간 과정으로 집에 굴러다니는 라즈베리파이와 함께 NFC로 티머니 카드를 읽어보려고 한다.
금융IC 표준
예전에 조사해보니 이전에는 티머니, 캐시비 등 파편화되어있던 KS X 6924 규격들이 이를 하나로 묶는 전국호환교통카드 사업으로 규격이 통일되었다. 2025년 기준 주변에서 끌어모은 대부분의 교통카드가 전국호환교통카드 규격을 구현하고 있기 때문에, 대부분이 티머니 카드더라도 굳이 티머니 전용 AID를 사용하지는 않았다.

카드번호를 조회하기 위해서는 카드로 AID a0000004520001를 날리고, FCI파일을 찾아서, 12태그의 값 8바이트를 읽으면 된다. FCI파일은 TLV구조로 바운더리 체크를 해가면서 쭉 읽어나가면 쉽게 구현할 수 있다.
만약, 잔액조회가 필요하면 4f태그를 읽어서 나온 AID로 다시 셀렉하고, 11태그를 읽어서 나온 명령어를 다시 카드로 날리면 4바이트 데이터를 돌려준다(Int32+빅엔디언).
NFC
내가 사용한 부품은 pn532인데, 거의 표준격으로 많이 사용되고 있는 것 같았다.
하드웨어 컨트롤을 위한 라이브러리로는 https://github.com/adafruit/Adafruit_CircuitPython_PN532 를 사용하면 되는데, 중요한건 APDU를 쏘는 InDataExchange 명령어가 아두이노 버전에서는 있으나 파이썬 버전에는 고차원 수준의 함수가 없어서 직접 구현해야 한다.
(솔직히 여기서 아두이노로 갈아타고 싶었다).
_CMD_INDATAEXCHANGE = 0x40
def _indataexchange(self, *, apdu: bytes, tg: int) -> bytes:
params = bytearray([tg])
params.extend(apdu)
resp = self._pn532.call_function(
_CMD_INDATAEXCHANGE,
response_length=128,
params=params,
timeout=1,
)
if not resp:
raise CardReadError("no_response")
if resp[0] != 0x00:
raise CardReadError(f"status=0x{resp[0]:02X}")
# PN532 status 제거 후 APDU 응답
data = bytes(resp[1:])
# SW1 SW2 체크
if len(data) < 2:
raise CardReadError("APDU response too short")
sw = data[-2:]
if sw != b"\x90\x00":
raise CardReadError(f"SW1SW2={sw.hex().upper()}")
return data[:-2] # SW1 SW2 제외한 데이터 반환뷰

티머니 카드에서 번호를 가져왔다면, 이제 LCD로 보여줄 차례다. 평범한 I2C 2줄짜리 LCD 모듈을 연결해 주었다.
완성!

역시 모두 납땜하고 모든게 잘 나오는걸 볼 때는 꽤 보람찬 순간이다. 이 장치는 추후에 전기차충전기의 데모로 확장할 예정인데, 잔액은 사실상 필요가 없어서 멤버십카드 채번까지만 구현하였다. 충전모듈은 있고 이제 OCPP 구현해서 CSMS에 붙이면 될거같은데…허허