243 lines
9.4 KiB
Python
243 lines
9.4 KiB
Python
# -*- coding: latin-1 -*-
|
|
# -----------------------------------------------------------------------------
|
|
# Copyright 2011, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
|
|
#
|
|
# Licensed under the EUPL, Version 1.1 or - as soon they
|
|
# will be approved by the European Commission - subsequent
|
|
# versions of the EUPL (the "Licence");
|
|
# You may not use this work except in compliance with the
|
|
# Licence.
|
|
# You may obtain a copy of the Licence at:
|
|
#
|
|
# https://joinup.ec.europa.eu/software/page/eupl
|
|
#
|
|
# Unless required by applicable law or agreed to in
|
|
# writing, software distributed under the Licence is
|
|
# distributed on an "AS IS" basis,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
|
# express or implied.
|
|
# See the Licence for the specific language governing
|
|
# permissions and limitations under the Licence.
|
|
# -----------------------------------------------------------------------------
|
|
"""Device driver for the Arygon ACR122U contactless reader.
|
|
|
|
The Arygon ACR122U is a PC/SC compliant contactless reader that
|
|
connects via USB and uses the USB CCID profile. It is normally
|
|
intented to be used with a PC/SC stack but this driver interfaces
|
|
directly with the inbuilt PN532 chipset by tunneling commands through
|
|
the PC/SC Escape command. The driver is limited in functionality
|
|
because the embedded microprocessor (that implements the PC/SC stack)
|
|
also operates the PN532; it does not allow all commands to pass as
|
|
desired and reacts on chip responses with its own (legitimate)
|
|
interpretation of state.
|
|
|
|
========== ======= ============
|
|
function support remarks
|
|
========== ======= ============
|
|
sense_tta yes Type 1 (Topaz) Tags are not supported
|
|
sense_ttb yes ATTRIB by firmware voided with S(DESELECT)
|
|
sense_ttf yes
|
|
sense_dep yes
|
|
listen_tta no
|
|
listen_ttb no
|
|
listen_ttf no
|
|
listen_dep no
|
|
========== ======= ============
|
|
|
|
"""
|
|
import nfc.clf
|
|
from . import pn532
|
|
|
|
import os
|
|
import errno
|
|
import struct
|
|
from binascii import hexlify
|
|
|
|
import logging
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def init(transport):
|
|
device = Device(Chipset(transport))
|
|
device._vendor_name = transport.manufacturer_name
|
|
device._device_name = transport.product_name.split()[0]
|
|
return device
|
|
|
|
|
|
class Device(pn532.Device):
|
|
# Device driver class for the ACR122U.
|
|
|
|
def __init__(self, chipset):
|
|
super(Device, self).__init__(chipset, logger=log)
|
|
|
|
def sense_tta(self, target):
|
|
"""Activate the RF field and probe for a Type A Target at 106
|
|
kbps. Other bitrates are not supported. Type 1 Tags are not
|
|
supported because the device does not allow to send the
|
|
correct RID command (even though the PN532 does).
|
|
|
|
"""
|
|
return super(Device, self).sense_tta(target)
|
|
|
|
def sense_ttb(self, target):
|
|
"""Activate the RF field and probe for a Type B Target.
|
|
|
|
The RC-S956 can discover Type B Targets (Type 4B Tag) at 106
|
|
kbps. For a Type 4B Tag the firmware automatically sends an
|
|
ATTRIB command that configures the use of DID and 64 byte
|
|
maximum frame size. The driver reverts this configuration with
|
|
a DESELECT and WUPB command to return the target prepared for
|
|
activation (which nfcpy does in the tag activation code).
|
|
|
|
"""
|
|
return super(Device, self).sense_ttb(target)
|
|
|
|
def sense_ttf(self, target):
|
|
"""Activate the RF field and probe for a Type F Target. Bitrates 212
|
|
and 424 kpbs are supported.
|
|
|
|
"""
|
|
return super(Device, self).sense_ttf(target)
|
|
|
|
def sense_dep(self, target):
|
|
"""Search for a DEP Target. Both passive and passive communication
|
|
mode are supported.
|
|
|
|
"""
|
|
return super(Device, self).sense_dep(target)
|
|
|
|
def listen_tta(self, target, timeout):
|
|
"""Listen as Type A Target is not supported."""
|
|
info = "{device} does not support listen as Type A Target"
|
|
raise nfc.clf.UnsupportedTargetError(info.format(device=self))
|
|
|
|
def listen_ttb(self, target, timeout):
|
|
"""Listen as Type B Target is not supported."""
|
|
info = "{device} does not support listen as Type B Target"
|
|
raise nfc.clf.UnsupportedTargetError(info.format(device=self))
|
|
|
|
def listen_ttf(self, target, timeout):
|
|
"""Listen as Type F Target is not supported."""
|
|
info = "{device} does not support listen as Type F Target"
|
|
raise nfc.clf.UnsupportedTargetError(info.format(device=self))
|
|
|
|
def listen_dep(self, target, timeout):
|
|
"""Listen as DEP Target is not supported."""
|
|
info = "{device} does not support listen as DEP Target"
|
|
raise nfc.clf.UnsupportedTargetError(info.format(device=self))
|
|
|
|
def turn_on_led_and_buzzer(self):
|
|
"""Buzz and turn red."""
|
|
self.chipset.set_buzzer_and_led_to_active()
|
|
|
|
def turn_off_led_and_buzzer(self):
|
|
"""Back to green."""
|
|
self.chipset.set_buzzer_and_led_to_default()
|
|
|
|
|
|
class Chipset(pn532.Chipset):
|
|
# Maximum size of a host command frame to the contactless chip.
|
|
host_command_frame_max_size = 254
|
|
|
|
# Supported BrTy (baud rate / modulation type) values for the
|
|
# InListPassiveTarget command. Corresponds to 106 kbps Type A, 212
|
|
# kbps Type F, 424 kbps Type F, and 106 kbps Type B. The value for
|
|
# 106 kbps Innovision Jewel Tag (although supported by PN532) is
|
|
# removed because the RID command can not be send.
|
|
in_list_passive_target_brty_range = (0, 1, 2, 3)
|
|
|
|
def __init__(self, transport):
|
|
self.transport = transport
|
|
|
|
# read ACR122U firmware version string
|
|
reader_version = self.ccid_xfr_block(bytearray.fromhex("FF00480000"))
|
|
if not reader_version.startswith(b"ACR122U"):
|
|
log.error("failed to retrieve ACR122U version string")
|
|
raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
|
|
|
|
if int(chr(reader_version[7])) < 2:
|
|
log.error("{0} not supported, need 2.x".format(reader_version[7:]))
|
|
raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
|
|
|
|
log.debug("initialize " + reader_version.decode())
|
|
|
|
# set icc power on
|
|
log.debug("CCID ICC-POWER-ON")
|
|
frame = bytearray.fromhex("62000000000000000000")
|
|
transport.write(frame)
|
|
transport.read(100)
|
|
|
|
# disable autodetection
|
|
log.debug("Set PICC Operating Parameters")
|
|
self.ccid_xfr_block(bytearray.fromhex("FF00517F00"))
|
|
|
|
# switch red/green led off/on
|
|
log.debug("Configure Buzzer and LED")
|
|
self.set_buzzer_and_led_to_default()
|
|
|
|
super(Chipset, self).__init__(transport, logger=log)
|
|
|
|
def close(self):
|
|
self.ccid_xfr_block(bytearray.fromhex("FF00400C0400000000"))
|
|
self.transport.close()
|
|
self.transport = None
|
|
|
|
def set_buzzer_and_led_to_default(self):
|
|
"""Turn off buzzer and set LED to default (green only). """
|
|
self.ccid_xfr_block(bytearray.fromhex("FF00400E0400000000"))
|
|
|
|
def set_buzzer_and_led_to_active(self, duration_in_ms=300):
|
|
"""Turn on buzzer and set LED to red only. The timeout here must exceed
|
|
the total buzzer/flash duration defined in bytes 5-8. """
|
|
duration_in_tenths_of_second = int(min(duration_in_ms / 100, 255))
|
|
timeout_in_seconds = (duration_in_tenths_of_second + 1) / 10.0
|
|
data = "FF00400D04{:02X}000101".format(duration_in_tenths_of_second)
|
|
self.ccid_xfr_block(bytearray.fromhex(data),
|
|
timeout=timeout_in_seconds)
|
|
|
|
def send_ack(self):
|
|
# Send an ACK frame, usually to terminate most recent command.
|
|
self.ccid_xfr_block(Chipset.ACK)
|
|
|
|
def ccid_xfr_block(self, data, timeout=0.1):
|
|
"""Encapsulate host command *data* into an PC/SC Escape command to
|
|
send to the device and extract the chip response if received
|
|
within *timeout* seconds.
|
|
|
|
"""
|
|
frame = struct.pack("<BI5B", 0x6F, len(data), 0, 0, 0, 0, 0) + data
|
|
self.transport.write(bytearray(frame))
|
|
frame = self.transport.read(int(timeout * 1000))
|
|
if not frame or len(frame) < 10:
|
|
log.error("insufficient data for decoding ccid response")
|
|
raise IOError(errno.EIO, os.strerror(errno.EIO))
|
|
if frame[0] != 0x80:
|
|
log.error("expected a RDR_to_PC_DataBlock")
|
|
raise IOError(errno.EIO, os.strerror(errno.EIO))
|
|
if len(frame) != 10 + struct.unpack("<I", memoryview(frame)[1:5])[0]:
|
|
log.error("RDR_to_PC_DataBlock length mismatch")
|
|
raise IOError(errno.EIO, os.strerror(errno.EIO))
|
|
return frame[10:]
|
|
|
|
def command(self, cmd_code, cmd_data, timeout):
|
|
"""Send a host command and return the chip response.
|
|
|
|
"""
|
|
log.log(logging.DEBUG-1, "{} {}".format(self.CMD[cmd_code],
|
|
hexlify(cmd_data).decode()))
|
|
|
|
frame = bytearray([0xD4, cmd_code]) + bytearray(cmd_data)
|
|
frame = bytearray([0xFF, 0x00, 0x00, 0x00, len(frame)]) + frame
|
|
|
|
frame = self.ccid_xfr_block(frame, timeout)
|
|
if not frame or len(frame) < 4:
|
|
log.error("insufficient data for decoding chip response")
|
|
raise IOError(errno.EIO, os.strerror(errno.EIO))
|
|
if not (frame[0] == 0xD5 and frame[1] == cmd_code + 1):
|
|
log.error("received invalid chip response")
|
|
raise IOError(errno.EIO, os.strerror(errno.EIO))
|
|
if not (frame[-2] == 0x90 and frame[-1] == 0x00):
|
|
log.error("received pseudo apdu with error status")
|
|
raise IOError(errno.EIO, os.strerror(errno.EIO))
|
|
return frame[2:-2]
|