Skip to content

API Reference

Core Classes

ThaiIDCardReader

pythaiidcard.reader.ThaiIDCardReader

Reader for Thai National ID Cards.

Source code in pythaiidcard/reader.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
class ThaiIDCardReader:
    """Reader for Thai National ID Cards."""

    def __init__(
        self,
        reader_index: Optional[int] = None,
        retry_count: int = 3,
        skip_system_check: bool = False,
    ):
        """Initialize the Thai ID Card reader.

        Args:
            reader_index: Index of the reader to use (None for auto-select)
            retry_count: Number of retries for failed operations
            skip_system_check: Skip system dependency check (default: False)

        Raises:
            SystemDependencyError: If required system dependencies are missing
        """
        # Check system dependencies on Linux apt-based systems
        check_and_raise_if_missing(skip_check=skip_system_check)

        self.reader_index = reader_index
        self.retry_count = retry_count
        self.connection: Optional[CardConnection] = None
        self.atr: Optional[List[int]] = None
        self._request_command: Optional[List[int]] = None

    @staticmethod
    def list_readers(skip_system_check: bool = False) -> List[CardReaderInfo]:
        """List all available smart card readers.

        Args:
            skip_system_check: Skip system dependency check (default: False)

        Returns:
            List of CardReaderInfo objects

        Raises:
            NoReaderFoundError: If no readers are found
            SystemDependencyError: If required system dependencies are missing
        """
        # Check system dependencies on Linux apt-based systems
        check_and_raise_if_missing(skip_check=skip_system_check)

        reader_list = readers()

        if not reader_list:
            raise NoReaderFoundError()

        readers_info = []
        for index, reader in enumerate(reader_list):
            info = CardReaderInfo(index=index, name=str(reader), connected=False)

            # Try to get ATR if card is present
            try:
                connection = reader.createConnection()
                connection.connect(mode=SCARD_SHARE_SHARED)
                atr = connection.getATR()
                info.atr = toHexString(atr)
                info.connected = True
                connection.disconnect()
            except Exception:
                pass

            readers_info.append(info)

        return readers_info

    def _get_reader(self) -> "Reader":
        """Get the smart card reader.

        Returns:
            Selected reader instance

        Raises:
            NoReaderFoundError: If no readers are found
            InvalidReaderIndexError: If specified index is invalid
        """
        reader_list = readers()

        if not reader_list:
            raise NoReaderFoundError()

        if self.reader_index is None:
            # Auto-select first reader
            logger.info(f"Auto-selecting first reader: {reader_list[0]}")
            return reader_list[0]

        if self.reader_index < 0 or self.reader_index >= len(reader_list):
            raise InvalidReaderIndexError(self.reader_index, len(reader_list))

        logger.info(
            f"Using reader {self.reader_index}: {reader_list[self.reader_index]}"
        )
        return reader_list[self.reader_index]

    def connect(self) -> None:
        """Connect to the Thai ID card.

        Raises:
            NoCardDetectedError: If no card is detected
            CardConnectionError: If connection fails
            InvalidCardError: If card is not a Thai ID card
        """
        try:
            reader = self._get_reader()
            self.connection = reader.createConnection()
            # Use SHARED mode instead of EXCLUSIVE to avoid card reset issues
            # This matches the Node.js pcsclite implementation with SCARD_SHARE_SHARED
            self.connection.connect(mode=SCARD_SHARE_SHARED)

            self.atr = self.connection.getATR()
            logger.info(f"Connected to card, ATR: {toHexString(self.atr)}")

            # Determine request command based on ATR
            self._request_command = CardCommands.get_read_request(self.atr)

            # Select Thai ID applet
            self._select_applet()

        except NoReaderFoundError:
            raise
        except InvalidReaderIndexError:
            raise
        except Exception as e:
            if "No card in reader" in str(e):
                raise NoCardDetectedError()
            raise CardConnectionError(error=e)

    def disconnect(self) -> None:
        """Disconnect from the card."""
        if self.connection:
            try:
                self.connection.disconnect()
                logger.info("Disconnected from card")
            except Exception as e:
                logger.error(f"Error disconnecting: {e}")
            finally:
                self.connection = None
                self.atr = None
                self._request_command = None

    def _select_applet(self) -> None:
        """Select the Thai ID card applet.

        Raises:
            InvalidCardError: If not a Thai ID card
        """
        command = CardCommands.SELECT_APPLET.command + CardCommands.THAI_ID_APPLET
        data, sw1, sw2 = self.connection.transmit(command)

        if not ResponseStatus.is_success(sw1, sw2):
            raise InvalidCardError(
                f"Failed to select Thai ID applet: {sw1:02X} {sw2:02X}"
            )

        logger.info("Thai ID applet selected successfully")

    def _select_nhso_applet(self) -> None:
        """Select the NHSO health insurance applet.

        Raises:
            InvalidCardError: If NHSO applet selection fails
        """
        command = CardCommands.SELECT_APPLET.command + CardCommands.NHSO_APPLET
        data, sw1, sw2 = self.connection.transmit(command)

        if not ResponseStatus.is_success(sw1, sw2):
            raise InvalidCardError(f"Failed to select NHSO applet: {sw1:02X} {sw2:02X}")

        logger.info("NHSO applet selected successfully")

    def _select_card_applet(self) -> None:
        """Select the Card applet for laser ID reading.

        Raises:
            InvalidCardError: If Card applet selection fails
        """
        command = CardCommands.SELECT_APPLET.command + CardCommands.CARD_APPLET
        data, sw1, sw2 = self.connection.transmit(command)

        if not ResponseStatus.is_success(sw1, sw2):
            raise InvalidCardError(f"Failed to select Card applet: {sw1:02X} {sw2:02X}")

        logger.info("Card applet selected successfully")

    def _send_command(self, apdu_command: APDUCommand) -> bytes:
        """Send an APDU command and get the response.

        Args:
            apdu_command: APDU command to send

        Returns:
            Response data as bytes

        Raises:
            CommandError: If command fails
        """
        if not self.connection:
            raise CardConnectionError("Not connected to card")

        # Send initial command
        data, sw1, sw2 = self.connection.transmit(apdu_command.command)

        # Get response data
        if ResponseStatus.has_more_data(sw1):
            # Read the response
            read_cmd = self._request_command + [apdu_command.command[-1]]
            data, sw1, sw2 = self.connection.transmit(read_cmd)

            if not ResponseStatus.is_success(sw1, sw2):
                raise CommandError(apdu_command.description, sw1, sw2)

        return bytes(data)

    def _read_text_field(self, apdu_command: APDUCommand) -> str:
        """Read a text field from the card.

        Args:
            apdu_command: APDU command for the field

        Returns:
            Decoded text string

        Raises:
            DataReadError: If reading fails
        """
        for attempt in range(self.retry_count):
            try:
                data = self._send_command(apdu_command)
                return thai_to_unicode(data)
            except Exception as e:
                if attempt == self.retry_count - 1:
                    raise DataReadError(apdu_command.description, e)
                logger.warning(f"Retry {attempt + 1} for {apdu_command.description}")

    def _read_photo(self, progress_callback: Optional[callable] = None) -> bytes:
        """Read photo data from the card.

        Args:
            progress_callback: Optional callback for progress updates

        Returns:
            Complete photo data as bytes

        Raises:
            DataReadError: If reading photo fails
        """
        photo_data = b""
        total_parts = len(CardCommands.PHOTO_COMMANDS)

        for i, photo_cmd in enumerate(CardCommands.PHOTO_COMMANDS):
            try:
                data = self._send_command(photo_cmd)
                photo_data += data

                if progress_callback:
                    progress_callback(i + 1, total_parts)

                logger.debug(f"Read photo part {i + 1}/{total_parts}")
            except Exception as e:
                raise DataReadError(f"Photo part {i + 1}", e)

        return photo_data

    def read_card(
        self,
        include_photo: bool = True,
        photo_progress_callback: Optional[callable] = None,
    ) -> ThaiIDCard:
        """Read all data from Thai ID card.

        Args:
            include_photo: Whether to read the photo
            photo_progress_callback: Optional callback for photo reading progress

        Returns:
            ThaiIDCard object with all card data

        Raises:
            CardConnectionError: If not connected to card
            DataReadError: If reading any field fails
            CommandError: If APDU command fails
        """
        if not self.connection:
            raise CardConnectionError("Not connected to card. Call connect() first.")

        logger.info("Reading Thai ID card data...")

        # Re-select Thai ID applet in case another applet was selected
        self._select_applet()

        # Read all fields
        cid = self._read_text_field(CardCommands.CID)
        thai_fullname = self._read_text_field(CardCommands.THAI_FULLNAME)
        english_fullname = self._read_text_field(CardCommands.ENGLISH_FULLNAME)
        date_of_birth = self._read_text_field(CardCommands.DATE_OF_BIRTH)
        gender = self._read_text_field(CardCommands.GENDER)
        card_issuer = self._read_text_field(CardCommands.CARD_ISSUER)
        issue_date = self._read_text_field(CardCommands.ISSUE_DATE)
        expire_date = self._read_text_field(CardCommands.EXPIRE_DATE)
        address = self._read_text_field(CardCommands.ADDRESS)

        # Read photo if requested
        photo = None
        if include_photo:
            logger.info("Reading photo data...")
            photo = self._read_photo(photo_progress_callback)

        # Create and return the card model
        # The validators will automatically parse the raw strings into Name and Address objects
        card = ThaiIDCard(
            cid=cid,
            thai_name=thai_fullname,  # Will be parsed by validator
            english_name=english_fullname,  # Will be parsed by validator
            date_of_birth=date_of_birth,
            gender=gender,
            card_issuer=card_issuer,
            issue_date=issue_date,
            expire_date=expire_date,
            address_info=address,  # Will be parsed by validator
            photo=photo,
        )

        logger.info(f"Successfully read card for CID: {cid}")
        return card

    def read_nhso_data(self) -> NHSOData:
        """Read NHSO (National Health Security Office) data from Thai ID card.

        Returns:
            NHSOData object with health insurance information

        Raises:
            CardConnectionError: If not connected to card
            DataReadError: If reading any field fails
            CommandError: If APDU command fails
            InvalidCardError: If NHSO applet selection fails
        """
        if not self.connection:
            raise CardConnectionError("Not connected to card. Call connect() first.")

        logger.info("Reading NHSO health insurance data...")

        # Select NHSO applet
        self._select_nhso_applet()

        # Read all NHSO fields
        main_inscl = self._read_text_field(CardCommands.NHSO_MAIN_INSCL)
        sub_inscl = self._read_text_field(CardCommands.NHSO_SUB_INSCL)
        main_hospital_name = self._read_text_field(CardCommands.NHSO_MAIN_HOSPITAL_NAME)
        sub_hospital_name = self._read_text_field(CardCommands.NHSO_SUB_HOSPITAL_NAME)
        paid_type = self._read_text_field(CardCommands.NHSO_PAID_TYPE)
        issue_date = self._read_text_field(CardCommands.NHSO_ISSUE_DATE)
        expire_date = self._read_text_field(CardCommands.NHSO_EXPIRE_DATE)
        update_date = self._read_text_field(CardCommands.NHSO_UPDATE_DATE)
        change_hospital_amount = self._read_text_field(
            CardCommands.NHSO_CHANGE_HOSPITAL_AMOUNT
        )

        # Create and return the NHSO data model
        nhso_data = NHSOData(
            main_inscl=main_inscl,
            sub_inscl=sub_inscl,
            main_hospital_name=main_hospital_name,
            sub_hospital_name=sub_hospital_name,
            paid_type=paid_type,
            issue_date=issue_date,
            expire_date=expire_date,
            update_date=update_date,
            change_hospital_amount=change_hospital_amount,
        )

        logger.info("Successfully read NHSO data")
        return nhso_data

    def read_laser_id(self) -> str:
        """Read laser-engraved ID from Thai ID card.

        Returns:
            Laser ID as a string

        Raises:
            CardConnectionError: If not connected to card
            DataReadError: If reading fails
            CommandError: If APDU command fails
            InvalidCardError: If Card applet selection fails
        """
        if not self.connection:
            raise CardConnectionError("Not connected to card. Call connect() first.")

        logger.info("Reading laser ID...")

        # Select Card applet
        self._select_card_applet()

        # Read laser ID
        laser_id = self._read_text_field(CardCommands.LASER_ID)

        logger.info(f"Successfully read laser ID: {laser_id}")
        return laser_id

    @contextmanager
    def card_session(self) -> Generator["ThaiIDCardReader", None, None]:
        """Context manager for card operations.

        Usage:
            with reader.card_session():
                card = reader.read_card()
        """
        try:
            self.connect()
            yield self
        finally:
            self.disconnect()

Functions

__init__(reader_index=None, retry_count=3, skip_system_check=False)

Initialize the Thai ID Card reader.

Parameters:

Name Type Description Default
reader_index Optional[int]

Index of the reader to use (None for auto-select)

None
retry_count int

Number of retries for failed operations

3
skip_system_check bool

Skip system dependency check (default: False)

False

Raises:

Type Description
SystemDependencyError

If required system dependencies are missing

Source code in pythaiidcard/reader.py
def __init__(
    self,
    reader_index: Optional[int] = None,
    retry_count: int = 3,
    skip_system_check: bool = False,
):
    """Initialize the Thai ID Card reader.

    Args:
        reader_index: Index of the reader to use (None for auto-select)
        retry_count: Number of retries for failed operations
        skip_system_check: Skip system dependency check (default: False)

    Raises:
        SystemDependencyError: If required system dependencies are missing
    """
    # Check system dependencies on Linux apt-based systems
    check_and_raise_if_missing(skip_check=skip_system_check)

    self.reader_index = reader_index
    self.retry_count = retry_count
    self.connection: Optional[CardConnection] = None
    self.atr: Optional[List[int]] = None
    self._request_command: Optional[List[int]] = None

list_readers(skip_system_check=False) staticmethod

List all available smart card readers.

Parameters:

Name Type Description Default
skip_system_check bool

Skip system dependency check (default: False)

False

Returns:

Type Description
List[CardReaderInfo]

List of CardReaderInfo objects

Raises:

Type Description
NoReaderFoundError

If no readers are found

SystemDependencyError

If required system dependencies are missing

Source code in pythaiidcard/reader.py
@staticmethod
def list_readers(skip_system_check: bool = False) -> List[CardReaderInfo]:
    """List all available smart card readers.

    Args:
        skip_system_check: Skip system dependency check (default: False)

    Returns:
        List of CardReaderInfo objects

    Raises:
        NoReaderFoundError: If no readers are found
        SystemDependencyError: If required system dependencies are missing
    """
    # Check system dependencies on Linux apt-based systems
    check_and_raise_if_missing(skip_check=skip_system_check)

    reader_list = readers()

    if not reader_list:
        raise NoReaderFoundError()

    readers_info = []
    for index, reader in enumerate(reader_list):
        info = CardReaderInfo(index=index, name=str(reader), connected=False)

        # Try to get ATR if card is present
        try:
            connection = reader.createConnection()
            connection.connect(mode=SCARD_SHARE_SHARED)
            atr = connection.getATR()
            info.atr = toHexString(atr)
            info.connected = True
            connection.disconnect()
        except Exception:
            pass

        readers_info.append(info)

    return readers_info

connect()

Connect to the Thai ID card.

Raises:

Type Description
NoCardDetectedError

If no card is detected

CardConnectionError

If connection fails

InvalidCardError

If card is not a Thai ID card

Source code in pythaiidcard/reader.py
def connect(self) -> None:
    """Connect to the Thai ID card.

    Raises:
        NoCardDetectedError: If no card is detected
        CardConnectionError: If connection fails
        InvalidCardError: If card is not a Thai ID card
    """
    try:
        reader = self._get_reader()
        self.connection = reader.createConnection()
        # Use SHARED mode instead of EXCLUSIVE to avoid card reset issues
        # This matches the Node.js pcsclite implementation with SCARD_SHARE_SHARED
        self.connection.connect(mode=SCARD_SHARE_SHARED)

        self.atr = self.connection.getATR()
        logger.info(f"Connected to card, ATR: {toHexString(self.atr)}")

        # Determine request command based on ATR
        self._request_command = CardCommands.get_read_request(self.atr)

        # Select Thai ID applet
        self._select_applet()

    except NoReaderFoundError:
        raise
    except InvalidReaderIndexError:
        raise
    except Exception as e:
        if "No card in reader" in str(e):
            raise NoCardDetectedError()
        raise CardConnectionError(error=e)

disconnect()

Disconnect from the card.

Source code in pythaiidcard/reader.py
def disconnect(self) -> None:
    """Disconnect from the card."""
    if self.connection:
        try:
            self.connection.disconnect()
            logger.info("Disconnected from card")
        except Exception as e:
            logger.error(f"Error disconnecting: {e}")
        finally:
            self.connection = None
            self.atr = None
            self._request_command = None

read_card(include_photo=True, photo_progress_callback=None)

Read all data from Thai ID card.

Parameters:

Name Type Description Default
include_photo bool

Whether to read the photo

True
photo_progress_callback Optional[callable]

Optional callback for photo reading progress

None

Returns:

Type Description
ThaiIDCard

ThaiIDCard object with all card data

Raises:

Type Description
CardConnectionError

If not connected to card

DataReadError

If reading any field fails

CommandError

If APDU command fails

Source code in pythaiidcard/reader.py
def read_card(
    self,
    include_photo: bool = True,
    photo_progress_callback: Optional[callable] = None,
) -> ThaiIDCard:
    """Read all data from Thai ID card.

    Args:
        include_photo: Whether to read the photo
        photo_progress_callback: Optional callback for photo reading progress

    Returns:
        ThaiIDCard object with all card data

    Raises:
        CardConnectionError: If not connected to card
        DataReadError: If reading any field fails
        CommandError: If APDU command fails
    """
    if not self.connection:
        raise CardConnectionError("Not connected to card. Call connect() first.")

    logger.info("Reading Thai ID card data...")

    # Re-select Thai ID applet in case another applet was selected
    self._select_applet()

    # Read all fields
    cid = self._read_text_field(CardCommands.CID)
    thai_fullname = self._read_text_field(CardCommands.THAI_FULLNAME)
    english_fullname = self._read_text_field(CardCommands.ENGLISH_FULLNAME)
    date_of_birth = self._read_text_field(CardCommands.DATE_OF_BIRTH)
    gender = self._read_text_field(CardCommands.GENDER)
    card_issuer = self._read_text_field(CardCommands.CARD_ISSUER)
    issue_date = self._read_text_field(CardCommands.ISSUE_DATE)
    expire_date = self._read_text_field(CardCommands.EXPIRE_DATE)
    address = self._read_text_field(CardCommands.ADDRESS)

    # Read photo if requested
    photo = None
    if include_photo:
        logger.info("Reading photo data...")
        photo = self._read_photo(photo_progress_callback)

    # Create and return the card model
    # The validators will automatically parse the raw strings into Name and Address objects
    card = ThaiIDCard(
        cid=cid,
        thai_name=thai_fullname,  # Will be parsed by validator
        english_name=english_fullname,  # Will be parsed by validator
        date_of_birth=date_of_birth,
        gender=gender,
        card_issuer=card_issuer,
        issue_date=issue_date,
        expire_date=expire_date,
        address_info=address,  # Will be parsed by validator
        photo=photo,
    )

    logger.info(f"Successfully read card for CID: {cid}")
    return card

card_session()

Context manager for card operations.

Usage

with reader.card_session(): card = reader.read_card()

Source code in pythaiidcard/reader.py
@contextmanager
def card_session(self) -> Generator["ThaiIDCardReader", None, None]:
    """Context manager for card operations.

    Usage:
        with reader.card_session():
            card = reader.read_card()
    """
    try:
        self.connect()
        yield self
    finally:
        self.disconnect()

ThaiIDCard

pythaiidcard.models.ThaiIDCard

Bases: BaseModel

Model representing data from a Thai National ID Card.

Source code in pythaiidcard/models.py
class ThaiIDCard(BaseModel):
    """Model representing data from a Thai National ID Card."""

    cid: str = Field(
        ...,
        description="13-digit citizen identification number",
        min_length=13,
        max_length=13,
        pattern=r"^\d{13}$",
    )
    thai_name: Name = Field(..., description="Name in Thai language")
    english_name: Name = Field(..., description="Name in English")
    date_of_birth: date = Field(..., description="Date of birth")
    gender: str = Field(..., description="Gender (1=Male, 2=Female)", pattern=r"^[12]$")
    card_issuer: str = Field(..., description="Card issuing organization")
    issue_date: date = Field(..., description="Date when the card was issued")
    expire_date: date = Field(..., description="Date when the card expires")
    address_info: Address = Field(..., description="Registered address information")
    photo: Optional[bytes] = Field(None, description="JPEG photo data")

    @field_validator("cid")
    @classmethod
    def validate_cid(cls, v: str) -> str:
        """Validate Thai citizen ID with checksum."""
        if not v.isdigit() or len(v) != 13:
            raise ValueError("CID must be exactly 13 digits")

        # Calculate checksum (Thai ID card algorithm)
        checksum = 0
        for i in range(12):
            checksum += int(v[i]) * (13 - i)
        checksum = (11 - (checksum % 11)) % 10

        if checksum != int(v[12]):
            raise ValueError(f"Invalid CID checksum: expected {checksum}, got {v[12]}")

        return v

    @field_validator("thai_name", "english_name", mode="before")
    @classmethod
    def parse_name_field(cls, v: str | Name) -> Name:
        """Parse name from raw string or pass through Name object."""
        if isinstance(v, Name):
            return v
        if isinstance(v, str):
            return Name.from_raw(v)
        raise ValueError(f"Invalid name type: {type(v)}")

    @field_validator("address_info", mode="before")
    @classmethod
    def parse_address_field(cls, v: str | Address) -> Address:
        """Parse address from raw string or pass through Address object."""
        if isinstance(v, Address):
            return v
        if isinstance(v, str):
            return Address.from_raw(v)
        raise ValueError(f"Invalid address type: {type(v)}")

    @field_validator("date_of_birth", "issue_date", "expire_date", mode="before")
    @classmethod
    def parse_date_field(cls, v: str | date) -> date:
        """Parse date from Thai Buddhist calendar string (YYYYMMDD)."""
        if isinstance(v, date):
            return v

        if not v or len(v) != 8:
            raise ValueError(f"Invalid date format: {v}")

        # Parse as YYYYMMDD in Buddhist Era
        year = int(v[:4]) - 543  # Convert from Buddhist Era to Gregorian
        month = int(v[4:6])
        day = int(v[6:8])

        try:
            return date(year, month, day)
        except ValueError as e:
            raise ValueError(f"Invalid date: {v} - {e}")

    @computed_field
    @property
    def thai_fullname(self) -> str:
        """Get Thai full name as string (backward compatibility)."""
        return self.thai_name.full_name

    @computed_field
    @property
    def english_fullname(self) -> str:
        """Get English full name as string (backward compatibility)."""
        return self.english_name.full_name

    @computed_field
    @property
    def address(self) -> str:
        """Get full address as string (backward compatibility)."""
        return self.address_info.address

    @computed_field
    @property
    def age(self) -> int:
        """Calculate current age from date of birth."""
        today = date.today()
        age = today.year - self.date_of_birth.year
        if (today.month, today.day) < (
            self.date_of_birth.month,
            self.date_of_birth.day,
        ):
            age -= 1
        return age

    @computed_field
    @property
    def gender_text(self) -> str:
        """Get gender as text."""
        return "Male" if self.gender == "1" else "Female"

    @computed_field
    @property
    def is_expired(self) -> bool:
        """Check if the card has expired."""
        return date.today() > self.expire_date

    @computed_field
    @property
    def days_until_expiry(self) -> int:
        """Calculate days until card expiry."""
        return (self.expire_date - date.today()).days

    def save_photo(self, path: Optional[Path] = None) -> Optional[Path]:
        """Save photo to file.

        Args:
            path: Path to save the photo. If None, saves as {cid}.jpg

        Returns:
            Path where photo was saved, or None if no photo data
        """
        if not self.photo:
            return None

        if path is None:
            path = Path(f"{self.cid}.jpg")

        path.write_bytes(self.photo)
        return path

    class Config:
        """Pydantic configuration."""

        json_encoders = {
            date: lambda v: v.isoformat(),
            bytes: lambda v: None,  # Don't include photo in JSON
        }

Attributes

cid = Field(..., description='13-digit citizen identification number', min_length=13, max_length=13, pattern='^\\d{13}$') class-attribute instance-attribute

thai_fullname property

Get Thai full name as string (backward compatibility).

english_fullname property

Get English full name as string (backward compatibility).

date_of_birth = Field(..., description='Date of birth') class-attribute instance-attribute

gender = Field(..., description='Gender (1=Male, 2=Female)', pattern='^[12]$') class-attribute instance-attribute

gender_text property

Get gender as text.

card_issuer = Field(..., description='Card issuing organization') class-attribute instance-attribute

issue_date = Field(..., description='Date when the card was issued') class-attribute instance-attribute

expire_date = Field(..., description='Date when the card expires') class-attribute instance-attribute

address property

Get full address as string (backward compatibility).

photo = Field(None, description='JPEG photo data') class-attribute instance-attribute

age property

Calculate current age from date of birth.

is_expired property

Check if the card has expired.

days_until_expiry property

Calculate days until card expiry.

Functions

save_photo(path=None)

Save photo to file.

Parameters:

Name Type Description Default
path Optional[Path]

Path to save the photo. If None, saves as {cid}.jpg

None

Returns:

Type Description
Optional[Path]

Path where photo was saved, or None if no photo data

Source code in pythaiidcard/models.py
def save_photo(self, path: Optional[Path] = None) -> Optional[Path]:
    """Save photo to file.

    Args:
        path: Path to save the photo. If None, saves as {cid}.jpg

    Returns:
        Path where photo was saved, or None if no photo data
    """
    if not self.photo:
        return None

    if path is None:
        path = Path(f"{self.cid}.jpg")

    path.write_bytes(self.photo)
    return path

CardReaderInfo

pythaiidcard.models.CardReaderInfo

Bases: BaseModel

Information about a smart card reader.

Source code in pythaiidcard/models.py
class CardReaderInfo(BaseModel):
    """Information about a smart card reader."""

    index: int = Field(..., description="Reader index in the system")
    name: str = Field(..., description="Reader name/model")
    atr: Optional[str] = Field(None, description="Answer to Reset (ATR) hex string")
    connected: bool = Field(False, description="Whether a card is connected")

Convenience Functions

read_thai_id_card

pythaiidcard.reader.read_thai_id_card(reader_index=None, include_photo=True)

Convenience function to read a Thai ID card.

Parameters:

Name Type Description Default
reader_index Optional[int]

Reader index to use (None for auto-select)

None
include_photo bool

Whether to include photo data

True

Returns:

Type Description
ThaiIDCard

ThaiIDCard object with card data

Raises:

Type Description
SystemDependencyError

If system dependencies are missing

NoReaderFoundError

If no readers are found

NoCardDetectedError

If no card is detected

CardConnectionError

If connection fails

InvalidCardError

If not a Thai ID card

DataReadError

If reading data fails

Source code in pythaiidcard/reader.py
def read_thai_id_card(
    reader_index: Optional[int] = None, include_photo: bool = True
) -> ThaiIDCard:
    """Convenience function to read a Thai ID card.

    Args:
        reader_index: Reader index to use (None for auto-select)
        include_photo: Whether to include photo data

    Returns:
        ThaiIDCard object with card data

    Raises:
        SystemDependencyError: If system dependencies are missing
        NoReaderFoundError: If no readers are found
        NoCardDetectedError: If no card is detected
        CardConnectionError: If connection fails
        InvalidCardError: If not a Thai ID card
        DataReadError: If reading data fails
    """
    reader = ThaiIDCardReader(reader_index)

    with reader.card_session():
        return reader.read_card(include_photo)

Utility Functions

CID Utilities

pythaiidcard.utils.validate_cid(cid)

Validate Thai citizen ID with checksum.

Parameters:

Name Type Description Default
cid str

13-digit citizen ID

required

Returns:

Type Description
bool

True if valid, False otherwise

Source code in pythaiidcard/utils.py
def validate_cid(cid: str) -> bool:
    """Validate Thai citizen ID with checksum.

    Args:
        cid: 13-digit citizen ID

    Returns:
        True if valid, False otherwise
    """
    if not cid.isdigit() or len(cid) != 13:
        return False

    # Calculate checksum (Thai ID card algorithm)
    checksum = 0
    for i in range(12):
        checksum += int(cid[i]) * (13 - i)
    checksum = (11 - (checksum % 11)) % 10

    return checksum == int(cid[12])

pythaiidcard.utils.format_cid(cid)

Format citizen ID with dashes for readability.

Parameters:

Name Type Description Default
cid str

13-digit citizen ID

required

Returns:

Type Description
str

Formatted CID (e.g., 1-2345-67890-12-3)

Source code in pythaiidcard/utils.py
def format_cid(cid: str) -> str:
    """Format citizen ID with dashes for readability.

    Args:
        cid: 13-digit citizen ID

    Returns:
        Formatted CID (e.g., 1-2345-67890-12-3)
    """
    if len(cid) != 13:
        return cid

    return f"{cid[0]}-{cid[1:5]}-{cid[5:10]}-{cid[10:12]}-{cid[12]}"

Date Utilities

pythaiidcard.utils.parse_buddhist_date(date_str)

Parse Buddhist Era date string to Gregorian.

Parameters:

Name Type Description Default
date_str str

Date in YYYYMMDD format (Buddhist Era)

required

Returns:

Type Description
str

Date in YYYY-MM-DD format (Gregorian)

Source code in pythaiidcard/utils.py
def parse_buddhist_date(date_str: str) -> str:
    """Parse Buddhist Era date string to Gregorian.

    Args:
        date_str: Date in YYYYMMDD format (Buddhist Era)

    Returns:
        Date in YYYY-MM-DD format (Gregorian)
    """
    if not date_str or len(date_str) != 8:
        return date_str

    try:
        # Convert Buddhist year to Gregorian
        year = int(date_str[:4]) - 543
        month = date_str[4:6]
        day = date_str[6:8]

        return f"{year:04d}-{month}-{day}"
    except (ValueError, IndexError):
        return date_str

pythaiidcard.utils.calculate_age(birth_date_str)

Calculate age from Buddhist Era birth date.

Parameters:

Name Type Description Default
birth_date_str str

Birth date in YYYYMMDD format (Buddhist Era)

required

Returns:

Type Description
Optional[int]

Age in years, or None if invalid date

Source code in pythaiidcard/utils.py
def calculate_age(birth_date_str: str) -> Optional[int]:
    """Calculate age from Buddhist Era birth date.

    Args:
        birth_date_str: Birth date in YYYYMMDD format (Buddhist Era)

    Returns:
        Age in years, or None if invalid date
    """
    from datetime import date

    if not birth_date_str or len(birth_date_str) != 8:
        return None

    try:
        # Convert Buddhist year to Gregorian
        year = int(birth_date_str[:4]) - 543
        month = int(birth_date_str[4:6])
        day = int(birth_date_str[6:8])

        birth_date = date(year, month, day)
        today = date.today()

        age = today.year - birth_date.year
        if (today.month, today.day) < (birth_date.month, birth_date.day):
            age -= 1

        return age
    except (ValueError, IndexError):
        return None

pythaiidcard.utils.is_card_expired(expire_date_str)

Check if card has expired based on Buddhist Era date.

Parameters:

Name Type Description Default
expire_date_str str

Expiry date in YYYYMMDD format (Buddhist Era)

required

Returns:

Type Description
bool

True if expired, False otherwise

Source code in pythaiidcard/utils.py
def is_card_expired(expire_date_str: str) -> bool:
    """Check if card has expired based on Buddhist Era date.

    Args:
        expire_date_str: Expiry date in YYYYMMDD format (Buddhist Era)

    Returns:
        True if expired, False otherwise
    """
    from datetime import date

    if not expire_date_str or len(expire_date_str) != 8:
        return False

    try:
        # Convert Buddhist year to Gregorian
        year = int(expire_date_str[:4]) - 543
        month = int(expire_date_str[4:6])
        day = int(expire_date_str[6:8])

        expire_date = date(year, month, day)
        return date.today() > expire_date
    except (ValueError, IndexError):
        return False

Text Utilities

pythaiidcard.utils.thai_to_unicode(data)

Convert Thai TIS-620 encoded data to Unicode string.

Parameters:

Name Type Description Default
data bytes | List[int]

Bytes or list of integers in TIS-620 encoding

required

Returns:

Type Description
str

Decoded Unicode string with # replaced by spaces and trimmed

Source code in pythaiidcard/utils.py
def thai_to_unicode(data: bytes | List[int]) -> str:
    """Convert Thai TIS-620 encoded data to Unicode string.

    Args:
        data: Bytes or list of integers in TIS-620 encoding

    Returns:
        Decoded Unicode string with # replaced by spaces and trimmed
    """
    try:
        if isinstance(data, list):
            data = bytes(data)

        # Decode from TIS-620 (Thai encoding)
        result = data.decode('tis-620', errors='ignore')

        # Replace # with space and strip whitespace
        result = result.replace("#", " ").strip()

        return result
    except Exception as e:
        logger.error(f"Error decoding Thai data: {e}")
        return ""

pythaiidcard.utils.format_address(address)

Format address for better readability.

Parameters:

Name Type Description Default
address str

Raw address string

required

Returns:

Type Description
str

Formatted address with proper spacing

Source code in pythaiidcard/utils.py
def format_address(address: str) -> str:
    """Format address for better readability.

    Args:
        address: Raw address string

    Returns:
        Formatted address with proper spacing
    """
    # Remove excessive spaces
    address = ' '.join(address.split())

    # Common Thai address keywords to add line breaks after
    keywords = ['หมู่ที่', 'ตำบล', 'อำเภอ', 'จังหวัด']

    for keyword in keywords:
        if keyword in address:
            address = address.replace(keyword, f"\n{keyword}")

    return address.strip()

Exceptions

ThaiIDCardException

pythaiidcard.exceptions.ThaiIDCardException

Bases: Exception

Base exception for Thai ID Card operations.

Source code in pythaiidcard/exceptions.py
4
5
6
class ThaiIDCardException(Exception):
    """Base exception for Thai ID Card operations."""
    pass

SystemDependencyError

pythaiidcard.exceptions.SystemDependencyError

Bases: ThaiIDCardException

Raised when required system dependencies are missing.

Source code in pythaiidcard/exceptions.py
class SystemDependencyError(ThaiIDCardException):
    """Raised when required system dependencies are missing."""

    def __init__(self, message: str, missing_dependencies: list = None):
        super().__init__(message)
        self.missing_dependencies = missing_dependencies or []

NoReaderFoundError

pythaiidcard.exceptions.NoReaderFoundError

Bases: ThaiIDCardException

Raised when no smart card reader is detected.

Source code in pythaiidcard/exceptions.py
class NoReaderFoundError(ThaiIDCardException):
    """Raised when no smart card reader is detected."""

    def __init__(self, message: str = "No smart card reader found. Please connect a reader."):
        super().__init__(message)

NoCardDetectedError

pythaiidcard.exceptions.NoCardDetectedError

Bases: ThaiIDCardException

Raised when no card is inserted in the reader.

Source code in pythaiidcard/exceptions.py
class NoCardDetectedError(ThaiIDCardException):
    """Raised when no card is inserted in the reader."""

    def __init__(self, message: str = "No card detected. Please insert a Thai ID card."):
        super().__init__(message)

CardConnectionError

pythaiidcard.exceptions.CardConnectionError

Bases: ThaiIDCardException

Raised when connection to the card fails.

Source code in pythaiidcard/exceptions.py
class CardConnectionError(ThaiIDCardException):
    """Raised when connection to the card fails."""

    def __init__(self, message: str = "Failed to connect to the card.", error: Exception = None):
        if error:
            message = f"{message} Error: {error}"
        super().__init__(message)
        self.original_error = error

InvalidCardError

pythaiidcard.exceptions.InvalidCardError

Bases: ThaiIDCardException

Raised when the card is not a valid Thai ID card.

Source code in pythaiidcard/exceptions.py
class InvalidCardError(ThaiIDCardException):
    """Raised when the card is not a valid Thai ID card."""

    def __init__(self, message: str = "The card is not a valid Thai National ID card."):
        super().__init__(message)

CommandError

pythaiidcard.exceptions.CommandError

Bases: ThaiIDCardException

Raised when an APDU command fails.

Source code in pythaiidcard/exceptions.py
class CommandError(ThaiIDCardException):
    """Raised when an APDU command fails."""

    def __init__(self, command: str, sw1: int, sw2: int):
        self.command = command
        self.sw1 = sw1
        self.sw2 = sw2
        message = f"Command '{command}' failed with status: {sw1:02X} {sw2:02X}"
        super().__init__(message)

DataReadError

pythaiidcard.exceptions.DataReadError

Bases: ThaiIDCardException

Raised when reading data from the card fails.

Source code in pythaiidcard/exceptions.py
class DataReadError(ThaiIDCardException):
    """Raised when reading data from the card fails."""

    def __init__(self, field: str, error: Exception = None):
        self.field = field
        self.original_error = error
        message = f"Failed to read {field} from card"
        if error:
            message = f"{message}: {error}"
        super().__init__(message)

InvalidReaderIndexError

pythaiidcard.exceptions.InvalidReaderIndexError

Bases: ThaiIDCardException

Raised when an invalid reader index is specified.

Source code in pythaiidcard/exceptions.py
class InvalidReaderIndexError(ThaiIDCardException):
    """Raised when an invalid reader index is specified."""

    def __init__(self, index: int, available: int):
        self.index = index
        self.available = available
        message = f"Invalid reader index {index}. Available readers: 0-{available-1}"
        super().__init__(message)

CardTimeoutError

pythaiidcard.exceptions.CardTimeoutError

Bases: ThaiIDCardException

Raised when card operation times out.

Source code in pythaiidcard/exceptions.py
class CardTimeoutError(ThaiIDCardException):
    """Raised when card operation times out."""

    def __init__(self, operation: str = "Card operation"):
        message = f"{operation} timed out. Please check the card is properly inserted."
        super().__init__(message)

System Check

check_system_dependencies

pythaiidcard.system_check.check_system_dependencies(skip_check=False)

Check if all required system dependencies are installed.

Parameters:

Name Type Description Default
skip_check bool

If True, skip the check and return empty results

False

Returns:

Type Description
Dict[str, List[SystemDependency]]

Dict with 'missing' and 'installed' lists of dependencies

Source code in pythaiidcard/system_check.py
def check_system_dependencies(skip_check: bool = False) -> Dict[str, List[SystemDependency]]:
    """Check if all required system dependencies are installed.

    Args:
        skip_check: If True, skip the check and return empty results

    Returns:
        Dict with 'missing' and 'installed' lists of dependencies
    """
    if skip_check:
        return {"missing": [], "installed": REQUIRED_DEPENDENCIES}

    missing = []
    installed = []

    for dep in REQUIRED_DEPENDENCIES:
        if dep.is_installed():
            installed.append(dep)
            logger.debug(f"✓ {dep.name} is installed")
        else:
            missing.append(dep)
            logger.debug(f"✗ {dep.name} is missing")

    return {"missing": missing, "installed": installed}

check_and_raise_if_missing

pythaiidcard.system_check.check_and_raise_if_missing(skip_check=False)

Check system dependencies and raise exception if any are missing.

Parameters:

Name Type Description Default
skip_check bool

If True, skip the check entirely

False

Raises:

Type Description
SystemDependencyError

If required dependencies are missing

Source code in pythaiidcard/system_check.py
def check_and_raise_if_missing(skip_check: bool = False) -> None:
    """Check system dependencies and raise exception if any are missing.

    Args:
        skip_check: If True, skip the check entirely

    Raises:
        SystemDependencyError: If required dependencies are missing
    """
    if skip_check:
        return

    # Only check on Linux systems
    system = platform.system().lower()
    if system != "linux":
        logger.debug("Skipping system dependency check (not a Linux system)")
        return

    # First, check if pyscard can be imported - this is the ultimate test
    try:
        import smartcard.System
        logger.debug("✓ pyscard is working (dependencies satisfied)")
        return  # If pyscard works, all dependencies are satisfied
    except ImportError as e:
        logger.debug(f"pyscard import failed: {e}")
        # Continue with system check
    except Exception as e:
        logger.debug(f"pyscard check failed: {e}")
        # Continue with system check

    result = check_system_dependencies(skip_check=False)
    missing = result["missing"]

    if missing:
        from .exceptions import SystemDependencyError
        message = format_missing_dependencies_message(missing)
        raise SystemDependencyError(message, missing)

    logger.debug("All system dependencies are installed")

Constants

APDUCommand

pythaiidcard.constants.APDUCommand

Base class for APDU commands.

Source code in pythaiidcard/constants.py
class APDUCommand:
    """Base class for APDU commands."""

    def __init__(self, command: List[int], description: str):
        self.command = command
        self.description = description

    def __repr__(self) -> str:
        hex_cmd = " ".join(f"{b:02X}" for b in self.command)
        return f"{self.description}: {hex_cmd}"

CardCommands

pythaiidcard.constants.CardCommands

APDU commands for Thai National ID Card operations.

Source code in pythaiidcard/constants.py
class CardCommands:
    """APDU commands for Thai National ID Card operations."""

    # Card selection
    SELECT_APPLET = APDUCommand(
        [0x00, 0xA4, 0x04, 0x00, 0x08],
        "Select Thai ID Card applet"
    )

    THAI_ID_APPLET = [0xA0, 0x00, 0x00, 0x00, 0x54, 0x48, 0x00, 0x01]
    NHSO_APPLET = [0xA0, 0x00, 0x00, 0x00, 0x54, 0x48, 0x00, 0x83]
    CARD_APPLET = [0xA0, 0x00, 0x00, 0x00, 0x84, 0x06, 0x00, 0x02]

    # Personal information commands
    CID = APDUCommand(
        [0x80, 0xb0, 0x00, 0x04, 0x02, 0x00, 0x0d],
        "Citizen ID (13 digits)"
    )

    THAI_FULLNAME = APDUCommand(
        [0x80, 0xb0, 0x00, 0x11, 0x02, 0x00, 0x64],
        "Full name in Thai"
    )

    ENGLISH_FULLNAME = APDUCommand(
        [0x80, 0xb0, 0x00, 0x75, 0x02, 0x00, 0x64],
        "Full name in English"
    )

    DATE_OF_BIRTH = APDUCommand(
        [0x80, 0xb0, 0x00, 0xD9, 0x02, 0x00, 0x08],
        "Date of birth (YYYYMMDD in Buddhist Era)"
    )

    GENDER = APDUCommand(
        [0x80, 0xb0, 0x00, 0xE1, 0x02, 0x00, 0x01],
        "Gender (1=Male, 2=Female)"
    )

    CARD_ISSUER = APDUCommand(
        [0x80, 0xb0, 0x00, 0xF6, 0x02, 0x00, 0x64],
        "Card issuing organization"
    )

    ISSUE_DATE = APDUCommand(
        [0x80, 0xb0, 0x01, 0x67, 0x02, 0x00, 0x08],
        "Card issue date (YYYYMMDD in Buddhist Era)"
    )

    EXPIRE_DATE = APDUCommand(
        [0x80, 0xb0, 0x01, 0x6F, 0x02, 0x00, 0x08],
        "Card expiry date (YYYYMMDD in Buddhist Era)"
    )

    ADDRESS = APDUCommand(
        [0x80, 0xb0, 0x15, 0x79, 0x02, 0x00, 0x64],
        "Registered address"
    )

    # Photo commands (20 parts)
    PHOTO_COMMANDS = [
        APDUCommand([0x80, 0xb0, 0x01, 0x7B, 0x02, 0x00, 0xFF], "Photo part 1/20"),
        APDUCommand([0x80, 0xb0, 0x02, 0x7A, 0x02, 0x00, 0xFF], "Photo part 2/20"),
        APDUCommand([0x80, 0xb0, 0x03, 0x79, 0x02, 0x00, 0xFF], "Photo part 3/20"),
        APDUCommand([0x80, 0xb0, 0x04, 0x78, 0x02, 0x00, 0xFF], "Photo part 4/20"),
        APDUCommand([0x80, 0xb0, 0x05, 0x77, 0x02, 0x00, 0xFF], "Photo part 5/20"),
        APDUCommand([0x80, 0xb0, 0x06, 0x76, 0x02, 0x00, 0xFF], "Photo part 6/20"),
        APDUCommand([0x80, 0xb0, 0x07, 0x75, 0x02, 0x00, 0xFF], "Photo part 7/20"),
        APDUCommand([0x80, 0xb0, 0x08, 0x74, 0x02, 0x00, 0xFF], "Photo part 8/20"),
        APDUCommand([0x80, 0xb0, 0x09, 0x73, 0x02, 0x00, 0xFF], "Photo part 9/20"),
        APDUCommand([0x80, 0xb0, 0x0A, 0x72, 0x02, 0x00, 0xFF], "Photo part 10/20"),
        APDUCommand([0x80, 0xb0, 0x0B, 0x71, 0x02, 0x00, 0xFF], "Photo part 11/20"),
        APDUCommand([0x80, 0xb0, 0x0C, 0x70, 0x02, 0x00, 0xFF], "Photo part 12/20"),
        APDUCommand([0x80, 0xb0, 0x0D, 0x6F, 0x02, 0x00, 0xFF], "Photo part 13/20"),
        APDUCommand([0x80, 0xb0, 0x0E, 0x6E, 0x02, 0x00, 0xFF], "Photo part 14/20"),
        APDUCommand([0x80, 0xb0, 0x0F, 0x6D, 0x02, 0x00, 0xFF], "Photo part 15/20"),
        APDUCommand([0x80, 0xb0, 0x10, 0x6C, 0x02, 0x00, 0xFF], "Photo part 16/20"),
        APDUCommand([0x80, 0xb0, 0x11, 0x6B, 0x02, 0x00, 0xFF], "Photo part 17/20"),
        APDUCommand([0x80, 0xb0, 0x12, 0x6A, 0x02, 0x00, 0xFF], "Photo part 18/20"),
        APDUCommand([0x80, 0xb0, 0x13, 0x69, 0x02, 0x00, 0xFF], "Photo part 19/20"),
        APDUCommand([0x80, 0xb0, 0x14, 0x68, 0x02, 0x00, 0xFF], "Photo part 20/20"),
    ]

    # NHSO Health Insurance commands
    NHSO_MAIN_INSCL = APDUCommand(
        [0x80, 0xb0, 0x00, 0x04, 0x02, 0x00, 0x3c],
        "Main insurance classification (60 bytes)"
    )

    NHSO_SUB_INSCL = APDUCommand(
        [0x80, 0xb0, 0x00, 0x40, 0x02, 0x00, 0x64],
        "Sub insurance classification (100 bytes)"
    )

    NHSO_MAIN_HOSPITAL_NAME = APDUCommand(
        [0x80, 0xb0, 0x00, 0xa4, 0x02, 0x00, 0x50],
        "Main hospital name (80 bytes)"
    )

    NHSO_SUB_HOSPITAL_NAME = APDUCommand(
        [0x80, 0xb0, 0x00, 0xf4, 0x02, 0x00, 0x50],
        "Sub hospital name (80 bytes)"
    )

    NHSO_PAID_TYPE = APDUCommand(
        [0x80, 0xb0, 0x01, 0x44, 0x02, 0x00, 0x01],
        "Paid type (1 byte)"
    )

    NHSO_ISSUE_DATE = APDUCommand(
        [0x80, 0xb0, 0x01, 0x45, 0x02, 0x00, 0x08],
        "NHSO issue date (YYYYMMDD in Buddhist Era)"
    )

    NHSO_EXPIRE_DATE = APDUCommand(
        [0x80, 0xb0, 0x01, 0x4d, 0x02, 0x00, 0x08],
        "NHSO expiry date (YYYYMMDD in Buddhist Era)"
    )

    NHSO_UPDATE_DATE = APDUCommand(
        [0x80, 0xb0, 0x01, 0x55, 0x02, 0x00, 0x08],
        "NHSO update date (YYYYMMDD in Buddhist Era)"
    )

    NHSO_CHANGE_HOSPITAL_AMOUNT = APDUCommand(
        [0x80, 0xb0, 0x01, 0x5d, 0x02, 0x00, 0x01],
        "Change hospital amount (1 byte)"
    )

    # Card/Laser ID commands
    LASER_ID = APDUCommand(
        [0x80, 0x00, 0x00, 0x00, 0x07],
        "Laser engraved ID (7 bytes)"
    )

    @classmethod
    def get_read_request(cls, atr: List[int]) -> List[int]:
        """Get the appropriate read request based on ATR.

        Args:
            atr: Answer to Reset bytes

        Returns:
            Read request command bytes
        """
        if len(atr) >= 2 and atr[0] == 0x3B and atr[1] == 0x67:
            return [0x00, 0xc0, 0x00, 0x01]
        else:
            return [0x00, 0xc0, 0x00, 0x00]

Functions

get_read_request(atr) classmethod

Get the appropriate read request based on ATR.

Parameters:

Name Type Description Default
atr List[int]

Answer to Reset bytes

required

Returns:

Type Description
List[int]

Read request command bytes

Source code in pythaiidcard/constants.py
@classmethod
def get_read_request(cls, atr: List[int]) -> List[int]:
    """Get the appropriate read request based on ATR.

    Args:
        atr: Answer to Reset bytes

    Returns:
        Read request command bytes
    """
    if len(atr) >= 2 and atr[0] == 0x3B and atr[1] == 0x67:
        return [0x00, 0xc0, 0x00, 0x01]
    else:
        return [0x00, 0xc0, 0x00, 0x00]

ResponseStatus

pythaiidcard.constants.ResponseStatus

Bases: Enum

Smart card response status codes.

Source code in pythaiidcard/constants.py
class ResponseStatus(Enum):
    """Smart card response status codes."""

    SUCCESS = (0x90, 0x00)
    MORE_DATA = (0x61, None)  # SW2 contains the length
    WRONG_LENGTH = (0x6C, None)  # SW2 contains correct length
    COMMAND_NOT_ALLOWED = (0x69, 0x86)
    WRONG_PARAMETERS = (0x6A, 0x86)
    FILE_NOT_FOUND = (0x6A, 0x82)

    @classmethod
    def is_success(cls, sw1: int, sw2: int) -> bool:
        """Check if response indicates success.

        Returns True for:
        - 90 00: Success
        - 61 XX: Success with more data available
        """
        return (sw1 == 0x90 and sw2 == 0x00) or sw1 == 0x61

    @classmethod
    def has_more_data(cls, sw1: int) -> bool:
        """Check if more data is available."""
        return sw1 == 0x61

Functions

is_success(sw1, sw2) classmethod

Check if response indicates success.

Returns True for: - 90 00: Success - 61 XX: Success with more data available

Source code in pythaiidcard/constants.py
@classmethod
def is_success(cls, sw1: int, sw2: int) -> bool:
    """Check if response indicates success.

    Returns True for:
    - 90 00: Success
    - 61 XX: Success with more data available
    """
    return (sw1 == 0x90 and sw2 == 0x00) or sw1 == 0x61

has_more_data(sw1) classmethod

Check if more data is available.

Source code in pythaiidcard/constants.py
@classmethod
def has_more_data(cls, sw1: int) -> bool:
    """Check if more data is available."""
    return sw1 == 0x61