npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

thai-national-id-reader

v0.1.1

Published

อ่านบัตรประชาชนไทยผ่านเครื่องอ่านสมาร์ทการ์ด PC/SC — Read Thai national ID cards from any PC/SC smart card reader

Readme

thai-national-id-reader

อ่านบัตรประชาชนไทยผ่านเครื่องอ่านสมาร์ทการ์ดมาตรฐาน PC/SC — พัฒนาและทดสอบกับ ACS ACR39U-NF แต่ใช้ได้กับเครื่องอ่าน PC/SC รุ่นอื่นเช่นกัน เพราะคุยผ่าน APDU มาตรฐาน ไม่ได้ผูกกับไดรเวอร์ของยี่ห้อใด

รองรับทั้ง ESM (import) และ CommonJS (require) พร้อม type definitions ครบ

ติดตั้ง

npm install thai-national-id-reader

ความต้องการของระบบ

  • Node.js 18 ขึ้นไป
  • เครื่องอ่านสมาร์ทการ์ดแบบเสียบที่รองรับ PC/SC
  • macOS ใช้ได้ทันที (มี PCSC.framework มาในตัว)
  • Windows ใช้ไดรเวอร์ CCID ที่มากับระบบ หรือติดตั้งไดรเวอร์จาก ACS
  • Linux ต้องติดตั้ง pcscd และ libpcsclite-dev แล้วสั่ง sudo systemctl start pcscd

เรื่อง native module ที่ควรรู้ก่อนติดตั้ง

แพ็กเกจนี้พึ่ง @pokusew/pcsclite ซึ่งเป็น native addon และ มี binary สำเร็จรูปถึงแค่ Node 16 บน Node รุ่นใหม่กว่านั้นเครื่องจะ compile เอง จึงต้องมี build toolchain (Xcode Command Line Tools บน macOS, build-essential บน Linux, Visual Studio Build Tools บน Windows)

ถ้าติดตั้งไม่ผ่านด้วย ModuleNotFoundError: No module named 'distutils'

Python 3.12 ขึ้นไปถอด distutils ออกจาก standard library แล้ว แต่ node-gyp รุ่นที่มากับ npm ยังเรียกใช้อยู่

# ทางเลือกที่ 1 — ติดตั้ง setuptools ซึ่งมี distutils ให้
python3 -m pip install setuptools
npm install thai-national-id-reader

# ทางเลือกที่ 2 — ชี้ไปที่ Python รุ่นที่ยังมี distutils
npm_config_python=$(which python3.10) npm install thai-national-id-reader

ถ้าใช้กับ Electron ต้อง rebuild ให้ตรง ABI ของ Electron ทุกครั้งที่ติดตั้งหรืออัปเกรด Electron

npx @electron/rebuild -v <เวอร์ชัน Electron> -f -w @pokusew/pcsclite

ตรวจว่าสำเร็จโดยดูว่ามีโฟลเดอร์ที่ลงท้ายด้วย ABI ของ Electron เช่น darwin-arm64-114 สำหรับ Electron 24

ls node_modules/@pokusew/pcsclite/bin/

หากไม่อยากยุ่งกับ native module เลย ใช้ทางเข้า thai-national-id-reader/card แทนได้

ใช้งานผ่าน CLI

# เฝ้ารอบัตรตลอดเวลา — เสียบกี่ใบก็อ่านต่อเนื่อง (ค่าเริ่มต้น)
npx thai-id

# เฝ้ารอพร้อมอ่านรูปถ่าย (บันทึกเป็น photo-<เลขบัตร>.jpg)
npx thai-id --photo

# อ่านใบเดียวแล้วออก เหมาะกับการเรียกจากสคริปต์อื่น
npx thai-id --once

# JSON บรรทัดเดียวต่อหนึ่งใบ ต่อท่อเข้าโปรแกรมอื่นได้เลย
npx thai-id --json --quiet | while read line; do echo "$line" | jq .cid; done

ตัวอย่างผลลัพธ์:

  เลขประจำตัวประชาชน  1-2345-67890-12-3
  ชื่อ-สกุล (ไทย)      นาย สมชาย ใจดี
  ชื่อ-สกุล (อังกฤษ)   Mr. Somchai Jaidee
  วันเกิด              1987-12-31
  เพศ                  ชาย
  ที่อยู่               99/1 ซอยสุขุมวิท 5 ถนนสุขุมวิท ตำบลคลองเตย อำเภอคลองเตย จังหวัดกรุงเทพมหานคร
  ผู้ออกบัตร            สำนักงานเขตคลองเตย
  วันออกบัตร           2017-01-15
  วันหมดอายุ           2027-01-14

ใช้งานเป็น library

แบบครั้งเดียวจบ — เหมาะกับ endpoint ที่ผู้ใช้กดปุ่มแล้วเสียบบัตร:

import { readCardOnce } from 'thai-national-id-reader';

const card = await readCardOnce({ includePhoto: true, timeoutMs: 30_000 });
console.log(card.cid, card.firstNameTH, card.lastNameTH);

หรือแบบ CommonJS:

const { readCardOnce } = require('thai-national-id-reader');

readCardOnce({ includePhoto: true }).then((card) => console.log(card.cid));

แบบเฝ้ารอต่อเนื่อง — เหมาะกับเครื่องลงเวลาหรือจุดลงทะเบียนที่เปิดค้างไว้:

import { ThaiIdCardWatcher } from 'thai-national-id-reader';

const watcher = new ThaiIdCardWatcher({ includePhoto: true });

watcher.on('reader-connected', (name) => console.log('เชื่อมต่อเครื่องอ่าน:', name));
watcher.on('card-inserted', ({ atr }) => showWaiting(atr));
watcher.on('reading', () => showSpinner());
watcher.on('progress', ({ step, percent }) => updateBar(step, percent));
watcher.on('card', (card) => saveEmployee(card));
watcher.on('card-removed', () => resetForm());
watcher.on('read-error', (error, { reason, willRetry }) => {
  if (!willRetry) showError(reason);
});

Event ทั้งหมด

| Event | ยิงเมื่อ | ข้อมูลที่ส่งมา | |---|---|---| | started | เริ่มเฝ้ารอ | — | | reader-connected | เสียบเครื่องอ่าน USB หรือพบตอนเริ่ม | readerName: string | | reader-disconnected | ถอดเครื่องอ่านออกจาก USB | readerName: string | | card-inserted | เสียบบัตร (ก่อนเริ่มอ่าน) | { readerName, atr } | | reading | เริ่มอ่านข้อมูล | { readerName } | | progress | ระหว่างอ่าน | { step, percent } | | card | อ่านสำเร็จ | ThaiIdCard | | read-error | อ่านไม่สำเร็จ | error, { reason, attempt, willRetry } | | card-removed | ถอดบัตรออก | { readerName } | | error | ข้อผิดพลาดระดับระบบ PC/SC | Error | | stopped | หยุดเฝ้ารอ | — |

reason ใน read-error มีค่าเป็น 'not-thai-id' 'card-removed' 'card-unresponsive' 'corrupt-data' 'timeout' หรือ 'unknown' ใช้แยกได้ว่าควรบอกผู้ใช้ให้เปลี่ยนบัตรหรือให้เสียบใหม่

ข้อควรรู้เรื่องประสิทธิภาพ

watcher.close() บล็อก event loop ราวหนึ่งวินาที เพราะต้องรอ thread เฝ้าดูสถานะของ pcsclite จบก่อน

ระบบที่อ่านบัตรบ่อย เช่น HTTP endpoint ต้องใช้ watcher ตัวเดียวยาว ๆ อย่าเรียก readCardOnce() ต่อหนึ่งคำขอ เพราะแต่ละครั้งจะสร้างและปิด watcher ใหม่ ทำให้ทั้งเซิร์ฟเวอร์หยุดนิ่งหนึ่งวินาทีต่อการอ่านหนึ่งใบ

ความทนทาน

ตัวเฝ้ารอออกแบบให้ทำงานค้างไว้ตลอดโดยไม่ต้องมีคนดูแล:

  • ทุกคำสั่งที่คุยกับบัตรมีตัวจับเวลา (operationTimeoutMs ค่าเริ่มต้น 5 วินาที) — พบจากการทดสอบจริงว่า SCardConnect ค้างได้แบบไม่มี callback กลับมาเลย ถ้าไม่มีตัวจับเวลา ระบบจะแข็งค้างถาวร
  • ลองใหม่อัตโนมัติเมื่อบัตรไม่ตอบสนอง (retryAttempts ค่าเริ่มต้น 3 ครั้ง) โดยสั่งรีเซ็ตบัตรก่อนลองรอบใหม่ทุกครั้ง
  • ไม่หยุดทำงานเมื่อเกิดข้อผิดพลาด — อ่านพลาดแล้วกลับไปรอบัตรใบถัดไปเสมอ
  • รองรับการถอด/เสียบเครื่องอ่านระหว่างทำงาน โดยไม่ต้องรีสตาร์ต
  • ตรวจข้อมูลที่อ่านมาด้วยหลักตรวจสอบของเลขบัตร — จับกรณีที่คำตอบจากบัตรเลื่อนกัน ซึ่งเป็นความล้มเหลวที่เงียบที่สุด เพราะทุกฟิลด์ยังดูมีรูปแบบถูกต้อง แล้วลองอ่านใหม่อัตโนมัติ

หลักการที่ใช้ตัดสินว่าจะลองใหม่หรือไม่: ลองใหม่ได้เฉพาะเมื่อคำสั่งก่อนหน้า จบไปแล้วจริง (card-unresponsive, corrupt-data) ส่วน timeout ห้ามลองใหม่ เพราะตัวจับเวลาแค่เลิกรอ ไม่ได้ยกเลิก SCardConnect ที่ยังค้างถือ handle อยู่ การเชื่อมต่อซ้อนเข้าไปจะทำให้ slot ของ PC/SC พังจนต้องถอดสาย USB

ใช้ร่วมกับชั้นฮาร์ดแวร์ของคุณเอง

ถ้าโปรเจกต์มีไลบรารีเชื่อมต่อเครื่องอ่านอยู่แล้ว (เช่น smartcard) หรือรับ APDU ผ่านเครือข่าย ใช้ทางเข้า thai-national-id-reader/card ซึ่ง ไม่ import native module ใด ๆ เลย

import { readThaiIdCard } from 'thai-national-id-reader/card';

const card = await readThaiIdCard(
  async (command, expectedLength) => myTransport.send(command, expectedLength),
  'T=0',
  { includePhoto: true },
);

transmit ที่ส่งเข้าไปมีหน้าที่เดียวคือส่งไบต์ไปยังบัตรแล้วคืนคำตอบกลับมา ตรรกะเรื่องโปรโตคอล T=0/T=1, GET RESPONSE, TIS-620 และการตรวจข้อมูลจัดการให้ทั้งหมด

ตัวอย่าง adapter สำหรับ smartcard — สั้นแค่บรรทัดเดียว:

const transmit = async (command) =>
  Buffer.from(await card.issueCommand(new CommandApdu({ bytes: [...command] })));

ข้อมูลที่ได้

| ฟิลด์ | ชนิด | หมายเหตุ | |---|---|---| | cid | string | เลขประจำตัว 13 หลัก ไม่มีขีดคั่น | | titleTH firstNameTH middleNameTH lastNameTH | string | ชื่อภาษาไทย | | titleEN firstNameEN middleNameEN lastNameEN | string | ชื่อภาษาอังกฤษ | | birthDate | string \| null | รูปแบบ ISO YYYY-MM-DD แปลงจาก พ.ศ. แล้ว | | gender | 'male' \| 'female' \| 'unknown' | | | address | string \| null | ที่อยู่ตามทะเบียนบ้าน ประกอบเป็นบรรทัดเดียว | | issuer | string \| null | หน่วยงานที่ออกบัตร | | issueDate expireDate | string \| null | รูปแบบ ISO | | photo | Buffer \| null | JPEG ประมาณ 5 KB (เมื่อสั่ง includePhoto) |

ฟิลด์ที่เป็น null แปลว่าบัตรใบนั้นอ่านฟิลด์ดังกล่าวไม่ได้ — ระบบจะคืนฟิลด์ที่เหลือให้ตามปกติ ไม่ทิ้งข้อมูลทั้งใบ ยกเว้น cid ที่ถือว่าจำเป็น

โครงสร้างโค้ด

src/
├── card/           ตรรกะการอ่านบัตรทั้งหมด — ไม่รู้จัก PC/SC เลย
│   ├── apdu.ts     ประกอบคำสั่ง APDU และซ่อนความต่างของโปรโตคอล T=0 / T=1
│   ├── fields.ts   ตาราง offset บนบัตร (ข้อมูลล้วน ไม่มี logic)
│   ├── decode.ts   TIS-620 → ข้อความ, พ.ศ. → ISO, แยกชื่อและที่อยู่
│   ├── thai-id.ts  ลำดับขั้นการอ่านบัตรหนึ่งใบ
│   └── errors.ts   ชนิดข้อผิดพลาดที่แยกกันชัดเจน
├── pcsc/
│   ├── reader.ts       ชั้นเชื่อมฮาร์ดแวร์ — แปลง event ของ pcsclite เป็น event ของโดเมน
│   ├── card-state.ts   ตีความบิตสถานะของ PC/SC (pure function)
│   └── async-utils.ts  ตัวจับเวลาและการลองใหม่
├── cli.ts
└── index.ts

เส้นแบ่งสำคัญคือ readThaiIdCard() รับฟังก์ชัน transmit เข้ามาเป็นพารามิเตอร์ แทนที่จะเรียก PC/SC เอง ทำให้ตรรกะการอ่านบัตรทั้งหมดทดสอบได้โดยไม่ต้องมีเครื่องอ่านและบัตรจริง

พัฒนาต่อ

npm install          # ติดตั้ง dependency
npm test             # รันเทสต์ทั้งหมด (ไม่ต้องมีเครื่องอ่านเสียบ)
npm run typecheck    # ตรวจ type
npm run read         # รัน CLI จากซอร์สโดยตรง
npm run build        # สร้าง dist/ ทั้ง ESM และ CommonJS พร้อม .d.ts
npm run verify:package  # แพ็กแล้วติดตั้งจริงในโฟลเดอร์ชั่วคราว เพื่อทดสอบว่าใช้งานได้

ซอร์สเขียนด้วย TypeScript และรันตรง ๆ ได้บน Node 22.18+ ผ่าน type stripping ในตัวของ Node จึงไม่ต้อง build ระหว่างพัฒนา ส่วน npm run build ใช้ตอนจะเผยแพร่เท่านั้น

verify:package ตรวจสิ่งที่เทสต์ปกติจับไม่ได้ เพราะเทสต์รันบนซอร์ส ไม่ใช่บนแพ็กเกจที่ติดตั้งแล้ว — เช่น exports ชี้ผิดไฟล์ ไฟล์จำเป็นไม่ถูกใส่ใน files หรือ type ที่ผู้ใช้มองไม่เห็น

เทสต์

เทสต์ใช้บัตรจำลองใน tests/fake-card.ts ซึ่งเป็น Buffer ก้อนเดียวที่เขียนข้อมูลไว้ตาม offset จริง และจำลองพฤติกรรมของทั้ง T=0 (ตอบ 61xx แล้วต้องสั่ง GET RESPONSE ตามหลัง) และ T=1

ชุดเทสต์แบ่งเป็น:

| ไฟล์ | ครอบคลุม | |---|---| | decode.test.ts | ถอด TIS-620, แปลง พ.ศ., แยกชื่อและที่อยู่ | | apdu.test.ts | ประกอบคำสั่ง, ความต่างของ T=0 กับ T=1 | | thai-id.test.ts | ลำดับการอ่านบัตรทั้งใบ | | card-state.test.ts | ตีความบิตสถานะของ PC/SC | | async-utils.test.ts | ตัวจับเวลาและการลองใหม่ | | recovery.test.ts | จัดประเภทข้อผิดพลาดและตัดสินใจว่าจะลองใหม่ไหม | | edge-cases.test.ts | ค่าที่ขอบเขต, วันที่ที่ไม่มีจริง, ข้อมูลไม่ครบ | | fuzz.test.ts | ยิงข้อมูลขยะ 3,000 รอบต่อเคส ตัวถอดรหัสต้องไม่ระเบิด | | watcher-lifecycle.test.ts | เริ่ม/หยุด/ปิดซ้ำ และการไม่รั่วของทรัพยากร |

แก้ปัญหาที่พบบ่อย

ขึ้นว่าไม่พบเครื่องอ่านบัตร ตรวจว่าระบบเห็นเครื่องอ่านหรือยัง — บน macOS สั่ง pcsctest แล้วดูรายการ reader

อ่านได้แต่ภาษาไทยเพี้ยน แปลว่าที่ไหนสักแห่งตีความข้อมูลเป็น UTF-8 บัตรเก็บข้อความเป็น TIS-620 ซึ่งไลบรารีนี้ถอดรหัสให้แล้ว ปัญหามักอยู่ที่ปลายทางที่รับค่าต่อ

บน Linux ขึ้น SCARD_E_NO_SERVICE pcscd ยังไม่ทำงาน — สั่ง sudo systemctl start pcscd ถ้าเครื่องอ่านถูกแย่งจับ ให้ blacklist kernel module pn533 และ nfc

บัตรบางใบอ่านฟิลด์ใดฟิลด์หนึ่งไม่ได้ offset ของบัตรบางรุ่นต่างกันเล็กน้อย แก้ได้ที่ src/card/fields.ts จุดเดียว

ขึ้น timeout เป็นครั้งคราวทั้งที่บัตรเสียบอยู่ มักเกิดจากบัตรเสียบไม่สุดหรือขยับระหว่างอ่าน ถอดบัตรออกแล้วเสียบใหม่ให้สุด ตัวเฝ้ารอจะอ่านใหม่เองเมื่อตรวจพบว่าเสียบบัตรอีกครั้ง

จากการทดสอบจริงแบบเปิดโปรเซสค้างแล้วเสียบ-ถอดบัตรต่อเนื่อง อ่านสำเร็จ 9 ครั้งติด และหลังเจอ timeout หนึ่งครั้ง ระบบยังตรวจจับการถอด/เสียบบัตรได้ตามปกติ ไม่จำเป็นต้องรีสตาร์ตหรือถอดสาย USB

ขึ้น timeout ทุกครั้งไม่ว่าจะทำอะไร และ ATR ยังอ่านได้ กรณีนี้คือ slot ของ PC/SC daemon ค้างจริง มักเกิดเมื่อมีโปรเซสถูกปิดกลางคัน ระหว่างที่กำลังเชื่อมต่อบัตร วิธีแก้ตามลำดับ:

  1. ถอดบัตรออกแล้วเสียบใหม่
  2. ถอดสาย USB ของเครื่องอ่านออกทั้งเส้น รอ 3 วินาที แล้วเสียบกลับ
  3. บน macOS ถ้ายังไม่หาย: sudo killall -9 com.apple.ifdreader

ตรวจว่าใช่อาการนี้ไหม: system_profiler SPSmartCardsDataType ยังเห็น ATR แต่โปรแกรมต่อบัตรไม่ได้ และชื่อเครื่องอ่านมีเลขต่อท้าย (เช่น ... Reader 01) ซึ่งเป็นสัญญาณว่ามี slot เก่าค้างอยู่