Skip to content

Commit

Permalink
ISO 14443 single (4 byte) and double (7 byte) sized Tag IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
layereight committed Nov 26, 2023
1 parent a917f23 commit 4080b96
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 20 deletions.
66 changes: 58 additions & 8 deletions MFRC522-trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def execute_action(event: NfcEvent, tag_id: str):
ACTION_MAP[action["type"]](action)


def tag_id_from_bytes(uid):
return ':'.join(x.to_bytes(1, 'big').hex() for x in uid).upper()


# welcome message
logging.info("Welcome to MFRC522-trigger!")

Expand All @@ -58,11 +62,15 @@ def execute_action(event: NfcEvent, tag_id: str):
# create a reader
reader = pirc522.RFID()

current_nuid = ''

current_tag = ''
last_tag = ''
count = 0
polling = False

last_was_7_byte_id = False

# This loop keeps checking for chips. If one is near it will get the UID and authenticate
while True:
try:
Expand All @@ -74,38 +82,80 @@ def execute_action(event: NfcEvent, tag_id: str):
logging.debug("Reader loop %d", count)

# scan for cards
if not polling:
if not polling or last_was_7_byte_id:
(error, tag_type) = reader.request()

# on error continue and retry
if error:
# logging.info("error request")
logging.debug("error request")
if current_tag != '':
execute_action(NfcEvent.REMOVE, current_tag)
current_tag = ''
current_nuid = ''

polling = False
continue

# reset after request
last_was_7_byte_id = False

# get the UID of the card
(error, uid) = reader.anticoll()

# on error continue and retry
if error:
# logging.info("error anticoll")
execute_action(NfcEvent.REMOVE, current_tag)
current_tag = ''
logging.debug("error anticoll")
if current_tag != '':
execute_action(NfcEvent.REMOVE, current_tag)
current_tag = ''
current_nuid = ''
polling = False
continue

# transform UID into string representation
tag_id = ''.join((str(x) for x in uid))
nuid = tag_id_from_bytes(uid)

got_uid = True
tag_id = ''

# Do we have an incomplete UID?!
if uid[0] != 0x88:
got_uid = False
tag_id = tag_id_from_bytes(uid[0:4])

if got_uid and (current_nuid != nuid):
# Activate the tag with the incomplete UID
error = reader.select_tag(uid)
if error:
logging.debug("error select_tag")
polling = False
continue

# Get the remaining bytes
error, uid2 = reader.anticoll2()
if error:
logging.debug("error anticoll2")
polling = False
continue

reader.halt()
last_was_7_byte_id = True

# Build the final UID without checksums
tag_id = tag_id_from_bytes(uid[1:-1] + uid2[:-1])

logging.debug("Tag ID: " + tag_id)

polling = True

# when we're still reading the same tag
if current_tag == tag_id:
if current_nuid == nuid:
# don't busy wait while there's a rfid tag near the reader
logging.debug("Sleep!")
time.sleep(0.1)
continue

current_tag = tag_id
current_nuid = nuid

# execute an action for the reading tag
execute_action(NfcEvent.REDETECT if current_tag == last_tag else NfcEvent.DETECT, tag_id)
Expand Down
39 changes: 33 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ my_raspi_host : ok=11 changed=11 unreachable=0 failed=0 skipped=2 rescued=0 igno
"title": "The root schema",
"additionalProperties": false,
"patternProperties": {
"^[0-9]+$": {
"^[0-9A-F:]+$": {
"type": "object",
"title": "Schema holding name and actions for a tag",
"required": ["name", "ondetect"],
Expand All @@ -199,8 +199,8 @@ my_raspi_host : ok=11 changed=11 unreachable=0 failed=0 skipped=2 rescued=0 igno

```json
{
"1234567890123": {
"name": "A very nice tag, triggering 2 actions: playing a playlist and setting the volume.",
"01:23:AB:CD": {
"name": "A very nice 4-byte NUID tag, triggering 2 actions: playing a playlist and setting the volume.",
"ondetect": [
{
"type": "curl",
Expand All @@ -212,8 +212,8 @@ my_raspi_host : ok=11 changed=11 unreachable=0 failed=0 skipped=2 rescued=0 igno
}
]
},
"9876543210987": {
"name": "An even nicer tag",
"01:23:45:67:89:0A:BC": {
"name": "An even nicer 7-byte UID tag",
"ondetect": [
{
"type": "curl",
Expand All @@ -233,7 +233,7 @@ my_raspi_host : ok=11 changed=11 unreachable=0 failed=0 skipped=2 rescued=0 igno
}
]
},
"5432109876543": {
"AA:BB:CC:DD:EE:FF:01": {
"name": "This tag is also nice",
"ondetect": [
{
Expand All @@ -243,8 +243,34 @@ my_raspi_host : ok=11 changed=11 unreachable=0 failed=0 skipped=2 rescued=0 igno
]
}
}

```

# Version 2 Breaking Changes

Version 2.x of this software respects single (4 bytes) and double (7 bytes) sized UIDs as defined by ISO standard 14443-3.
Version 1.x was always assuming single sized UIDs and simply concatenated decimal string representations of each byte
(e.g. `1364182229223`). Version 2.x assumes hexadecimal representation of each byte separated by a colon
(e.g. 4-byte `01:23:AB:CD`, e.g. 7-byte `01:23:45:67:89:0A:BC`).

This incompatability makes config files from version 1.x unusable with version 2.x. For easier config migration a tool
called `id-convert.py` is provided.

## Config Tag ID Migration Steps

* start `id-convert.py` and rescan all your tags, a file `ids.csv` is written containing a list of old style and new style tag ids
```
$ ./id-convert.py | tee ids.csv
```
* backup your current config
```
$ cp config.json config.json.bak
```
* replace old tag ids with new tag ids in your config file `config.json`
```
$ for i in $(cat ids.csv); old=$(echo $i | cut -d '_' -f 1) && new=$(echo $i | cut -d '_' -f 2) && echo "${old} ${new}" && sed -i "s/${old}/${new}/g" config.json; done
```

# Roadmap

* more python unit tests
Expand All @@ -255,6 +281,7 @@ my_raspi_host : ok=11 changed=11 unreachable=0 failed=0 skipped=2 rescued=0 igno

# Roadmap done

* ISO 14443 Tag IDs
* quit with error when config is broken
* validate config with JSON schema and log a warning when it's invalid
* multiple actions per event
Expand Down
10 changes: 5 additions & 5 deletions config.sample.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"1234567890123": {
"name": "A very nice tag, triggering 2 actions: playing a playlist and setting the volume.",
"01:23:AB:CD": {
"name": "A very nice 4-byte NUID tag, triggering 2 actions: playing a playlist and setting the volume.",
"ondetect": [
{
"type": "curl",
Expand All @@ -12,8 +12,8 @@
}
]
},
"9876543210987": {
"name": "An even nicer tag",
"01:23:45:67:89:0A:BC": {
"name": "An even nicer 7-byte UID tag",
"ondetect": [
{
"type": "curl",
Expand All @@ -33,7 +33,7 @@
}
]
},
"5432109876543": {
"AA:BB:CC:DD:EE:FF:01": {
"name": "This tag is also nice",
"ondetect": [
{
Expand Down
2 changes: 1 addition & 1 deletion config/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"title": "The root schema",
"additionalProperties": false,
"patternProperties": {
"^[0-9]+$": {
"^[0-9A-F:]+$": {
"type": "object",
"title": "Schema holding name and actions for a tag",
"required": ["name", "ondetect"],
Expand Down
67 changes: 67 additions & 0 deletions id-convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-


import pirc522
import sys


def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)


# welcome message
eprint("Welcome to id-convert!")
eprint("Press Ctrl-C to stop.")

# create a reader
reader = pirc522.RFID()

found_tags = {}

# This loop keeps checking for chips. If one is near it will get the UID and authenticate
while True:
try:
# wait for reader to send an interrupt
reader.wait_for_tag()

# scan for cards
(error, tag_type) = reader.request()

# on error continue and retry
if error:
# eprint("error request")
continue

# get the UID of the card
(error, uid_old) = reader.anticoll()

# on error continue and retry
if error:
# eprint("error anticoll")
continue

uid_new = reader.read_id(as_number=False)

if uid_new is None:
# eprint("error read_id")
continue

# transform UID into string representation
tag_id_old = ''.join((str(x) for x in uid_old))
tag_id_new = ':'.join(x.to_bytes(1, 'big').hex() for x in uid_new).upper()

if tag_id_new not in found_tags:
print(tag_id_old + "_" + tag_id_new, flush=True)
found_tags[tag_id_new] = 1
else:
eprint("Already got it! " + str(len(found_tags)))

except KeyboardInterrupt:
eprint("Shutdown!")
break
except Exception:
eprint("Unexpected exception '%s' occurred!", str(sys.exc_info()[0].__name__))
break

reader.cleanup()

0 comments on commit 4080b96

Please sign in to comment.