Tools & Examples

Ready-to-run scripts in the tools/ directory demonstrating practical use of all tag families. Run from the project root.

Common options: Every script accepts two sub-commands — uart <port> and tcp — for selecting the transport. Use --help to see all options.

read_uid.py Download

Purpose: The simplest example — detect all tags in the field, print their type and UID. No activation or authentication required.

Usage

python read_uid.py uart COM3
python read_uid.py uart /dev/ttyUSB0
python read_uid.py tcp --host 192.168.1.100 --port 1234

Options

Sub-command / OptionDescription
uart <port>Connect via UART. port is e.g. COM3 or /dev/ttyUSB0.
uart --baudrate INTSerial baud rate (default: 115200).
tcp --host STRTCP host (default: 192.168.100.1).
tcp --port INTTCP port (default: 1234).

Sample Output

Firmware  : 1.0.3
Tags found: 2
  [0] MIFARE Classic 1K              UID: 04a1b2c3
  [1] ICODE SLIX                     UID: e004030200a1b2c3

How it works

with PepperC1(_make_transport(args)) as reader:
    tag_count = reader.get_tag_count()
    for i in range(tag_count):
        uid_info = reader.get_uid(i)
        type_byte = uid_info[0]   # tag type identifier
        uid       = uid_info[2:]  # UID bytes

mifare_classic.py Download

Compatible tags: MIFARE Classic 1K (0x04), 4K (0x05), Mini (0x10), generic Classic (0x03).
Purpose: Demonstrates data block read/write and value block operations.

Usage

python mifare_classic.py uart COM3
python mifare_classic.py uart COM3 --block 4 --vblock 7 --key AABBCCDDEEFF

Options

OptionDefaultDescription
--block INT5Block number for the read/write test. Must be a data block (not a sector trailer).
--vblock INT6Block number for the value block test.
--key HEXFFFFFFFFFFFF6-byte MIFARE key as 12 hex characters. Used as both key A and key B.

What it tests

  1. Load key into reader slot 0 (KEY_TYPE_MIFARE, key A + key B)
  2. Detect and activate tag
  3. Read block → write random 16 bytes → verify readback → restore original
  4. Initialise value block to 1000 → increment by 50 → verify result is 1050

Sample Output

Firmware : 1.0.3
Tag type : 0x04
UID      : 04a1b2c3
Key loaded: FFFFFFFFFFFF

--- Data block 5 ---
  original : 00000000000000000000000000000000
  written  : 3f8a12c491e76b205d03f8a4c2917e3b
  readback : 3f8a12c491e76b205d03f8a4c2917e3b
  PASS
  restored : 00000000000000000000000000000000

--- Value block 6 ---
  after write 1000 : 1000
  after increment 50: 1050

Done.

Key code pattern

from pepper_c1.commands import KEY_TYPE_MIFARE

reader.set_key(0, KEY_TYPE_MIFARE, key + key)  # key A + key B
reader.activate_tag(0)

original = reader.mf_read_block(block)
reader.mf_write_block(block, new_data)
reader.mf_write_value(vblock, 1000)
reader.mf_increment(vblock, 50)

mifare_ultralight.py Download

Compatible tags: MIFARE Ultralight (0x01), Ultralight-C (0x02).
Purpose: Demonstrates page read/write, NFC counter, card version, and signature operations.

Usage

python mifare_ultralight.py uart COM3
python mifare_ultralight.py uart COM3 --page 5 --counter 1

Options

OptionDefaultDescription
--page INT4Page number for read/write test. Page 4 is the first user-writable page on Ultralight.
--counter INT0NFC counter index (0–2, EV1 only).

What it tests

  1. Read page → write random 4 bytes → verify readback → restore original
  2. Read NFC counter → increment by 1 → verify new value
  3. Read card version bytes (EV1 only — gracefully skipped on plain Ultralight)
  4. Read NXP originality signature (EV1 only)

Sample Output

Firmware : 1.0.3
Tag type : 0x01
UID      : 04d3e5f601234

--- Page 4 ---
  original : a1b2c3d4
  written  : 9f3e7a2b
  readback : 9f3e7a2b
  PASS
  restored : a1b2c3d4

--- NFC counter 0 ---
  current value : 42
  after +1      : 43

--- Card version (EV1 only) ---
  not available on this card variant: Command 0x42 failed...

Key code pattern

reader.activate_tag(0)

original = reader.mfu_read_page(page)
reader.mfu_write_page(page, new_data)

val = reader.mfu_read_counter(counter)
reader.mfu_increment_counter(counter, 1)

# EV1 only — use send_command for unwrapped commands
ver = reader.send_command(Command.MFU_GET_VERSION)
sig = reader.send_command(Command.MFU_READ_SIG)

desfire.py Download

Compatible tags: MIFARE DESFire (0x0D).
Purpose: Full DESFire feature test — data file, value file, cyclic record file.

Warning: This script formats the card at step 2, permanently erasing all existing applications and files. Use only on test cards.

Usage

python desfire.py uart COM3
python desfire.py tcp --host 192.168.1.100 --port 1234

Test Sequence

  1. Load keys — slot 0: DES all-zeros (factory PICC master); slot 1: AES-128 all-zeros; slot 2: AES-128 0x01×16
  2. Select & auth PICC master appAID=000000, DES key 0
  3. Format card — erase all apps/files, print free memory and card version
  4. Create test appAID=AA55AA, 4 AES-128 keys, key_settings=0xED
  5. Data file (0x01, 32B backup) — write "Ala ma kota" → commit → read back → verify
  6. Value file (0x02, range −100..100, init −5) — credit +10 → verify 5; debit −25 → verify −20
  7. Cyclic record file (0x03, 34B records, max 10) — write 5 records → read back (newest-first) → clear
  8. Cleanup — delete files 0x01/0x02/0x03 → delete app AA55AA

Sample Output

Firmware : 1.0.3
Tag type : 0x0D
UID      : 042e3f4a5b6c7d

Keys loaded into reader slots 0–2

--- PICC master app ---
  auth OK
  formatting card (erases all apps and files) ...
  free memory  : 7936 bytes
  card version : 04 01 01 01 00 16 05 04 01 01 01 05 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00

--- Create app AID=AA55AA ---
  app selected and authenticated

--- Data file 0x01 (backup, 32 bytes) ---
  wrote   : b'Ala ma kota'
  readback: b'Ala ma kota'
  PASS

--- Value file 0x02 (range -100..100, init -5) ---
  initial value    : -5
  after credit +10 : 5
  after debit  -25 : -20

--- Cyclic record file 0x03 (34B records, max 10) ---
  wrote record 0 ... wrote record 4
  read  record 0 (offset 0): nr=4, text='This is record nr 4'
  ...
  records cleared

--- Cleanup ---
  deleted file 0x01 / 0x02 / 0x03
  deleted app AA55AA

Done.

Key code patterns

from pepper_c1.commands import KEY_TYPE_DES, KEY_TYPE_AES128

# 1. Load keys
reader.set_key(0, KEY_TYPE_DES,    bytes(16))
reader.set_key(1, KEY_TYPE_AES128, bytes(16))

# 2. Auth and format
reader.mfdf_select_app(b'\x00\x00\x00')
reader.mfdf_auth(0, key_slot=0)
reader.mfdf_format()

# 3. Create app + auth with AES
reader.mfdf_create_app(TEST_AID, key_settings=0xED, num_keys=0x84)
reader.mfdf_select_app(TEST_AID)
reader.mfdf_auth_aes(1, key_slot=0)

# 4. Backup data file
reader.mfdf_create_data_file(0x01, access_rights=0xEEEE, size=32, backup=True)
reader.mfdf_write_data(0x01, offset=0, data=b"Ala ma kota")
reader.mfdf_commit_transaction()

# 5. Value file
reader.mfdf_create_value_file(0x02, 0xEEEE, -100, 100, -5)
reader.mfdf_credit(0x02, 10); reader.mfdf_commit_transaction()
reader.mfdf_debit(0x02, 25);  reader.mfdf_commit_transaction()

# 6. Cyclic record file
reader.mfdf_create_record_file(0x03, 0xEEEE, record_size=34, max_records=10)
reader.mfdf_write_record(0x03, record_bytes)
reader.mfdf_commit_transaction()
raw = reader.mfdf_read_record(0x03, offset=0, record_size=34)

icode.py Download

Compatible tags: ICODE SLI (0x21), SLI-S (0x22), SLI-L (0x23), SLIX (0x24), SLIX-S (0x25), SLIX-L (0x26), SLIX2 (0x27), DNA (0x28).
Purpose: Block read/write, system information, and block security status.

Usage

python icode.py uart COM3
python icode.py uart COM3 --block 3

Options

OptionDefaultDescription
--block INT5Block number for the read/write test.

What it tests

  1. Read ICODE system information (UID, block size, block count, AFI, DSFID)
  2. Read block → write random 4 bytes → verify readback → restore original
  3. Read block security status for blocks 0–9 (lock bits)

Sample Output

Firmware : 1.0.3
Tag type : 0x24
UID      : e004030200a1b2c3

--- System information ---
  0f e0 04 03 02 00 a1 b2 c3 00 1b 03 00

--- Block 5 ---
  original : 00000000
  written  : 7a2b9c1d
  readback : 7a2b9c1d
  PASS
  restored : 00000000

--- Block security status (blocks 0–9) ---
  block  0: LOCKED
  block  1: LOCKED
  block  2: unlocked
  ...

Done.

Key code pattern

info = reader.icode_get_system_information()
original = reader.icode_read_block(block)
reader.icode_write_block(block, new_data)

# Block security status — direct send_command
bss = reader.send_command(Command.ICODE_GET_MULTIPLE_BSS, bytes([0, 10]))

example_client.py Download

Compatible tags: All supported families (auto-detected).
Purpose: Full-featured demo that auto-detects the tag type and runs the appropriate test.

Usage

python example_client.py uart COM3
python example_client.py tcp --host 192.168.100.1 --port 1234

Tag dispatch logic

uid_info  = reader.get_uid(tag_index)
type_byte = uid_info[0]

if   type_byte in ICODE_TYPES:      test_icode(reader)
elif type_byte in DESFIRE_TYPES:    test_desfire(reader)
elif type_byte in MF_CLASSIC_TYPES:  test_mifare_classic(reader)
elif type_byte in MF_ULTRALIGHT_TYPES: test_mifare_ultralight(reader)

Included test functions

FunctionTag typesOperations
test_mifare_classic0x03/04/05/10set_key, mf_read_block, mf_write_block (read/write/verify/restore)
test_mifare_ultralight0x01/02mfu_read_page, mfu_write_page (read/write/verify/restore)
test_desfire0x0DFull DESFire suite: auth, format, data file, value file, record file, cleanup
test_icode0x21–0x28icode_read_block, icode_write_block (read/write/verify/restore)

TAG_NAMES mapping

The script defines a TAG_NAMES dictionary mapping type bytes to human-readable names, which is a useful pattern to copy into your own applications:

TAG_NAMES = {
    0x01: "MIFARE Ultralight",
    0x02: "MIFARE Ultralight-C",
    0x03: "MIFARE Classic",
    0x04: "MIFARE Classic 1K",
    0x05: "MIFARE Classic 4K",
    0x0D: "MIFARE DESFire",
    0x21: "ICODE SLI",
    # ...
}

tag_name = TAG_NAMES.get(type_byte, f"Unknown (0x{type_byte:02X})")

mux_example.py Download

Purpose: Continuously cycles through all antennas (1–8), detects tags on each one and prints their type and UID. Useful for multi-antenna setups and antenna multiplexer hardware.

Usage

python mux_example.py uart COM3
python mux_example.py uart /dev/ttyUSB0 --antennas 1 2 3 4 --dwell 50
python mux_example.py tcp --host 192.168.100.1 --port 1234

Options

OptionDefaultDescription
--antennas N [N ...]1–8Antenna numbers to cycle. Pass a space-separated list to scan a subset, e.g. --antennas 1 2 3 4.
--dwell MS0Milliseconds to wait on each antenna after the tag check before switching to the next one.
uart <port>Connect via UART. port is e.g. COM3 or /dev/ttyUSB0.
uart --baudrate INT115200Serial baud rate.
tcp --host STR192.168.100.1TCP host.
tcp --port INT1234TCP port.

Sample Output

Connected. Firmware: v2.56.2249
Scanning antennas [1, 2, 3, 4, 5, 6, 7, 8] — press Ctrl+C to stop

  [ant 1] --
  [ant 2] MIFARE Classic 1K  UID: A1B2C3D4
  [ant 3] --
  [ant 4] ICODE SLIX         UID: E004030200A1B2C3
  [ant 5] --
  [ant 6] --
  [ant 7] MIFARE DESFire     UID: 042E3F4A5B6C7D
  [ant 8] --
  [ant 1] --
  ...

Key code pattern

while True:
    for ant in antennas:
        reader.set_active_antenna(ant)
        count = reader.get_tag_count()
        if count:
            uid_info = reader.get_uid()
            type_byte = uid_info[0]
            uid       = uid_info[2:].hex().upper()
            print(f"[ant {ant}] {TAG_NAMES.get(type_byte)}  UID: {uid}")

firmware_update.py Download

Purpose: OTA (over-the-air) firmware update for the Peeper C1. Sends a .bin firmware image using the three-phase OTA protocol: OTA_BEGINOTA_FRAME × N → OTA_FINISH.

Warning: Interrupting a firmware update mid-way may leave the device in an unbootable state. Ensure a stable connection before starting.

Usage

python firmware_update.py uart COM3 firmware.bin
python firmware_update.py uart /dev/ttyUSB0 firmware.bin --chunk-size 1024
python firmware_update.py tcp 192.168.100.1 firmware.bin
python firmware_update.py tcp 192.168.100.1 firmware.bin --port 1234 --chunk-size 1024

Options

Sub-command / OptionDefaultDescription
uart <port>Connect via UART. port is e.g. COM3 or /dev/ttyUSB0.
uart --baudrate INT115200Serial baud rate.
tcp <host>TCP host (positional), e.g. 192.168.100.1.
tcp --port INT1234TCP port.
--chunk-size INT1024Bytes per OTA_FRAME packet.

OTA protocol phases

CommandPayloadACK timeoutNotes
OTA_BEGIN (0xF0)emptyup to 5 sDevice erases flash — ACK may arrive after ~2 s.
OTA_FRAME (0xF1)raw chunk bytes2 sSent once per chunk. Last chunk is sent as-is — no padding required.
OTA_FINISH (0xF2)emptyup to 5 sDevice validates image; ACK confirms success.

Sample Output

Device firmware : 2.55.2201
File size : 114688 bytes
Chunk size: 1024 bytes
Frames    : 112

OTA_BEGIN ... ACK (1.83 s)
  [####################] 100%  frame 112/112
OTA_FINISH ... ACK (0.12 s)
Rebooting ... done

Firmware update complete in 14.2 s.

Key code pattern

import time
from pepper_c1.commands import Command

t_start = time.monotonic()

# Phase 1 — announce transfer (long timeout for flash erase)
reader._timeout = 5.0
reader.send_command(Command.OTA_BEGIN)

# Phase 2 — stream frames (raw chunks, no padding)
reader._timeout = 2.0
for i in range(num_frames):
    chunk = fw_data[i * chunk_size : (i + 1) * chunk_size]
    reader.send_command(Command.OTA_FRAME, chunk)

# Phase 3 — signal end, reboot, print elapsed time
reader._timeout = 5.0
reader.send_command(Command.OTA_FINISH)
reader.reboot()
print(f"Done in {time.monotonic() - t_start:.1f} s")