Files
ChameleonUltra/software/script/chameleon_cmd.py
T
Milan DavídekandClaude Sonnet 4.6 fcb6eb4718 Add Jablotron LF tag support (read, emulate, write to T55xx)
Jablotron uses differential biphase (inverted) at RF/64, 64-bit frames:
  bits  0-15: 0xFFFF preamble
  bits 16-55: 40-bit data (5 bytes), bit 16 must be 0
  bits 56-63: 8-bit checksum = (sum of data bytes) XOR 0x3A

Firmware:
  - rfid/nfctag/lf/protocols/jablotron.c  - encoder/decoder codec
  - rfid/nfctag/lf/utils/diphase.c        - inverted-biphase state machine
    (shared util, reusable by other diphase protocols)
  - rfid/reader/lf/lf_jablotron_data.c    - GPIO-interval reader path
  - app_cmd.c: JABLOTRON_SCAN, JABLOTRON_WRITE_TO_T55XX,
                JABLOTRON_SET_EMU_ID, JABLOTRON_GET_EMU_ID
  - tag_base_type.h: TAG_TYPE_JABLOTRON enum
  - t55xx.h: T5577_JABLOTRON_CONFIG (DIPHASE modulation, RF/64)
  - lf_tag_em.c: load callback, factory-default data, save callback

Python CLI (software/script/):
  - lf jablotron read               - scan a real tag
  - lf jablotron write --id         - clone onto T55xx
  - lf jablotron econfig -s N --id  - set emulator ID on a slot
  - hw slot list shows Jablotron ID and decimal card number

Python test (software/script/tests/test_jablotron_modulator.py):
  Pure-Python round-trip validator that reimplements the modulator and
  diphase decoder, expands PWM entries to an edge stream, and confirms
  the decoded data matches the input.  Regression guard for both the
  firmware's double-frame encoding and the single-frame variant.

Notable PWM design choices:
  - Constant-level diphase encoding uses the same PAC pattern:
    CC=0 for LOW, CC=counter_top+1 for HIGH.  counter_top=31 gives
    exactly 32 carrier cycles per half-bit at NRF_PWM_CLK_125kHz.
  - The 64-bit frame is encoded twice in the 256-entry PWM buffer with
    the internal level variable persisting between the two passes.
    This is required for clean PWM looping: a single 64-bit diphase
    frame with an odd number of zero bits ends at a level opposite the
    starting level, leaving no transition at the loop boundary where
    the reader expects one.  Encoding twice guarantees a continuous
    diphase stream regardless of the data's zero-count parity.

Reference: Proxmark3 cmdlfjablotron.c

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:15:55 +02:00

1697 lines
61 KiB
Python

import struct
import ctypes
from typing import Union
import chameleon_com
from chameleon_utils import expect_response, reconstruct_full_nt, parity_to_str
from chameleon_enum import Command, SlotNumber, Status, TagSenseType, TagSpecificType
from chameleon_enum import ButtonPressFunction, ButtonType, MifareClassicDarksideStatus
from chameleon_enum import MfcKeyType, MfcValueBlockOperator
CURRENT_VERSION_SETTINGS = 5
new_key = b'\x20\x20\x66\x66'
old_keys = [b'\x51\x24\x36\x48', b'\x19\x92\x04\x27']
class ChameleonCMD:
"""
Chameleon cmd function
"""
def __init__(self, chameleon: chameleon_com.ChameleonCom):
"""
:param chameleon: chameleon instance, @see chameleon_device.Chameleon
"""
self.device = chameleon
@expect_response(Status.SUCCESS)
def get_app_version(self):
"""
Get firmware version number(application)
"""
resp = self.device.send_cmd_sync(Command.GET_APP_VERSION)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack('!BB', resp.data)
# older protocol, must upgrade!
if resp.status == 0 and resp.data == b'\x00\x01':
print("Chameleon does not understand new protocol. Please update firmware")
return chameleon_com.Response(cmd=Command.GET_APP_VERSION,
status=Status.NOT_IMPLEMENTED)
return resp
@expect_response(Status.SUCCESS)
def get_device_chip_id(self):
"""
Get device chip id
"""
resp = self.device.send_cmd_sync(Command.GET_DEVICE_CHIP_ID)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data.hex()
return resp
@expect_response(Status.SUCCESS)
def get_device_address(self):
"""
Get device address
"""
resp = self.device.send_cmd_sync(Command.GET_DEVICE_ADDRESS)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data.hex()
return resp
@expect_response(Status.SUCCESS)
def get_git_version(self):
resp = self.device.send_cmd_sync(Command.GET_GIT_VERSION)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data.decode('utf-8')
return resp
@expect_response(Status.SUCCESS)
def get_device_mode(self):
resp = self.device.send_cmd_sync(Command.GET_DEVICE_MODE)
if resp.status == Status.SUCCESS:
resp.parsed, = struct.unpack('!?', resp.data)
return resp
def is_device_reader_mode(self) -> bool:
"""
Get device mode, reader or tag.
:return: True is reader mode, else tag mode
"""
return self.get_device_mode()
# Note: Will return NOT_IMPLEMENTED if one tries to set reader mode on Lite
@expect_response(Status.SUCCESS)
def change_device_mode(self, mode):
data = struct.pack('!B', mode)
return self.device.send_cmd_sync(Command.CHANGE_DEVICE_MODE, data)
def set_device_reader_mode(self, reader_mode: bool = True):
"""
Change device mode, reader or tag.
:param reader_mode: True if reader mode, False if tag mode.
:return:
"""
self.change_device_mode(reader_mode)
@expect_response(Status.HF_TAG_OK)
def hf14a_scan(self):
"""
14a tags in the scanning field.
:return:
"""
resp = self.device.send_cmd_sync(Command.HF14A_SCAN)
if resp.status == Status.HF_TAG_OK:
# uidlen[1]|uid[uidlen]|atqa[2]|sak[1]|atslen[1]|ats[atslen]
offset = 0
data = []
while offset < len(resp.data):
uidlen, = struct.unpack_from('!B', resp.data, offset)
offset += struct.calcsize('!B')
uid, atqa, sak, atslen = struct.unpack_from(f'!{uidlen}s2s1sB', resp.data, offset)
offset += struct.calcsize(f'!{uidlen}s2s1sB')
ats, = struct.unpack_from(f'!{atslen}s', resp.data, offset)
offset += struct.calcsize(f'!{atslen}s')
data.append({'uid': uid, 'atqa': atqa, 'sak': sak, 'ats': ats})
resp.parsed = data
return resp
def mf1_detect_support(self):
"""
Detect whether it is mifare classic tag.
:return:
"""
resp = self.device.send_cmd_sync(Command.MF1_DETECT_SUPPORT)
return resp.status == Status.HF_TAG_OK
@expect_response(Status.HF_TAG_OK)
def mf1_detect_prng(self):
"""
Detect mifare Class of classic nt vulnerabilities.
:return:
"""
resp = self.device.send_cmd_sync(Command.MF1_DETECT_PRNG)
if resp.status == Status.HF_TAG_OK:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_detect_nt_dist(self, block_known, type_known, key_known):
"""
Detect the random number distance of the card.
:return:
"""
data = struct.pack('!BB6s', type_known, block_known, key_known)
resp = self.device.send_cmd_sync(Command.MF1_DETECT_NT_DIST, data)
if resp.status == Status.HF_TAG_OK:
uid, dist = struct.unpack('!II', resp.data)
resp.parsed = {'uid': uid, 'dist': dist}
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_nested_acquire(self, block_known, type_known, key_known, block_target, type_target):
"""
Collect the key NT parameters needed for Nested decryption
:return:
"""
data = struct.pack('!BB6sBB', type_known, block_known, key_known, type_target, block_target)
resp = self.device.send_cmd_sync(Command.MF1_NESTED_ACQUIRE, data)
if resp.status == Status.HF_TAG_OK:
resp.parsed = [{'nt': nt, 'nt_enc': nt_enc, 'par': par}
for nt, nt_enc, par in struct.iter_unpack('!IIB', resp.data)]
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_darkside_acquire(self, block_target, type_target, first_recover: Union[int, bool], sync_max):
"""
Collect the key parameters needed for Darkside decryption.
:param block_target:
:param type_target:
:param first_recover:
:param sync_max:
:return:
"""
data = struct.pack('!BBBB', type_target, block_target, first_recover, sync_max)
resp = self.device.send_cmd_sync(Command.MF1_DARKSIDE_ACQUIRE, data, timeout=sync_max * 10)
if resp.status == Status.HF_TAG_OK:
if resp.data[0] == MifareClassicDarksideStatus.OK:
darkside_status, uid, nt1, par, ks1, nr, ar = struct.unpack('!BIIQQII', resp.data)
resp.parsed = (darkside_status, {'uid': uid, 'nt1': nt1, 'par': par, 'ks1': ks1, 'nr': nr, 'ar': ar})
else:
resp.parsed = (resp.data[0],)
return resp
@expect_response([Status.HF_TAG_OK, Status.MF_ERR_AUTH])
def mf1_auth_one_key_block(self, block, type_value: MfcKeyType, key):
"""
Verify the mf1 key, only verify the specified type of key for a single sector.
:param block:
:param type_value:
:param key:
:return:
"""
data = struct.pack('!BB6s', type_value, block, key)
resp = self.device.send_cmd_sync(Command.MF1_AUTH_ONE_KEY_BLOCK, data)
resp.parsed = resp.status == Status.HF_TAG_OK
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_read_one_block(self, block, type_value: MfcKeyType, key):
"""
Read one mf1 block.
:param block:
:param type_value:
:param key:
:return:
"""
data = struct.pack('!BB6s', type_value, block, key)
resp = self.device.send_cmd_sync(Command.MF1_READ_ONE_BLOCK, data)
resp.parsed = resp.data
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_write_one_block(self, block, type_value: MfcKeyType, key, block_data):
"""
Write mf1 single block.
:param block:
:param type_value:
:param key:
:param block_data:
:return:
"""
data = struct.pack('!BB6s16s', type_value, block, key, block_data)
resp = self.device.send_cmd_sync(Command.MF1_WRITE_ONE_BLOCK, data)
resp.parsed = resp.status == Status.HF_TAG_OK
return resp
@expect_response(Status.HF_TAG_OK)
def hf14a_raw(self, options, resp_timeout_ms=100, data=[], bitlen=None):
"""
Send raw cmd to 14a tag.
:param options:
:param resp_timeout_ms:
:param data:
:param bit_owned_by_the_last_byte:
:return:
"""
class CStruct(ctypes.BigEndianStructure):
_fields_ = [
("activate_rf_field", ctypes.c_uint8, 1),
("wait_response", ctypes.c_uint8, 1),
("append_crc", ctypes.c_uint8, 1),
("auto_select", ctypes.c_uint8, 1),
("keep_rf_field", ctypes.c_uint8, 1),
("check_response_crc", ctypes.c_uint8, 1),
("reserved", ctypes.c_uint8, 2),
]
cs = CStruct()
cs.activate_rf_field = options['activate_rf_field']
cs.wait_response = options['wait_response']
cs.append_crc = options['append_crc']
cs.auto_select = options['auto_select']
cs.keep_rf_field = options['keep_rf_field']
cs.check_response_crc = options['check_response_crc']
if bitlen is None:
bitlen = len(data) * 8 # bits = bytes * 8(bit)
else:
if len(data) == 0:
raise ValueError(f'bitlen={bitlen} but missing data')
if not ((len(data) - 1) * 8 < bitlen <= len(data) * 8):
raise ValueError(f'bitlen={bitlen} incompatible with provided data ({len(data)} bytes), '
f'must be between {((len(data) - 1) * 8)+1} and {len(data) * 8} included')
data = bytes(cs)+struct.pack(f'!HH{len(data)}s', resp_timeout_ms, bitlen, bytearray(data))
resp = self.device.send_cmd_sync(Command.HF14A_RAW, data, timeout=(resp_timeout_ms // 1000) + 1)
resp.parsed = resp.data
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_manipulate_value_block(self, src_block, src_type: MfcKeyType, src_key, operator: MfcValueBlockOperator, operand, dst_block, dst_type: MfcKeyType, dst_key):
"""
1. Increment: increments value from source block and write to dest block
2. Decrement: decrements value from source block and write to dest block
3. Restore: copy value from source block and write to dest block
:param src_block:
:param src_type:
:param src_key:
:param operator:
:param operand:
:param dst_block:
:param dst_type:
:param dst_key:
:return:
"""
data = struct.pack('!BB6sBiBB6s', src_type, src_block, src_key, operator, operand, dst_type, dst_block, dst_key)
resp = self.device.send_cmd_sync(Command.MF1_MANIPULATE_VALUE_BLOCK, data)
resp.parsed = resp.status == Status.HF_TAG_OK
return resp
@expect_response([Status.HF_TAG_OK, Status.HF_TAG_NO])
def mf1_check_keys_of_sectors(self, mask: bytes, keys: list[bytes]):
"""
Check keys of sectors.
:return:
"""
if len(mask) != 10:
raise ValueError("len(mask) should be 10")
if len(keys) < 1 or len(keys) > 83:
raise ValueError("Invalid len(keys)")
data = struct.pack(f'!10s{6*len(keys)}s', mask, b''.join(keys))
bitsCnt = 80 # maximum sectorKey_to_be_checked
for b in mask:
while b > 0:
[bitsCnt, b] = [bitsCnt - (b & 0b1), b >> 1]
if bitsCnt < 1:
# All sectorKey is masked
return chameleon_com.Response(
cmd=Command.MF1_CHECK_KEYS_OF_SECTORS,
status=Status.HF_TAG_OK,
parsed={'status': Status.HF_TAG_OK},
)
# base timeout: 1s
# auth: len(keys) * sectorKey_to_be_checked * 0.1s
# read keyB from trailer block: 0.1s
timeout = 1 + (bitsCnt + 1) * len(keys) * 0.1
resp = self.device.send_cmd_sync(Command.MF1_CHECK_KEYS_OF_SECTORS, data, timeout=timeout)
resp.parsed = {'status': resp.status}
if len(resp.data) == 490:
found = ''.join([format(i, '08b') for i in resp.data[0:10]])
# print(f'{found = }')
resp.parsed.update({
'found': resp.data[0:10],
'sectorKeys': {k: resp.data[6 * k + 10:6 * k + 16] for k, v in enumerate(found) if v == '1'}
})
return resp
@expect_response([Status.HF_TAG_OK, Status.HF_TAG_NO, Status.MF_ERR_AUTH])
def mf1_check_keys_on_block(self, block: int, key_type: int, keys: list[bytes]):
if key_type not in [0x60, 0x61]:
raise ValueError("Wrong key type")
if len(keys) < 1 or len(keys) > 83:
raise ValueError("Invalid len(keys)")
data = struct.pack(f'!BBB{6*len(keys)}s', block, key_type, len(keys), b''.join(keys))
resp = self.device.send_cmd_sync(Command.MF1_CHECK_KEYS_ON_BLOCK, data, timeout=10)
if resp.status == Status.HF_TAG_OK and len(resp.data) == 7:
found, key = struct.unpack('!B6s', resp.data)
if found:
resp.parsed = key
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_static_nested_acquire(self, block_known, type_known, key_known, block_target, type_target):
"""
Collect the key NT parameters needed for StaticNested decryption
:return:
"""
data = struct.pack('!BB6sBB', type_known, block_known, key_known, type_target, block_target)
resp = self.device.send_cmd_sync(Command.MF1_STATIC_NESTED_ACQUIRE, data)
if resp.status == Status.HF_TAG_OK:
resp.parsed = {
'uid': struct.unpack('!I', resp.data[0:4])[0],
'nts': [
{
'nt': nt,
'nt_enc': nt_enc
} for nt, nt_enc in struct.iter_unpack('!II', resp.data[4:])
]
}
return resp
@expect_response(Status.HF_TAG_OK)
def mf1_hard_nested_acquire(self, slow, block_known, type_known, key_known, block_target, type_target):
"""
Collect the NT_ENC list for HardNested decryption
:return:
"""
data = struct.pack('!BBB6sBB', slow, type_known, block_known, key_known, type_target, block_target)
resp = self.device.send_cmd_sync(Command.MF1_HARDNESTED_ACQUIRE, data, timeout=30)
if resp.status == Status.HF_TAG_OK:
resp.parsed = resp.data # we can return the raw nonces bytes
return resp
@expect_response([Status.HF_TAG_OK, Status.HF_TAG_NO])
def mf1_static_encrypted_nested_acquire(self, backdoor_key, sector_count, starting_sector):
data = struct.pack('!6sBB', backdoor_key, sector_count, starting_sector)
resp = self.device.send_cmd_sync(Command.MF1_ENC_NESTED_ACQUIRE, data, timeout=30)
if resp.status == Status.HF_TAG_OK:
resp.parsed = {
'uid': struct.unpack('!I', resp.data[0:4])[0],
'nts': {
'a': [],
'b': []
}
}
i = 4
while i < len(resp.data):
resp.parsed['nts']['a'].append(
{
'nt': reconstruct_full_nt(resp.data, i),
'nt_enc': int.from_bytes(resp.data[i + 3: i + 7], byteorder='big'),
'parity': parity_to_str(resp.data[i + 2])
}
)
resp.parsed['nts']['b'].append(
{
'nt': reconstruct_full_nt(resp.data, i + 7),
'nt_enc': int.from_bytes(resp.data[i + 10: i + 14], byteorder='big'),
'parity': parity_to_str(resp.data[i + 9])
}
)
i += 14
return resp
def hf14a_sniff(self, timeout_ms: int = 5000):
"""
Capture ISO14443A reader frames while CU acts as a tag emulator.
The firmware installs a sniff callback into the HF14A stack for the
requested duration, then returns all captured frames packed as:
[2 bytes: bit count, big-endian] [N bytes: frame data, ceil(bits/8)] ...
:param timeout_ms: Listen duration in ms (1-30000, default 5000)
:return: Raw response — check .status and .data
"""
timeout_ms = max(1, min(30000, timeout_ms))
payload = bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF])
timeout_s = (timeout_ms // 1000) + 5
return self.device.send_cmd_sync(Command.HF14A_SNIFF, payload, timeout=timeout_s)
@expect_response(Status.SUCCESS)
def hf14a_get_config(self):
"""
Get hf 14a config
:return:
"""
resp = self.device.send_cmd_sync(Command.HF14A_GET_CONFIG)
if resp.status == Status.SUCCESS:
bcc, cl2, cl3, rats = struct.unpack('!bbbb', resp.data)
resp.parsed = {'bcc': bcc,
'cl2': cl2,
'cl3': cl3,
'rats': rats}
return resp
@expect_response(Status.SUCCESS)
def hf14a_set_config(self, data):
"""
Set hf 14a config
:return:
"""
data = struct.pack('!bbbb', data['bcc'], data['cl2'], data['cl3'], data['rats'])
return self.device.send_cmd_sync(Command.HF14A_SET_CONFIG, data)
@expect_response(Status.LF_TAG_OK)
def em410x_scan(self):
"""
Read the card number of EM410X.
:return:
"""
resp = self.device.send_cmd_sync(Command.EM410X_SCAN)
if resp.status == Status.LF_TAG_OK:
tag_type = struct.unpack('!H', resp.data[:2])[0]
if tag_type == TagSpecificType.EM410X_ELECTRA:
fmt = '!H13s'
else:
fmt = '!H5s'
resp.parsed = struct.unpack(fmt, resp.data[:struct.calcsize(fmt)]) # tag type + uid
return resp
@expect_response(Status.LF_TAG_OK)
def em410x_write_to_t55xx(self, id_bytes: bytes):
"""
Write EM410X card number into T55XX.
:param id_bytes: ID card number
:return:
"""
if len(id_bytes) == 5:
data = struct.pack(f'!5s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
return self.device.send_cmd_sync(Command.EM410X_WRITE_TO_T55XX, data)
if len(id_bytes) == 13:
data = struct.pack(f'!13s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
return self.device.send_cmd_sync(Command.EM410X_ELECTRA_WRITE_TO_T55XX, data)
raise ValueError("The id bytes length must equal 5 (EM410X) or 13 (Electra)")
@expect_response(Status.LF_TAG_OK)
def hidprox_scan(self, format: int):
"""
Read the length, facility code and card number of HID Prox.
:return:
"""
resp = self.device.send_cmd_sync(Command.HIDPROX_SCAN, struct.pack('!B', format))
if resp.status == Status.LF_TAG_OK:
resp.parsed = struct.unpack('>BIBIBH', resp.data[:13])
return resp
@expect_response(Status.LF_TAG_OK)
def hidprox_write_to_t55xx(self, id_bytes: bytes):
"""
Write HID Prox card number into T55XX.
:param id_bytes: ID card number
:return:
"""
if len(id_bytes) != 13:
raise ValueError("The id bytes length must equal 13")
data = struct.pack(f'!13s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
return self.device.send_cmd_sync(Command.HIDPROX_WRITE_TO_T55XX, data)
@expect_response(Status.LF_TAG_OK)
def ioprox_scan(self):
"""
Read ioProx (XSF): version, facility, number, raw.
"""
resp = self.device.send_cmd_sync(Command.IOPROX_SCAN)
if resp.status == Status.LF_TAG_OK:
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
return resp
@expect_response(Status.LF_TAG_OK)
def ioprox_write_to_t55xx(self, id_bytes: bytes):
"""
Write ioProx card data to a T55XX tag.
"""
if len(id_bytes) != 16:
raise ValueError("The ioProx id bytes length must equal 16")
# Pack id_bytes (16), new_key (4), and all old_keys (4 each) into one buffer
fmt = f'!16s4s{4 * len(old_keys)}s'
data = struct.pack(fmt, id_bytes, new_key, b''.join(old_keys))
return self.device.send_cmd_sync(Command.IOPROX_WRITE_TO_T55XX, data)
@expect_response(Status.SUCCESS)
def ioprox_decode_raw(self, raw8_bytes):
"""
Send 8 raw card bytes to firmware and return 16-byte card data structure.
Response layout: [0]=ver, [1]=fc, [2..3]=cn, [4..11]=raw8, [12..15]=padding.
"""
resp = self.device.send_cmd_sync(Command.IOPROX_DECODE_RAW, data=raw8_bytes)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
return resp
@expect_response(Status.SUCCESS)
def ioprox_compose_id(self, ver, fc, cn):
"""
Encode ioProx parameters into a 16-byte card data structure via firmware.
Response layout: [0]=ver, [1]=fc, [2..3]=cn, [4..11]=raw8, [12..15]=padding.
"""
payload = struct.pack(">BBH", ver, fc, cn)
resp = self.device.send_cmd_sync(Command.IOPROX_COMPOSE_ID, data=payload)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
return resp
def lf_sniff(self, timeout_ms: int = 2000):
"""
Capture raw LF field ADC samples.
The ChameleonUltra samples the LF antenna at 125kHz (8µs/sample).
Each byte is an 8-bit ADC value: ~0x80 = field on, lower = gap/no field.
:param timeout_ms: Capture duration in ms (1-10000, default 2000)
:return: Raw response object — check .status and .data
"""
timeout_ms = max(1, min(10000, timeout_ms))
payload = bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF])
timeout_s = (timeout_ms // 1000) + 2
return self.device.send_cmd_sync(Command.LF_SNIFF, payload, timeout=timeout_s)
@expect_response(Status.LF_TAG_OK)
def em4x05_scan(self, pwd: int = 0):
"""
Read an EM4x05 or EM4x69 tag (reader-talk-first).
Response payload (14 bytes, big-endian):
config 4 bytes — block 0 configuration word
uid 4 bytes — EM4x05 UID
uid_hi 4 bytes — EM4x69 uid_hi (zero for plain EM4x05)
is_em4x69 1 byte — 1 if a 64-bit EM4x69 UID was read
uid_block 1 byte — block number UID was read from
:param pwd: 32-bit password for LOGIN (default 0x00000000)
:return: parsed tuple (config, uid, uid_hi, is_em4x69, uid_block)
"""
pwd_bytes = struct.pack('!I', pwd & 0xFFFFFFFF)
resp = self.device.send_cmd_sync(Command.EM4X05_SCAN, pwd_bytes)
if resp.status == Status.LF_TAG_OK:
resp.parsed = struct.unpack('!IIIBB', resp.data[:14])
return resp
@expect_response(Status.LF_TAG_OK)
def viking_scan(self):
"""
Read the card number of Viking.
:return:
"""
resp = self.device.send_cmd_sync(Command.VIKING_SCAN)
if resp.status == Status.LF_TAG_OK:
resp.parsed = resp.data # uid
return resp
@expect_response(Status.LF_TAG_OK)
def viking_write_to_t55xx(self, id_bytes: bytes):
"""
Write Viking card number into T55XX.
:param id_bytes: ID card number
:return:
"""
if len(id_bytes) != 4:
raise ValueError("The id bytes length must equal 4")
data = struct.pack(f'!4s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
return self.device.send_cmd_sync(Command.VIKING_WRITE_TO_T55XX, data)
@expect_response(Status.LF_TAG_OK)
def pac_scan(self):
"""
Read the card ID of PAC/Stanley.
:return:
"""
resp = self.device.send_cmd_sync(Command.PAC_SCAN)
if resp.status == Status.LF_TAG_OK:
resp.parsed = resp.data[:8]
return resp
@expect_response(Status.LF_TAG_OK)
def pac_write_to_t55xx(self, id_bytes: bytes):
"""
Write PAC/Stanley card data to a T55XX tag.
:param id_bytes: 8-byte ASCII card ID
:return:
"""
if len(id_bytes) != 8:
raise ValueError("The id bytes length must equal 8")
data = struct.pack(f'!8s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
return self.device.send_cmd_sync(Command.PAC_WRITE_TO_T55XX, data)
@expect_response(Status.LF_TAG_OK)
def jablotron_scan(self):
"""
Read the card number of Jablotron.
:return:
"""
resp = self.device.send_cmd_sync(Command.JABLOTRON_SCAN)
if resp.status == Status.LF_TAG_OK:
resp.parsed = resp.data[:5]
return resp
@expect_response(Status.LF_TAG_OK)
def jablotron_write_to_t55xx(self, id_bytes: bytes):
"""
Write Jablotron card number into T55XX.
:param id_bytes: 5-byte Jablotron card ID
:return:
"""
if len(id_bytes) != 5:
raise ValueError("The id bytes length must equal 5")
data = struct.pack(f'!5s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
return self.device.send_cmd_sync(Command.JABLOTRON_WRITE_TO_T55XX, data)
@expect_response(Status.LF_TAG_OK)
def adc_generic_read(self):
"""
Read the ADC when the field is on.
:return:
"""
resp = self.device.send_cmd_sync(Command.ADC_GENERIC_READ, None)
if resp.status == Status.LF_TAG_OK:
resp.parsed = resp.data
return resp
@expect_response(Status.SUCCESS)
def get_slot_info(self):
"""
Get slots info.
:return:
"""
resp = self.device.send_cmd_sync(Command.GET_SLOT_INFO)
if resp.status == Status.SUCCESS:
resp.parsed = [{'hf': hf, 'lf': lf}
for hf, lf in struct.iter_unpack('!HH', resp.data)]
return resp
@expect_response(Status.SUCCESS)
def get_active_slot(self):
"""
Get selected slot.
:return:
"""
resp = self.device.send_cmd_sync(Command.GET_ACTIVE_SLOT)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def set_active_slot(self, slot_index: SlotNumber):
"""
Set the card slot currently active for use.
:param slot_index: Card slot index
:return:
"""
# SlotNumber() will raise error for us if slot_index not in slot range
data = struct.pack('!B', SlotNumber.to_fw(slot_index))
return self.device.send_cmd_sync(Command.SET_ACTIVE_SLOT, data)
@expect_response(Status.SUCCESS)
def set_slot_tag_type(self, slot_index: SlotNumber, tag_type: TagSpecificType):
"""
Set the label type of the emulated card of the current card slot
Note: This operation will not change the data in the flash,
and the change of the data in the flash will only be updated at the next save.
:param slot_index: Card slot number
:param tag_type: label type
:return:
"""
# SlotNumber() will raise error for us if slot_index not in slot range
data = struct.pack('!BH', SlotNumber.to_fw(slot_index), tag_type)
return self.device.send_cmd_sync(Command.SET_SLOT_TAG_TYPE, data)
@expect_response(Status.SUCCESS)
def delete_slot_sense_type(self, slot_index: SlotNumber, sense_type: TagSenseType):
"""
Delete a sense type for a specific slot.
:param slot_index: Slot index
:param sense_type: Sense type to disable
:return:
"""
data = struct.pack('!BB', SlotNumber.to_fw(slot_index), sense_type)
return self.device.send_cmd_sync(Command.DELETE_SLOT_SENSE_TYPE, data)
@expect_response(Status.SUCCESS)
def set_slot_data_default(self, slot_index: SlotNumber, tag_type: TagSpecificType):
"""
Set the data of the emulated card in the specified card slot as the default data
Note: This API will set the data in the flash together.
:param slot_index: Card slot number
:param tag_type: The default label type to set
:return:
"""
# SlotNumber() will raise error for us if slot_index not in slot range
data = struct.pack('!BH', SlotNumber.to_fw(slot_index), tag_type)
return self.device.send_cmd_sync(Command.SET_SLOT_DATA_DEFAULT, data)
@expect_response(Status.SUCCESS)
def set_slot_enable(self, slot_index: SlotNumber, sense_type: TagSenseType, enabled: bool):
"""
Set whether the specified card slot is enabled.
:param slot_index: Card slot number
:param enable: Whether to enable
:return:
"""
# SlotNumber() will raise error for us if slot_index not in slot range
data = struct.pack('!BBB', SlotNumber.to_fw(slot_index), sense_type, enabled)
return self.device.send_cmd_sync(Command.SET_SLOT_ENABLE, data)
def _get_active_lf_tag_type(self) -> TagSpecificType:
slotinfo = self.get_slot_info()
active_slot = SlotNumber.from_fw(self.get_active_slot())
lf_tag_value = slotinfo[active_slot - 1]['lf']
return TagSpecificType(lf_tag_value)
@expect_response(Status.SUCCESS)
def em410x_set_emu_id(self, id: bytes):
"""
Set the card number emulated by EM410x.
:param id_bytes: byte of the card number
:return:
"""
lf_tag_type = self._get_active_lf_tag_type()
if lf_tag_type == TagSpecificType.EM410X_ELECTRA:
expected_len = 13
elif lf_tag_type == TagSpecificType.EM410X:
expected_len = 5
else:
raise ValueError(f"Active LF slot type {lf_tag_type} is not EM410X")
if len(id) != expected_len:
raise ValueError(f"The id bytes length must equal {expected_len}")
data = struct.pack(f'!{expected_len}s', id)
return self.device.send_cmd_sync(Command.EM410X_SET_EMU_ID, data)
@expect_response(Status.SUCCESS)
def em410x_get_emu_id(self):
"""
Get the emulated EM410x card id
"""
resp = self.device.send_cmd_sync(Command.EM410X_GET_EMU_ID)
if resp.status == Status.SUCCESS:
data = resp.data
id_bytes = data
tag_type = None
if len(data) >= 2:
try:
candidate = TagSpecificType(int.from_bytes(data[:2], byteorder='big'))
except ValueError:
candidate = None
if candidate in (TagSpecificType.EM410X, TagSpecificType.EM410X_ELECTRA):
expected_len = 13 if candidate == TagSpecificType.EM410X_ELECTRA else 5
if len(data) == expected_len + 2:
tag_type = candidate
id_bytes = data[2:2 + expected_len]
if tag_type is None:
lf_tag_type = self._get_active_lf_tag_type()
if lf_tag_type == TagSpecificType.EM410X_ELECTRA:
expected_len = 13
elif lf_tag_type == TagSpecificType.EM410X:
expected_len = 5
else:
expected_len = len(data)
id_bytes = data[:expected_len]
resp.parsed = id_bytes
return resp
@expect_response(Status.SUCCESS)
def hidprox_set_emu_id(self, id: bytes):
"""
Set the card number emulated by HID Prox.
:param id_bytes: byte of the card number
:return:
"""
if len(id) != 13:
raise ValueError("The id bytes length must equal 13")
return self.device.send_cmd_sync(Command.HIDPROX_SET_EMU_ID, id)
@expect_response(Status.SUCCESS)
def hidprox_get_emu_id(self):
"""
Get the emulated HID Prox card id
"""
resp = self.device.send_cmd_sync(Command.HIDPROX_GET_EMU_ID)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack('>BIBIBH', resp.data[:13])
return resp
@expect_response(Status.SUCCESS)
def ioprox_set_emu_id(self, id: bytes):
"""
Set the card number emulated by ioProx.
:param id_bytes: byte of the card number
:return:
"""
if len(id) != 16:
raise ValueError("The id bytes length must equal 16")
return self.device.send_cmd_sync(Command.IOPROX_SET_EMU_ID, id)
@expect_response(Status.SUCCESS)
def ioprox_get_emu_id(self):
"""
Get the emulated ioProx card id
"""
resp = self.device.send_cmd_sync(Command.IOPROX_GET_EMU_ID)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
return resp
@expect_response(Status.SUCCESS)
def viking_set_emu_id(self, id: bytes):
"""
Set the card number emulated by Viking.
:param id_bytes: byte of the card number
:return:
"""
if len(id) != 4:
raise ValueError("The id bytes length must equal 4")
data = struct.pack('4s', id)
return self.device.send_cmd_sync(Command.VIKING_SET_EMU_ID, data)
@expect_response(Status.SUCCESS)
def viking_get_emu_id(self):
"""
Get the emulated Viking card id
"""
resp = self.device.send_cmd_sync(Command.VIKING_GET_EMU_ID)
resp.parsed = resp.data
return resp
@expect_response(Status.SUCCESS)
def pac_set_emu_id(self, id: bytes):
"""
Set the card ID emulated by PAC/Stanley.
:param id: 8-byte ASCII card ID
:return:
"""
if len(id) != 8:
raise ValueError("The id bytes length must equal 8")
data = struct.pack('8s', id)
return self.device.send_cmd_sync(Command.PAC_SET_EMU_ID, data)
@expect_response(Status.SUCCESS)
def pac_get_emu_id(self):
"""
Get the emulated PAC/Stanley card ID
"""
resp = self.device.send_cmd_sync(Command.PAC_GET_EMU_ID)
resp.parsed = resp.data[:8]
return resp
@expect_response(Status.SUCCESS)
def jablotron_set_emu_id(self, id: bytes):
"""
Set the card number emulated by Jablotron.
:param id: 5-byte Jablotron card ID
:return:
"""
if len(id) != 5:
raise ValueError("The id bytes length must equal 5")
data = struct.pack('5s', id)
return self.device.send_cmd_sync(Command.JABLOTRON_SET_EMU_ID, data)
@expect_response(Status.SUCCESS)
def jablotron_get_emu_id(self):
"""
Get the emulated Jablotron card id
"""
resp = self.device.send_cmd_sync(Command.JABLOTRON_GET_EMU_ID)
resp.parsed = resp.data[:5]
return resp
@expect_response(Status.SUCCESS)
def mf1_set_detection_enable(self, enabled: bool):
"""
Set whether to enable the detection of the current card slot.
:param enable: Whether to enable
:return:
"""
data = struct.pack('!B', enabled)
return self.device.send_cmd_sync(Command.MF1_SET_DETECTION_ENABLE, data)
@expect_response(Status.SUCCESS)
def mf1_get_detection_count(self):
"""
Get the statistics of the current detection records.
:return:
"""
resp = self.device.send_cmd_sync(Command.MF1_GET_DETECTION_COUNT)
if resp.status == Status.SUCCESS:
resp.parsed, = struct.unpack('!I', resp.data)
return resp
@expect_response(Status.SUCCESS)
def mf1_get_detection_log(self, index: int):
"""
Get detection logs from the specified index position.
:param index: start index
:return:
"""
data = struct.pack('!I', index)
resp = self.device.send_cmd_sync(Command.MF1_GET_DETECTION_LOG, data)
if resp.status == Status.SUCCESS:
# convert
result_list = []
pos = 0
while pos < len(resp.data):
block, bitfield, uid, nt, nr, ar = struct.unpack_from('!BB4s4s4s4s', resp.data, pos)
result_list.append({
'block': block,
'type': ['A', 'B'][bitfield & 0x01],
'is_nested': bool(bitfield & 0x02),
'uid': uid.hex(),
'nt': nt.hex(),
'nr': nr.hex(),
'ar': ar.hex()
})
pos += struct.calcsize('!BB4s4s4s4s')
resp.parsed = result_list
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_get_detection_enable(self):
"""
Get whether NTAG password detection is enabled.
:return:
"""
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_DETECTION_ENABLE)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack('!B', resp.data)[0] == 1
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_set_detection_enable(self, enabled: bool):
"""
Set whether to enable NTAG password detection.
:param enable: Whether to enable
:return:
"""
data = struct.pack('!B', enabled)
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_DETECTION_ENABLE, data)
@expect_response(Status.SUCCESS)
def mf0_ntag_get_detection_count(self):
"""
Get the statistics of the current NTAG password detection records.
:return:
"""
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_DETECTION_COUNT)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack('!I', resp.data)[0]
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_get_detection_log(self, index: int):
"""
Get NTAG password detection logs from the specified index position.
:param index: start index
:return:
"""
data = struct.pack('!I', index)
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_DETECTION_LOG, data)
if resp.status == Status.SUCCESS:
# convert - each log entry is just a 4-byte password
result_list = []
pos = 0
while pos < len(resp.data):
password = resp.data[pos:pos+4]
result_list.append({
'password': password.hex()
})
pos += 4
resp.parsed = result_list
return resp
@expect_response(Status.SUCCESS)
def mf1_write_emu_block_data(self, block_start: int, block_data: bytes):
"""
Set the block data of the analog card of MF1.
:param block_start: Start setting the location of block data, including this location
:param block_data: The byte buffer of the block data to be set can contain multiple block data,
automatically from block_start increment
:return:
"""
data = struct.pack(f'!B{len(block_data)}s', block_start, block_data)
return self.device.send_cmd_sync(Command.MF1_WRITE_EMU_BLOCK_DATA, data)
@expect_response(Status.SUCCESS)
def mf1_read_emu_block_data(self, block_start: int, block_count: int):
"""
Gets data for selected block range
"""
data = struct.pack('!BB', block_start, block_count)
resp = self.device.send_cmd_sync(Command.MF1_READ_EMU_BLOCK_DATA, data)
resp.parsed = resp.data
return resp
@expect_response(Status.SUCCESS)
def mfu_get_emu_pages_count(self):
"""
Gets the number of pages available in the current MF0 / NTAG slot
"""
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_PAGE_COUNT)
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def mfu_read_emu_page_data(self, page_start: int, page_count: int):
"""
Gets data for selected block range
"""
data = struct.pack('!BB', page_start, page_count)
resp = self.device.send_cmd_sync(Command.MF0_NTAG_READ_EMU_PAGE_DATA, data)
resp.parsed = resp.data
return resp
@expect_response(Status.SUCCESS)
def mfu_write_emu_page_data(self, page_start: int, data: bytes):
"""
Gets data for selected block range
"""
count = len(data) >> 2
assert (len(data) % 4) == 0
assert (page_start >= 0) and (count + page_start) <= 256
data = struct.pack('!BB', page_start, count) + data
resp = self.device.send_cmd_sync(Command.MF0_NTAG_WRITE_EMU_PAGE_DATA, data)
return resp
@expect_response(Status.SUCCESS)
def mfu_read_emu_counter_data(self, index: int) -> tuple[int, bool]:
"""
Gets data for selected counter
"""
data = struct.pack('!B', index)
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_COUNTER_DATA, data)
if resp.status == Status.SUCCESS:
resp.parsed = (((resp.data[2] << 16) | (resp.data[1] << 8) | resp.data[0]), resp.data[3] == 0xBD)
return resp
@expect_response(Status.SUCCESS)
def mfu_write_emu_counter_data(self, index: int, value: int, reset_tearing: bool):
"""
Sets data for selected counter
"""
data = struct.pack('!BBBB', index | (int(reset_tearing) << 7),
value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF)
resp = self.device.send_cmd_sync(Command.MF0_NTAG_SET_COUNTER_DATA, data)
return resp
@expect_response(Status.SUCCESS)
def mfu_reset_auth_cnt(self):
"""
Resets authentication counter
"""
resp = self.device.send_cmd_sync(Command.MF0_NTAG_RESET_AUTH_CNT, bytes())
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def hf14a_set_anti_coll_data(self, uid: bytes, atqa: bytes, sak: bytes, ats: bytes = b''):
"""
Set anti-collision data of current HF slot (UID/SAK/ATQA/ATS).
:param uid: uid bytes
:param atqa: atqa bytes
:param sak: sak bytes
:param ats: ats bytes (optional)
:return:
"""
data = struct.pack(f'!B{len(uid)}s2s1sB{len(ats)}s', len(uid), uid, atqa, sak, len(ats), ats)
return self.device.send_cmd_sync(Command.HF14A_SET_ANTI_COLL_DATA, data)
@expect_response(Status.SUCCESS)
def set_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType, name: str):
"""
Set the nick name of the slot.
:param slot: Card slot number
:param sense_type: field type
:param name: Card slot nickname
:return:
"""
encoded_name = name.encode(encoding="utf8")
if len(encoded_name) > 32:
raise ValueError("Your tag nick name too long.")
# SlotNumber() will raise error for us if slot not in slot range
data = struct.pack(f'!BB{len(encoded_name)}s', SlotNumber.to_fw(slot), sense_type, encoded_name)
return self.device.send_cmd_sync(Command.SET_SLOT_TAG_NICK, data)
@expect_response(Status.SUCCESS)
def get_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType):
"""
Get the nick name of the slot.
:param slot: Card slot number
:param sense_type: field type
:return:
"""
# SlotNumber() will raise error for us if slot not in slot range
data = struct.pack('!BB', SlotNumber.to_fw(slot), sense_type)
resp = self.device.send_cmd_sync(Command.GET_SLOT_TAG_NICK, data)
resp.parsed = resp.data.decode(encoding="utf8")
return resp
@expect_response(Status.SUCCESS)
def get_all_slot_nicks(self):
resp = self.device.send_cmd_sync(Command.GET_ALL_SLOT_NICKS, b'')
slots = []
i = 0
slot_index = 0
while i < len(resp.data) and slot_index < 8:
slot_names = {'hf': '', 'lf': ''}
if i < len(resp.data):
hf_len = resp.data[i]
i += 1
if hf_len > 0 and i + hf_len <= len(resp.data):
slot_names['hf'] = resp.data[i:i + hf_len].decode(encoding="utf8", errors="ignore")
i += hf_len
else:
i += hf_len
if i < len(resp.data):
lf_len = resp.data[i]
i += 1
if lf_len > 0 and i + lf_len <= len(resp.data):
slot_names['lf'] = resp.data[i:i + lf_len].decode(encoding="utf8", errors="ignore")
i += lf_len
else:
i += lf_len
slots.append(slot_names)
slot_index += 1
resp.parsed = slots
return resp
@expect_response(Status.SUCCESS)
def delete_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType):
"""
Delete the nick name of the slot.
:param slot: Card slot number
:param sense_type: field type
:return:
"""
# SlotNumber() will raise error for us if slot not in slot range
data = struct.pack('!BB', SlotNumber.to_fw(slot), sense_type)
return self.device.send_cmd_sync(Command.DELETE_SLOT_TAG_NICK, data)
@expect_response(Status.SUCCESS)
def mf1_get_emulator_config(self):
"""
Get array of Mifare Classic emulators settings:
[0] - mf1_is_detection_enable (mfkey32)
[1] - mf1_is_gen1a_magic_mode
[2] - mf1_is_gen2_magic_mode
[3] - mf1_is_use_mf1_coll_res (use UID/BCC/SAK/ATQA from 0 block)
[4] - mf1_get_write_mode
:return:
"""
resp = self.device.send_cmd_sync(Command.MF1_GET_EMULATOR_CONFIG)
if resp.status == Status.SUCCESS:
b1, b2, b3, b4, b5 = struct.unpack('!????B', resp.data)
resp.parsed = {'detection': b1,
'gen1a_mode': b2,
'gen2_mode': b3,
'block_anti_coll_mode': b4,
'write_mode': b5}
return resp
@expect_response(Status.SUCCESS)
def mf1_set_gen1a_mode(self, enabled: bool):
"""
Set gen1a magic mode
"""
data = struct.pack('!B', enabled)
return self.device.send_cmd_sync(Command.MF1_SET_GEN1A_MODE, data)
@expect_response(Status.SUCCESS)
def mf1_set_gen2_mode(self, enabled: bool):
"""
Set gen2 magic mode
"""
data = struct.pack('!B', enabled)
return self.device.send_cmd_sync(Command.MF1_SET_GEN2_MODE, data)
@expect_response(Status.SUCCESS)
def mf1_set_block_anti_coll_mode(self, enabled: bool):
"""
Set 0 block anti-collision data
"""
data = struct.pack('!B', enabled)
return self.device.send_cmd_sync(Command.MF1_SET_BLOCK_ANTI_COLL_MODE, data)
@expect_response(Status.SUCCESS)
def mf1_set_write_mode(self, mode: int):
"""
Set write mode
"""
data = struct.pack('!B', mode)
return self.device.send_cmd_sync(Command.MF1_SET_WRITE_MODE, data)
@expect_response(Status.SUCCESS)
def slot_data_config_save(self):
"""
Update the configuration and data of the card slot to flash.
:return:
"""
return self.device.send_cmd_sync(Command.SLOT_DATA_CONFIG_SAVE)
def enter_bootloader(self):
"""
Reboot into DFU mode (bootloader)
:return:
"""
self.device.send_cmd_auto(Command.ENTER_BOOTLOADER, close=True)
@expect_response(Status.SUCCESS)
def get_animation_mode(self):
"""
Get animation mode value
"""
resp = self.device.send_cmd_sync(Command.GET_ANIMATION_MODE)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def get_enabled_slots(self):
"""
Get enabled slots
"""
resp = self.device.send_cmd_sync(Command.GET_ENABLED_SLOTS)
if resp.status == Status.SUCCESS:
resp.parsed = [{'hf': hf, 'lf': lf} for hf, lf in struct.iter_unpack('!BB', resp.data)]
return resp
@expect_response(Status.SUCCESS)
def set_animation_mode(self, value: int):
"""
Set animation mode value
"""
data = struct.pack('!B', value)
return self.device.send_cmd_sync(Command.SET_ANIMATION_MODE, data)
@expect_response(Status.SUCCESS)
def reset_settings(self):
"""
Reset settings stored in flash memory
"""
resp = self.device.send_cmd_sync(Command.RESET_SETTINGS)
resp.parsed = resp.status == Status.SUCCESS
return resp
@expect_response(Status.SUCCESS)
def save_settings(self):
"""
Store settings to flash memory
"""
resp = self.device.send_cmd_sync(Command.SAVE_SETTINGS)
resp.parsed = resp.status == Status.SUCCESS
return resp
@expect_response(Status.SUCCESS)
def wipe_fds(self):
"""
Reset to factory settings
"""
resp = self.device.send_cmd_sync(Command.WIPE_FDS)
resp.parsed = resp.status == Status.SUCCESS
self.device.close()
return resp
@expect_response(Status.SUCCESS)
def get_battery_info(self):
"""
Get battery info
"""
resp = self.device.send_cmd_sync(Command.GET_BATTERY_INFO)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack('!HB', resp.data)
return resp
@expect_response(Status.SUCCESS)
def get_button_press_config(self, button: ButtonType):
"""
Get config of button press function
"""
data = struct.pack('!B', button)
resp = self.device.send_cmd_sync(Command.GET_BUTTON_PRESS_CONFIG, data)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def set_button_press_config(self, button: ButtonType, function: ButtonPressFunction):
"""
Set config of button press function
"""
data = struct.pack('!BB', button, function)
return self.device.send_cmd_sync(Command.SET_BUTTON_PRESS_CONFIG, data)
@expect_response(Status.SUCCESS)
def get_long_button_press_config(self, button: ButtonType):
"""
Get config of long button press function
"""
data = struct.pack('!B', button)
resp = self.device.send_cmd_sync(Command.GET_LONG_BUTTON_PRESS_CONFIG, data)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def set_long_button_press_config(self, button: ButtonType, function: ButtonPressFunction):
"""
Set config of long button press function
"""
data = struct.pack('!BB', button, function)
return self.device.send_cmd_sync(Command.SET_LONG_BUTTON_PRESS_CONFIG, data)
@expect_response(Status.SUCCESS)
def set_ble_connect_key(self, key: str):
"""
Set config of ble connect key
"""
data_bytes = key.encode(encoding='ascii')
# check key length
if len(data_bytes) != 6:
raise ValueError("The ble connect key length must be 6")
data = struct.pack('6s', data_bytes)
return self.device.send_cmd_sync(Command.SET_BLE_PAIRING_KEY, data)
@expect_response(Status.SUCCESS)
def get_ble_pairing_key(self):
"""
Get config of ble connect key
"""
resp = self.device.send_cmd_sync(Command.GET_BLE_PAIRING_KEY)
resp.parsed = resp.data.decode(encoding='ascii')
return resp
@expect_response(Status.SUCCESS)
def delete_all_ble_bonds(self):
"""
From peer manager delete all bonds.
"""
return self.device.send_cmd_sync(Command.DELETE_ALL_BLE_BONDS)
@expect_response(Status.SUCCESS)
def get_device_capabilities(self):
"""
Get list of commands that client understands
"""
try:
resp = self.device.send_cmd_sync(Command.GET_DEVICE_CAPABILITIES)
except chameleon_com.CMDInvalidException:
print("Chameleon does not understand get_device_capabilities command. Please update firmware")
return chameleon_com.Response(cmd=Command.GET_DEVICE_CAPABILITIES,
status=Status.NOT_IMPLEMENTED)
else:
if resp.status == Status.SUCCESS:
resp.parsed = [x[0] for x in struct.iter_unpack('!H', resp.data)]
return resp
@expect_response(Status.SUCCESS)
def get_device_model(self):
"""
Get device model
0 - Chameleon Ultra
1 - Chameleon Lite
"""
resp = self.device.send_cmd_sync(Command.GET_DEVICE_MODEL)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def get_device_settings(self):
"""
Get all possible settings
For version 5:
settings[0] = SETTINGS_CURRENT_VERSION; // current version
settings[1] = settings_get_animation_config(); // animation mode
settings[2] = settings_get_button_press_config('A'); // short A button press mode
settings[3] = settings_get_button_press_config('B'); // short B button press mode
settings[4] = settings_get_long_button_press_config('A'); // long A button press mode
settings[5] = settings_get_long_button_press_config('B'); // long B button press mode
settings[6] = settings_get_ble_pairing_enable(); // does device require pairing
settings[7:13] = settings_get_ble_pairing_key(); // BLE pairing key
"""
resp = self.device.send_cmd_sync(Command.GET_DEVICE_SETTINGS)
if resp.status == Status.SUCCESS:
if resp.data[0] > CURRENT_VERSION_SETTINGS:
raise ValueError("Settings version in app older than Chameleon. "
"Please upgrade client")
if resp.data[0] < CURRENT_VERSION_SETTINGS:
raise ValueError("Settings version in app newer than Chameleon. "
"Please upgrade Chameleon firmware")
settings_version, animation_mode, btn_press_A, btn_press_B, btn_long_press_A, \
btn_long_press_B, ble_pairing_enable, ble_pairing_key = \
struct.unpack('!BBBBBBB6s', resp.data)
resp.parsed = {'settings_version': settings_version,
'animation_mode': animation_mode,
'btn_press_A': btn_press_A,
'btn_press_B': btn_press_B,
'btn_long_press_A': btn_long_press_A,
'btn_long_press_B': btn_long_press_B,
'ble_pairing_enable': ble_pairing_enable,
'ble_pairing_key': ble_pairing_key}
return resp
@expect_response(Status.SUCCESS)
def hf14a_get_anti_coll_data(self):
"""
Get anti-collision data from current HF slot (UID/SAK/ATQA/ATS)
:return:
"""
resp = self.device.send_cmd_sync(Command.HF14A_GET_ANTI_COLL_DATA)
if resp.status == Status.SUCCESS and len(resp.data) > 0:
# uidlen[1]|uid[uidlen]|atqa[2]|sak[1]|atslen[1]|ats[atslen]
offset = 0
uidlen, = struct.unpack_from('!B', resp.data, offset)
offset += struct.calcsize('!B')
uid, atqa, sak, atslen = struct.unpack_from(f'!{uidlen}s2s1sB', resp.data, offset)
offset += struct.calcsize(f'!{uidlen}s2s1sB')
ats, = struct.unpack_from(f'!{atslen}s', resp.data, offset)
offset += struct.calcsize(f'!{atslen}s')
resp.parsed = {'uid': uid, 'atqa': atqa, 'sak': sak, 'ats': ats}
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_get_uid_magic_mode(self):
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_UID_MAGIC_MODE)
if resp.status == Status.SUCCESS:
resp.parsed, = struct.unpack('!?', resp.data)
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_set_uid_magic_mode(self, enabled: bool):
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_UID_MAGIC_MODE, struct.pack('?', enabled))
@expect_response(Status.SUCCESS)
def mf0_ntag_get_version_data(self):
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_VERSION_DATA)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[:8]
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_set_version_data(self, data: bytes):
assert len(data) == 8
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_VERSION_DATA, data)
@expect_response(Status.SUCCESS)
def mf0_ntag_get_signature_data(self):
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_SIGNATURE_DATA)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[:32]
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_set_signature_data(self, data: bytes):
assert len(data) == 32
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_SIGNATURE_DATA, data)
@expect_response(Status.SUCCESS)
def mf0_ntag_get_write_mode(self):
"""
Get write mode for MF0/NTAG
"""
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_WRITE_MODE)
if resp.status == Status.SUCCESS:
resp.parsed = resp.data[0]
return resp
@expect_response(Status.SUCCESS)
def mf0_ntag_set_write_mode(self, mode: int):
"""
Set write mode for MF0/NTAG
"""
data = struct.pack('!B', mode)
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_WRITE_MODE, data)
@expect_response(Status.SUCCESS)
def get_ble_pairing_enable(self):
"""
Is ble pairing enable?
:return: True if pairing is enable, False if pairing disabled
"""
resp = self.device.send_cmd_sync(Command.GET_BLE_PAIRING_ENABLE)
if resp.status == Status.SUCCESS:
resp.parsed, = struct.unpack('!?', resp.data)
return resp
@expect_response(Status.SUCCESS)
def set_ble_pairing_enable(self, enabled: bool):
data = struct.pack('!B', enabled)
return self.device.send_cmd_sync(Command.SET_BLE_PAIRING_ENABLE, data)
@expect_response(Status.SUCCESS)
def mf1_get_field_off_do_reset(self):
resp = self.device.send_cmd_sync(Command.MF1_GET_FIELD_OFF_DO_RESET)
if resp.status == Status.SUCCESS:
resp.parsed = struct.unpack('!B', resp.data)[0] == 1
return resp
@expect_response(Status.SUCCESS)
def mf1_set_field_off_do_reset(self, enabled: bool):
data = struct.pack('!B', enabled)
return self.device.send_cmd_sync(Command.MF1_SET_FIELD_OFF_DO_RESET, data)
def test_fn():
# connect to chameleon
dev = chameleon_com.ChameleonCom()
try:
dev.open('com19')
except chameleon_com.OpenFailException:
dev.open('/dev/ttyACM0')
cml = ChameleonCMD(dev)
ver = cml.get_app_version()
print(f"Firmware number of application: {ver[0]}.{ver[1]}")
chip = cml.get_device_chip_id()
print(f"Device chip id: {chip}")
# change to reader mode
cml.set_device_reader_mode()
options = {
'activate_rf_field': 1,
'wait_response': 1,
'append_crc': 0,
'auto_select': 0,
'keep_rf_field': 1,
'check_response_crc': 0,
}
try:
# unlock 1
resp = cml.hf14a_raw(options=options, resp_timeout_ms=1000, data=[0x40], bitlen=7)
if resp[0] == 0x0a:
print("Gen1A unlock 1 success")
# unlock 2
resp = cml.hf14a_raw(options=options, resp_timeout_ms=1000, data=[0x43])
if resp[0] == 0x0a:
print("Gen1A unlock 2 success")
print("Start dump gen1a memory...")
# Transfer with crc
options['append_crc'] = 1
options['check_response_crc'] = 1
block = 0
while block < 64:
# Tag read block cmd
cmd_read_gen1a_block = [0x30, block]
if block == 63:
options['keep_rf_field'] = 0
resp = cml.hf14a_raw(options=options, resp_timeout_ms=100, data=cmd_read_gen1a_block)
print(f"Block {block} : {resp.hex()}")
block += 1
else:
print("Gen1A unlock 2 fail")
raise
else:
print("Gen1A unlock 1 fail")
raise
except Exception:
options['keep_rf_field'] = 0
options['wait_response'] = 0
cml.hf14a_raw(options=options)
# disconnect
dev.close()
if __name__ == '__main__':
test_fn()