1from dataclasses import dataclass
2from typing import Optional
3from abc import ABCMeta, abstractmethod
4
5
6@dataclass
7class TransceiveConfiguration:
8    """Defines settings used for NFC communication"""
9    type: str
10    crc: int = True
11    bits: int = 8
12    bitrate: int = 106
13    timeout: float = None
14    # Output power as a percentage of maximum supported by the reader
15    power: float = 100
16
17    def replace(self, **kwargs):
18        """Return a new instance with specific values replaced by name."""
19        return self.__class__(**{
20            "type": self.type,
21            "crc": self.crc,
22            "bits": self.bits,
23            "bitrate": self.bitrate,
24            "timeout": self.timeout,
25            "power": self.power,
26            **kwargs
27        })
28
29
30CARRIER = 13.56e6
31A_TIMEOUT = (1236 + 384) / CARRIER
32CONFIGURATION_A_LONG = TransceiveConfiguration(
33    type="A", crc=True, bits=8, timeout=A_TIMEOUT
34)
35
36
37class ReaderTag(metaclass=ABCMeta):
38    """Describes a generic target which implements ISODEP protocol"""
39
40    @abstractmethod
41    def transact(self, command_apdus, response_apdus):
42        """Sends command_apdus and verifies reception of matching response_apdus
43        """
44
45
46class Reader(metaclass=ABCMeta):
47    """Describes a generic NFC reader which can be used for running tests"""
48
49    @abstractmethod
50    def poll_a(self) -> Optional[ReaderTag]:
51        """Attempts to perform target discovery by issuing Type A WUP/REQ
52        and performing anticollision in case one is detected.
53        Returns a tag object if one was found, None otherwise
54        """
55
56    @abstractmethod
57    def poll_b(self) -> Optional[ReaderTag]:
58        """Attempts to perform target discovery by issuing Type B WUP/REQ
59        and performing anticollision in case one is detected.
60        Returns a tag object if one was found, None otherwise
61        """
62
63    @abstractmethod
64    def send_broadcast(
65        self,
66        data: bytes, *,
67        configuration: TransceiveConfiguration = CONFIGURATION_A_LONG
68    ):
69        """Broadcasts a custom data frame into the RF field.
70        Does not require an active target to be detected to do that.
71        By default, uses 'Long A' frame configuration, which can be overridden.
72        """
73
74    @abstractmethod
75    def mute(self):
76        """Disables the RF field generated by the reader"""
77
78    @abstractmethod
79    def unmute(self):
80        """Enables the RF field generated by the reader"""
81
82    @abstractmethod
83    def reset(self):
84        """Auxiliary function to reset reader to starting conditions"""
85
86    def reset_buffers(self):
87        """Forwards a call to .reset() for compatibility reasons"""
88        self.reset()
89