PepperC1 Client

Main driver class for the Eccel Peeper C1 RFID reader. Located in pepper_c1/client.py.

PepperC1 (transport, response_timeout=2.0) constructor
Create a new reader driver instance. Use as a context manager to ensure the transport is closed on exit.
ParameterTypeDefaultDescription
transportTransportA UARTTransport or TCPTransport instance.
response_timeoutfloat2.0Maximum seconds to wait for a device response before raising TimeoutError.
from pepper_c1 import PepperC1, UARTTransport

# as context manager (recommended)
with PepperC1(UARTTransport('COM3')) as reader:
    print(reader.get_version())

# manual lifecycle
reader = PepperC1(UARTTransport('COM3'), response_timeout=5.0)
try:
    print(reader.get_version())
finally:
    reader.close()

Low-Level Interface

send_command (cmd, data=b"") low-level
Send a raw command frame and return the response payload (ACK byte and echoed command byte are stripped). Async events received during the wait are stored in self.async_events.
ParameterTypeDefaultDescription
cmdCommandCommand opcode from the Command enum.
databytesb""Optional payload bytes appended after the command byte.
Returnsbytes — response payload without ACK/CMD prefix.
RaisesCommandError — if device responds with CMD_ERROR (0xFF).
TimeoutError — if no response within response_timeout.
from pepper_c1.commands import Command

# direct use of a command not wrapped by a convenience method
version_raw = reader.send_command(Command.MFU_GET_VERSION)
sig = reader.send_command(Command.MFU_READ_SIG)

General Commands

get_version () general
Return the reader firmware version string.
Returnsstr — firmware version, e.g. "1.0.3".
print(reader.get_version())  # e.g. "1.0.3"
get_tag_count () general
Return the number of tags currently detected in the RF field.
Returnsint — number of tags (0 if none).
count = reader.get_tag_count()
if count == 0:
    print("No tag in field")
get_uid (tag_index=0) general
Return UID information for the tag at the given index. The response payload is structured as:
  • payload[0] — tag type byte (see type table)
  • payload[1] — parameter byte
  • payload[2:] — UID bytes
ParameterTypeDefaultDescription
tag_indexint0Zero-based index of the tag (up to get_tag_count() - 1).
Returnsbytes — raw payload: [type, param, uid_byte...].
uid_info = reader.get_uid(0)
type_byte = uid_info[0]
uid = uid_info[2:]
print(f"Type: 0x{type_byte:02X}, UID: {uid.hex()}")
activate_tag (tag_index=0) general
Activate (select) the tag at the given index so that subsequent commands target it. Required before MIFARE Classic and Ultralight operations.
ParameterTypeDefaultDescription
tag_indexint0Zero-based index of the tag to activate.
Returnsbytes — response payload from the device.
reader.activate_tag(0)
# now MIFARE operations target this tag
halt () general
Halt (deselect) the currently active tag, returning it to HALT state.
set_polling (enable, interval_ms=500) general
Enable or disable automatic tag polling. When enabled, the reader periodically scans for tags and sends ASYNC events (stored in reader.async_events) when a tag is detected or removed.
ParameterTypeDefaultDescription
enableboolTrue to start polling, False to stop.
interval_msint500Polling interval in milliseconds (0–65535).
reader.set_polling(True, interval_ms=250)
# ... do other work; async events accumulate in reader.async_events
reader.set_polling(False)
reboot () general
Reboot the reader module. The reader will restart and the connection will need to be re-established.
set_active_antenna (antenna) general
Select the active antenna on readers equipped with an antenna multiplexer.
ParameterTypeDescription
antennaintAntenna number, 1–8.
Raises: ValueError if antenna is outside the range 1–8.
reader.set_active_antenna(1)   # switch to antenna 1
reader.set_active_antenna(4)   # switch to antenna 4

# cycle through all antennas
for ant in range(1, 9):
    reader.set_active_antenna(ant)
    count = reader.get_tag_count()
    if count:
        print(f"antenna {ant}: tag found")
close () general
Close the underlying transport connection. Called automatically by the context manager __exit__.

Key Management

Keys are stored in numbered slots in the reader's volatile memory. Use save_keys() to persist them to non-volatile storage. Key type constants are available in pepper_c1.commands.

ConstantValueKey size
KEY_TYPE_AES1280x0016 bytes
KEY_TYPE_AES1920x0124 bytes
KEY_TYPE_AES2560x0232 bytes
KEY_TYPE_DES0x038 bytes
KEY_TYPE_2K3DES0x0416 bytes
KEY_TYPE_3K3DES0x0524 bytes
KEY_TYPE_MIFARE0x0612 bytes (key A + key B, 6+6)
set_key (key_slot, key_type, key_data) keys
Store a cryptographic key in a reader key slot. Keys are volatile — call save_keys() to persist them.
ParameterTypeDescription
key_slotintSlot index (0-based) to store the key in.
key_typeintOne of the KEY_TYPE_* constants (see table above).
key_databytesRaw key bytes. For KEY_TYPE_MIFARE, concatenate key A (6 bytes) + key B (6 bytes).
from pepper_c1.commands import KEY_TYPE_MIFARE, KEY_TYPE_AES128, KEY_TYPE_DES

# MIFARE Classic: factory default key FFFFFFFFFFFF for both A and B
reader.set_key(0, KEY_TYPE_MIFARE, bytes([0xFF] * 12))

# DESFire: DES all-zeros (factory default PICC master key)
reader.set_key(0, KEY_TYPE_DES, bytes(16))

# DESFire: AES-128 all-zeros
reader.set_key(1, KEY_TYPE_AES128, bytes(16))
save_keys () keys
Persist all currently loaded keys to the reader's non-volatile memory. After this call, keys survive a reboot.

MIFARE Classic

Supports MIFARE Classic 1K, 4K, and Mini tags. Call activate_tag() and set_key() (with KEY_TYPE_MIFARE) before any Classic operation.

mf_read_block (block, count=1, auth_param=0x0A, key_no=0) MIFARE Classic
Read one or more consecutive 16-byte blocks from a MIFARE Classic tag.
ParameterTypeDefaultDescription
blockintFirst block number to read.
countint1Number of consecutive blocks to read.
auth_paramint0x0AFirmware authentication parameter (empirically 0x0A; selects key A).
key_noint0Key slot index (set via set_key()).
Returnsbytes — exactly count × 16 bytes.
RaisesProtocolError — if response length doesn't match.
data = reader.mf_read_block(5)                    # 16 bytes
two_blocks = reader.mf_read_block(4, count=2)    # 32 bytes
mf_write_block (block, data, count=1, auth_param=0x0A, key_no=0) MIFARE Classic
Write one or more consecutive 16-byte blocks to a MIFARE Classic tag.
ParameterTypeDefaultDescription
blockintFirst block number to write.
databytesRaw bytes, must be exactly count × 16 bytes.
countint1Number of consecutive blocks.
auth_paramint0x0AFirmware authentication parameter.
key_noint0Key slot index.
RaisesValueError — if len(data) != count * 16.
reader.mf_write_block(5, b'\x00' * 16)  # clear block 5
mf_read_value (block, auth_param=0x0A, key_no=0) MIFARE Classic
Read a MIFARE Classic value block. Value blocks store a signed 32-bit integer with redundancy checking.
Returnsint — signed 32-bit integer value.
val = reader.mf_read_value(6)
print(f"Value: {val}")
mf_write_value (block, value, auth_param=0x0A, key_no=0) MIFARE Classic
Initialise a MIFARE Classic value block with a signed 32-bit integer.
ParameterTypeDescription
blockintBlock number.
valueintSigned 32-bit initial value.
reader.mf_write_value(6, 1000)  # set value block to 1000
mf_increment (block, delta, auth_param=0x0A, key_no=0) MIFARE Classic
Increment a MIFARE Classic value block by delta. The firmware also executes the TRANSFER step internally.
ParameterTypeDescription
blockintValue block number.
deltaintUnsigned 32-bit increment amount.
reader.mf_increment(6, 50)
val = reader.mf_read_value(6)  # 1050

MIFARE Ultralight

Supports MIFARE Ultralight, Ultralight-C, and Ultralight EV1. Call activate_tag() before page operations.

mfu_read_page (page, count=1) Ultralight
Read one or more 4-byte pages from a MIFARE Ultralight tag.
ParameterTypeDefaultDescription
pageintFirst page number. User data starts at page 4.
countint1Number of consecutive 4-byte pages to read.
Returnsbytescount × 4 bytes.
page_data = reader.mfu_read_page(4)       # 4 bytes
eight_bytes = reader.mfu_read_page(4, count=2)
mfu_write_page (page, data, count=1) Ultralight
Write one or more 4-byte pages to a MIFARE Ultralight tag.
ParameterTypeDefaultDescription
pageintFirst page number to write.
databytesRaw bytes, must be exactly count × 4 bytes.
countint1Number of consecutive pages.
RaisesValueError — if len(data) != count * 4.
reader.mfu_write_page(4, b'\xDE\xAD\xBE\xEF')
mfu_read_counter (counter) Ultralight
Read a 3-byte NFC counter value (Ultralight EV1 only).
ParameterTypeDescription
counterintCounter index (0–2).
Returnsint — 24-bit counter value (0–16777215).
val = reader.mfu_read_counter(0)
print(f"Counter 0: {val}")
mfu_increment_counter (counter, delta) Ultralight
Increment a 3-byte NFC counter by delta (Ultralight EV1 only). NFC counters are one-way — they cannot be decremented or reset.
ParameterTypeDescription
counterintCounter index (0–2).
deltaintAmount to add (24-bit unsigned).
reader.mfu_increment_counter(0, 1)
mfu_passwd_auth (password, pack=b"\x00\x00") Ultralight
Authenticate with a 4-byte password (Ultralight EV1). Returns the 2-byte PACK response from the card.
ParameterTypeDefaultDescription
passwordbytes4-byte password. Factory default: b'\xFF\xFF\xFF\xFF'.
packbytesb"\x00\x00"2-byte expected PACK response.
Returnsbytes — 2-byte PACK from card.
RaisesValueError — if password is not exactly 4 bytes.
pack = reader.mfu_passwd_auth(b'\xFF\xFF\xFF\xFF')
print(f"PACK: {pack.hex()}")

MIFARE DESFire

Full support for DESFire EV1/EV2. Typical workflow: select application → authenticate → file operations → commit transaction. DESFire operations do not require activate_tag().

Transactions: Write operations (data, value, record files) are transactional. Changes are only persisted to the card after calling mfdf_commit_transaction(). Use mfdf_abort_transaction() to discard.
mfdf_select_app (aid) DESFire
Select a DESFire application by its 3-byte Application Identifier (AID). Use b'\x00\x00\x00' to select the PICC master application.
ParameterTypeDescription
aidbytes3-byte AID. Master app: b'\x00\x00\x00'.
RaisesValueError — if AID is not exactly 3 bytes.
reader.mfdf_select_app(b'\x00\x00\x00')   # PICC master
reader.mfdf_select_app(bytes([0xAA, 0x55, 0xAA]))  # custom app
mfdf_get_app_ids () DESFire
Return a list of all 3-byte AIDs present on the card.
Returnslist[bytes] — list of 3-byte AID objects.
aids = reader.mfdf_get_app_ids()
for aid in aids:
    print(aid.hex())
mfdf_auth (key_no, key_slot=0) DESFire
Authenticate with a DES or 3DES key stored in a reader key slot.
ParameterTypeDefaultDescription
key_nointKey number within the selected DESFire application.
key_slotint0Reader key slot that holds the key bytes.
reader.mfdf_select_app(b'\x00\x00\x00')
reader.mfdf_auth(0, key_slot=0)   # DES key 0 from reader slot 0
mfdf_auth_aes (key_no, key_slot=0) DESFire
Authenticate with an AES-128 key stored in a reader key slot.
ParameterTypeDefaultDescription
key_nointKey number within the selected DESFire application.
key_slotint0Reader key slot (must contain an AES key).
reader.mfdf_auth_aes(1, key_slot=1)  # AES key 1 from reader slot 1
mfdf_create_app (aid, key_settings, num_keys) DESFire
Create a new DESFire application on the card. Must be authenticated to the PICC master application first.
ParameterTypeDescription
aidbytes3-byte Application Identifier.
key_settingsintKey settings byte (e.g. 0xED: master key changeable, config changeable, free dir access).
num_keysintNumber and type of keys. Upper nibble: key type (0x8=AES). Lower nibble: number of keys.
# Create app with 4 AES-128 keys (0x84 = 0x8«AES» | 4«keys»)
reader.mfdf_create_app(bytes([0xAA, 0x55, 0xAA]), key_settings=0xED, num_keys=0x84)
mfdf_delete_app (aid) DESFire
Delete an application and all its files. Must be authenticated to PICC master app.
ParameterTypeDescription
aidbytes3-byte AID of the application to delete.
mfdf_create_data_file (file_no, access_rights, size, backup=False) DESFire
Create a standard data file or a backup data file within the selected application.
ParameterTypeDefaultDescription
file_nointFile number (0–14).
access_rightsint16-bit access rights field. 0xEEEE = free access for all operations.
sizeintFile size in bytes.
backupboolFalseIf True, creates a backup data file (changes require commit_transaction()).
reader.mfdf_create_data_file(0x01, access_rights=0xEEEE, size=32, backup=True)
mfdf_write_data (file_no, offset, data, comm_mode=0x00) DESFire
Write data to a standard or backup data file. For backup files, call mfdf_commit_transaction() afterwards to persist the data.
ParameterTypeDefaultDescription
file_nointFile number.
offsetintByte offset within the file to start writing.
databytesData bytes to write.
comm_modeint0x00Communication mode: 0x00=plain, 0x01=MAC, 0x03=encrypted.
reader.mfdf_write_data(0x01, offset=0, data=b"Hello, DESFire!")
reader.mfdf_commit_transaction()
mfdf_read_data (file_no, offset, length, comm_mode=0x00) DESFire
Read data from a standard or backup data file.
ParameterTypeDefaultDescription
file_nointFile number.
offsetintByte offset to start reading from.
lengthintNumber of bytes to read.
comm_modeint0x00Communication mode.
Returnsbytes — read data (may be zero-padded to block size).
data = reader.mfdf_read_data(0x01, offset=0, length=15)
print(data.rstrip(b'\x00'))
mfdf_create_value_file (file_no, access_rights, lower_limit, upper_limit, init_value, limited_credit=False) DESFire
Create a DESFire value file that stores a signed 32-bit integer with configurable limits and credit/debit operations.
ParameterTypeDefaultDescription
file_nointFile number.
access_rightsint16-bit access rights.
lower_limitintMinimum allowed value (signed 32-bit).
upper_limitintMaximum allowed value (signed 32-bit).
init_valueintInitial value stored in the file.
limited_creditboolFalseEnable limited credit feature.
reader.mfdf_create_value_file(0x02, access_rights=0xEEEE,
    lower_limit=-100, upper_limit=100,
    init_value=-5, limited_credit=True)
mfdf_get_value (file_no) DESFire
Read the current value from a DESFire value file.
Returnsint — signed 32-bit integer.
val = reader.mfdf_get_value(0x02)  # e.g. -5
mfdf_credit (file_no, amount) DESFire
Add amount to a value file. Call mfdf_commit_transaction() to persist. The result must not exceed upper_limit.
reader.mfdf_credit(0x02, 10)
reader.mfdf_commit_transaction()
mfdf_debit (file_no, amount) DESFire
Subtract amount from a value file. Call mfdf_commit_transaction() to persist. The result must not go below lower_limit.
reader.mfdf_debit(0x02, 25)
reader.mfdf_commit_transaction()
mfdf_create_record_file (file_no, access_rights, record_size, max_records, cyclic=True) DESFire
Create a linear or cyclic record file. In cyclic mode the oldest record is overwritten when the file is full.
ParameterTypeDefaultDescription
file_nointFile number.
access_rightsint16-bit access rights.
record_sizeintSize of each record in bytes.
max_recordsintMaximum number of records the file can hold.
cyclicboolTrueIf True, cyclic (ring buffer). If False, linear.
mfdf_write_record (file_no, data) DESFire
Append a record to a record file. Call mfdf_commit_transaction() after each write.
record = b"\x01\x00" + b"My record data".ljust(32, b'\x00')
reader.mfdf_write_record(0x03, record)
reader.mfdf_commit_transaction()
mfdf_read_record (file_no, offset, record_size) DESFire
Read records from a record file. Records are returned newest-first; offset skips N newest records. The full response contains all records from offset concatenated — slice [:record_size] to extract one record.
ParameterTypeDescription
file_nointFile number.
offsetintNumber of newest records to skip.
record_sizeintRecord size as configured when creating the file.
Returnsbytes — concatenated records from offset.
# Read the newest record
raw = reader.mfdf_read_record(0x03, offset=0, record_size=34)
first_record = raw[:34]
mfdf_clear_records (file_no) DESFire
Delete all records from a record file. Requires mfdf_commit_transaction().
mfdf_delete_file (file_no) DESFire
Delete a file from the currently selected application.
mfdf_commit_transaction () DESFire
Commit the current DESFire transaction, making all pending write/credit/debit/record operations permanent on the card.
mfdf_abort_transaction () DESFire
Abort the current DESFire transaction, discarding all pending operations.
mfdf_format () DESFire
Erase all applications and files from the card, restoring it to factory state. Must be authenticated to the PICC master application.
Warning: This permanently erases all data on the card.
mfdf_get_free_mem () DESFire
Return the number of free bytes available on the card.
Returnsint — free memory in bytes.
mfdf_card_version () DESFire
Return raw 28-byte hardware and software version information from the card.
Returnsbytes — 28 bytes of version data.

ICODE

Supports ISO 15693 tags: ICODE SLI, SLI-S, SLI-L, SLIX, SLIX-S, SLIX-L, SLIX2, and DNA. ICODE tags do not require activate_tag().

icode_inventory_start () ICODE
Start an ICODE inventory scan. Returns the UID of the first tag found.
Returnsbytes — UID of the first ICODE tag found.
icode_read_block (block, count=1) ICODE
Read one or more 4-byte ICODE blocks.
ParameterTypeDefaultDescription
blockintFirst block number to read.
countint1Number of consecutive 4-byte blocks to read.
Returnsbytescount × 4 bytes.
data = reader.icode_read_block(5)   # 4 bytes
icode_write_block (block, data, count=1) ICODE
Write one or more 4-byte ICODE blocks.
ParameterTypeDefaultDescription
blockintFirst block number to write.
databytesRaw bytes, must be exactly count × 4 bytes.
countint1Number of consecutive blocks.
RaisesValueError — if len(data) != count * 4.
reader.icode_write_block(5, b'\x01\x02\x03\x04')
icode_get_system_information () ICODE
Return the ICODE system information block containing UID, block count, block size, AFI, and DSFID.
Returnsbytes — raw system information payload.
info = reader.icode_get_system_information()
print(info.hex(' '))