nfcscript 0.0.1__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nfcscript-0.0.1 → nfcscript-0.0.6}/PKG-INFO +3 -3
- {nfcscript-0.0.1 → nfcscript-0.0.6}/pyproject.toml +9 -7
- nfcscript-0.0.6/src/nfc/__init__.py +6 -0
- nfcscript-0.0.6/src/nfc/assertions.py +68 -0
- {nfcscript-0.0.1/src/nfcscript → nfcscript-0.0.6/src/nfc}/bits.py +6 -4
- nfcscript-0.0.6/src/nfc/checksum.py +10 -0
- nfcscript-0.0.6/src/nfc/cli.py +27 -0
- nfcscript-0.0.6/src/nfc/delay.py +4 -0
- nfcscript-0.0.6/src/nfc/hex_util.py +99 -0
- nfcscript-0.0.6/src/nfc/reader.py +197 -0
- {nfcscript-0.0.1 → nfcscript-0.0.6}/src/nfcscript.egg-info/PKG-INFO +3 -3
- {nfcscript-0.0.1 → nfcscript-0.0.6}/src/nfcscript.egg-info/SOURCES.txt +8 -4
- nfcscript-0.0.6/src/nfcscript.egg-info/entry_points.txt +2 -0
- nfcscript-0.0.6/src/nfcscript.egg-info/requires.txt +1 -0
- nfcscript-0.0.6/src/nfcscript.egg-info/top_level.txt +1 -0
- nfcscript-0.0.1/src/nfcscript/__init__.py +0 -0
- nfcscript-0.0.1/src/nfcscript/assertions.py +0 -37
- nfcscript-0.0.1/src/nfcscript/main.py +0 -5
- nfcscript-0.0.1/src/nfcscript.egg-info/entry_points.txt +0 -2
- nfcscript-0.0.1/src/nfcscript.egg-info/requires.txt +0 -1
- nfcscript-0.0.1/src/nfcscript.egg-info/top_level.txt +0 -1
- {nfcscript-0.0.1 → nfcscript-0.0.6}/README.md +0 -0
- {nfcscript-0.0.1 → nfcscript-0.0.6}/setup.cfg +0 -0
- {nfcscript-0.0.1 → nfcscript-0.0.6}/src/nfcscript.egg-info/dependency_links.txt +0 -0
- {nfcscript-0.0.1 → nfcscript-0.0.6}/tests/test_main.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nfcscript
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: NFC
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: A plugin-based core engine for DSL-style NFC testing automation.
|
|
5
5
|
Author: crthu
|
|
6
6
|
Requires-Python: >=3.14
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist:
|
|
8
|
+
Requires-Dist: nfctester
|
|
9
9
|
|
|
10
10
|
# nfcscript
|
|
11
11
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nfcscript"
|
|
3
|
-
version = "0.0.
|
|
4
|
-
description = "NFC
|
|
3
|
+
version = "0.0.6"
|
|
4
|
+
description = "A plugin-based core engine for DSL-style NFC testing automation."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
7
7
|
authors = [{name = "crthu"}]
|
|
8
8
|
dependencies = [
|
|
9
|
-
"
|
|
9
|
+
"nfctester",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[tool.setuptools.packages.find]
|
|
13
13
|
where = ["src"]
|
|
14
14
|
|
|
15
15
|
[project.scripts]
|
|
16
|
-
nfcscript = "
|
|
16
|
+
nfcscript = "nfc.cli:main"
|
|
17
17
|
|
|
18
18
|
[tool.pytest.ini_options]
|
|
19
19
|
pythonpath = ["src"]
|
|
@@ -29,12 +29,14 @@ dev = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[tool.bumpversion]
|
|
32
|
-
current_version = "0.0.
|
|
32
|
+
current_version = "0.0.6"
|
|
33
|
+
commit = true
|
|
34
|
+
tag = true
|
|
33
35
|
allow_dirty = true
|
|
36
|
+
tag_name = "v{new_version}"
|
|
37
|
+
message = "chore(release): bump version {current_version} → {new_version}"
|
|
34
38
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
35
39
|
serialize = ["{major}.{minor}.{patch}"]
|
|
36
|
-
search = 'version = "{current_version}"'
|
|
37
|
-
replace = 'version = "{new_version}"'
|
|
38
40
|
|
|
39
41
|
[[tool.bumpversion.files]]
|
|
40
42
|
filename = "pyproject.toml"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Any, TypeGuard, TypeVar
|
|
3
|
+
|
|
4
|
+
from nfc.hex_util import FORMAT_HEX
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
def _get_stack_trace() -> str:
|
|
9
|
+
"""获取格式化的调用堆栈"""
|
|
10
|
+
# inspect.stack()[0] 是 _get_stack_trace
|
|
11
|
+
# inspect.stack()[1] 是 ASSERT_... 函数
|
|
12
|
+
# inspect.stack()[2:] 是实际的业务调用代码
|
|
13
|
+
stack = inspect.stack()[2:]
|
|
14
|
+
chain = []
|
|
15
|
+
for frame in stack:
|
|
16
|
+
# 简化路径,仅显示文件名
|
|
17
|
+
filename = frame.filename.split('\\')[-1].split('/')[-1]
|
|
18
|
+
chain.append(f" at {filename}:{frame.lineno} in {frame.function}")
|
|
19
|
+
return "\n".join(chain)
|
|
20
|
+
|
|
21
|
+
def ASSERT_LEN(data: Any, expected_len: int, msg: str | None = None) -> None:
|
|
22
|
+
actual_len = len(data)
|
|
23
|
+
if actual_len != expected_len:
|
|
24
|
+
error_msg = (
|
|
25
|
+
f"\nAssertion Failed: {msg}\n" if msg else ""
|
|
26
|
+
) + (
|
|
27
|
+
f"Assertion Failed: Length mismatch\n"
|
|
28
|
+
f" Expected: {expected_len}\n"
|
|
29
|
+
f" Actual : {actual_len}\n"
|
|
30
|
+
f" Data : {data}\n"
|
|
31
|
+
f"Stack Trace:\n{_get_stack_trace()}"
|
|
32
|
+
)
|
|
33
|
+
raise AssertionError(error_msg)
|
|
34
|
+
|
|
35
|
+
def ASSERT_EQUAL(expected: Any, actual: Any, msg: str | None = None) -> None:
|
|
36
|
+
if actual != expected:
|
|
37
|
+
error_msg = (
|
|
38
|
+
f"\nAssertion Failed: {msg}\n" if msg else ""
|
|
39
|
+
) + (
|
|
40
|
+
f"Assertion Failed: Value mismatch\n"
|
|
41
|
+
f" Expected: {FORMAT_HEX(expected)} ({type(expected).__name__})\n"
|
|
42
|
+
f" Actual : {FORMAT_HEX(actual)} ({type(actual).__name__})\n"
|
|
43
|
+
f"Stack Trace:\n{_get_stack_trace()}"
|
|
44
|
+
)
|
|
45
|
+
raise AssertionError(error_msg)
|
|
46
|
+
|
|
47
|
+
def ASSERT_IS_NONE(value: Any, msg: str | None = None) -> None:
|
|
48
|
+
if value is not None:
|
|
49
|
+
error_msg = (
|
|
50
|
+
f"\nAssertion Failed: {msg}\n" if msg else ""
|
|
51
|
+
) + (
|
|
52
|
+
f"Assertion Failed: Expected None\n"
|
|
53
|
+
f" Actual : {value} ({type(value).__name__})\n"
|
|
54
|
+
f"Stack Trace:\n{_get_stack_trace()}"
|
|
55
|
+
)
|
|
56
|
+
raise AssertionError(error_msg)
|
|
57
|
+
|
|
58
|
+
def ASSERT_IS_NOT_NONE(value: T | None, msg: str | None = None) -> TypeGuard[T]:
|
|
59
|
+
if value is None:
|
|
60
|
+
error_msg = (
|
|
61
|
+
f"\nAssertion Failed: {msg}\n" if msg else ""
|
|
62
|
+
) + (
|
|
63
|
+
f"Assertion Failed: Expected Not None\n"
|
|
64
|
+
f" Actual : {value}\n"
|
|
65
|
+
f"Stack Trace:\n{_get_stack_trace()}"
|
|
66
|
+
)
|
|
67
|
+
raise AssertionError(error_msg)
|
|
68
|
+
return True
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
from .hex_util import FORMAT_HEX
|
|
2
|
+
|
|
3
|
+
def BITS_UPDATE(val: int, mask: int, data: int) -> int:
|
|
2
4
|
"""
|
|
3
5
|
更新寄存器中的特定位域 (Read-Modify-Write)
|
|
4
6
|
:param val: 当前寄存器的原始值
|
|
@@ -6,10 +8,10 @@ def bits_update(val: int, mask: int, data: int) -> int:
|
|
|
6
8
|
:param data: 已移位对齐的新数值 (例如: 0x20, 即 1 << 5)
|
|
7
9
|
:return: 更新后的寄存器值
|
|
8
10
|
"""
|
|
9
|
-
assert (data & ~mask) == 0, f"Data {
|
|
11
|
+
assert (data & ~mask) == 0, f"Data {FORMAT_HEX(data)} contains bits outside mask {FORMAT_HEX(mask)}"
|
|
10
12
|
return (val & ~mask) | (data & mask)
|
|
11
13
|
|
|
12
|
-
def
|
|
14
|
+
def BITS_SET(val: int, mask: int) -> int:
|
|
13
15
|
"""
|
|
14
16
|
将寄存器中的指定位强制置 1 (OR 操作)
|
|
15
17
|
|
|
@@ -17,7 +19,7 @@ def bits_set(val: int, mask: int) -> int:
|
|
|
17
19
|
"""
|
|
18
20
|
return val | mask
|
|
19
21
|
|
|
20
|
-
def
|
|
22
|
+
def BITS_RESET(val: int, mask: int) -> int:
|
|
21
23
|
"""
|
|
22
24
|
将寄存器中的指定位强制清 0 (AND-NOT 操作)
|
|
23
25
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .assertions import ASSERT_LEN
|
|
2
|
+
from .hex_util import FORMAT_HEX
|
|
3
|
+
|
|
4
|
+
def GET_BCC(data: list[int]) -> int:
|
|
5
|
+
"""
|
|
6
|
+
计算 BCC: data[0]^data[1]^data[2]^data[3]
|
|
7
|
+
:param data: 4字节的UID数据
|
|
8
|
+
"""
|
|
9
|
+
ASSERT_LEN(data, 4, msg=f"计算BCC时输入数据长度应为4: {FORMAT_HEX(data)}")
|
|
10
|
+
return data[0] ^ data[1] ^ data[2] ^ data[3]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import runpy
|
|
5
|
+
|
|
6
|
+
def run_script(script_path):
|
|
7
|
+
# Add the script's directory to sys.path so it can find its dependencies
|
|
8
|
+
script_dir = os.path.dirname(os.path.abspath(script_path))
|
|
9
|
+
sys.path.insert(0, script_dir)
|
|
10
|
+
|
|
11
|
+
# Run the script as __main__ so __name__ == "__main__" works inside the script
|
|
12
|
+
runpy.run_path(script_path, run_name="__main__")
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
print("Hello from nfcscript!")
|
|
16
|
+
parser = argparse.ArgumentParser(description="NFC Script Runner")
|
|
17
|
+
parser.add_argument("script", help="Path to the script to run")
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
if not os.path.exists(args.script):
|
|
21
|
+
print(f"Error: Script not found: {args.script}")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
run_script(args.script)
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
main()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
# 位标记正则:匹配 N'hXX 格式 (如 7'h26, 4'hC)
|
|
4
|
+
BIT_PATTERN = re.compile(r"(\d+)\s*\'h\s*([0-9a-fA-F]+)")
|
|
5
|
+
|
|
6
|
+
def PARSE_HEX(text: str) -> tuple[str, int]:
|
|
7
|
+
"""
|
|
8
|
+
解析文本 -> (纯十六进制字符串, 最后字节有效位数)
|
|
9
|
+
支持: "AA BB 7'h03" -> ("AABB03", 7)
|
|
10
|
+
"""
|
|
11
|
+
last_bits = 0
|
|
12
|
+
cleaned = text.strip()
|
|
13
|
+
|
|
14
|
+
# 查找所有位标记
|
|
15
|
+
matches = list(BIT_PATTERN.finditer(cleaned))
|
|
16
|
+
if matches:
|
|
17
|
+
last_match = matches[-1]
|
|
18
|
+
last_bits = int(last_match.group(1))
|
|
19
|
+
hex_val = last_match.group(2)
|
|
20
|
+
|
|
21
|
+
# 替换位标记为纯 hex
|
|
22
|
+
cleaned = cleaned[:last_match.start()] + hex_val + cleaned[last_match.end():]
|
|
23
|
+
|
|
24
|
+
# 去空格得到纯 hex
|
|
25
|
+
clean_hex = cleaned.replace(" ", "")
|
|
26
|
+
|
|
27
|
+
# 简单的合法性检查
|
|
28
|
+
if clean_hex:
|
|
29
|
+
try:
|
|
30
|
+
bytes.fromhex(clean_hex)
|
|
31
|
+
except ValueError:
|
|
32
|
+
raise ValueError(f"不合法的十六进制数据: {clean_hex}")
|
|
33
|
+
|
|
34
|
+
return clean_hex, last_bits
|
|
35
|
+
|
|
36
|
+
def FORMAT_HEX(data: int | bytes | list[int] | str, last_bits: int = 0) -> str:
|
|
37
|
+
"""
|
|
38
|
+
格式化数据为带位标记的可视化格式
|
|
39
|
+
支持输入类型: int, bytes, list[int], str (纯 hex)
|
|
40
|
+
示例:
|
|
41
|
+
FORMAT_HEX(0xAABB03, 7) -> "AA BB 7'h03"
|
|
42
|
+
FORMAT_HEX("AABB03", 7) -> "AA BB 7'h03"
|
|
43
|
+
FORMAT_HEX(b'\xaa\xbb\x03', 7) -> "AA BB 7'h03"
|
|
44
|
+
FORMAT_HEX([0xAA, 0xBB, 0x03], 7) -> "AA BB 7'h03"
|
|
45
|
+
"""
|
|
46
|
+
# 1. 统一转换为纯十六进制字符串
|
|
47
|
+
if isinstance(data, int):
|
|
48
|
+
hex_str = hex(data)[2:].upper()
|
|
49
|
+
elif isinstance(data, bytes):
|
|
50
|
+
hex_str = data.hex().upper()
|
|
51
|
+
elif isinstance(data, list):
|
|
52
|
+
hex_str = bytes(data).hex().upper()
|
|
53
|
+
elif isinstance(data, str):
|
|
54
|
+
hex_str = data.replace(" ", "").upper()
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError(f"不支持的输入类型: {type(data)}")
|
|
57
|
+
|
|
58
|
+
if not hex_str:
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
# 2. 如果是整字节 (无 last_bits)
|
|
62
|
+
if last_bits == 0:
|
|
63
|
+
return ' '.join(hex_str[i:i+2] for i in range(0, len(hex_str), 2))
|
|
64
|
+
|
|
65
|
+
# 3. 有 last_bits,处理最后一个字节
|
|
66
|
+
if len(hex_str) <= 2:
|
|
67
|
+
return f"{last_bits}'h{hex_str}"
|
|
68
|
+
|
|
69
|
+
prefix = hex_str[:-2]
|
|
70
|
+
last_byte = hex_str[-2:]
|
|
71
|
+
|
|
72
|
+
formatted_prefix = ' '.join(prefix[i:i+2] for i in range(0, len(prefix), 2))
|
|
73
|
+
return f"{formatted_prefix} {last_bits}'h{last_byte}"
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
# --- 测试例程 ---
|
|
77
|
+
print("=== Hex 工具测试 ===")
|
|
78
|
+
|
|
79
|
+
# 测试用例: (data, last_bits, 类型名)
|
|
80
|
+
test_cases = [
|
|
81
|
+
("AABB03", 7, "str"),
|
|
82
|
+
(0xAABB03, 7, "int"),
|
|
83
|
+
(b'\xAA\xBB\x03', 7, "bytes"),
|
|
84
|
+
([0xAA, 0xBB, 0x03], 7, "list"),
|
|
85
|
+
("FF", 4, "str"),
|
|
86
|
+
(0xFF, 4, "int"),
|
|
87
|
+
("AABBCC", 0, "str"),
|
|
88
|
+
(0xAABBCC, 0, "int"),
|
|
89
|
+
(b'\xAA\xBB\xCC', 0, "bytes"),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
for data, b, typename in test_cases:
|
|
93
|
+
formatted = FORMAT_HEX(data, b)
|
|
94
|
+
print(f"输入: ({data}, {b}, {typename}) -> 格式化: {formatted}")
|
|
95
|
+
|
|
96
|
+
# 反向解析验证
|
|
97
|
+
parsed_h, parsed_b = PARSE_HEX(formatted)
|
|
98
|
+
print(f"解析: {formatted} -> Hex: {parsed_h}, Bits: {parsed_b}")
|
|
99
|
+
print("-" * 20)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .assertions import ASSERT_EQUAL, ASSERT_IS_NOT_NONE, ASSERT_LEN
|
|
4
|
+
from .checksum import GET_BCC
|
|
5
|
+
from .hex_util import FORMAT_HEX
|
|
6
|
+
from nfctester.hardware.serial_transport import SerialTransport
|
|
7
|
+
from nfctester.drivers.pn532_hsu import PN532_HSU
|
|
8
|
+
|
|
9
|
+
# Global state
|
|
10
|
+
_transport: SerialTransport | None = None
|
|
11
|
+
_driver: PN532_HSU | None = None
|
|
12
|
+
_is_connected = False
|
|
13
|
+
|
|
14
|
+
def connect(port="COM20"):
|
|
15
|
+
"""
|
|
16
|
+
连接并初始化 PN532 读卡器。
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
port: 串口号,默认为 "COM20"。
|
|
20
|
+
"""
|
|
21
|
+
global _transport, _driver, _is_connected
|
|
22
|
+
_transport = SerialTransport(port=port)
|
|
23
|
+
_driver = PN532_HSU(_transport)
|
|
24
|
+
_driver.connect()
|
|
25
|
+
_is_connected = True
|
|
26
|
+
print(f"Connected to PN532 on {_transport.ser.port}")
|
|
27
|
+
|
|
28
|
+
def active(ll: bool = False, ignore_error: bool = False) -> dict | None:
|
|
29
|
+
"""
|
|
30
|
+
寻卡操作,检测并激活目标卡片。
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
ll: 如果为 True,则执行底层抗冲突流程以兼容非标卡;
|
|
34
|
+
如果为 False,则使用 PN532 原生 find()。
|
|
35
|
+
ignore_error: 如果为 True,当 BCC 或 SAK 校验失败时,记录警告而不停止执行。
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
dict: 包含卡片信息的字典,失败时返回 None。
|
|
39
|
+
Key 说明:
|
|
40
|
+
- 'uid' (bytes): 卡片的 UID。
|
|
41
|
+
- 'atq' (bytes): 卡片的 ATQ (Answer To Request)。
|
|
42
|
+
- 'sak' (int): 卡片的 SAK (Select Acknowledge)。
|
|
43
|
+
- 'raw' (bytes): PN532 返回的原始响应数据。
|
|
44
|
+
"""
|
|
45
|
+
ASSERT_IS_NOT_NONE(_driver, msg="NFC driver not connected. Call connect() first.")
|
|
46
|
+
|
|
47
|
+
if not ll:
|
|
48
|
+
return _driver.find()
|
|
49
|
+
else:
|
|
50
|
+
# 手动底层寻卡流程
|
|
51
|
+
# 1. REQA
|
|
52
|
+
res_reqa = reqa()
|
|
53
|
+
if not res_reqa:
|
|
54
|
+
return None
|
|
55
|
+
atq = bytes(res_reqa[0])
|
|
56
|
+
|
|
57
|
+
# 2. 抗冲突与选择
|
|
58
|
+
full_uid = []
|
|
59
|
+
sak = 0
|
|
60
|
+
for cl in [1, 2, 3]:
|
|
61
|
+
res = anticoll(cl_level=cl, nvb=0x20)
|
|
62
|
+
if not res or not res[0]:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
data = res[0]
|
|
66
|
+
# 只有当有数据时才进行长度校验
|
|
67
|
+
if len(data) > 0:
|
|
68
|
+
ASSERT_LEN(data, 5, msg=f"CL{cl} 抗冲突返回数据长度不符: {FORMAT_HEX(data)}")
|
|
69
|
+
|
|
70
|
+
# 计算期望的 BCC
|
|
71
|
+
expected_bcc = GET_BCC(data[0:4])
|
|
72
|
+
|
|
73
|
+
if not ignore_error:
|
|
74
|
+
ASSERT_EQUAL(expected_bcc, data[4], msg=f"CL{cl} BCC 校验失败: data={FORMAT_HEX(data)}")
|
|
75
|
+
elif data[4] != expected_bcc:
|
|
76
|
+
print(f"Warning: CL{cl} BCC 校验失败")
|
|
77
|
+
|
|
78
|
+
has_next = (data[0] == 0x88)
|
|
79
|
+
uid_to_select = data[0:5]
|
|
80
|
+
sak_res = select(cl_level=cl, uid=uid_to_select)
|
|
81
|
+
|
|
82
|
+
# SAK 校验
|
|
83
|
+
if not sak_res:
|
|
84
|
+
if not ignore_error: ASSERT_IS_NOT_NONE(sak_res, msg=f"CL{cl} SAK 选择失败")
|
|
85
|
+
else: print(f"Warning: CL{cl} SAK 选择失败")
|
|
86
|
+
|
|
87
|
+
if has_next:
|
|
88
|
+
full_uid.extend(data[1:4])
|
|
89
|
+
else:
|
|
90
|
+
full_uid.extend(data[0:4])
|
|
91
|
+
# 记录最后一次 SELECT 的 SAK
|
|
92
|
+
sak = sak_res[0] if sak_res else 0
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
# 返回兼容的字典格式
|
|
96
|
+
return {
|
|
97
|
+
'uid': bytes(full_uid),
|
|
98
|
+
'atq': atq,
|
|
99
|
+
'sak': sak,
|
|
100
|
+
'raw': bytes(full_uid) # 兼容原有的 raw 字段
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def transceive(data: list[int], tx_crc: bool = True, rx_crc: bool = True) -> list[int]:
|
|
104
|
+
"""
|
|
105
|
+
使用底层帧交互方式发送数据。
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data: 要发送的字节列表。
|
|
109
|
+
tx_crc: 发送时是否自动添加 CRC。
|
|
110
|
+
rx_crc: 接收时是否自动校验 CRC。
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
list[int]: 接收到的数据字节列表。
|
|
114
|
+
"""
|
|
115
|
+
ASSERT_IS_NOT_NONE(_driver, msg="NFC driver not connected.")
|
|
116
|
+
_driver.set_crc(tx_crc, rx_crc)
|
|
117
|
+
res = _driver.transceive(bytes(data))
|
|
118
|
+
# print(f"transceive: {FORMAT_HEX(data)}")
|
|
119
|
+
return list(res) if res is not None else []
|
|
120
|
+
|
|
121
|
+
def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) -> tuple[list[int], int]:
|
|
122
|
+
"""
|
|
123
|
+
使用底层帧交互方式发送数据,并支持对最后一个字节进行位控制。
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
data: 要发送的字节列表。
|
|
127
|
+
last_tx_bits: 最后一个字节实际发送的有效位数 (1-7),
|
|
128
|
+
0 表示发送完整字节。
|
|
129
|
+
tx_crc: 发送时是否自动添加 CRC。
|
|
130
|
+
rx_crc: 接收时是否自动校验 CRC。
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
tuple[list[int], int]: (接收到的数据字节列表, 最后一个字节的有效位数)。
|
|
134
|
+
最后一个字节的有效位数 0 表示完整字节。
|
|
135
|
+
"""
|
|
136
|
+
ASSERT_IS_NOT_NONE(_driver, msg="NFC driver not connected.")
|
|
137
|
+
_driver.set_crc(tx_crc, rx_crc)
|
|
138
|
+
res = _driver.transceive(bytes(data), last_tx_bits=last_tx_bits)
|
|
139
|
+
# print(f"transceive: {FORMAT_HEX(data, last_tx_bits)}")
|
|
140
|
+
return (list(res) if res is not None else []), _driver.last_rx_bits
|
|
141
|
+
|
|
142
|
+
def reqa() -> tuple[list[int], int] | None:
|
|
143
|
+
"""ISO14443-A REQA (7 bits)"""
|
|
144
|
+
# REQA: 0x26,短帧,仅发送 7 bits,响应无 CRC
|
|
145
|
+
return transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
146
|
+
|
|
147
|
+
def wupa() -> tuple[list[int], int] | None:
|
|
148
|
+
"""ISO14443-A WUPA (7 bits)"""
|
|
149
|
+
# WUPA: 0x52,短帧,仅发送 7 bits,响应无 CRC
|
|
150
|
+
return transceive_bits([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
151
|
+
|
|
152
|
+
def halt() -> list[int] | None:
|
|
153
|
+
"""ISO14443-A HALT (Standard Frame)"""
|
|
154
|
+
# HALT: 0x50 0x00 + CRC
|
|
155
|
+
return transceive([0x50, 0x00], tx_crc=True, rx_crc=True)
|
|
156
|
+
|
|
157
|
+
def anticoll(cl_level: int, nvb: int = 0x20, uid_prefix: list[int] = []) -> tuple[list[int], int] | None:
|
|
158
|
+
"""
|
|
159
|
+
ISO14443-A ANTICOLL (Anti-collision)
|
|
160
|
+
:param cl_level: 1 (0x93) or 2 (0x95)
|
|
161
|
+
:param nvb: Number of Valid Bits,定义了包含 SEL 和 NVB 在内的有效位数
|
|
162
|
+
:param uid_prefix: 已知的部分 UID (0-4 字节)
|
|
163
|
+
:return: (响应数据, last_rx_bits)
|
|
164
|
+
"""
|
|
165
|
+
cmd = [0x93 if cl_level == 1 else 0x95, nvb] + uid_prefix
|
|
166
|
+
# 抗冲突响应无 CRC
|
|
167
|
+
return transceive_bits(cmd, last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
168
|
+
|
|
169
|
+
def select(cl_level: int, uid: list[int]) -> list[int] | None:
|
|
170
|
+
"""
|
|
171
|
+
ISO14443-A SELECT (Standard Frame)
|
|
172
|
+
:param cl_level: 1 (0x93) or 2 (0x95)
|
|
173
|
+
:param uid: 完整 5 字节 UID (包含 BCC)
|
|
174
|
+
"""
|
|
175
|
+
cmd = [0x93 if cl_level == 1 else 0x95, 0x70] + uid
|
|
176
|
+
# 选择帧需带 CRC
|
|
177
|
+
return transceive(cmd, tx_crc=True, rx_crc=True)
|
|
178
|
+
|
|
179
|
+
def field_on():
|
|
180
|
+
"""开启 PN532 的 RF 场。"""
|
|
181
|
+
ASSERT_IS_NOT_NONE(_driver, msg="NFC driver not connected.")
|
|
182
|
+
_driver.set_rf_field(True)
|
|
183
|
+
|
|
184
|
+
def field_off():
|
|
185
|
+
"""关闭 PN532 的 RF 场。"""
|
|
186
|
+
ASSERT_IS_NOT_NONE(_driver, msg="NFC driver not connected.")
|
|
187
|
+
_driver.set_rf_field(False)
|
|
188
|
+
|
|
189
|
+
def close():
|
|
190
|
+
"""
|
|
191
|
+
断开与 PN532 读卡器的连接。
|
|
192
|
+
"""
|
|
193
|
+
global _is_connected
|
|
194
|
+
if _is_connected and _driver:
|
|
195
|
+
_driver.disconnect()
|
|
196
|
+
_is_connected = False
|
|
197
|
+
print("Disconnected")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nfcscript
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: NFC
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: A plugin-based core engine for DSL-style NFC testing automation.
|
|
5
5
|
Author: crthu
|
|
6
6
|
Requires-Python: >=3.14
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist:
|
|
8
|
+
Requires-Dist: nfctester
|
|
9
9
|
|
|
10
10
|
# nfcscript
|
|
11
11
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
|
-
src/
|
|
4
|
-
src/
|
|
5
|
-
src/
|
|
6
|
-
src/
|
|
3
|
+
src/nfc/__init__.py
|
|
4
|
+
src/nfc/assertions.py
|
|
5
|
+
src/nfc/bits.py
|
|
6
|
+
src/nfc/checksum.py
|
|
7
|
+
src/nfc/cli.py
|
|
8
|
+
src/nfc/delay.py
|
|
9
|
+
src/nfc/hex_util.py
|
|
10
|
+
src/nfc/reader.py
|
|
7
11
|
src/nfcscript.egg-info/PKG-INFO
|
|
8
12
|
src/nfcscript.egg-info/SOURCES.txt
|
|
9
13
|
src/nfcscript.egg-info/dependency_links.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nfctester
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nfc
|
|
File without changes
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
def _get_stack_trace() -> str:
|
|
5
|
-
"""获取格式化的调用堆栈"""
|
|
6
|
-
# inspect.stack()[0] 是 _get_stack_trace
|
|
7
|
-
# inspect.stack()[1] 是 ASSERT_... 函数
|
|
8
|
-
# inspect.stack()[2:] 是实际的业务调用代码
|
|
9
|
-
stack = inspect.stack()[2:]
|
|
10
|
-
chain = []
|
|
11
|
-
for frame in stack:
|
|
12
|
-
# 简化路径,仅显示文件名
|
|
13
|
-
filename = frame.filename.split('\\')[-1].split('/')[-1]
|
|
14
|
-
chain.append(f" at {filename}:{frame.lineno} in {frame.function}")
|
|
15
|
-
return "\n".join(chain)
|
|
16
|
-
|
|
17
|
-
def ASSERT_LEN(data: Any, expected_len: int) -> None:
|
|
18
|
-
actual_len = len(data)
|
|
19
|
-
if actual_len != expected_len:
|
|
20
|
-
error_msg = (
|
|
21
|
-
f"\nAssertion Failed: Length mismatch\n"
|
|
22
|
-
f" Expected: {expected_len}\n"
|
|
23
|
-
f" Actual : {actual_len}\n"
|
|
24
|
-
f" Data : {data}\n"
|
|
25
|
-
f"Stack Trace:\n{_get_stack_trace()}"
|
|
26
|
-
)
|
|
27
|
-
raise AssertionError(error_msg)
|
|
28
|
-
|
|
29
|
-
def ASSERT_EQUAL(actual: Any, expected: Any) -> None:
|
|
30
|
-
if actual != expected:
|
|
31
|
-
error_msg = (
|
|
32
|
-
f"\nAssertion Failed: Value mismatch\n"
|
|
33
|
-
f" Expected: {expected} ({type(expected).__name__})\n"
|
|
34
|
-
f" Actual : {actual} ({type(actual).__name__})\n"
|
|
35
|
-
f"Stack Trace:\n{_get_stack_trace()}"
|
|
36
|
-
)
|
|
37
|
-
raise AssertionError(error_msg)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
CarrotRFIDTester
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
nfcscript
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|