Here’s yet another version of the script: this one can talk to an ACR122U through the PS/SC, or directly to any PN53x reader through USB (including an ACR122U):
#!/usr/bin/python3
"""Small program to flash a morse code message on an NFC LED implant using
either a ACR122U reader through PC/SC, or a PN53x-based USB reader directly
(including a ACR122U)
The pyscard module is required for operation with PC/SC
The nfcpy module is required for direct USB operation with a PN53x-based reader
"""
### Parameters
default_interface = "pn53x_usb"
default_wpm = 10 #words per minute
msg_start_pause = " "
msg_end_pause = " "
### Modules
import sys
import argparse
from time import sleep
from datetime import datetime
### Defines
# Morse code dictionary
morsecode = {
"A": ".-",
"B": "-...",
"C": "-.-.",
"D": "-..",
"E": ".",
"F": "..-.",
"G": "--.",
"H": "....",
"I": "..",
"J": ".---",
"K": "-.-",
"L": ".-..",
"M": "--",
"N": "-.",
"O": "---",
"P": ".--.",
"Q": "--.-",
"R": ".-.",
"S": "...",
"T": "-",
"U": "..-",
"V": "...-",
"W": ".--",
"X": "-..-",
"Y": "-.--",
"Z": "--..",
"1": ".----",
"2": "..---",
"3": "...--",
"4": "....-",
"5": ".....",
"6": "-....",
"7": "--...",
"8": "---..",
"9": "----.",
"0": "-----",
"=": "-...-",
"/": "-..-.",
"?": "..--..",
",": "--..--",
".": ".-.-.-",
":": "---...",
"'": ".----.",
'"': ".-..-.",
"_": "..--.-",
"(": "-.--.",
")": "-.--.-",
"#": "-.---",
"-": "-....-",
"|": "...-..",
"\\": "-.....",
"*": "-----.",
";": "-.-.-.",
"@": ".--.-.",
"^": "....--.-.",
"$": "...-..-",
"!": "....-.",
">": "....---.",
"]": "....-....",
"[": "....-..",
"<": "....-.-..",
"&": "....--.",
"%": "....-.--.",
"~": "....--",
"+": ".-.-.",
"{": "....-.--",
"}": "....--..-",
"[AR]": ".-.-.",
"[AS]": ".-...",
"[BK]": "-...-.-",
"[BT]": "-...-",
"[KA]": "-.-.-",
"[CL]": "-.-..-..",
"[KN]": "-.--.",
"[VA]": "...-.-",
"[VE]": "...-.",
"[GR]": "--..-.",
"[HM]": "....--",
"[IX]": "..-..-",
"[IMI]": "..--..",
"[INT]": "..-.-",
"[SOS]": "...---..."}
# CCID escape command
ioctl_ccid_escape_code = 1 # 1 for PCSC-Lite, 3500 for Windows
# ACR122U pseudo-APDU commands
cmd_get_fw_revision = [0xff, 0x00, 0x48, 0x00, 0x00]
cmd_disable_polling = [0xff, 0x00, 0x51, 0x00, 0x00]
cmd_enable_polling = [0xff, 0x00, 0x51, 0xff, 0x00]
# PN53x commands, wrapped in ACR122U direct transmit pseudo-APDUs
cmd_rf_field_off = [0xff, 0x00, 0x00, 0x00, 0x04, 0xd4, 0x32, 0x01, 0x00]
cmd_rf_field_on = [0xff, 0x00, 0x00, 0x00, 0x04, 0xd4, 0x32, 0x01, 0x01]
### Routines
def send_acr122u_control_command(cmd, hcard):
"""Send the ACR122U a control command, return the raw result and raise an
exception in case of error
"""
hresult, response = SCardControl(hcard, ioctl_ccid_escape, cmd)
if hresult != SCARD_S_SUCCESS:
raise SystemError("Failure to control: {}".format(
SCardGetErrorMessage(hresult)))
return(response)
def nfc_morse_player(do_pcsc, wpm, msg):
"""Morse code player
"""
if not msg:
print("Nothing to do!")
return(0)
# Add a pause at the beginning and at the end of the message
msg = msg_start_pause + msg + msg_end_pause
# Turn the message into a sequence of field-on durations (positive) and
# field-off durations (negative)
msg = msg.upper()
morsechars = []
i = 0
while i < len(msg):
if msg[i] == "[":
j = msg.find("]", i + 1)
if j > 0 and msg[i : j + 1] in morsecode:
morsechars.append(morsecode[msg[i : j + 1]])
i = j + 1
continue
if msg[i] == " " or msg[i] == "\t":
morsechars.append(" ")
elif msg[i] in morsecode:
morsechars.append(morsecode[msg[i]])
else:
print("Untranslatable in morse code: {} - dropped".format(msg[i]))
i += 1
ditlen = 1.2 / wpm
morseseq = []
for mc in morsechars:
if mc == " ":
if morseseq:
morseseq[-1] = -ditlen * 7
else:
morseseq.append(-ditlen * 7)
continue
for c in mc:
if c == ".":
morseseq.append(ditlen)
else:
morseseq.append(ditlen * 3)
morseseq.append(-ditlen)
morseseq[-1] = -ditlen * 3
# Set things up using the PC/SC interface
if do_pcsc:
# Establish context and connect to the reader
try:
hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
hresult, hcard, dwActiveProtocol = SCardConnect(hcontext,
'ACS ACR122U PICC Interface 00 00',
SCARD_SHARE_DIRECT, SCARD_PROTOCOL_T0)
except:
print("Cannot connect to ACR122U")
return(-1)
# Get the ACR122U's firmware revision number, to test that CCID escape has
# been enabled and to double-check that the reader is indeed an ACR122U
try:
fwrev = "".join([chr(v) for v in send_acr122u_control_command(
cmd_get_fw_revision, hcard)])
except:
print("Error getting ACR122U firmware revision number.")
print("Is the CCID exchange command allowed in /etc/libccid_Info.plist")
return(-2)
if fwrev[:7].upper() != "ACR122U":
print('Error: "{}" does not appear to be an ACR122U'.format(fwrev))
return(-3)
# Disable polling so the reader won't try to read the implant when the field
# is turned on
try:
send_acr122u_control_command(cmd_disable_polling, hcard)
except:
print("Error disabling polling")
return(-4)
# Set things up using the direct PN53x USB interface
else:
# Open the PN53x USB reader
try:
clf = ContactlessFrontend()
if not clf.open("usb"):
raise SystemError
except:
print("Error opening the PN53x USB reader")
return(-1)
# Play the morse code sequence
warn_if_too_fast = True
for d in morseseq:
# Flip the RF field on or off
cmd_start_tstamp = datetime.now().timestamp()
try:
if do_pcsc: # Use the PC/SC interface
send_acr122u_control_command(cmd_rf_field_off if d < 0 \
else cmd_rf_field_on, hcard)
else: # Use the direct PN53x USb interface
clf.device.chipset.rf_configuration(0x01, b"\00" if d < 0 else b"\01")
except:
print("Error switching the RF field {}".format("off" if d < 0 else "on"))
return(-5)
cmd_duration = datetime.now().timestamp() - cmd_start_tstamp
# Wait for however long it takes to match the duration in the sequence
remaining_wait = abs(d) - cmd_duration
if remaining_wait > 0:
sleep(remaining_wait)
else:
if warn_if_too_fast:
print("Warning: {} WPM is too fast for the ACR122U and/or your " \
"computer.".format(wpm))
print("Morse code timing may appear incorrect")
warn_if_too_fast = False
# Clean things up using the PC/SC interface
if do_pcsc:
# Re-enable the RF field and re-enable polling so the reader can work again
# as a regular reader
try:
send_acr122u_control_command(cmd_rf_field_on, hcard)
except:
print("Error re-enabling the RF field")
return(-5)
try:
send_acr122u_control_command(cmd_enable_polling, hcard)
except:
print("Error re-enabling polling")
return(-6)
# Clean things up using the direct PN53x USb interface
else:
clf.close()
### Main routine
if __name__ == "__main__":
# Read the command line arguments
argparser = argparse.ArgumentParser()
argparser.add_argument(
"-i", "--interface",
type = str,
help = "Reader interface (default: {})".format(default_interface),
choices = ("pcsc", "pn53x_usb"),
default = default_interface,
required = False
)
argparser.add_argument(
"-w", "--words-per-minute",
type = int,
help = "Rate of the morse code (default: {})".format(default_wpm),
default = default_wpm,
required = False
)
argparser.add_argument(
"message",
type = str,
help = "Text to output in morse code on LED implant",
)
args = argparser.parse_args()
do_pcsc = args.interface == "pcsc"
# Conditionally import the relevant module here, so the user doesn't have to
# install the other if they don't plan on using that particular interface
if do_pcsc:
from smartcard.scard import * # From the pyscard package
# We can only set this variable here
ioctl_ccid_escape = SCARD_CTL_CODE(ioctl_ccid_escape_code)
else:
from nfc import * # From the nfcpy package
sys.exit(nfc_morse_player(do_pcsc, args.words_per_minute, args.message))
Essentially, any USB PN53x reader that works with libnfc will work with the direct USB mode. The reason I added it is because I wanted to use my DL533N-XL long-range reader to drive my doNExT’s LEDs, and the DL533N-XL isn’t supported by PCSC-Lite.
And look how gloriously it works: my arm is about half an inch off the reader and it lights up like a Christmas tree!