diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml new file mode 100644 index 0000000..66da492 --- /dev/null +++ b/.github/workflows/precommit.yml @@ -0,0 +1,19 @@ +name: pre-commit + +on: + push: + branches: ["**"] + pull_request: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.8" + cache: "pip" + - run: pip install -r crispy-api/requirements-dev.txt + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..78ffd9e --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,41 @@ +name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.8" + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + - name: Publish distribution 📦 to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ + - name: Publish distribution 📦 to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..78b9a9f --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,27 @@ +name: Pytest + +on: + push: + branches: ["**"] + pull_request: + branches: [master] + +defaults: + run: + working-directory: src + +jobs: + pytest: + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.8", "3.9", "3.10", "3.11"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + - run: pip install -r requirements-dev.txt + - run: pytest diff --git a/.gitignore b/.gitignore index 1a98112..8be5b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -py-acr122u/ - # Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files @@ -150,4 +148,4 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties -fabric.properties \ No newline at end of file +fabric.properties diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..76d2627 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.3.0 + hooks: + - id: end-of-file-fixer + stages: [commit] + - repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black + require_serial: true + name: Coding style (Black) + language_version: python3.8 + stages: [commit] + exclude: ^legacy-backend.*$ + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + require_serial: true + name: isort + args: ["--profile", "black"] + stages: [commit] + exclude: ^legacy-backend.*$ diff --git a/README.md b/README.md index 72c6d0d..03d77e9 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ # PY-ACR122U - +[![PyPI - Version](https://img.shields.io/pypi/v/py122u)](https://pypi.org/project/py122u/) +[![PyPI - License](https://img.shields.io/pypi/l/py122u)](https://pypi.org/project/py122u/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/py122u)](https://pypi.org/project/py122u/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/py122u)](https://pypi.org/project/py122u/) +[![PyPI - Wheel](https://img.shields.io/pypi/wheel/py122u)](https://pypi.org/project/py122u/) This is a python library for the ACR122U NFC reader ## Installation -- git clone https://github.com/Flowtter/py-acr122u.git -- cd py-acr122u -- pip install -r requirements.txt + +```shell +pip install py122u +``` ## Usage + ```python -from src import nfc + +from py122u import nfc reader = nfc.Reader() +reader.connect() reader.print_data(reader.get_uid()) reader.info() ``` -- python main.py diff --git a/Example/write_and_read.py b/example/write_and_read.py similarity index 94% rename from Example/write_and_read.py rename to example/write_and_read.py index 4c89944..f66b456 100644 --- a/Example/write_and_read.py +++ b/example/write_and_read.py @@ -1,6 +1,7 @@ -from src import nfc +from py122u import nfc reader = nfc.Reader() +reader.connect() reader.load_authentication_data(0x01, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) reader.authentication(0x00, 0x61, 0x01) @@ -29,5 +30,6 @@ def read(r, position, number): def read_16(r, position, number): return r.read_binary_blocks(position, number) + write(reader, 0x01, 0x20, [0x00 for i in range(16)]) print(read(reader, 0x01, 0x20)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3f862d3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "py122u" +version = "2.3.1" +description = "This is a python library for the ACR122U NFC reader" +readme = "README.md" +authors = [{ name = "Brice PARENT", email = "briceparent.it@gmail.com"}, { name = "Robert van Dijk", email = "contact@robertvandijk.nl" }] +license = { file = "LICENSE" } +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", +] +keywords = ["nfc", "acr", "acr122u"] +dependencies = [ + "pyscard>=2.0.7", +] +requires-python = ">=3.8" + +[project.optional-dependencies] +dev = ["black", "bumpver", "isort", "pip-tools", "pytest", "pytest-mock"] + +[project.urls] +Homepage = "https://github.com/Flowtter/py-acr122u" + +[tool.bumpver] +current_version = "2.3.1" +version_pattern = "MAJOR.MINOR.PATCH" +commit_message = "Bump version {old_version} -> {new_version}" +commit = true +tag = true +push = false + +[tool.bumpver.file_patterns] +"pyproject.toml" = [ + 'current_version = "{version}"', + 'version = "{version}"', +] +"src/py122u/__init__.py" = ["{version}"] + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "tests", + "integration", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..61eb233 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +testpaths = tests +python_files = *.py +addopts = -vv --showlocals diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..6d0a45d --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt +black +isort +pytest +pytest-mock diff --git a/requirements.txt b/requirements.txt index b67b53d..5635940 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pyscard \ No newline at end of file +pyscard diff --git a/src/main.py b/src/main.py deleted file mode 100644 index cacfa90..0000000 --- a/src/main.py +++ /dev/null @@ -1,5 +0,0 @@ -from src import nfc - -reader = nfc.Reader() -reader.print_data(reader.get_uid()) -reader.info() diff --git a/src/py122u/__init__.py b/src/py122u/__init__.py new file mode 100644 index 0000000..3a5935a --- /dev/null +++ b/src/py122u/__init__.py @@ -0,0 +1 @@ +__version__ = "2.3.1" diff --git a/src/error.py b/src/py122u/error.py similarity index 81% rename from src/error.py rename to src/py122u/error.py index c2d4694..2e7a8d6 100644 --- a/src/error.py +++ b/src/py122u/error.py @@ -1,5 +1,6 @@ class Error(Exception): """Base class for exceptions in this module.""" + pass @@ -45,3 +46,14 @@ class InstructionFailed(Error): def __init__(self, message): self.message = message + + +class BitOutOfRange(Error): + """Exception raised when you try to set a bit that does not exsist + + Attributes: + message -- explanation of the error + """ + + def __init__(self, message): + self.message = message diff --git a/src/nfc.py b/src/py122u/nfc.py similarity index 56% rename from src/nfc.py rename to src/py122u/nfc.py index 344bf8f..0ed36e3 100644 --- a/src/nfc.py +++ b/src/py122u/nfc.py @@ -1,8 +1,14 @@ +import logging +from typing import List + import smartcard.System -from smartcard.util import toHexString from smartcard.ATR import ATR +from smartcard.CardConnection import CardConnection +from smartcard.util import toHexString + +from . import error, option, utils -from src import utils, option, error +logger = logging.getLogger(__name__) class Reader: @@ -10,25 +16,35 @@ def __init__(self): """create an ACR122U object doc available here: http://downloads.acs.com.hk/drivers/en/API-ACR122U-2.02.pdf""" self.reader_name, self.connection = self.instantiate_reader() + self.pn532 = self._PN532(self) @staticmethod def instantiate_reader(): readers = smartcard.System.readers() + logger.debug(f"Available readers: {readers}") + if len(readers) == 0: raise error.NoReader("No readers available") reader = readers[0] c = reader.createConnection() + logger.info(f"Using reader {reader}") + + return reader, c + + def connect(self): + """connect to the card + only works if a card is on the reader""" try: - c.connect() + self.connection.connect() + logger.debug("Reader connected") except: raise error.NoCommunication( "The reader has been deleted and no communication is now possible. Smartcard error code : 0x7FEFFF97" - "\nHint: try to connect a card to the reader") - - return reader, c + "\nHint: try to connect a card to the reader" + ) def command(self, mode, arguments=None): """send a payload to the reader @@ -52,10 +68,13 @@ def command(self, mode, arguments=None): payload = option.options.get(mode) if not payload: - raise error.OptionOutOfRange("Option do not exist\nHint: try to call help(nfc.Reader().command) to see all options") + raise error.OptionOutOfRange( + "Option do not exist\nHint: try to call help(nfc.Reader().command) to see all options" + ) payload = utils.replace_arguments(payload, arguments) - result = self.connection.transmit(payload) + logger.debug(f"Transmitting {payload}") + result = self.connection.transmit(payload, protocol=CardConnection.T1_protocol) if len(result) == 3: data, sw1, sw2 = result @@ -65,7 +84,8 @@ def command(self, mode, arguments=None): if [sw1, sw2] == option.answers.get("fail"): raise error.InstructionFailed(f"Instruction {mode} failed") - print(f"success: {mode}") + logger.debug(f"Success: {mode}, result: {result}") + if data: return data @@ -77,6 +97,7 @@ def custom(self, payload): Format: CLA INS P1 P2 P3 Lc Data Le""" + logger.debug(f"Transmitting {payload}") result = self.connection.transmit(payload) if len(result) == 3: @@ -87,6 +108,8 @@ def custom(self, payload): if [sw1, sw2] == option.answers.get("fail"): raise error.InstructionFailed(f"Payload {payload} failed") + logger.debug(f"Success transmitting payload: {payload}") + def get_uid(self): """get the uid of the card""" return self.command("get_uid") @@ -127,7 +150,9 @@ def read_binary_blocks(self, block_number, number_of_byte_to_read): Example: E.g. 0x00, 0x02""" - return self.command("read_binary_blocks", [block_number, number_of_byte_to_read]) + return self.command( + "read_binary_blocks", [block_number, number_of_byte_to_read] + ) def update_binary_blocks(self, block_number, number_of_byte_to_update, block_data): """update n bytes in the card with block_data at the block_number index @@ -140,7 +165,8 @@ def update_binary_blocks(self, block_number, number_of_byte_to_update, block_dat Examples: 0x01, 0x10, [0x00, 0x01, 0x02, 0x03, 0x04, 0x05 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15]""" - self.command("update_binary_blocks",[block_number, number_of_byte_to_update, block_data] + self.command( + "update_binary_blocks", [block_number, number_of_byte_to_update, block_data] ) def create_value_block(self, block_number, value): @@ -198,8 +224,7 @@ def restore_value_block(self, source_block_number, target_block_number): Example: 0x01, 0x02""" - self.command("restore_value_block", [ - source_block_number, target_block_number]) + self.command("restore_value_block", [source_block_number, target_block_number]) def led_control(self, led_state, t1, t2, number_of_repetition, link_to_buzzer): """control led state @@ -213,7 +238,9 @@ def led_control(self, led_state, t1, t2, number_of_repetition, link_to_buzzer): Example: 0x05, 0x01, 0x01, 0x01, 0x01""" - self.command("led_control", [led_state, t1, t2, number_of_repetition, link_to_buzzer]) + self.command( + "led_control", [led_state, t1, t2, number_of_repetition, link_to_buzzer] + ) def get_picc_version(self): """get the PICC version of the reader""" @@ -249,6 +276,60 @@ def set_timeout(self, timeout_parameter): 0x01""" self.command("set_timeout", [timeout_parameter]) + def direct_transmit(self, payload: List[int]): + """send the payload to the tag or reader. + using this you can send messages directly to the PN532 chip + doc available here: https://www.nxp.com/docs/en/user-guide/141520.pdf + + Attributes: + payload: the payload to send to the PN532 chip + + Example: + [0xd4, 0x60, 0xFF, 0x02, 0x10] + """ + return self.command("direct_transmit", [len(payload), payload]) + + def set_auto_polling(self, enabled: bool): + """enable or disable Auto PICC Polling + + Attributes: + enabled: True to enable, False to disable + + """ + self.set_picc_bit(7, enabled) + + def set_picc_bit(self, bit: int, value: bool): + """set a PICC bit to update the PICC operating parameter as described in section 6.5 + of API-ACR122U-2.02.pdf + + Attributes: + bit: the bit to set + value: True for 1, False for 0 + """ + if bit < 0 or bit > 7: + raise error.BitOutOfRange( + f"Bit {bit} is not in the picc operating parameter" + ) + + picc = self.get_picc_version()[1] + if value: + picc |= 1 << bit + else: + picc &= ~(1 << bit) + self.set_picc_version(picc) + + def mute_buzzer(self): + """mute the buzzer for when a card is scanned""" + self.buzzer_sound(0x00) + + def unmute_buzzer(self): + """unmute the buzzer for when a card is scanned""" + self.buzzer_sound(0xFF) + + def reset_lights(self): + """turn the red and green LED off""" + self.led_control(0b00001100, 0x00, 0x00, 0x00, 0x00) + def info(self): """print the type of the card on the reader""" atr = ATR(self.connection.getATR()) @@ -257,15 +338,88 @@ def info(self): print(historical_byte[-17:-12]) card_name = historical_byte[-17:-12] name = option.cards.get(card_name, "") - print(f"Card Name: {name}\n\tT0 {atr.isT0Supported()}\n\tT1 {atr.isT1Supported()}\n\tT1 {atr.isT15Supported()}") + print( + f"Card Name: {name}\n\tT0 {atr.isT0Supported()}\n\tT1 {atr.isT1Supported()}\n\tT1 {atr.isT15Supported()}" + ) @staticmethod def print_data(data): - print(f"data:\n\t{data}" - f"\n\t{utils.int_list_to_hexadecimal_list(data)}" - f"\n\t{utils.int_list_to_string_list(data)}") + print( + f"data:\n\t{data}" + f"\n\t{utils.int_list_to_hexadecimal_list(data)}" + f"\n\t{utils.int_list_to_string_list(data)}" + ) @staticmethod def print_sw1_sw2(sw1, sw2): - print(f"sw1 : {sw1} {hex(sw1)}\n" - f"sw2 : {sw2} {hex(sw2)}") + print(f"sw1 : {sw1} {hex(sw1)}\n" f"sw2 : {sw2} {hex(sw2)}") + + class _PN532: + """the PN532 chip inside the ACR122U + Methods in the class can be used to communicate with the chip + see docs at: https://www.nxp.com/docs/en/user-guide/141520.pdf + """ + + def __init__(self, acr122u): + """create a PN532 object + + Attributes: + acr122u: the reader used to communicate with the chip (i.e. the reader the chip is in) + """ + self.acr122u: Reader = acr122u + + def transmit(self, payload: List[int]): + """send a payload to the chip + + Attributes: + payload: the payload to send + + Returns: + the response from the chip + """ + logger.debug(f"Transmitting payload {payload} to PN532") + return self.acr122u.direct_transmit(payload) + + def command(self, mode, arguments=None): + """send a command to the chip + + Attributes: + mode: key value of option.pn532_options + arguments: replace `-1` in the payload by arguments + + Returns: + the response from the chip + """ + payload = option.pn532_options.get(mode) + + if not payload: + raise error.OptionOutOfRange( + "Option do not exist\nHint: try to call help(nfc.Reader().command) to see all options" + ) + + payload = utils.replace_arguments(payload, arguments) + result = self.transmit(payload) + + return result + + def in_auto_poll(self, poll_nr: int, period: int, type1: int, *types): + """ + this command is used to poll card(s) / target(s) of specified Type present in the RF field. + docs: https://www.nxp.com/docs/en/user-guide/141520.pdf section 7.3.13 + + Attributes: + poll_nr - specifies the number of polling (one polling is a polling for each Type j) + 0x01: 0xFE:1 up to 254 polling + 0xFF: Endless polling + period - (0x01 – 0x0F) indicates the polling period in units of 150 ms + type1 - indicates the mandatory target type to be polled at the 1st time + types - indicate the optional target types to be polled at the 2nd up to the Nth time (N ≤ 15). + + + Returns: + the response from the chip + """ + arguments = [poll_nr, period, type1] + list(types) + + data = self.command("in_auto_poll", arguments) + return data diff --git a/src/option.py b/src/py122u/option.py similarity index 73% rename from src/option.py rename to src/py122u/option.py index e5abfbb..811b7e7 100644 --- a/src/option.py +++ b/src/py122u/option.py @@ -10,11 +10,12 @@ "decrement_value_block": [0xFF, 0xD7, 0x00, -1, 0x05, 0x02, -1], "read_value_block": [0xFF, 0xB1, 0x00, -1, 0x04], "restore_value_block": [0xFF, 0xD7, 0x00, -1, 0x02, 0x03, -1], - "led-control": [0xFF, 0x00, 0x40, -1, -0x04, -1, -1, -1, -1], + "led_control": [0xFF, 0x00, 0x40, -1, -0x04, -1, -1, -1, -1], "get_picc_version": [0xFF, 0x00, 0x50, 0x00, 0x00], "set_picc_version": [0xFF, 0x00, 0x51, -1, 0x00], "buzzer_sound": [0xFF, 0x00, 0x52, -1, 0x00], "set_timeout": [0xFF, 0x00, 0x41, -1, 0x00], + "direct_transmit": [0xFF, 0x00, 0x00, 0x00, -1, -1], } alias = { "gu": "get_uid", @@ -23,16 +24,13 @@ "auth": "authentication", "rbb": "read_binary_blocks", "ubb": "update_binary_blocks", - "ld": "led-control", + "ld": "led_control", "gpv": "get_picc_version", "spv": "set_picc_version", "b": "buzzer_sound_mute", "st": "set_timeout", } -answers = { - "success": [0x90, 0x0], - "fail": [0x63, 0x0] -} +answers = {"success": [0x90, 0x0], "fail": [0x63, 0x0]} cards = { "00 01": "MIFARE Classic 1K", "00 02": "MIFARE Classic 4K", @@ -40,5 +38,28 @@ "00 26": "MIFARE Mini", "F0 04": "Topaz and Jewel", "F0 11": "FeliCa 212K", - "F0 12": "FeliCa 424K" + "F0 12": "FeliCa 424K", +} +pn532_options = { + "in_auto_poll": [ + 0xD4, + 0x60, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], } diff --git a/src/utils.py b/src/py122u/utils.py similarity index 93% rename from src/utils.py rename to src/py122u/utils.py index ebb3206..ccbcd12 100644 --- a/src/utils.py +++ b/src/py122u/utils.py @@ -3,7 +3,7 @@ def int_list_to_hexadecimal_list(data): def int_list_to_string_list(data): - return ''.join([chr(e) for e in data]) + return "".join([chr(e) for e in data]) def replace_arguments(data, arguments): diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/in_auto_poll_test.py b/tests/in_auto_poll_test.py new file mode 100644 index 0000000..dcb210d --- /dev/null +++ b/tests/in_auto_poll_test.py @@ -0,0 +1,66 @@ +import pytest + +from src.py122u.nfc import Reader + + +@pytest.fixture +def reader(mocker) -> Reader: + mocker.patch("src.py122u.nfc.Reader.instantiate_reader", return_value=(None, None)) + r = Reader() + return r + + +@pytest.mark.parametrize( + "transmitted, poll_nr, period, type1", + [ + ([0xD4, 0x60, 0xFF, 0x01, 0x10], 0xFF, 0x01, 0x10), + ([0xD4, 0x60, 0x01, 0x02, 0x40], 0x01, 0x02, 0x40), + ([0xD4, 0x60, 0xFE, 0x0F, 0x50], 0xFE, 0x0F, 0x50), + ], +) +def test_in_aut_poll_commands(transmitted, poll_nr, period, type1, reader, mocker): + mock_transmit = mocker.patch("src.py122u.nfc.Reader._PN532.transmit") + + reader.pn532.in_auto_poll( + poll_nr, + period, + type1, + ) + + mock_transmit.assert_called_with(transmitted) + + +@pytest.mark.parametrize( + "transmitted, poll_nr, period, type1, type2", + [ + ([0xD4, 0x60, 0xFF, 0x01, 0x10, 0x20], 0xFF, 0x01, 0x10, 0x20), + ([0xD4, 0x60, 0x01, 0x02, 0x40, 0x20], 0x01, 0x02, 0x40, 0x20), + ([0xD4, 0x60, 0xFE, 0x0F, 0x50, 0x20], 0xFE, 0x0F, 0x50, 0x20), + ], +) +def test_in_aut_poll_commands_more_types( + transmitted, poll_nr, period, type1, type2, reader, mocker +): + mock_transmit = mocker.patch("src.py122u.nfc.Reader._PN532.transmit") + + reader.pn532.in_auto_poll(poll_nr, period, type1, type2) + + mock_transmit.assert_called_with(transmitted) + + +@pytest.mark.parametrize( + "transmitted, poll_nr, period, type1, type2, type3", + [ + ([0xD4, 0x60, 0xFF, 0x01, 0x10, 0x20, 0x15], 0xFF, 0x01, 0x10, 0x20, 0x15), + ([0xD4, 0x60, 0x01, 0x02, 0x40, 0x20, 0x15], 0x01, 0x02, 0x40, 0x20, 0x15), + ([0xD4, 0x60, 0xFE, 0x0F, 0x50, 0x20, 0x15], 0xFE, 0x0F, 0x50, 0x20, 0x15), + ], +) +def test_in_aut_poll_commands_even_more_types( + transmitted, poll_nr, period, type1, type2, type3, reader, mocker +): + mock_transmit = mocker.patch("src.py122u.nfc.Reader._PN532.transmit") + + reader.pn532.in_auto_poll(poll_nr, period, type1, type2, type3) + + mock_transmit.assert_called_with(transmitted) diff --git a/tests/set_picc_bit_test.py b/tests/set_picc_bit_test.py new file mode 100644 index 0000000..84fadd4 --- /dev/null +++ b/tests/set_picc_bit_test.py @@ -0,0 +1,52 @@ +import pytest + +from src.py122u import error +from src.py122u.nfc import Reader + + +@pytest.mark.parametrize("bit", [0, 1, 2, 3, 4, 5, 6, 7]) +def test_picc_bit_range_pass(bit, mocker): + mocker.patch("src.py122u.nfc.Reader.instantiate_reader", return_value=(None, None)) + mocker.patch("src.py122u.nfc.Reader.get_picc_version", return_value=(144, 255)) + mocker.patch("src.py122u.nfc.Reader.set_picc_version") + r = Reader() + + r.set_picc_bit(bit, False) + r.set_picc_bit(bit, True) + + +@pytest.mark.parametrize("bit", [-2, -1, 8, 9, 10]) +def test_picc_bit_range_fail(bit, mocker): + mocker.patch("src.py122u.nfc.Reader.instantiate_reader", return_value=(None, None)) + mocker.patch("src.py122u.nfc.Reader.get_picc_version", return_value=(144, 255)) + mocker.patch("src.py122u.nfc.Reader.set_picc_version") + r = Reader() + + with pytest.raises(error.BitOutOfRange): + r.set_picc_bit(bit, False) + with pytest.raises(error.BitOutOfRange): + r.set_picc_bit(bit, True) + + +@pytest.mark.parametrize( + "old_picc, bit, value, new_picc", + [ + (0b00000000, 0, True, 0b00000001), + (0b00000000, 0, False, 0b00000000), + (0b00000000, 1, True, 0b00000010), + (0b00000000, 1, False, 0b00000000), + (0b11111111, 0, True, 0b11111111), + (0b11111111, 0, False, 0b11111110), + (0b11111111, 1, True, 0b11111111), + (0b11111111, 1, False, 0b11111101), + ], +) +def test_picc_bit_set_bit(old_picc, bit, value, new_picc, mocker): + mocker.patch("src.py122u.nfc.Reader.instantiate_reader", return_value=(None, None)) + mocker.patch("src.py122u.nfc.Reader.get_picc_version", return_value=(144, old_picc)) + mock_set_picc = mocker.patch("src.py122u.nfc.Reader.set_picc_version") + r = Reader() + + r.set_picc_bit(bit, value) + + mock_set_picc.assert_called_with(new_picc)