Global firmware+CLI versions are following the semantic versioning logic mostly regarding the protocol version, so third party clients (GUIs, mobile apps, SDKs) can rely on firmware version to know their level of compatibility.
Given a version number MAJOR.MINOR.PATCH, we will increment the:
Besides compatibility with a given firmware version, third party clients may choose to offer to the users the possibility to follow a stable release channel (installing only tagged releases) or the development channel (installing latest commits).
For the stable channel, a client compatible with versions X.y.z can accept any version > X.y’.z’ but should refuse to work with a version X’>X.
For the development channel, a client compatible with versions X.y.z can accept any latest commit unless a tag X’.0.0 with X’>X is present in the repo, indicating that the corresponding commit and all the commits above are incompatible with the client version. There is still a non negligible risk that breaking changes are pushed while forgetting about putting a new tag, or artefacts being built before the tag being pushed. Here be dragons… It’s always a good practice for the client to validate whatever data is transmitted by the firmware, and fail gracefully in case of hiccups.
Cf GET_APP_VERSION and GET_GIT_VERSION.
When GET_GIT_VERSION returns only a tag and no commit hash info (on a release tag), one can query the corresponding hash with the GitHub API, e.g.
"4747d3884d21e0df8549e3029a920ea390e0b00a"
The communication between the firmware and the client is made of frames structured as follows:

packet
+8: "SOF"
+8: "LRC1"
+16: "CMD"
+16: "STATUS"
+16: "LEN"
+8: "LRC2"
+48: "DATA (variable length)"
+8: "LRC3"
1 byte, “Start-Of-Frame byte” represents the start of a packet, and must be equal to 0x11.1 byte, LRC over SOF byte, therefore must be equal to 0xEF.2 bytes, each command have been assigned a unique number (e.g. DATA_CMD_SET_SLOT_TAG_NICK = 1007).2 bytes.
0x0000.2 bytes, length of the DATA field, maximum is 512.1 byte, LRC over CMD|STATUS|LEN bytes.LEN bytes, data to be sent or received, maximum is 512 bytes. This payload depends on the exact command or response to command being used. See Packet payloads below.1 byte, LRC over DATA bytes.Notes:
LEN + 10 bytes, therefore it is between 10 and 522 bytes.Each command and response have their own payload formats.
Standard response status is STATUS_SUCCESS for general commands, STATUS_HF_TAG_OK for HF commands and STATUS_LF_TAG_OK for LF commands.
See Guidelines for more info.
Beware, slots in protocol count from 0 to 7 (and from 1 to 8 in the CLI…).
In the following list, “CLI” refers to one typical CLI command using the described protocol command. But it’s not a 1:1 match, there can be other protocol commands used by the CLI command and there can be other CLI commands using the same protocol command…
hw version---
title: "Command Packet"
---
packet
+8: "SOF"
+8: "LRC1"
+16: "CMD (=1000)"
+16: "STATUS"
+16: "LEN (=0)"
+8: "LRC2"
+8: "LRC3"
---
title: "Response Packet"
---
packet
+8: "SOF"
+8: "LRC1"
+16: "CMD (=1000)"
+16: "STATUS"
+16: "LEN (=2)"
+8: "LRC2"
+8: "major version"
+8: "minor version"
+8: "LRC3"
version_major|version_minorhw mode---
title: "Command Packet"
---
packet
+8: "SOF"
+8: "LRC1"
+16: "CMD (=1001)"
+16: "STATUS"
+16: "LEN (=1)"
+8: "LRC2"
+8: "new device mode"
+8: "LRC3"
0x00=emulator mode, 0x01=reader mode---
title: "Response Packet"
---
packet
+8: "SOF"
+8: "LRC1"
+16: "CMD (=1001)"
+16: "STATUS"
+16: "LEN (=0)"
+8: "LRC2"
+8: "LRC3"
0x00=emulator mode, 0x01=reader modehw mode
slot_number between 0 and 7hw slot change
slot_number|tag_type[2] with slot_number between 0 and 7 and tag_type according to tag_specific_type_t enum, U16 in Network byte order.hw slot type
slot_number|tag_type[2] with slot_number between 0 and 7 and tag_type U16 according to tag_specific_type_t enum, U16 in Network byte order.hw slot init
slot_number|sense_type|enable with slot_number between 0 and 7, sense_type according to tag_sense_type_t enum and enable = 0x01 to enable, 0x00 to disablehw slot enable/hw slot disable
slot_number|sense_type|name[N] with slot_number between 0 and 7, sense_type according to tag_sense_type_t enum and name a UTF-8 encoded string of max 32 bytes, no null terminator.hw slot nick
slot_number|sense_type with slot_number between 0 and 7 and sense_type according to tag_sense_type_t enum.STATUS_FLASH_READ_FAIL.hw slot nick
hw slot store
hw dfu
DEVICEID[8] U64 in Network byte order.hw chipid
DEVICEADDR[6] U48 in Network byte order. First 2 MSBits forced to 0b11 to match BLE static address.hw address
hw settings store
hw settings reset
settings_animation_mode_t enum.hw settings animation
settings_animation_mode_t enum.hw settings animation
hw versionNotes: the returned string is the output of git describe --abbrev=7 --dirty --always --tags --match "v*.*" so, depending on the status of the repo it can be
v2.0.0 if the firmware is built from the tagged commitg, e.g. 5 commits away from v2.0.0: v2.0.0-5-g617d6d0-dirty if the local repo contains changes not yet committed, e.g. v2.0.0-5-g617d6d0-dirty
hw slot list
hf_tag_type[2]|lf_tag_type[2] according to tag_specific_type_t enum, for slots from 0 to 7, U16 in Network byte order.hw slot list
STATUS_SUCCESS or STATUS_FLASH_WRITE_FAIL. The device will reboot shortly after this command.hw factory_reset
slot_number|sense_type with slot_number between 0 and 7 and sense_type according to tag_sense_type_t enum.hw slot nick
0x00 or 0x01, 2 bytes for each slot from 0 to 7, as enabled_hf|enabled_lfhw slot list
slot_number|sense_type with slot_number between 0 and 7 and sense_type according to tag_sense_type_t enum.hw slot delete
voltage[2]|percentage. Voltage: U16 in Network byte order.hw batteryNotes: wait about 5 seconds after wake-up, before querying the battery status, else the device won’t be able to give a proper measure and will return zeroes.
A or B (a/b tolerated too)button_function according to settings_button_function_t enum.hw settings btnpress
button|button_function with button char A or B (a/b tolerated too) and button_function according to settings_button_function_t enum.hw settings btnpress
A or B (a/b tolerated too)button_function according to settings_button_function_t enum.hw settings btnpress
button|button_function with button char A or B (a/b tolerated too) and button_function according to settings_button_function_t enum.hw settings btnpress
hw settings blekey
hw settings blekey
hw settings bleclearbonds
hw_version aka NRF_DFU_HW_VERSION according to chameleon_device_type_t enum (0=Ultra, 1=Lite)hw version
settings_current_version = 5animation_mode, cf GET_ANIMATION_MODEbtn_press_A, cf GET_BUTTON_PRESS_CONFIGbtn_press_B, cf GET_BUTTON_PRESS_CONFIGbtn_long_press_A, cf GET_LONG_BUTTON_PRESS_CONFIGbtn_long_press_B, cf GET_LONG_BUTTON_PRESS_CONFIGble_pairing_enable, cf GET_BLE_PAIRING_ENABLEble_pairing_key[6], cf GET_BLE_PAIRING_KEY0x00 or 0x01hw settings blepair
0x00 or 0x01hw settings blepair
tag1_data|tag2_data|... with each tag: uidlen|uid[uidlen]|atqa[2]|sak|atslen|ats[atslen]. UID, ATQA, SAK and ATS as bytes.hf 14a scanNotes:
STATUS_HF_TAG_NO and Response empty.atslen must not be confused with ats[0]==TL. So atslen|ats = 00 means no ATS while 0100 would be an empty ATS.
0x00 or 0x01hf 14a info
mf1_nested_type_t enumhf 14a info
type_known|block_known|key_known[6]|type_target|block_target. Key as 6 bytes.uid[4] followed by N tuples of nt[4]|nt_enc[4]. All values as U32.hf mf nested on static nonce tag
type_target|block_target|first_recover|sync_max. Type=0x60 for key A, 0x61 for key B.mf1_darkside_status_t enum,
else 33 bytes darkside_status|uid[4]|nt1[4]|par[8]|ks1[8]|nr[4]|ar[4]
darkside_statusuid[4] U32 (format expected by darkside tool)nt1[4] U32par[8] U64ks1[8] U64nr[4] U32ar[4] U32hf mf darkside
type_known|block_known|key_known[6]. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.uid[4]|dist[4]
uid[4] U32 (format expected by nested tool)dist[4] U32hf mf nested
type_known|block_known|key_known[6]|type_target|block_target. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.nt[4]|nt_enc[4]|par
nt[4] U32nt_enc[4] U32parhf mf nested
type|block|key[6]. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.STATUS_HF_TAG_OK if auth succeeded, else STATUS_MF_ERR_AUTHhf mf nested
type|block|key[6]. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.block_data[16]hf mf rdbl
type|block|key[6]|block_data[16]. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.hf mf wrbl
options|resp_timeout_ms[2]|bitlen[2] followed by data to be transmitted, with options a 1-byte BigEndian bitfield, so starting from MSB:
activate_rf_field:1wait_response:1append_crc:1auto_select:1keep_rf_field:1check_response_crc:1reserved:2hf 14a raw
src_type|src_block|src_key[6]|operator|operand[4]|dst_type|dst_block|dst_key[6]. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B. Operator=0xC0 for decrement, 0xC1 for increment, 0xC2 for restore. Operand as I32 in Network byte order.hf mf value
mask[10]|keys[N][6] (1<=N<=83)
mask: 40 sectors, 2 bits/sector, MSB: 0A|0B|1A|1B|...|39A|39B. 0b1 represent to skip checking the key.found[10]|sectorKey[40][2][6].
found: 40 sectors, 2 bits/sector, MSB: 0A|0B|1A|1B|...|39A|39B. 0b1 represent the key is found.sectorKey: 40 sectors, 2 keys/sector, 6 bytes/key: key0A[6]|key0B[6]|key1A[6]|key1B[6]|...|key39A[6]|key39B[6]hf mf fchk
id[5]. ID as 5 bytes.lf em 410x read
id[5]|new_key[4]|old_key1[4]|old_key2[4]|... (N>=1). . ID as 5 bytes. Keys as 4 bytes.lf em 410x write
format[1]|facility_code[4]|card_number[5]|issue_level[1]|oem[2].lf hid prox read
format[1]|facility_code[4]|card_number[5]|issue_level[1]|oem[2]|new_key[4]|old_key1[4]|old_key2[4]|... (N>=1). Keys as 4 bytes.lf hid prox write
block_start|block_data1[16]|block_data2[16]|... (1<=N<=31)hf mf eload
uidlen|uid[uidlen]|atqa[2]|sak|atslen|ats[atslen]. UID, ATQA, SAK and ATS as bytes.hf mf econfig/hf mfu econfig
0x00 or 0x01hf mf econfig
count[4], U32 in Network byte order.hf mf elog
index, U32 in Network byte order.block...|is_nested|is_key_b 1-byte bitfield, starting from LSBuid[4] ?nt[4] ?nr[4] ?ar[4] ?hf mf elog
0x00 or 0x01hw slot list
block_start|block_count with 1<=block_count <=32block_count*16 byteshf mf eread
detection, cf MF1_GET_DETECTION_ENABLEgen1a_mode, cf MF1_GET_GEN1A_MODEgen2_mode, cf MF1_GET_GEN2_MODEblock_anti_coll_mode, cf MF1_GET_BLOCK_ANTI_COLL_MODEwrite_mode, cf MF1_GET_WRITE_MODEhf mf econfig
0x00 or 0x010x00 or 0x01hf mf econfig
0x00 or 0x010x00 or 0x01hf mf econfig
0x00 or 0x010x00 or 0x01hf mf econfig
nfc_tag_mf1_write_mode_t aka MifareClassicWriteMode enumnfc_tag_mf1_write_mode_t aka MifareClassicWriteMode enumhf mf econfig
uidlen|uid[uidlen]|atqa[2]|sak|atslen|ats[atslen]. UID, ATQA, SAK and ATS as bytes.hw slot list/hf mf econfig/hf mfu econfig
hf mfu econfig
hf mfu econfig --enable-uid-magic/hf mfu econfig --disable-uid-magic
4 * n bytes where n is the number if pages to be readhf mfu eview
n * 4 bytes: one for first page index, one for count of pages to be read, n * 4 for n pages data.hf mfu econfig
hf mfu econfig --set-version <hex>
hf mfu econfig
hf mfu econfig --set-signature <hex>
0xBD means tearing flag is not set.hf mfu ercnt
hf mfu ewcnt
hf mfu econfig --reset-auth-cnt
id[5]. ID as 5 bytes.lf em 410x econfig
id[5]. ID as 5 bytes.lf em 410x econfig
format[1]|facility_code[4]|card_number[5]|issue_level[1]|oem[2].lf hid prox econfig
format[1]|facility_code[4]|card_number[5]|issue_level[1]|oem[2].lf hid prox econfig -s 1 --fc 107 --cn 10044 --format H10301If you need to define new payloads for new commands, try to follow these guidelines.
Be verbose, explicit and reuse conventions, in order to enhance code maintainability and understandability for the other contributors
struct for cmd/resp data greater than a single byte, use and abuse of struct.pack/struct.unpack in Python. So one can understand the payload format at a simple glimpse. Exceptions to C struct are when the formats are of variable length (but Python struct is still flexible enough to cope with such formats!)sizeof(), offsetof(struct, field) in C and struct.calcsize() in PythonIf single byte of data to return, still use a 1-byte data, not status. Standard response status is STATUS_SUCCESS for general commands, STATUS_HF_TAG_OK for HF commands and STATUS_LF_TAG_OK for LF commands. If the response status is different than those, the response data is empty. Response status are generic and cover things like tag disappearance or tag non-conformities with the ISO standard. If a command needs more specific response status, it is added in the first byte of the data, to avoid cluttering the 1-byte general status enum with command-specific statuses. See e.g. MF1_DARKSIDE_ACQUIRE.
uint16_t, not int or enum. Cast explicitly int and enum to uint_t of proper sizeU16NTOHS, U32NTOHL must be used on reception of a command payload.U16HTONS, U32HTONL must be used on creation of a response payload.! with all struct.pack/struct.unpack
length/data parameters for creating the response content
14a_scan not possible in Python)m_data_cmd_map, data_cmd.h and chameleon_cmd.py definitionsdata_cmd.h and chameleon_cmd.py with some FIXME: to be implemented comment
num_to_bytes bytes_to_num could use hton*, ntoh* instead, to make endianess explicit